From 727412f1b1907f428da13e0d0eafd248e051098e Mon Sep 17 00:00:00 2001 From: Raphael Jansen Date: Fri, 24 Mar 2023 11:17:11 -0300 Subject: [PATCH] update indaband fork due to an old version of the video_player (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [quick_actions]Migrates all remaining components to Swift, and deprecate OCMock (#6597) * [quick_actions]migrate shortcut state manager, deprecate OCMock and use POP * remove objc proj settings * rename shortcut state manager * bump version * run swift-format * nit * remove public_header_files * use shortcut item parser instead of shortcut state manager * some nit * rename AppShortcutControlling to ShortcutItemProviding * nit * do not crash if no type or title * update license * [quick_actions] Fix Android integration test flake (#6688) * Make fix * Formatting * [google_sign_in] Correctly passes `serverClientId` to native libs (#6691) * Correctly passes serverClientId to native libs * Bumps versions * Revert "[ci]Temporarily disable starqlteue on firebase device lab (#6657)" (#6710) This reverts commit f8122dc89ad3e76e8018d113c40b3cb3ccdb3e51. * [ci] Manually Roll Flutter from 61e927d22fe6 to d2e6dfefa5ca (143 revisions) (#6723) * Slow down link_widget_test scroll extent so test passes (framework/engine regression?) * Manually Roll Flutter from 61e927d22fe6 to d2e6dfefa5ca (143 revisions) * Roll Flutter from d2e6dfefa5ca to 46d868c52517 (5 revisions) (#6724) * 22e1ac762 b32c3a307 Roll Skia from ab054a88d7c7 to 345bceacd298 (3 revisions) (flutter/engine#37745) (flutter/flutter#115658) * 01c1e8e58 Allows pushing page based route as pageless route (flutter/flutter#114362) * 0e57147db nav drawer (flutter/flutter#115668) * cf2c9f6ed Remove package:image dependency (flutter/flutter#115674) * 46d868c52 Roll Flutter Engine from b32c3a307bb5 to 7a390f97c798 (14 revisions) (flutter/flutter#115672) * [camera] Export VideoCaptureOptions so that implementers can use it (#6666) * Export VideoCaptureOptions so that it is accessible to other packages Also added tests so that missing exports can be more easily tested in the future. * Fix dart analyze warnings * Roll Flutter from 46d868c52517 to 633d7ef046c8 (8 revisions) (#6725) * 0fc4a3efa Revert "Remove package:image dependency (#115674)" (flutter/flutter#115680) * c69fbf150 a77dfaff3 Remove `Linux Web Engine` from recipes CQ (flutter/engine#37758) (flutter/flutter#115675) * c95b69354 f81ac3b19 Fix glyph sampling overlap (flutter/engine#37764) (flutter/flutter#115683) * be0c3a799 cef11cb9a Roll Skia from 12f01bc5b57e to c53d8cf5b823 (4 revisions) (flutter/engine#37767) (flutter/flutter#115684) * ae18d7b07 6da59402e Roll Skia from c53d8cf5b823 to 0c1fcbe97b1f (1 revision) (flutter/engine#37771) (flutter/flutter#115685) * a17b4c369 91bc694eb Roll Fuchsia Mac SDK from tklUfTsSOVKk49tYq... to UcfQiA4PBOCs_7GEK... (flutter/engine#37773) (flutter/flutter#115686) * 1bee3574b 2d5e0667e Roll Fuchsia Linux SDK from WdtwlLEce90PjFJ9z... to qc20R_3e8PoqMQWgw... (flutter/engine#37775) (flutter/flutter#115687) * 633d7ef04 916fd798d Roll Skia from 0c1fcbe97b1f to ad354e712b96 (2 revisions) (flutter/engine#37776) (flutter/flutter#115689) * [ci] Improve analysis_options alignment with flutter/packages (#6728) * Add more options that are in flutter/packages * Fix unnecessary awaits * More option alignment * Add and locally supress avoid_implementing_value_types * Fix release-info for test-only changes * Fix update-release-info handling of 'minimal' * Update release metadata * Roll Flutter from 633d7ef046c8 to 29622285dd9e (13 revisions) (#6730) * 271a1bf86 Roll Flutter Engine from 916fd798deb3 to 9a7336dce837 (2 revisions) (flutter/flutter#115701) * d8a091ce9 e9bdd5ef5 Roll Fuchsia Linux SDK from qc20R_3e8PoqMQWgw... to Cg4pM7Agigl6gZqq5... (flutter/engine#37782) (flutter/flutter#115703) * b556cc591 79bc94539 Roll Fuchsia Mac SDK from C8_lxtWKA4MIKeAu2... to KqMuhIlNeJZpycJLZ... (flutter/engine#37784) (flutter/flutter#115713) * 05683182d bb283b4ba Roll Fuchsia Linux SDK from Cg4pM7Agigl6gZqq5... to 2T1QqkhI-ef8AXGgn... (flutter/engine#37785) (flutter/flutter#115716) * afb479b46 c653da351 Roll Skia from 80b3e3d24a99 to dd5f384ae62a (1 revision) (flutter/engine#37786) (flutter/flutter#115717) * 20fa32e12 774bce877 Roll Skia from dd5f384ae62a to 1d7f785e3679 (1 revision) (flutter/engine#37787) (flutter/flutter#115718) * ee9bc784b Roll Flutter Engine from 774bce877488 to 271461837e78 (4 revisions) (flutter/flutter#115735) * f365c0632 4affb2af4 Roll Skia from db7a810dba5d to 645605735772 (1 revision) (flutter/engine#37796) (flutter/flutter#115742) * d9fb0dd8c 18a5b3596 Roll Skia from 645605735772 to ae61b83805e3 (3 revisions) (flutter/engine#37797) (flutter/flutter#115746) * 809ee4418 Roll Flutter Engine from 18a5b3596094 to 46a6b5429516 (2 revisions) (flutter/flutter#115758) * a9858ec52 Roll Plugins from b2fe01bc0f29 to 475caa00130d (5 revisions) (flutter/flutter#115761) * e2f84b596 Roll Flutter Engine from 46a6b5429516 to 5c3b8956d50b (2 revisions) (flutter/flutter#115769) * 29622285d 55b089131 Roll Fuchsia Linux SDK from EyQx0yUqK5TJxeHF7... to xBfEjlXUsix6Wka-i... (flutter/engine#37804) (flutter/flutter#115772) * [sign_in]: Bump play-services-auth from 20.3.0 to 20.4.0 in /packages/google_sign_in/google_sign_in_android/android (#6726) * [sign_in]: Bump play-services-auth Bumps play-services-auth from 20.3.0 to 20.4.0. --- updated-dependencies: - dependency-name: com.google.android.gms:play-services-auth dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Bump version Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: camsim99 * [ci] Import flutter/packages install_chromium.sh (#6727) * [ci] Import flutter/packages install_chromium.sh Brings over the newer flutter/packages version of install_chromium.sh as part of pre-aligning the repositories for later merging. Part of https://github.com/flutter/flutter/issues/113764 * Update .cirrus.yml Co-authored-by: David Iglesias Co-authored-by: David Iglesias * Switch `ios_platform_tests` from Cirrus to LUCI (#6729) * move to prod * remove cirrus * Roll Flutter from 29622285dd9e to 06d90b8b9e26 (17 revisions) (#6742) * 504f697d6 4a4df4bd6 [Impeller] Format shader sources. (flutter/engine#37770) (flutter/flutter#115773) * 7045a8b57 Add Spell Check to Editable Text (iOS) (flutter/flutter#110193) * 567d0045b Add clip option for navigator (flutter/flutter#115775) * 073cefad0 [RawKeyboard] Fix Linux remapped CapsLock throws (flutter/flutter#115009) * bd0115f04 [cirrus] Disable outside of tip of tree (flutter/flutter#115774) * ce2fa9a56 Roll Flutter Engine from 4a4df4bd685d to cc0ad66907b6 (4 revisions) (flutter/flutter#115777) * 0b9d18f8f [flutter_tools] Add flutter update-packages --synthetic-package-path (flutter/flutter#115665) * 13012082a 9ea061bd7 [macOS] Add explicit weak/strong/copy annotations (flutter/engine#37768) (flutter/flutter#115783) * 2b0c895fa Updated the kotlinlang version url. (flutter/flutter#115782) * 3bd53b084 2a8ac1e0c [Linux] Synthesize modifier keys events on pointer events (flutter/engine#37491) (flutter/flutter#115788) * 9827d0fc8 44f22ac39 Roll Dart SDK from 68291f382fb6 to 756f835dd84d (1 revision) (flutter/engine#37815) (flutter/flutter#115792) * eed385132 Support Host only bots apple signing (flutter/flutter#115780) * 0d7a3b7f4 Roll Flutter Engine from 44f22ac39f54 to 981fe92ab998 (2 revisions) (flutter/flutter#115798) * 3fe779425 BouncingScrollPhysics should propagate decelerationRate. (flutter/flutter#115797) * 182d6c4c9 ddf6a20b8 Roll Skia from 500aae3f2761 to 57b4252cf211 (7 revisions) (flutter/engine#37819) (flutter/flutter#115802) * 2921ca0c4 8dd8e092e Roll Dart SDK from 756f835dd84d to 6b8e98070f26 (1 revision) (flutter/engine#37825) (flutter/flutter#115810) * 06d90b8b9 c3645c3b8 [impeller] Remove declare_undefined_values (flutter/engine#37829) (flutter/flutter#115812) * [path_provider] Remove unused Guava dependency (#6744) * Remove unused Guava dependency * Metadata * Update Nullable import * [google_sign_in] Roll Guava dependency to 31.1 (#6746) * Roll guava * Metadata * [ios_platform_images] remove deprecated APIs (#6693) * [ios_platform_images] remove deprecated APIs * ++ * ++ * ++ * Update pubspec.yaml * Update CHANGELOG.md * Roll Flutter from 06d90b8b9e26 to 0eb2d51ec991 (17 revisions) (#6750) * f4ee61a39 Roll Plugins from 475caa00130d to 5d847ef391a5 (3 revisions) (flutter/flutter#115837) * 91aeda7bf Use the new pushImageFilter offset parameter to fix the transform of the children (flutter/flutter#113673) * 9bb07b5f7 Revert "Use the new pushImageFilter offset parameter to fix the transform of the children (#113673)" (flutter/flutter#115861) * 14754a261 roll packages (flutter/flutter#115764) * dfdec8984 [flutter_tools] Remove package:image (flutter/flutter#115848) * da963a902 Roll Flutter Engine from c3645c3b8947 to 37e2aaa901e8 (9 revisions) (flutter/flutter#115842) * b9caef58e Remove dev/md random file (flutter/flutter#115855) * 94b9fa411 Provide an option to update `Focus's semantics under `FocusableActionDetector` (flutter/flutter#115833) * 6a26305d1 Update documentation for `PlatformException.stacktrace` (flutter/flutter#114028) * 73024eb70 [flutter_tool] Adds --enable-dart-profiling flag (flutter/flutter#115863) * 259373d62 [flutter_tools] Add --dump-info, --no-frequency-based-minification flags (flutter/flutter#115862) * d95eab877 Roll Flutter Engine from 37e2aaa901e8 to a805efffba54 (12 revisions) (flutter/flutter#115881) * 7bb1d1b35 Roll Flutter Engine from a805efffba54 to 4bf16c369bfc (2 revisions) (flutter/flutter#115888) * b7023e843 Roll Flutter Engine from 4bf16c369bfc to f75287af0b19 (2 revisions) (flutter/flutter#115891) * a780a007e a43142d77 Roll Dart SDK from 27c45cd51796 to d2766b385c2a (5 revisions) (flutter/engine#37859) (flutter/flutter#115895) * 4edb76817 Roll Flutter Engine from a43142d77a58 to e6d9fffe8609 (2 revisions) (flutter/flutter#115897) * 0eb2d51ec Use the new pushImageFilter offset parameter to fix the transform of the children (#113673) (flutter/flutter#115884) * b8f7f1f98 [flutter_releases] Flutter stable 3.3.9 Framework Cherrypicks (flutter/flutter#115856) (#6751) * Roll Flutter from 0eb2d51ec991 to cb234730a11d (13 revisions) (#6755) * 16fb711f7 Roll Plugins from 5d847ef391a5 to a431b35bfa0b (7 revisions) (flutter/flutter#115930) * a27e3f2e8 94cdce89c Roll Dart SDK from d2766b385c2a to c32f12ffbef2 (2 revisions) (flutter/engine#37869) (flutter/flutter#115938) * 65168d17e 7959ffd2e Fix DomProgressEvent constructor (flutter/engine#37849) (flutter/flutter#115941) * e96fc9973 Roll Flutter Engine from 7959ffd2ee6d to e74cecbf830e (2 revisions) (flutter/flutter#115949) * f5c14807f Roll Flutter Engine from e74cecbf830e to 8a40e8324b48 (3 revisions) (flutter/flutter#115952) * a9f9293d9 3925231ea Call focus on input after detecting a tap (flutter/engine#37863) (flutter/flutter#115953) * 108f88c7c 96bbe3c5f Roll Skia from c098e3c5d932 to e3c0b382b072 (12 revisions) (flutter/engine#37875) (flutter/flutter#115954) * b52f0b6f9 d4fe02ab0 Roll Skia from e3c0b382b072 to 805fd7443f42 (3 revisions) (flutter/engine#37879) (flutter/flutter#115956) * d0352f55d Roll Flutter Engine from d4fe02ab0366 to 9b2895724c4d (2 revisions) (flutter/flutter#115957) * 09e2c67e4 Roll Flutter Engine from 9b2895724c4d to 497a485751a9 (2 revisions) (flutter/flutter#115966) * a7f8b31be 06e5e6ac5 Roll Skia from 665ae344c8ec to f45e40d01dac (4 revisions) (flutter/engine#37888) (flutter/flutter#115969) * 013179f55 05b8d011f Roll Fuchsia Mac SDK from i-NY382Y0Y8m9OqNp... to dxhpLq8MRsVs0e7OD... (flutter/engine#37891) (flutter/flutter#115972) * cb234730a c46300a66 Roll Dart SDK from 71241dd55373 to bb6aa5a412d7 (2 revisions) (flutter/engine#37893) (flutter/flutter#115975) * Roll Flutter from cb234730a11d to ff59250dbeb0 (3 revisions) (#6757) * 0b3b31e5b 0930a7633 Roll Skia from f45e40d01dac to a01af64f5cec (1 revision) (flutter/engine#37896) (flutter/flutter#115982) * a41376ede ad21c5adf Roll Dart SDK from bb6aa5a412d7 to eddf73d66119 (1 revision) (flutter/engine#37897) (flutter/flutter#115986) * ff59250db 7665ae518 Roll Fuchsia Mac SDK from dxhpLq8MRsVs0e7OD... to NvafPMjJLQ0fBSSAc... (flutter/engine#37898) (flutter/flutter#115991) * Roll Flutter from ff59250dbeb0 to 17c1dbc47378 (31 revisions) (#6764) * 96d7f9cb1 Updated tokens to v0_143. (flutter/flutter#115890) * 2703a2bcd Fix current day not being decorated when it was disabled for picking. (flutter/flutter#115240) * d841d3214 TabBar should adjust scroll position when Controller is changed (flutter/flutter#116019) * 7faacb5f7 Marks Windows_android channels_integration_test_win to be unflaky (flutter/flutter#115939) * 511c5341b Add test target for release scheduler (flutter/flutter#115781) * 224fae506 Fix iOS selectWordEdge doesn't account for affinity (flutter/flutter#115849) * 4a5dd9c9f Roll Flutter Engine from 7665ae51846f to 867214c062ee (20 revisions) (flutter/flutter#116121) * 215f6372c Refactor Message class to hold all translations (flutter/flutter#115506) * beaabb70c Add `IndicatorShape` to `NavigationRailTheme` and fix indicator ripple. (flutter/flutter#116108) * 3cafeb3e9 e612c3d75 Roll Skia from 3ab92e777da1 to 6f65f0631e5a (3 revisions) (flutter/engine#37941) (flutter/flutter#116133) * b22ab5117 Reland Cupertino text input padding (flutter/flutter#115164) * ccc277c38 Fix LayoutExplorer cycle (flutter/flutter#115526) * 202891cbe Roll Flutter Engine from e612c3d75676 to f326b630dd3a (2 revisions) (flutter/flutter#116140) * 9fba4296e Tiny code cleanup: remove unnecessary comparisons (flutter/flutter#114488) * db631f149 bd1400ef6 Roll ICU from da0744861976 to 1b7d391f0528 (4 revisions) (flutter/engine#37945) (flutter/flutter#116143) * 0cb9f7046 Menu bar accelerators (flutter/flutter#114852) * 9e2a27112 8417e2f52 [fuchsia] arm64 Dart runner (flutter/engine#37399) (flutter/flutter#116147) * 4c3b642f2 372ec3259 Roll Skia from 4a4cfedd1c20 to ddddafb88280 (2 revisions) (flutter/engine#37947) (flutter/flutter#116151) * f777c9f65 [flutter_tools] use absolute path for shader lib (flutter/flutter#116123) * dfa3d3332 [devicelab] track performance of animated image filter (flutter/flutter#115850) * 8535e716d ab8f921c4 Roll Fuchsia Mac SDK from CUPWWG1rEmonxuLpv... to 7NnCHy_b8ZWxdAtEU... (flutter/engine#37949) (flutter/flutter#116152) * d6995aa24 Ignore NullThrownError deprecation (flutter/flutter#116135) * 39a73cabe Add Escaping Option for ICU MessageFormat Syntax (flutter/flutter#116137) * a2fd68809 cc2f55d74 [Impeller] Cleanup shader generation and specify min macOS version. (flutter/engine#37952) (flutter/flutter#116158) * 61376de9b Generate local metadata even when not publishing. (flutter/flutter#116087) * 1c6526240 9fbd5bf4a [impellerc] speculative fix for include errors with impellerc shader lib (flutter/engine#37939) (flutter/flutter#116161) * 853b3080e ee8023432 Roll Dart SDK from eddf73d66119 to 962cd6e0d20a (1 revision) (flutter/engine#37954) (flutter/flutter#116166) * f8745c596 bc51ab52d Roll Skia from ddddafb88280 to 38cdadb76f51 (3 revisions) (flutter/engine#37956) (flutter/flutter#116171) * e66968367 46e9afaf1 Roll Skia from 38cdadb76f51 to d16a6bdb9542 (3 revisions) (flutter/engine#37957) (flutter/flutter#116172) * 24db45e79 Disable backspace/delete handling on iOS & macOS (flutter/flutter#115900) * 17c1dbc47 7c4b01fe9 Roll Skia from d16a6bdb9542 to 514203395396 (1 revision) (flutter/engine#37958) (flutter/flutter#116178) * [in_app_purchase_storekit] Add support for macOS (#6517) * Initial commit of adding MacOS support. Heavily inspired by https://github.com/flutter/plugins/pull/5854/ * Updated version and changelog. * Updated documentation. * Added missing license comments. * Fixed podspec lint issue. * Moved native tests to a shared location. * Decreased minimum macOS version from 10.15 to 10.11. This seems wrong to me, since StoreKit is only properly supported on MacOS 10.15+. However, now all existing tests should pass. * Added RunnerTests target to macos example. * Unified macOS capitalization. * Deleted generated macOS icon assets. * Removed empty function overrides. * Enabled tests for macOS. * Added OCMock to RunnerTests target. * Adapted tests for macOS. * Reverted relative path dependencies. * Updated to a proper version bump. * Make macOS no-op tests run only on iOS. * Replace directory symlinks with file symlinks. * Marked iOS-only API as iOS-only. Previously they were no-ops on macOS. * Re-worded doc-comment. * Formatted code. * Roll Flutter from 17c1dbc47378 to b2672fe8355f (26 revisions) (#6766) * d58855c49 Update SnackBar to support Material 3 (flutter/flutter#115750) * 8b32ac7a5 Revert "Update SnackBar to support Material 3" (flutter/flutter#116199) * 3219da9b5 Use Isolate.run as implementation for compute (flutter/flutter#115779) * 33f3c5350 Roll Flutter Engine from 7c4b01fe9a1f to e9dc20ed05c9 (2 revisions) (flutter/flutter#116197) * c37c0cc2e fbf31015f [gn] embed mac codesign metadata in android artifacts (flutter/engine#37951) (flutter/flutter#116209) * e5e5e6854 05e2de7e3 Build platform dills with unevaluated constants (flutter/engine#37940) (flutter/flutter#116215) * 7966d5584 Roll Flutter Engine from 05e2de7e3e67 to d85707af0987 (2 revisions) (flutter/flutter#116217) * 1cb16a1e3 iOS 16 context menu (flutter/flutter#115805) * 8b86d238b Create `DropdownMenu` Widget to Support Material 3 (flutter/flutter#116088) * 900b39545 Add Material 3 support for `TabBar` (flutter/flutter#116110) * e438a1205 [tools]build ipa validate app icon size (flutter/flutter#115594) * 50f101acd 3956f6d02 Roll Skia from 829527b29b32 to 6d759de2e5b2 (2 revisions) (flutter/engine#37967) (flutter/flutter#116228) * a9c2f8b9e Add onFocusChange property for ListTile widget (flutter/flutter#111498) * 9532b91c7 [flutter_tools] normalize windows file path cases in flutter validator (flutter/flutter#115889) * 6b98f2ca4 labeledTapTargetGuideline should passe if textfield does not have label (flutter/flutter#116221) * fa063eb4c 25a2ac85f [Impeller Scene] Wire up pipelines (flutter/engine#37961) (flutter/flutter#116233) * afda8153f Adjust Material 3 textfield padding to align with specs (flutter/flutter#116225) * 322dd06d6 Updated the M3 textTheme to use `onSurface` color for all styles. (flutter/flutter#116125) * 333397a0e Fix Material 3 `BottomSheet` example (flutter/flutter#116112) * 8473da22c Fix `Slider` semantic node size (flutter/flutter#115285) * 31ea315c3 Add offline docs, up the h2 size (flutter/flutter#116241) * b30eb6c54 Updated `useMaterial3` documentation to include missing M3 components. (flutter/flutter#116234) * 06979d4e2 shrinkWrap nuke (flutter/flutter#116236) * db8ded7a5 Roll Flutter Engine from 25a2ac85f6aa to e20362343564 (2 revisions) (flutter/flutter#116243) * 02de12947 Roll Flutter Engine from e20362343564 to d5690468da0f (2 revisions) (flutter/flutter#116244) * b2672fe83 Revert "Add Material 3 support for `TabBar` (#116110)" (flutter/flutter#116273) * enable test (#5312) * Revert "enable test (#5312)" (#6782) This reverts commit 89ad5a9d6a337ab63ee8c122a59b9302f454a40f. * Roll Flutter from b2672fe8355f to 33f3e13dfff6 (72 revisions) (#6787) * ef999051d Roll Flutter Engine from d5690468da0f to 1bda5f8c094d (5 revisions) (flutter/flutter#116290) * a52293843 [Reland] Add Material 3 support for `TabBar` (flutter/flutter#116283) * c461f4f66 0f17acdaf Roll Skia from 8f4f340f830c to bd9a7f3485b4 (3 revisions) (flutter/engine#37978) (flutter/flutter#116291) * 24b3c384c add debug trace when compiling dart2js (flutter/flutter#116238) * 29422d25f M3 snackbar [re-land] (flutter/flutter#116218) * d2af13457 Revert "Fix `Slider` semantic node size (#115285)" (flutter/flutter#116294) * 71139099c e00aeef24 Roll Fuchsia Mac SDK from ECusE22sNK6IbnL6L... to C4DamROwkxoF0YXyS... (flutter/engine#37983) (flutter/flutter#116295) * fcc8ea117 ff3a7789f ubuntu version (flutter/engine#37948) (flutter/flutter#116300) * a29796e33 [flutter_tools] Forward app.webLaunchUrl event from Flutter to DAP clients (flutter/flutter#116275) * 2ef2cc89e [flutter_tools] add deprecation message for "flutter format" (flutter/flutter#116145) * e62b6e799 Track entire web build directory size in web_size__compile_test (flutter/flutter#115682) * 3b89c981b Roll Flutter Engine from ff3a7789f45a to d8f151e24fda (2 revisions) (flutter/flutter#116301) * 0d0febc94 Add release_build parameter (flutter/flutter#116307) * 43d5cbb15 Roll Flutter Engine from d8f151e24fda to f8dc855ddb40 (2 revisions) (flutter/flutter#116305) * 7802c7acd [gen_l10n] Improvements to `gen_l10n` (flutter/flutter#116202) * 97195d1d5 Update CupertinoContextMenu to iOS 16 visuals (flutter/flutter#110616) * 762348630 be4c7e295 Roll Skia from d378c7817d8a to 2a75bac61922 (2 revisions) (flutter/engine#37989) (flutter/flutter#116309) * aaa4a5283 Add Material 3 `Slider` example (flutter/flutter#115638) * e5e21c983 Roll Flutter Engine from be4c7e2955c8 to 99b000e1e111 (2 revisions) (flutter/flutter#116310) * 6bb412e35 Added `controller` and `onSelected` properties to DropdownMenu (flutter/flutter#116259) * e93532748 3c1de6882 Fix typo on avoid_backing_store_cache param doc (flutter/engine#37985) (flutter/flutter#116313) * 014b441dd Revert "iOS 16 context menu (#115805)" (flutter/flutter#116312) * 4878f26ab 441229a95 Roll Skia from 2a75bac61922 to 767175530c11 (4 revisions) (flutter/engine#37994) (flutter/flutter#116318) * 7572ce907 51356bf89 [Impeller] Implement 'ui.Image.toByteData()' (flutter/engine#37709) (flutter/flutter#116321) * 8a806d675 677549602 Roll Skia from 767175530c11 to 4418e5468ee2 (3 revisions) (flutter/engine#37999) (flutter/flutter#116335) * 6e8ebb377 Reland "Upgrade targetSdkVersion and compileSdkVersion to 33" (flutter/flutter#116146) * 415545397 Roll Plugins from a431b35bfa0b to 89ad5a9d6a33 (7 revisions) (flutter/flutter#116356) * 929003e49 52054a327 Roll Fuchsia Mac SDK from C4DamROwkxoF0YXyS... to hODX8Qi_7J5kwKp4S... (flutter/engine#38002) (flutter/flutter#116354) * e86d0b3b8 31f099c21 Roll Skia from 4418e5468ee2 to 06801a30fc86 (1 revision) (flutter/engine#38003) (flutter/flutter#116361) * 458f129b8 d75b7ce37 Roll Skia from 06801a30fc86 to bfc7c3a83dc0 (4 revisions) (flutter/engine#38004) (flutter/flutter#116365) * 49f598097 Suggest Rosetta when x64 binary cannot be run (flutter/flutter#114558) * 3b15d6a50 Removes retries from "dart pub get" and un-buffers its stdout/stderr output (flutter/flutter#115801) * 6bd5e4798 303e26e96 [Impeller] Use glyph bounds to compute the max glyph size instead of font metrics (flutter/engine#37998) (flutter/flutter#116372) * 0bb71df75 Add clarification to CupertinoUserInterfaceLevel docs (flutter/flutter#116371) * 10c049da7 ec5de9b81 Roll Skia from bfc7c3a83dc0 to 6f6793b298ff (4 revisions) (flutter/engine#38007) (flutter/flutter#116378) * 75f61903e [flutter_tools] disable web compilation (flutter/flutter#116368) * f6224f368 [gen_l10n] keys can contain dollar sign (flutter/flutter#114808) * 4e8dacac8 Bump github/codeql-action from 2.1.32 to 2.1.35 (flutter/flutter#116379) * 5b8e36023 Roll Flutter Engine from ec5de9b810ef to abb68f40be70 (3 revisions) (flutter/flutter#116384) * b02a9c241 Roll Flutter Engine from abb68f40be70 to 1603fa1bb412 (2 revisions) (flutter/flutter#116387) * 0234b18f8 Tweak directional focus traversal (flutter/flutter#116230) * be81e9eae [tools]build ipa validate launch image using template files (flutter/flutter#116242) * 94e11624a 9463b5d19 Made responses to platform methods threadsafe in linux (flutter/engine#37689) (flutter/flutter#116397) * 9497dd8f5 025aefc7a Update Firefox to 106.0 (flutter/engine#38019) (flutter/flutter#116399) * ce9423028 [flutter_tools] Pin path_provider_android and roll pub packages (flutter/flutter#116377) * 7c0f882a9 Revert "[flutter_tools] Pin path_provider_android and roll pub packages (#116377)" (flutter/flutter#116424) * 748212afc 4c72d5c82 [Impeller] Add rect cutout (flutter/engine#38020) (flutter/flutter#116401) * 4a55eefa3 [conductor] Add instructions to update oncall logs (flutter/flutter#115871) * 5c9754302 Roll Flutter Engine from 4c72d5c82152 to 8e0747cc306f (7 revisions) (flutter/flutter#116426) * e065c7fea [framework] make ImageFiltered a repaint boundary (flutter/flutter#116385) * a1b8dc9ea Marks Linux_android animated_complex_image_filtered_perf__e2e_summary to be unflaky (flutter/flutter#116423) * e59a388b9 Roll Flutter Engine from 8e0747cc306f to 6bbf3e0e3c8b (2 revisions) (flutter/flutter#116432) * 22cbef305 [CP] Fix Snackbar TalkBack regression (flutter/flutter#116417) * 162be5933 [tools]build IPA validation bundle identifier using the default "com.example" prefix (flutter/flutter#116430) * 4643f833d a8a08d107 [web] Remove outdated information in web_ui/README (flutter/engine#38006) (flutter/flutter#116435) * 08a2635e2 [devicelab] add benchmark for complex non-intersecting widgets with platform views (flutter/flutter#116436) * db1c3e208 Platform binaries reland (flutter/flutter#115502) * 7751ec0fd 98b947a54 Fix typo in Animator comment (flutter/engine#38040) (flutter/flutter#116438) * c3e561236 Zip api docs using the repo under test script. (flutter/flutter#116437) * b75f1a941 afdc32933 Roll Fuchsia Mac SDK from aHgLxcRDjOQNKL7zH... to BDTULRXL5gDEHXmRA... (flutter/engine#38043) (flutter/flutter#116441) * dd5f96bfd Roll Flutter Engine from afdc32933be7 to f7df812d26b7 (5 revisions) (flutter/flutter#116447) * f36874cb8 6d0ff37a4 Documentation and other cleanup in dart:ui, plus a small performance improvement. (flutter/engine#38047) (flutter/flutter#116449) * 934e69008 Add widget of the week videos (flutter/flutter#116451) * e59081887 d746877f7 [web] use a permanent live region for a11y announcements (flutter/engine#38015) (flutter/flutter#116452) * 0f462d305 7a35191aa Roll Skia from 6fdf7181e374 to d0e3902c97b3 (6 revisions) (flutter/engine#38051) (flutter/flutter#116455) * 73822f82c f9067ed20 [Impeller Scene] Rename mesh importer to scenec (flutter/engine#38049) (flutter/flutter#116458) * 599ceb999 cf05ab426 [embedder] Ensure FlutterMetalTexture cleanup call (flutter/engine#38038) (flutter/flutter#116460) * a85222ea5 9b484713f Roll Fuchsia Mac SDK from BDTULRXL5gDEHXmRA... to w333oMghC5jK9C-YE... (flutter/engine#38054) (flutter/flutter#116465) * 9a7d8a1a9 b3e86c31c [Impeller] Make perspective transform resolve to left handed clip space (flutter/engine#38052) (flutter/flutter#116467) * 6e5582da9 662c5f8df [Impeller Scene] Wire up camera (flutter/engine#38053) (flutter/flutter#116468) * 964422c0d e10bf9c2c [Impeller] Add Quaternion to Matrix conversion (flutter/engine#38056) (flutter/flutter#116472) * 33f3e13df 5ac98f8a7 Roll Fuchsia Mac SDK from w333oMghC5jK9C-YE... to N9nk_ceXcPxQEjGEL... (flutter/engine#38057) (flutter/flutter#116475) * [camera] Handle empty grantResults on permission request (#6758) * [camera] Handle empty grantResults on permission request * update changelog and add test * fix typo * format * Update packages/camera/camera_android/CHANGELOG.md Co-authored-by: Camille Simon <43054281+camsim99@users.noreply.github.com> Co-authored-by: Camille Simon <43054281+camsim99@users.noreply.github.com> * Roll Flutter from 33f3e13dfff6 to 30fc993caed5 (6 revisions) (#6791) * [gh_actions]: Bump github/codeql-action from 2.1.27 to 2.1.35 (#6788) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2.1.27 to 2.1.35. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/807578363a7869ca324a79039e6db9c843e0e100...b2a92eb56d8cb930006a1c6ed86b0782dd8a4297) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [camera] Add ability to concurrently record and stream video (#6290) * Implement interface methods to allow concurrent stream and record There will be a subsequent change to the `camera` package to make use of these implementations. * Fix android test * Format android_camera_test.dart * Resolve analyze failures * Fix version bumps * Fix MethodChannelCameraTest * Fix comment on FLTCam * Add tests to confirm can't stream on windows or web * CHANGELOG updates * Fix analyze errors * Fix dart analyze warnings for web * Formatted * Revert "[camera] Add ability to concurrently record and stream video (#6290)" (#6796) This reverts commit 374e59804a6c544873209b3da30ea207f9d6aff4. * [ci] Fix macOS LUCI merge base (#6798) Currently the repo tooling relies on `FETCH_HEAD` being set to `origin/main` in CI, which is done in `prepare_tool.sh` for LUCI bots. Longer term we should restructure the CI scripts to be explicit about the base SHA instead of relying on `FETCH_HEAD`, but for now this adds the missing `prepare_tool.sh` step to the new iOS LUCI tests so that they compute diffs correctly. Fixes https://github.com/flutter/flutter/issues/116448 * Roll Flutter from 30fc993caed5 to e2fb672a440e (25 revisions) (#6802) * d52e2de5b 878fc6fc0 Roll Fuchsia Mac SDK from 1ZS93HM4ImgmL2EPK... to SDbR-S_A_fv-v_Sbb... (flutter/engine#38069) (flutter/flutter#116524) * 56cad89b1 Speed up first asset load by encoding asset manifest in binary rather than JSON (flutter/flutter#113637) * e54e73ab5 Roll Flutter Engine from 878fc6fc022e to b4de17db2552 (2 revisions) (flutter/flutter#116535) * 06e7c7a61 Incrementally update gradle to AGP 7.2.0 and 7.3.3 in some `integration_tests` (flutter/flutter#116201) * 27281dab8 [flutter_tools] dont include material shaders in web builds (flutter/flutter#116538) * 49d0b5b39 a14d15f07 Roll Dart SDK from 6b7e44ae494b to 52599799b666 (19 revisions) (flutter/engine#38076) (flutter/flutter#116542) * 05c6df6d1 Improve Flex layout comment (flutter/flutter#116004) * 55bcb784a Do not parse stack traces in _findResponsibleMethod on Web platforms that use a different format (flutter/flutter#115500) * 5d042eb35 Bump dessant/lock-threads from 3.0.0 to 4.0.0 (flutter/flutter#116545) * 9dc5b9e29 Roll Flutter Engine from a14d15f07a1a to e2b1919a7596 (2 revisions) (flutter/flutter#116547) * 1b05653ab use deploy suffix (flutter/flutter#116533) * 0a8e92a11 d82c8ad7a Bump buildroot (flutter/engine#38062) (flutter/flutter#116550) * e0a0190c5 Roll Flutter Engine from d82c8ad7ac7f to 0efb0ef40513 (2 revisions) (flutter/flutter#116552) * 174d3be86 a309d239c [Impeller Scene] Parse GLTF primitives (flutter/engine#38064) (flutter/flutter#116556) * 520185680 Use file:/// style uris when passing platform to the compiler. (flutter/flutter#116553) * c834b1d60 012826e19 Roll Skia from e9c0d4b83ca4 to ad85f404b97d (3 revisions) (flutter/engine#38089) (flutter/flutter#116564) * eaf625448 5d8530f79 Roll Fuchsia Mac SDK from SDbR-S_A_fv-v_Sbb... to 8p38Xk7Z7OLI7OA7R... (flutter/engine#38090) (flutter/flutter#116566) * 5993a613f Roll Flutter Engine from 5d8530f79009 to a588dbe8b3c5 (2 revisions) (flutter/flutter#116571) * 921f07768 6d05ea479 Roll Skia from ad85f404b97d to e2244ea470c0 (5 revisions) (flutter/engine#38096) (flutter/flutter#116579) * 1e696d304 Support theming `CupertinoSwitch`s (flutter/flutter#116510) * b78323c19 Roll Plugins from 2a330bc0afd5 to 374e59804a6c (4 revisions) (flutter/flutter#116591) * 09c921c34 ec211d21e Roll Skia from e2244ea470c0 to b63a254727f3 (1 revision) (flutter/engine#38098) (flutter/flutter#116592) * 09d489391 e6e6a0210 implement targetWidth and targetHeight (flutter/engine#38028) (flutter/flutter#116598) * a8b36c7da Fix windows version validator under Chinese (flutter/flutter#116282) * e2fb672a4 Roll Flutter Engine from e6e6a02106c6 to 86d7cbf09e09 (2 revisions) (flutter/flutter#116601) * [google_maps_flutter] Add support to request map renderer for Android (#6619) * [google_maps_flutter] support to request a specific map renderer for android * [google_maps_flutter] Minor fixes to comments and error messages * [google_maps_flutter] fix linting issues * Roll Flutter from e2fb672a440e to a570fd25d83b (15 revisions) (#6804) * 21f3ce8b6 [gen_l10n] Multiline descriptions (flutter/flutter#116380) * cd87e094e Allow creating packages for master/main. (flutter/flutter#116557) * ee8ba7010 12ad3d766 Disable an extension in Xvfb to work around errors seen when running Impeller/Vulkan unit tests (flutter/engine#38092) (flutter/flutter#116604) * 98d241387 4ff8cc3ef Add gradle option to allow/show System.out.print logs (flutter/engine#38104) (flutter/flutter#116612) * 76f03359b Update dartdoc to 6.1.3 (flutter/flutter#116474) * 577a88b22 Fix MenuAnchor padding (flutter/flutter#116573) * 30c575140 [Android] Refactor the `flutter run` Android console output test (flutter/flutter#115023) * 91568cc9b Adjust upper Dart SDK constraint (flutter/flutter#116586) * 676229f33 Roll Flutter Engine from 4ff8cc3ef53b to 4f22c2789023 (3 revisions) (flutter/flutter#116620) * 609fe35f1 [Android] Fix Linux Android flavors_test (flutter/flutter#116129) * fb9133b88 Add ListenableBuilder with examples (flutter/flutter#116543) * 31719941c Time picker precursors (flutter/flutter#116450) * 8bce55d1e Roll Flutter Engine from 4f22c2789023 to 185e2f3d451c (3 revisions) (flutter/flutter#116632) * ef6ead440 Roll Flutter Engine from 185e2f3d451c to 67254d6e4b03 (2 revisions) (flutter/flutter#116633) * a570fd25d Date picker special labeling for currentDate with localization and te… (flutter/flutter#116433) * [google_maps_flutter] Roll cupertino_icons for compatibility with Dart 3 (#6807) * [camera] Re-enable ability to concurrently record and stream video (#6808) * Re-enable stream and record This re-commits the content from https://github.com/flutter/plugins/pull/6290. Will make a subsequent commit to try and fix the broken integ tests. * Fix broken integration test for streaming The `whenComplete` call was sometimes causing a race condition. It is also isn't needed for the test (to reset the value of isDetecting, given it's local), so removing it makes the test reliable. * Trivial CHANGELOG change to trigger full CI tests Co-authored-by: stuartmorgan * [video_player] Fix file URI construction (#6803) * Fix bug in file constructor * Fix comment in interface package * Fix minicontroller for examples/tests/ * Version bumps * Skip file tests on web * Don't use file source in non-file tests, since web doesn't support it * Roll Flutter from a570fd25d83b to 521028c80827 (11 revisions) (#6810) * e07240e68 Update docs_deploy builder with the real name. (flutter/flutter#116631) * 182f9f666 Roll Plugins from 374e59804a6c to 7b5d8323efe3 (3 revisions) (flutter/flutter#116660) * 352ad3a9e Adds API in semanticsconfiguration to decide how to merge child semanticsConfigurations (flutter/flutter#110730) * 7673108d7 Revert "Speed up first asset load by encoding asset manifest in binary rather than JSON (#113637)" (flutter/flutter#116662) * 68ce1aeae Reland "Use semantics label for backbutton and closebutton for Android" (flutter/flutter#115776) * cc256c3e3 Revert "Use semantics label for backbutton and closebutton for Android" (flutter/flutter#116675) * 297f094c0 LookupBoundary (flutter/flutter#116429) * 7b458e5af Implement Linux part of examples (flutter/flutter#108068) * 3f1fb1b96 Update assets_for_android_views git dependency pins (flutter/flutter#116639) * 8b80552a3 Fix language version check logic to determine nullsafe soundness. (flutter/flutter#116679) * 521028c80 Reland "Use semantics label for backbutton and closebutton for Android" (flutter/flutter#116676) * Roll Flutter from 521028c80827 to eefbe85c8bd4 (10 revisions) (#6820) * [tools] Recognize Pigeon tests in version-check (#6813) Pigeon has an usual test structure since it generates test code to run in a dummy plugin; add that structure to the list of recognized tests so that changes to its platform tests won't be flagged by `version-check`. * [camera] Attempt to fix flaky new Android test (#6831) The recently added "recording with image stream" test is very flaky, often throwing on `stop`. This is a speculative fix for that flake based on the documentation of `stop` indicating that it will throw if nothing has been recorded. * [google_maps_flutter] Modified `README.md` to fix minor syntax issues (#6631) * Refactored Reaadme using code excerpts * Fixes * Updated Upstream and Fixed Test Errors * Re-added temp_exclude_excerpt.yaml back due to Failing Test * Restore deleted changelog entries Co-authored-by: stuartmorgan * Roll Flutter from eefbe85c8bd4 to bd0791be3ff2 (25 revisions) (#6832) * 48cfe2eb0 Opt dashing_postprocess.dart out of null safety until we figure out why (flutter/flutter#116786) * b4304dadc Update the Dart language version in the pubspec generated by the dartdoc script (flutter/flutter#116789) * 55e750115 Roll Plugins from 51434ec83dde to 6ab7d710d2fb (3 revisions) (flutter/flutter#116781) * e57b7f4ea Add Material 3 support for `ListTile` - Part 1 (flutter/flutter#116194) * 73cb7c2fc Squashed MediaQuery InheritedModel (flutter/flutter#114459) * 1da8f4edc Several fixes to packaging builders. (flutter/flutter#116800) * 86fa9e511 Roll Flutter Engine from 8d83b98c55b3 to 030950f3070c (29 revisions) (flutter/flutter#116802) * ca3ce3945 89fd33c62 Don't use sync*, as it is unimplemented in dart2wasm. (flutter/engine#38149) (flutter/flutter#116808) * 332032dda Roll Flutter Engine from 89fd33c62f2c to a259613ab871 (2 revisions) (flutter/flutter#116811) * 9dd30878d Add LookupBoundary to Material (flutter/flutter#116736) * cbdc763cf Roll Flutter Engine from a259613ab871 to 8b56b5a98ed4 (2 revisions) (flutter/flutter#116813) * c4b8046d9 Floating cursor cleanup (flutter/flutter#116746) * 7d7848aba d64a5129a [const_finder] Ignore constructor invocations from generated tear-off declarations (flutter/engine#38131) (flutter/flutter#116814) * be5c389e6 faae28965 Roll Skia from 44062eff3e25 to 1b194c67700e (2 revisions) (flutter/engine#38166) (flutter/flutter#116817) * 7549925c8 Revert "Adds API in semanticsconfiguration to decide how to merge child semanticsConfigurations (#110730)" (flutter/flutter#116839) * 68f02dd2e Roll Flutter Engine from faae28965a94 to fbb79e704b0a (6 revisions) (flutter/flutter#116843) * ec02f3bfb 656b67796 Roll Dart SDK from 0940b5e6ccd5 to 21f2997a8fc6 (9 revisions) (flutter/engine#38172) (flutter/flutter#116844) * c02d53fc0 More gracefully handle license loading failures (flutter/flutter#87841) * 4a1511166 0795bccae Roll Skia from 0d482f9fa8b3 to 80d9e679f909 (2 revisions) (flutter/engine#38195) (flutter/flutter#116853) * 1fc166a51 3ca497ebb Roll Skia from 80d9e679f909 to 29791c73ae16 (1 revision) (flutter/engine#38200) (flutter/flutter#116859) * 9fdb64b7e Taboo the word "simply" from our API documentation. (flutter/flutter#116061) * 92aebc953 922546c91 [Impeller] Fix asset names used for the generated entrypoint name can contain invalid identifiers for the target language (flutter/engine#38202) (flutter/flutter#116868) * 437f6f86e 9e37c9883 Roll Skia from 29791c73ae16 to 7bd37737e35d (1 revision) (flutter/engine#38207) (flutter/flutter#116871) * d19f77674 62a5de2ef Roll Skia from 7bd37737e35d to 0cb546781e89 (4 revisions) (flutter/engine#38213) (flutter/flutter#116880) * bd0791be3 Pass drone_dimensions as part of the main target. (flutter/flutter#116812) * Reland "[google_maps_flutter] ios: re-enable test with popup #5312" (#6783) * reland fix fix test for iOS 16 fix fix typos * format * update changelog * Update FlutterFire link (#6835) * Roll Flutter from bd0791be3ff2 to 15af81782e19 (27 revisions) (#6837) * d1436d1df Roll Plugins from 6ab7d710d2fb to 0609adb457fd (2 revisions) (flutter/flutter#116891) * 84ed058b4 [flutter_tools] Add remap sampler support (flutter/flutter#116861) * a8c9f9c6f Fix `NavigationBar` ripple for non-default `NavigationDestinationLabelBehavior` (flutter/flutter#116888) * 5a229e282 Add LookupBoundary to Overlay (flutter/flutter#116741) * d19047d8a [framework] make opacity widget create a repaint boundary (flutter/flutter#116788) * 882e105a4 Revert "Add Material 3 support for `ListTile` - Part 1 (#116194)" (flutter/flutter#116908) * 7a743c881 [flutter_tools] Pin and roll pub (flutter/flutter#116745) * 558b7e004 Adjust test to tolerate additional trace fields (flutter/flutter#116914) * c420562ef Fix output match (flutter/flutter#116912) * 8e1f8352b Fix MediaQuery.paddingOf (flutter/flutter#116858) * 8cfc6061e Roll Flutter Engine from 62a5de2efc9c to 2148fc003077 (5 revisions) (flutter/flutter#116920) * 601f48cd9 InteractiveViewer discrete trackpad panning (flutter/flutter#112171) * 41625b662 Test flutter run does not have unexpected engine logs (flutter/flutter#116798) * 9b46f2a69 ff2fe8381 [cpp20] Fix incompatible aggregate initialization (flutter/engine#38165) (flutter/flutter#116927) * 15939b477 Remove duped fix rules (flutter/flutter#116933) * e331dcda1 [framework] make transform with filterQuality a rpb (flutter/flutter#116792) * 6432fd1b1 6c190ea1e Roll Skia from bb9378b61c4f to 788fe69e7ade (6 revisions) (flutter/engine#38226) (flutter/flutter#116935) * 97df2b319 Fix scroll jump when NestedScrollPosition is inertia-cancelled. (flutter/flutter#116689) * ca7fe3348 Roll Flutter Engine from 6c190ea1e8df to e144f81e92d8 (3 revisions) (flutter/flutter#116939) * b713edcee 290f3a35e Roll Skia from 788fe69e7ade to 2e417d4f7993 (3 revisions) (flutter/engine#38235) (flutter/flutter#116941) * 04ee5926a Remove RenderEditable textPainter height hack (flutter/flutter#113301) * 7211ca09d a33e699de Roll Skia from 2e417d4f7993 to 08dc0c9e4e70 (1 revision) (flutter/engine#38239) (flutter/flutter#116952) * f5249bcb0 Remove use of NullThrownError (flutter/flutter#116122) * 9ccfb87ad 64a661d6d Roll Skia from 08dc0c9e4e70 to 9abf4b1bf242 (4 revisions) (flutter/engine#38240) (flutter/flutter#116955) * 256d54e17 47417ce80 [Impeller Scene] Node deserialization (flutter/engine#38190) (flutter/flutter#116959) * db26f486e afc2f9559 Roll Skia from 9abf4b1bf242 to c83eef7dc2a3 (3 revisions) (flutter/engine#38243) (flutter/flutter#116970) * 15af81782 74a9c7e0f Roll Fuchsia Mac SDK from aMW0DjntzFJj4RoR3... to Cd_ZtrDVcpQ85HRL3... (flutter/engine#38242) (flutter/flutter#116973) * [local_auth]: Bump fragment from 1.5.4 to 1.5.5 in /packages/local_auth/local_auth_android/android (#6826) * [local_auth]: Bump fragment Bumps fragment from 1.5.4 to 1.5.5. --- updated-dependencies: - dependency-name: androidx.fragment:fragment dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Version bump * Empty-Commit * Empty-Commit Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: camsim99 * Roll Flutter from 15af81782e19 to 028c6e29e0ca (13 revisions) (#6843) * dbb9aa8b9 Roll Plugins from 0609adb457fd to ec2041f82584 (6 revisions) (flutter/flutter#116996) * cd9a439c3 Roll Flutter Engine from 74a9c7e0f9b1 to 9872cc7addce (2 revisions) (flutter/flutter#116997) * 4e1372080 dd1dc7258 Always set orientation preferences on iOS 16+ (flutter/engine#38230) (flutter/flutter#117005) * 51945a0df Roll Flutter Engine from dd1dc7258b99 to 82dafdfc1c3d (2 revisions) (flutter/flutter#117015) * 96597c25e Bump Dartdoc version to 6.1.5 (flutter/flutter#117014) * 0c7d84aa7 Add AppBar.forceMaterialTransparency (#101248) (flutter/flutter#116867) * a59dd83d7 a327b48ca Roll Skia from 280ac8882cff to 537e1e8c1ca6 (9 revisions) (flutter/engine#38264) (flutter/flutter#117025) * fae458b92 Convert TimePicker to Material 3 (flutter/flutter#116396) * ba917d615 Remove workaround because issue is closed (flutter/flutter#110854) * 60635be59 984ec305a Run Mac Host clang-tidy on 12 cores (flutter/engine#38261) (flutter/flutter#117028) * f0ea37646 Roll Flutter Engine from 984ec305a0dd to 14194c40ec00 (2 revisions) (flutter/flutter#117031) * f07db4018 `NavigationBar` improvements (flutter/flutter#116992) * 028c6e29e [Android] Fix the `run_debug_test_android` device lab test (flutter/flutter#117016) * [camera_android_camerax] `unnecessary_parenthesis` lint fix (#6841) * fixed `unnecessary_parenthesis` * fmt * [various] Enable avoid_print (#6842) * [various] Enable avoid_print Enables the `avoid_print` lint, and fixes violations (mostly by opting example files out of it). * Version bumps * Add tooling analysis option file that was accidentally omitted * Fix typo in analysis_options found by adding tool sub-options * Revert most version bumps * Fix ios_platform_images * [webview_flutter_platform_interface] Updates platform interface to new interface (#6846) * Set new interface in main * update pubspec changelog and readme * exclude from all plugins app test * delete all of platform interface * add all back to platform interface * Roll Flutter (stable) from b8f7f1f9869b to 135454af3247 (6 revisions) (#6850) * 199c4bf4c CP: ci.yaml changes for packaging (flutter/flutter#117038) * 8461df291 Add release_build parameter (#116307) (flutter/flutter#117088) * 1545b041b Zip api docs using the repo under test script. (#116437) (flutter/flutter#117093) * f3bc66195 [flutter_releases] Flutter stable 3.3.10 Framework Cherrypicks (flutter/flutter#117041) * 3778e3c76 Revert "CP: ci.yaml changes for packaging (#117038)" (flutter/flutter#117132) * 135454af3 CP: ci.yaml changes for packaging (flutter/flutter#117133) * [webview_flutter_android] Copies Android implementation of webview_flutter from v4_webview (#6851) * new android release * Version bump * update all plugins config * fix lints and add action item to changelog * [webview_flutter_wkwebview] Copies iOS implementation of webview_flutter from v4_webview (#6852) * new ios release * version bump * add breaking change change * update excludes file * fix lints and add to changelog * [local_auth] Fix failed biometric authentication not throwing error (#6821) * fix failed biometric authentication not throwing error + tests * fix test names * Revert "fix test names" This reverts commit 89ba69ccc33c37b98092a5fbfa5c71ebc23cd468. * Revert "fix failed biometric authentication not throwing error + tests" This reverts commit 684790a7c7756c963ac3aa3b6e4de48cc9b33df5. * fix authentication not throwing error + tests * fix test name * auto format * cr fixes * addressed pr comments * formatting * formatting * formatting * format attempt * format attempt * formatting fixes * change incorrect versionning * fix test * add back macro * fixed up tests, removed unnecessary assertions, replaced isAMemberOf * add back error for unknown error codes * tests * changed enum to something thats not deprecated * remove redundant test * [webview_flutter_web] Copies web implementation of webview_flutter from v4_webview (#6854) * v4 web impl * add breaking change * fix lints * exclude web plugin * [image_picker] Don't store null paths in lost cache (#6678) If the user cancels image selection on Android, store nothing in the lost image cache rather than storing an array with a null path. While we could potentially keep this behavior and instead handle it differently on the Dart side, returning some new "cancelled" `LostDataResponse`, that would be semi-breaking; e.g., the current example's lost data handling would actually throw as written if we had a new non-`isEmpty`, non-exception, null-`file` response. Since nobody has requested the ability to specifically detect a "lost cancel" as being different from not having started the process of picking anything, this doesn't make that potentially-client-breaking change. If it turns out there's a use case for that in the future, we can revisit that (but should not do it by storing a null entry in a file array anyway). Fixes https://github.com/flutter/flutter/issues/114551 * [webview_flutter_android] Fix timeouts in the integration tests (#6857) * fix timeouts * remove broadcast instead * just use completers like a lame person * [flutter_plugin_tools] If `clang-format` does not run, fall back to other executables in PATH (#6853) * If clang-format does not run, fall back to other executables in PATH * Review edits * [video_player] Add compatibility with the current platform interface (#6855) * Relax version constraints * Update app-facing * Version bumps * [image_picker] Improve image_picker for iOS to handle more image types (#6812) * Improve image picker for ios to handle more image types * Update release info * different svg, remove raw test * change pro raw image * change pro raw image * add error log * fix formatting * fix image identifiers in test * get image type identifier from itemProvider in test * [webview_flutter] Copies app-facing implementation of webview_flutter from v4_webview (#6856) * copy code from v4_webview * version bump and readme update * work towards better readme * improvements * more readme progress * improvements * fix main and update more readme * excerpt changes and more 3.0 diffs * cookie manager update * remove packages from exclude list * lint * better range * isForMainFrame * load page after waiting for widget * fix integration tests * improve readme a bit * collapse changelong. update platform-specific wording. include in excerpt tests * use platform implementation packages * include missing exports * PR comments * correct spelling * interface dev dependency * move other usage above migration * remove interface classes * [image_picker_ios] Pass through error message from image saving (#6858) * [image_picker_ios] Pass through error message from image saving * Review edits * Format * addObject * [local_auth] Bump `intl` from ^0.17.0 to ">=0.17.0 <0.19.0" (#6848) * Update intl from 0.17.0 to 0.18.0 * [local_auth] Bump `intl` from ^0.17.0 to ">=0.17.0 <0.19.0" * [local_auth] improve changelog description * [local_auth] improve changelog description * [local_auth] removes unused `intl` dependency * [gh_actions]: Bump github/codeql-action from 2.1.35 to 2.1.37 (#6860) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2.1.35 to 2.1.37. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/b2a92eb56d8cb930006a1c6ed86b0782dd8a4297...959cbb7472c4d4ad70cdfe6f4976053fe48ab394) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [camera] Remove deprecated Optional type (#6870) * Remove Optional * Undo accidental order change * Fix examples analyze * Remove unused import * Bump versions * Correct version * [in_app_purchase] Add support for macOS (#6519) * Updated version and changelog. * Updated readme to mention MacOS as a supported platform. * Minor fixup. * Fixed capitalization in readme. * Added macos to example. * Updated MacOS example. * Unified macOS capitalization. * Removed generated app icons. * Updated version and deployment target. * Updated version to a minor version change. Was previously only a patch. * Roll Flutter from 028c6e29e0ca to dbc9306380d8 (11 revisions) (#6849) * 8e452be2f Marks Windows run_release_test_windows to be unflaky (flutter/flutter#117071) * 0e0f29fc8 Run packaging builders only on beta and stable. (flutter/flutter#117037) * ef3fe6a05 10c029399 [local_auth]: Bump fragment from 1.5.4 to 1.5.5 in /packages/local_auth/local_auth_android/android (flutter/plugins#6826) (flutter/flutter#117078) * 7a801f766 0baa4b5b3 Generate font fallback data to be const. (flutter/engine#38259) (flutter/flutter#117080) * c63d797f9 Upgrade dependencies (flutter/flutter#117007) * 57fb36ee0 [reland] Add Material 3 support for `ListTile` - Part 1 (flutter/flutter#116963) * f9acb1e88 -- unnecessary parens (flutter/flutter#117081) * aa0b68119 flutter/engine@0baa4b5...0a6a4a58 (flutter/flutter#117083) * d8b7eb6e2 Updated token templates to sync with master code. (flutter/flutter#117097) * 7b19b4d38 Fix CupertinoTextSelectionToolbar showing unnecessary pagination (flutter/flutter#104587) * dbc930638 Failure to construct ErrorWidget for build errors does not destroy tree (flutter/flutter#117090) * [webview_flutter_wkwebview] Adds support for `WKNavigationAction.navigationType` (#6863) * add navigation type to navigation action * version bump and export * remove import * ios unit test * lint warning * formatting * add readme change to CHANGELOG * remove unused method * undo mocks change * last mocks change * mention pigeon dependency * [webview_flutter_android] Adds support for selecting Hybrid Composition (#6864) * add displayWithHybridComposition flag * move duplicate method * unit tests * adds * update docs * create platformviewsserviceproxy * use proxy for platformviewsservice * remove usused code * update display mode * use clever imports * [webview_flutter_android] Fixes bug where a `AndroidNavigationDelegate` was required to load a request (#6872) * fix bug * comment * another test * fix spelling * Roll Flutter from dbc9306380d8 to 9fb1ae839e0a (106 revisions) (#6876) * 9aa2ea150 Roll Flutter Engine from 0a6a4a58f4f7 to db5605ea7115 (11 revisions) (flutter/flutter#117109) * 409a39dae remove debugPrint from timePicker test (flutter/flutter#117111) * 169b49fba Revert "[framework] make transform with filterQuality a rpb (#116792)" (flutter/flutter#117095) * 47300e0a6 Roll Plugins from 10c029399b3a to 78de28ca21c7 (4 revisions) (flutter/flutter#117145) * dcd2170d1 Fix typos in scale gesture recognizer docs (flutter/flutter#117116) * fc3571eff Improve documentation of `compute()` function (flutter/flutter#116878) * b122200d4 Roll Flutter Engine from db5605ea7115 to 29196519c124 (13 revisions) (flutter/flutter#117148) * f1d157bc2 Add an integration test to plugin template example (flutter/flutter#117062) * ada446050 Audit `covariant` usage in tool (flutter/flutter#116930) * 1eaf5c0f0 [flutter_tools] tree shake icons from web builds (flutter/flutter#115886) * 91c1c70bd Bump ossf/scorecard-action from 2.0.6 to 2.1.0 (flutter/flutter#117170) * c98978ae3 Update Navigator updatePages() (flutter/flutter#116945) * 0916375f4 [tools]Build IPA validation UI Polish (flutter/flutter#116744) * a41c447c8 Pass dimension explicitly to mac x64 packaging. (flutter/flutter#117172) * 86b62a3c2 Tiny fix about outdated message (flutter/flutter#114391) * a34e41948 Inject current `FlutterView` into tree and make available via `View.of(context)` (flutter/flutter#116924) * c7cb5f3f5 [flutter_tools] pin package intl and roll pub packages (flutter/flutter#117168) * 7336312b0 Do not filter the stderr output of "flutter run" in the devicelab run tests (flutter/flutter#117188) * fa711f77e Run packaging on presubtmit. (flutter/flutter#116943) * 76bb8ead5 Reland "Fix text field label animation duration and curve" (#114646)" * 80e1008cb fix: #110342 unable to update rich text widget gesture recognizer (flutter/flutter#116849) * da7b8327e Bottom App Bar M3 background color fix (flutter/flutter#117082) * ab47fc304 Roll Plugins from 78de28ca21c7 to cbcf50726fb9 (3 revisions) (flutter/flutter#117213) * 23a2fa31d Reland "Adds API in semanticsconfiguration to decide how to merge chi… (flutter/flutter#116895) * 9102f2fe0 Revert "Inject current `FlutterView` into tree and make available via `View.of(context)` (#116924)" (flutter/flutter#117214) * 3d0607b54 Defer `systemFontsDidChange` to the transientCallbacks phase (flutter/flutter#117123) * ecf9b2d20 Update localization of shortcut labels in menus (flutter/flutter#116681) * 0604a0e1e Add a recursive flag to the zip command - currently it is zipping nothing (flutter/flutter#117227) * 98e9032ca [web] Allow shift + left/right keyboard shortcuts to be handled by framework on web (flutter/flutter#117217) * ebeb49189 Use the name of errors, not the diagnostic messages. (flutter/flutter#117229) * 93c581a72 Formatted and removed lints from devicelab README.md (flutter/flutter#117239) * 5018a6c1f Roll Flutter Engine from 29196519c124 to d91e20879a67 (29 revisions) (flutter/flutter#117242) * 36d536a32 Roll Flutter Engine from d91e20879a67 to 60cf34e2abf1 (4 revisions) (flutter/flutter#117246) * c9dd45847 42689eafb Sped up FlutterStandardCodec writing speed. (flutter/engine#38345) (flutter/flutter#117249) * 0a2a1d91c d45d375ae [iOS, macOS] Migrate from assert to FML_DCHECK (flutter/engine#38368) (flutter/flutter#117256) * bf5fdb9f9 Reland "Inject current FlutterView into tree and make available via `View.of(context)` (#116924)" (flutter/flutter#117244) * 427b2fb42 901b455d0 Roll Fuchsia Mac SDK from bn5VF1-xDf-wKjIw8... to qYE6uXjRtAxy7p5HB... (flutter/engine#38373) (flutter/flutter#117258) * cee3e6cc3 b10769998 Even though the file is pure C code, it's useful to use a C++ or Objective-C++ filename in order to use FML (assertions) in the implementation. (flutter/engine#38365) (flutter/flutter#117260) * 7b850ef37 f74dd5331 Roll Fuchsia Linux SDK from H6B0UgW07fc1nBtnc... to PqyqxdbUFyd8xoYIP... (flutter/engine#38377) (flutter/flutter#117262) * b20a9e0a3 imporve gesture recognizer semantics test cases (flutter/flutter#117257) * a82c556a1 3626c487a Add a missing include to display_list_matrix_clip_tracker.h (flutter/engine#38371) (flutter/flutter#117269) * 9daf2a67e Roll Flutter Engine from 3626c487a610 to 7e296985f426 (2 revisions) (flutter/flutter#117270) * d0d13c545 51b84d69b Roll Fuchsia Mac SDK from qYE6uXjRtAxy7p5HB... to qk9nUlw83EeMMaWmE... (flutter/engine#38380) (flutter/flutter#117273) * 725049f2b 794370b9c Roll Fuchsia Linux SDK from PqyqxdbUFyd8xoYIP... to bloqad357AGI6lnOb... (flutter/engine#38381) (flutter/flutter#117276) * 49f3ca400 eeae936f9 Use canvaskit `toByteData` for unsupported videoFrame formats (flutter/engine#38361) (flutter/flutter#117279) * c0dddacb8 Fix is canvas kit bool (flutter/flutter#116944) * d88d52405 276327f7e Roll Fuchsia Mac SDK from qk9nUlw83EeMMaWmE... to DdU--deE0Xl4TQ2Bm... (flutter/engine#38383) (flutter/flutter#117286) * b7d9be0a7 747a9d8c7 Roll Skia from 7b0a9d9a3008 to 0362c030efa7 (9 revisions) (flutter/engine#38385) (flutter/flutter#117289) * 1233fc979 37387019b Roll Fuchsia Linux SDK from bloqad357AGI6lnOb... to mRBUNknZk43y-LHGS... (flutter/engine#38386) (flutter/flutter#117290) * a3a0048d7 3c6cab032 Roll Fuchsia Mac SDK from DdU--deE0Xl4TQ2Bm... to NLb_T58g0l_X46JEN... (flutter/engine#38387) (flutter/flutter#117295) * 420c6d6cc Roll Flutter Engine from 3c6cab03274f to 58ab5277a7c4 (2 revisions) (flutter/flutter#117312) * d238bedf4 Roll Plugins from cbcf50726fb9 to 840a04954fa0 (8 revisions) (flutter/flutter#117314) * 3eefb7af0 a9491515f Roll Skia from 0362c030efa7 to fc0ac31a46f8 (4 revisions) (flutter/engine#38399) (flutter/flutter#117317) * 9f9010f5e [flutter_tools] Update DAP progress when waiting for Dart Debug extension connection (flutter/flutter#116892) * 32da25053 a12dadfda Roll Fuchsia Mac SDK from NLb_T58g0l_X46JEN... to NS4fVXM2KhKcZ1uyD... (flutter/engine#38400) (flutter/flutter#117319) * cb988c7b6 Add `indicatorColor` & `indicatorShape` to `NavigationRail`, `NavigationDrawer` and move these properties from destination to `NavigationBar` (flutter/flutter#117049) * 5fcb48d59 Fix `NavigationRail` highlight (flutter/flutter#117320) * 70f391db7 7bc519375 Roll Skia from fc0ac31a46f8 to 46af4ad25426 (1 revision) (flutter/engine#38403) (flutter/flutter#117322) * 9f2c5d8e2 Support `flutter build web --wasm` (flutter/flutter#117075) * 55584ad50 Roll Flutter Engine from 7bc519375b7b to 45713ea10510 (2 revisions) (flutter/flutter#117330) * 4daff0857 Roll Flutter Engine from 45713ea10510 to cba3a3990138 (5 revisions) (flutter/flutter#117336) * 1adc27503 Bump min SDK to 2.19.0-0 (flutter/flutter#117345) * efadc3458 Roll Flutter Engine from cba3a3990138 to 6de29d1cba70 (3 revisions) (flutter/flutter#117354) * b30947bef roll packages (flutter/flutter#117226) * e625e5f46 3330cce60 Roll Fuchsia Linux SDK from yGQvkNl85l1TSeuo9... to uKNwsaf92uZcX_QiY... (flutter/engine#38411) (flutter/flutter#117358) * 50a23d962 339791f19 Roll Skia from 8876daf17554 to e8c3fa6d7d2f (3 revisions) (flutter/engine#38413) (flutter/flutter#117366) * 7f7a8778d Implemented Scrim Focus for BottomSheet (flutter/flutter#116743) * 38e3930f3 Exposed tooltip longPress action when available (flutter/flutter#117338) * 61fb6ea2d Manual roll Flutter Engine from 339791f190fa to 7ee3bf518036 (1 revision) #117367 (flutter/flutter#117372) * c64dcbefa Revert "Manual roll Flutter Engine from 339791f190fa to 7ee3bf518036 (1 revision) #117367 (#117372)" (flutter/flutter#117396) * 8289ea624 Move a comment where it belongs (flutter/flutter#117385) * fa3777bd3 Enable `sized_box_shrink_expand` lint (flutter/flutter#117371) * e0742ebb2 [Android] Add spell check suggestions toolbar (flutter/flutter#114460) * 0220afdd3 enable use_enums (flutter/flutter#117376) * d71fa885e Bump ossf/scorecard-action from 2.1.0 to 2.1.1 (flutter/flutter#117337) * 4591f057f roll packages (flutter/flutter#117357) * 46bb85376 Revert "Revert "Manual roll Flutter Engine from 339791f190fa to 7ee3bf518036 (1 revision) #117367 (#117372)" (#117396)" (flutter/flutter#117402) * 81bc54be7 Enable `use_colored_box` lint (flutter/flutter#117370) * fdd2d7d64 Sync analysis_options.yaml & cleanups (flutter/flutter#117327) * de357647b [Android] Bump template AGP and NDK versions (flutter/flutter#116536) * b308555ed Enable `dangling_library_doc_comments` and `library_annotations` lints (flutter/flutter#117365) * b3c7fe32e enable test_ownership in presubmit (flutter/flutter#117414) * 014b8f735 Roll Flutter Engine from 7ee3bf518036 to 75d75575d0ea (12 revisions) (flutter/flutter#117421) * cd0f15a77 Add support for double tap and drag for text selection (flutter/flutter#109573) * e8e26b684 c7eae2901 [Impeller] Remove depth/stencil attachments from imgui pipeline (flutter/engine#38427) (flutter/flutter#117425) * a3e7fe3ff de59f842a Roll Dart SDK from 35f6108ef685 to 1530a824fd5f (6 revisions) (flutter/engine#38431) (flutter/flutter#117429) * 169935168 4724a91af Roll Skia from 09d796c0a728 to a60f3f6214d3 (5 revisions) (flutter/engine#38432) (flutter/flutter#117433) * 9024c9543 28f344ceb Roll Dart SDK from 1530a824fd5f to 8078926ca996 (1 revision) (flutter/engine#38434) (flutter/flutter#117435) * cae784649 c9ee05b68 use min/max sandwich test on unit test bounds (flutter/engine#38435) (flutter/flutter#117442) * 6819f72a9 Roll Flutter Engine from c9ee05b68e6e to 2404db80ae80 (3 revisions) (flutter/flutter#117443) * f5c071659 4910ff889 Roll Fuchsia Mac SDK from nJJfWIwH5zElheIX8... to UsYNZnnfR_s0OGQoX... (flutter/engine#38444) (flutter/flutter#117454) * a7a5d14d2 Roll Plugins from 840a04954fa0 to 54fc2066d636 (6 revisions) (flutter/flutter#117456) * 51a3e3a33 1e695f453 Roll Dart SDK from 778a29535ab5 to 62ea309071c6 (1 revision) (flutter/engine#38445) (flutter/flutter#117459) * d1244b7c9 da77d1a3a Roll Skia from 2e3ee507e838 to 7ad6f27aff57 (1 revision) (flutter/engine#38447) (flutter/flutter#117474) * 400b05ac0 Manual package roll (flutter/flutter#117439) * 9a347fb06 Support safe area and scrolling in the NavigationDrawer (flutter/flutter#116995) * 2a502363e Add native unit tests to iOS and macOS templates (flutter/flutter#117147) * 1970bc919 cacheWidth cacheHeight support for canvaskit on web (flutter/flutter#117423) * ff347bfde Fix `InkRipple` doesn't respect `rectCallback` when rendering ink circle (flutter/flutter#117395) * ddb7e43d7 Roll Flutter Engine from da77d1a3abb8 to 84ba80331ffe (2 revisions) (flutter/flutter#117489) * 8ff1b6eb5 Fix Scaffold bottomSheet null exceptions (flutter/flutter#117008) * 2931e50c3 Handle the case of no selection rects (flutter/flutter#117419) * 39fa0117a Revert "Add support for double tap and drag for text selection (#109573)" (flutter/flutter#117497) * b8b356713 Remove single-view assumption from widgets library (flutter/flutter#117480) * ca7ca3b8f Roll Flutter Engine from 84ba80331ffe to a90c45db3f13 (2 revisions) (flutter/flutter#117499) * 9fb1ae839 [iOS] Add task for spell check integration test (flutter/flutter#116222) * Roll Flutter from 9fb1ae839e0a to a45a2f311990 (19 revisions) (#6879) * 95ff83976 Roll Plugins from 54fc2066d636 to 2dd85ec81d06 (3 revisions) (flutter/flutter#117535) * b8d5d9c46 Revert "Remove single-view assumption from widgets library (#117480)" (flutter/flutter#117545) * bd482ebc5 fixes android_semantics_integration_test to expect long press for tooltip (flutter/flutter#117547) * 725c1415d Fix screenshot testing for flutter web integration_test (flutter/flutter#117114) * 999356b77 Remove single-view assumption from ScrollPhysics (flutter/flutter#117503) * 6eb002a16 Reland "Remove single-view assumption from widgets library (#117480)" (flutter/flutter#117549) * 08209b7e0 Explain how to test onSubmitted in its docs (flutter/flutter#117550) * 20bc2bac3 Roll Flutter Engine from a90c45db3f13 to 234ab4c1e9c4 (1 revision) (flutter/flutter#117555) * abd5217f4 Bump ossf/scorecard-action from 2.1.1 to 2.1.2 (flutter/flutter#117554) * 6781576e8 Reland iOS 16 context menu (flutter/flutter#117234) * daa2ecf16 Roll Flutter Engine from 234ab4c1e9c4 to ca0c843bf75f (11 revisions) (flutter/flutter#117563) * 6441a7dc6 Roll Flutter Engine from ca0c843bf75f to 12badb54598d (2 revisions) (flutter/flutter#117566) * a0cecbe69 3b5fb86a5 delete unused lib/src/engine/canvaskit/viewport_metrics.dart (flutter/engine#38474) (flutter/flutter#117572) * fbfda23f2 Roll Flutter Engine from 3b5fb86a5982 to 6295d9198da1 (3 revisions) (flutter/flutter#117576) * 3eca29b3a f2a071692 Roll Skia from 3e39affa3e1d to 45466d04ca49 (1 revision) (flutter/engine#38480) (flutter/flutter#117579) * 05914e773 948d0bffa Roll Dart SDK from 47f192463696 to 442614a6c1bb (1 revision) (flutter/engine#38481) (flutter/flutter#117581) * a34dec2e7 00995b7b9 Roll Skia from 45466d04ca49 to e206aa0c44f0 (4 revisions) (flutter/engine#38482) (flutter/flutter#117585) * 4ef24cf21 238f40bb9 Roll Fuchsia Mac SDK from W0GUdjHi4gI48optN... to 9w7QDlttR9f7Gu7U6... (flutter/engine#38483) (flutter/flutter#117587) * a45a2f311 ad042d863 Roll Skia from e206aa0c44f0 to a8b7ce3b6391 (1 revision) (flutter/engine#38484) (flutter/flutter#117589) * expose webresourceeerrortype (#6877) * [image_picker] Fix check for iOS 14+ authorization status (#6845) * fix check for authorization status * add unit tests and update release info * update release info * Roll Flutter from a45a2f311990 to e766ad07e600 (7 revisions) (#6880) * dec6b2773 94a54bf3a Roll Flutter from dbc9306380d8 to 9fb1ae839e0a (106 revisions) (flutter/plugins#6876) (flutter/flutter#117598) * b3c321f1d 5bae18365 Reland "[web] Render in custom target (#37738)" (flutter/engine#38477) (flutter/flutter#117600) * 4b4783d1f [flutter roll] Revert both #117338 and #117547 (flutter/flutter#117557) * 102d03936 77db88672 Roll Fuchsia Mac SDK from 9w7QDlttR9f7Gu7U6... to 9qjOKSNAN2EiCgQxC... (flutter/engine#38487) (flutter/flutter#117603) * 393e156c2 e3edfadbd Roll Dart SDK from 442614a6c1bb to 6340d946feac (1 revision) (flutter/engine#38489) (flutter/flutter#117604) * bc7d2755c 1d5c44966 Roll Skia from a8b7ce3b6391 to 38d9c68d35c6 (2 revisions) (flutter/engine#38492) (flutter/flutter#117617) * e766ad07e 6bee6d768 Roll Fuchsia Mac SDK from 9qjOKSNAN2EiCgQxC... to hGNNd-oOWFLY86Tnl... (flutter/engine#38493) (flutter/flutter#117618) * Roll Flutter from e766ad07e600 to ae292cc4e5e4 (6 revisions) (#6885) * 484858b79 00ce1fd6d add virtual destructor to new virtual Culler class (flutter/engine#38494) (flutter/flutter#117624) * d674a4642 b2968296a Roll Fuchsia Mac SDK from hGNNd-oOWFLY86Tnl... to kV1stXDqE4asMxgjK... (flutter/engine#38495) (flutter/flutter#117626) * 239f80ed6 3ae55014f Roll Fuchsia Mac SDK from kV1stXDqE4asMxgjK... to 90MsGucOMFZ_grNUC... (flutter/engine#38498) (flutter/flutter#117633) * def09b4e9 b61200484 Roll Fuchsia Mac SDK from 90MsGucOMFZ_grNUC... to QOdpfMkM_LcPon_zm... (flutter/engine#38499) (flutter/flutter#117646) * a09faa11b Roll Flutter Engine from b61200484d28 to da181dfbfb27 (4 revisions) (flutter/flutter#117651) * ae292cc4e Roll Plugins from 94a54bf3acdf to 3eba2bf698e5 (4 revisions) (flutter/flutter#117653) * Roll Flutter from ae292cc4e5e4 to 17482fd425ee (28 revisions) (#6889) * fe3e93e19 eb8e52c59 Roll Fuchsia Mac SDK from QOdpfMkM_LcPon_zm... to ozbhYRHpQKfnPwJdh... (flutter/engine#38505) (flutter/flutter#117658) * 41d191187 becee173e Roll Skia from 7442335dce20 to eeec7a127312 (1 revision) (flutter/engine#38506) (flutter/flutter#117662) * d15db1518 84043c672 Roll Skia from eeec7a127312 to 7fe57dac0702 (1 revision) (flutter/engine#38508) (flutter/flutter#117665) * 4cce45f60 06b2eff9d Roll Dart SDK from 6340d946feac to 494e4d4bf58d (1 revision) (flutter/engine#38509) (flutter/flutter#117667) * d94768749 893e48763 Roll Skia from 7fe57dac0702 to 8099f53e7a43 (1 revision) (flutter/engine#38510) (flutter/flutter#117668) * a7cc010a9 dbb5a5739 Roll Fuchsia Mac SDK from ozbhYRHpQKfnPwJdh... to HHADjSDGmZSkODScd... (flutter/engine#38511) (flutter/flutter#117669) * c3f0c1308 dcde1faa8 Roll Skia from 8099f53e7a43 to 789552988917 (1 revision) (flutter/engine#38512) (flutter/flutter#117672) * 91c3f80c8 790604a09 Roll Skia from 789552988917 to 6abfcf819da1 (2 revisions) (flutter/engine#38513) (flutter/flutter#117674) * bf2701d2a 9d69a91bb Roll Dart SDK from 494e4d4bf58d to 742e1dc3e17f (1 revision) (flutter/engine#38514) (flutter/flutter#117681) * 00e9cf1d6 e11cb245b Roll Flutter from e766ad07e600 to ae292cc4e5e4 (6 revisions) (flutter/plugins#6885) (flutter/flutter#117682) * 5538fa136 c54228b5c Roll Skia from 6abfcf819da1 to 4f64211cd741 (1 revision) (flutter/engine#38515) (flutter/flutter#117684) * 1c273fbe8 27ebaec7d Roll Skia from 4f64211cd741 to 3939e68c4b4d (2 revisions) (flutter/engine#38517) (flutter/flutter#117686) * f11fbbafc [macOS] Fix the `run_debug_test_macos` on arm64 (flutter/flutter#117250) * d7abc0bbd a53f1e983 Roll Skia from 3939e68c4b4d to 2b6d44eb650b (2 revisions) (flutter/engine#38519) (flutter/flutter#117689) * 894ea20f1 e049bbf41 Roll Fuchsia Mac SDK from HHADjSDGmZSkODScd... to c1-ICa-ToxzhYLG7F... (flutter/engine#38520) (flutter/flutter#117690) * c956121ac Revert "Remove single-view assumption from ScrollPhysics (#117503)" (flutter/flutter#117647) * 0dc973955 6e6a6538f Roll Skia from 2b6d44eb650b to 34708fefacd0 (1 revision) (flutter/engine#38521) (flutter/flutter#117694) * 95a184dcf 10ce8cd38 [fuchsia] Debugging code for crash. (flutter/engine#38518) (flutter/flutter#117697) * a89d135f4 afe49825e Roll Skia from 34708fefacd0 to 1a93cfdae2fd (1 revision) (flutter/engine#38522) (flutter/flutter#117699) * 0ddfa72f0 6128780f1 Roll Dart SDK from 742e1dc3e17f to 68d8b0f58be7 (1 revision) (flutter/engine#38523) (flutter/flutter#117701) * 4e08ebb2b 8294e2693 Roll Skia from 1a93cfdae2fd to c5c0387b3399 (2 revisions) (flutter/engine#38524) (flutter/flutter#117702) * d108c912c bfecc4b18 Roll Dart SDK from 68d8b0f58be7 to 5a173adb22ed (1 revision) (flutter/engine#38525) (flutter/flutter#117707) * 2864acc82 2d431852d Roll Skia from c5c0387b3399 to 656bb22387ac (1 revision) (flutter/engine#38526) (flutter/flutter#117711) * 6f8d17609 74ef2cbc8 Roll Fuchsia Linux SDK from iQT5jpUhipvetxSiH... to yX7ot9Un0bpYQ-XX7... (flutter/engine#38527) (flutter/flutter#117713) * eda7aab33 dc6670f7b Roll Dart SDK from 5a173adb22ed to 2541cf36607f (1 revision) (flutter/engine#38528) (flutter/flutter#117715) * 222323589 8d4546d5b Roll Skia from 656bb22387ac to 913271ba5cbb (2 revisions) (flutter/engine#38529) (flutter/flutter#117719) * f24df972b 3bb936567 Roll Skia from 913271ba5cbb to f78bb848bbe1 (2 revisions) (flutter/engine#38531) (flutter/flutter#117721) * 17482fd42 9e63c1ae1 Roll Fuchsia Mac SDK from c1-ICa-ToxzhYLG7F... to jV7nfgH1Tb3Lw0w_S... (flutter/engine#38532) (flutter/flutter#117731) * [webview_flutter_web] Adds auto registration of the `WebViewPlatform` implementation (#6886) * Adss auto reg * test for registration * update readme * Roll Flutter from 17482fd425ee to d2127ad344e8 (14 revisions) (#6892) * 9bb4ffe15 Roll Flutter Engine from 9e63c1ae1480 to 190f743a8506 (2 revisions) (flutter/flutter#117744) * babeb6191 cc8055d5f Roll Skia from 4b578d72dc2e to e4c86c2fed9a (1 revision) (flutter/engine#38535) (flutter/flutter#117745) * 9afaf6b88 f3cc581d9 Roll Dart SDK from 6e07d9b025bd to f7b36d5e50a5 (1 revision) (flutter/engine#38536) (flutter/flutter#117749) * 0e83ada59 Update M3 IconButton unselected focused opacity (flutter/flutter#117321) * 2783d3168 Roll Flutter Engine from f3cc581d97e1 to 3655bf981d4c (2 revisions) (flutter/flutter#117751) * 4a6ab96c9 Roll Flutter Engine from 3655bf981d4c to 34f75ed27c9b (3 revisions) (flutter/flutter#117760) * 2ffa65c76 e9e79180e Roll Skia from 2923399853d9 to 0027eb334691 (6 revisions) (flutter/engine#38545) (flutter/flutter#117763) * cee9ec522 606e77f35 Roll Skia from 0027eb334691 to 668260c85e9d (1 revision) (flutter/engine#38547) (flutter/flutter#117766) * c089c19f1 Revert "[reland] Add Material 3 support for `ListTile` - Part 1 (#116963)" (flutter/flutter#117756) * bdefebcf7 9b534a5f8 Roll Skia from 668260c85e9d to 25ffa2b757e9 (1 revision) (flutter/engine#38548) (flutter/flutter#117768) * aa70994f6 8655ec022 [Impeller Scene] Add ColorSourceContents for drawing a node (flutter/engine#38485) (flutter/flutter#117769) * cbc184d9a Roll Flutter Engine from 8655ec022fe1 to ecd47e0b256a (2 revisions) (flutter/flutter#117772) * 0b6971946 Roll Flutter Engine from ecd47e0b256a to 45e689b15d1a (2 revisions) (flutter/flutter#117778) * d2127ad34 e44a0de4c Roll Fuchsia Mac SDK from JLTTlcNPJeScjSO2B... to FeFYsNPy64-PEXPer... (flutter/engine#38558) (flutter/flutter#117779) * Roll Flutter from d2127ad344e8 to 120058fd3ded (15 revisions) (#6896) * bafc1b87c Roll Plugins from e11cb245bb8e to 2d66f30e5825 (2 revisions) (flutter/flutter#117781) * 5c736ff53 4dd8a694f Roll Skia from cc3e0cd0a743 to c776239198f7 (1 revision) (flutter/engine#38560) (flutter/flutter#117783) * 46849be44 3460f349b [fuchsia] Set presentation interval (flutter/engine#38549) (flutter/flutter#117785) * d3829e422 Roll Flutter Engine from 3460f349b01d to 1752b5b84680 (2 revisions) (flutter/flutter#117788) * 2ac09d3e3 a63bd854a [fuchsia] Add trace flow for Flatland::Present (flutter/engine#38565) (flutter/flutter#117790) * 521d9a467 Roll Flutter Engine from a63bd854ac5a to 5713a216076f (2 revisions) (flutter/flutter#117795) * 6be445f97 Roll Flutter Engine from 5713a216076f to 780082203ea9 (2 revisions) (flutter/flutter#117797) * c6eec9ff6 9095f7a8b Roll Dart SDK from fa6cf7241184 to 224ac5ed9c66 (1 revision) (flutter/engine#38569) (flutter/flutter#117799) * 01bce6bb4 0118b461b Roll Fuchsia Mac SDK from FeFYsNPy64-PEXPer... to 2lzQU8FEjR5AkOr4d... (flutter/engine#38571) (flutter/flutter#117800) * 6e9beb9fd e03d7c8bb Roll Skia from 13435162b783 to 9e8f31e3020c (3 revisions) (flutter/engine#38572) (flutter/flutter#117802) * 5f30f026c af6078b5f Roll Skia from 9e8f31e3020c to 486deb23bc2a (2 revisions) (flutter/engine#38574) (flutter/flutter#117804) * 322397295 7e5cc7bb6 Roll Dart SDK from 224ac5ed9c66 to 9f0d8b9f20da (1 revision) (flutter/engine#38575) (flutter/flutter#117805) * ae253d7b0 d4a04a538 Roll Fuchsia Linux SDK from KCm_e3N4gosNuY4IW... to IApTRqW8UUSWAOcqA... (flutter/engine#38578) (flutter/flutter#117817) * f83986af1 b202b3db9 Roll Flutter from 17482fd425ee to d2127ad344e8 (14 revisions) (flutter/plugins#6892) (flutter/flutter#117824) * 120058fd3 Roll Flutter Engine from d4a04a538050 to 9153966bcb06 (2 revisions) (flutter/flutter#117830) * Roll Flutter from 120058fd3ded to 0196e6050b75 (3 revisions) (#6901) * a7db131a5 b9bf51d16 Roll Dart SDK from 9f0d8b9f20da to 881c0b56a1f7 (1 revision) (flutter/engine#38580) (flutter/flutter#117832) * ecfc29772 Roll Flutter Engine from b9bf51d16f25 to f6ad9b6d00e3 (2 revisions) (flutter/flutter#117834) * 0196e6050 932591ec0 Roll Fuchsia Linux SDK from CXcPP_JZKQbSu2eIP... to PkN8FdI4aC9z7W4mI... (flutter/engine#38584) (flutter/flutter#117840) * Roll Flutter from 0196e6050b75 to b938dc13df32 (7 revisions) (#6908) * bc1591ed5 3d8c5ef10 Roll Fuchsia Linux SDK from PkN8FdI4aC9z7W4mI... to OOL-jWRElkQ2P3vJz... (flutter/engine#38585) (flutter/flutter#117846) * 665987130 Roll Flutter Engine from 3d8c5ef1060c to a7decc3e459b (2 revisions) (flutter/flutter#117856) * e3429dd35 0a2029cf3 Roll Fuchsia Linux SDK from OOL-jWRElkQ2P3vJz... to AE3lAqTc632VsY14L... (flutter/engine#38588) (flutter/flutter#117858) * 45fe23e27 5fe7d5b4e Roll Skia from 01aeec883a43 to 2ffa04c2f77c (2 revisions) (flutter/engine#38591) (flutter/flutter#117863) * ced842718 e5d605b3a Roll Skia from 2ffa04c2f77c to 269dce7e16bb (1 revision) (flutter/engine#38592) (flutter/flutter#117865) * d393bf023 71c5f1704 Roll Fuchsia Linux SDK from AE3lAqTc632VsY14L... to UAq0LO56_kbgA_BUQ... (flutter/engine#38593) (flutter/flutter#117868) * b938dc13d 472e34cbb Roll Skia from 269dce7e16bb to fde37f5986fd (1 revision) (flutter/engine#38594) (flutter/flutter#117869) * [in_app_pur] Add screenshots to pubspec.yaml (#6540) * [in_app_pur] Add screenshots to pubspec.yaml * Style fix Co-authored-by: Stuart Morgan * [google_maps_flutter] Fixed minor syntax error in the README.md (#6909) * Fixed minor syntax error and added imports * [google_maps_flutter] Fixed minor syntax error and added imports * Fixed changes to match repo guidelines and tests Moved the changes to the readme_sample code excerpt, incremented the version, and removed the changes to imports. * Updated excerpts and pubspec version * Style fix Co-authored-by: stuartmorgan * [image_picker_ios] Fix FLTPHPickerSaveImageToPathOperation property attributes (#6890) * [image_picker_ios] Fix FLTPHPickerSaveImageToPathOperation property attributes * Format * Replace removed wait * Roll Flutter from b938dc13df32 to 231855fc87d0 (19 revisions) (#6913) * 64e733647 Roll Plugins from b202b3db98dc to e85f8ac1502d (3 revisions) (flutter/flutter#117875) * fe8dcf663 [flutter_tools] timeline_test.dart flaky (flutter/flutter#116667) * f1905593b 7e51aef0a Roll Skia from fde37f5986fd to 809e328ed55c (1 revision) (flutter/engine#38596) (flutter/flutter#117874) * ccfd14b05 Updated to tokens v0.150. (flutter/flutter#117350) * b9ead3724 Simplify null check. (flutter/flutter#117026) * 084be5e6d Roll Flutter Engine from 7e51aef0a1be to 1d2ba73d1059 (9 revisions) (flutter/flutter#117923) * fdc25a170 Reland "Remove single-view assumption from ScrollPhysics (#117503)" (flutter/flutter#117916) * 6b9f1c228 Minor documentation fix on BorderRadiusDirectional.zero (flutter/flutter#117661) * 889e35b3f fix typos (flutter/flutter#117592) * bd69ef70a c0b3f8fce Make `AccessibilityBridge` a `AXPlatformTreeManager` (flutter/engine#38610) (flutter/flutter#117931) * a7942e80d Add convenience constructors for SliverList (flutter/flutter#116605) * dbd36fb13 2213b80dd [Impeller Scene] Use std::chrono for animation durations (flutter/engine#38606) (flutter/flutter#117935) * 9080d1acc Reland "Add support for double tap and drag for text selection #109573" (flutter/flutter#117502) * 63653e827 == override parameters are non-nullable (flutter/flutter#117839) * 906761cf9 Fix the message strings for xcodeMissing and xcodeIncomplete (flutter/flutter#117922) * e599e5c9a 32c468507 Roll quiver to 3.2.1 (flutter/engine#38617) (flutter/flutter#117942) * c53501d83 Send text direction in selection rects (flutter/flutter#117436) * 025ce117b Correctly propagate verbosity to subtasks in flutter.gradle (flutter/flutter#117897) * 231855fc8 Roll Plugins from e85f8ac1502d to f9dda6a27b79 (3 revisions) (flutter/flutter#117972) * Update image_picker_ios CODEOWNER (#6891) * Roll Flutter from 231855fc87d0 to 43b912090224 (11 revisions) (#6918) * 672fe20bd [flutter_tools] Fix null check in parsing web plugin from pubspec.yaml (flutter/flutter#117939) * fe1271f89 roll packages (flutter/flutter#117940) * 341ae18f6 roll packages (flutter/flutter#118001) * 873cf5176 [Android] Increase timeout duration for spell check integration test (flutter/flutter#117989) * e2f390a77 Roll Flutter Engine from 32c468507b32 to cdd3bf29e27a (8 revisions) (flutter/flutter#118014) * fbb743dc1 60515762e [Impeller Scene] Compute joint transforms and apply them to skinned meshes (flutter/engine#38628) (flutter/flutter#118016) * da1a85491 35b7dee32 [Impeller] Set adaptive tolerance when rendering FillPathGeometry (flutter/engine#38497) (flutter/flutter#118017) * a562fe2d8 b9b0193ea Roll Skia from 60e4a4a27375 to 158d51b34caa (19 revisions) (flutter/engine#38654) (flutter/flutter#118018) * 45886068c a01548f5f [Impeller Scene] Fix material/vertex color overlapping (flutter/engine#38653) (flutter/flutter#118027) * 7a5ae7cfc Roll Plugins from f9dda6a27b79 to 320461910156 (2 revisions) (flutter/flutter#118040) * 43b912090 072a9ca37 Add `TextProvider` and `TextEdit` patterns to `AXPlatformNodeWin` (flutter/engine#38646) (flutter/flutter#118039) * [shared_preferences] Convert macOS to Pigeon (#6914) * Add Pigeon and update Dart, based on iOS implementation * Update Swift implementation * Update Swift tests * Adjust Dart test and add Pigeon TODOs * Version bump * Format * Update to Pigeon 5.0 for Swift warning fix * Roll Flutter from 43b912090224 to 507062032fa4 (9 revisions) (#6919) * 5a87a8290 bb4015269 Roll Skia from 158d51b34caa to ecd3a2f865ba (1 revision) (flutter/engine#38659) (flutter/flutter#118042) * 0a2e0a473 Avoid using `TextAffinity` in `TextBoundary` (flutter/flutter#117446) * 36a1caf29 74861f369 Reduce the size of Overlay FlutterImageView in HC mode (flutter/engine#38393) (flutter/flutter#118048) * 9b864b84b 5bd90d6e7 Consider more roles as text (flutter/engine#38645) (flutter/flutter#118049) * 70d583401 [EMPTY] Commit to refresh the tree that is currently red (flutter/flutter#118062) * 4f3ed8040 Remove doc reference to the deprecated ui.FlutterWindow API (flutter/flutter#118064) * de2a42497 Fix `flutter update-packages` regression by fixing parameters in "pub get" runner (flutter/flutter#116687) * 57dc071f7 Adding 'is' to list of kotlin reserved keywords (flutter/flutter#116299) * 507062032 Added expandIconColor property on ExpansionPanelList Widget (flutter/flutter#115950) * [tool] Don't add Guava in the all-packages app (#6747) It's not clear why we are adding an outdated version of Guava; it is likely cruft, so this test removing it. * [local_auth]: Bump espresso-core (#6925) Bumps espresso-core from 3.2.0 to 3.5.1. --- updated-dependencies: - dependency-name: androidx.test.espresso:espresso-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [webview_flutter_platform_interface] Improves error message when `WebViewPlatform.instance` is null (#6938) * add assertion * formatting * [google_maps]: Bump espresso-core from 3.4.0 to 3.5.1 in /packages/google_maps_flutter/google_maps_flutter_android/android (#6937) * [google_maps]: Bump espresso-core Bumps espresso-core from 3.4.0 to 3.5.1. --- updated-dependencies: - dependency-name: androidx.test.espresso:espresso-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Changelog and version bump Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gary Qian * [espresso]: Bump truth from 1.4.0 to 1.5.0 in /packages/espresso/android (#6707) Bumps truth from 1.4.0 to 1.5.0. --- updated-dependencies: - dependency-name: androidx.test.ext:truth dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [camera]: Bump camerax_version from 1.3.0-alpha01 to 1.3.0-alpha02 in /packages/camera/camera_android_camerax/android (#6828) * [camera]: Bump camerax_version Bumps `camerax_version` from 1.3.0-alpha01 to 1.3.0-alpha02. Updates `camera-core` from 1.3.0-alpha01 to 1.3.0-alpha02 Updates `camera-camera2` from 1.3.0-alpha01 to 1.3.0-alpha02 Updates `camera-lifecycle` from 1.3.0-alpha01 to 1.3.0-alpha02 --- updated-dependencies: - dependency-name: androidx.camera:camera-core dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: androidx.camera:camera-camera2 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: androidx.camera:camera-lifecycle dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Add changelog change Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: camsim99 * [shared_preferences] Merge iOS and macOS implementations (#6920) This merges `shared_preferences_ios` and `shared_preferences_macos` into a new `shared_preferences_foundations` that replaces both of those packages, as described in https://github.com/flutter/flutter/issues/117941: - Renames `shared_preferences_macos` to `shared_prefrences_foundation`, since the macOS implementation is the Swift implementation, which is what we want to use going forward. - Moves the implementation files to a shared directory (called `darwin/` in anticipation of https://github.com/flutter/flutter/pull/115337), adjusting the code and podspec slightly to make it iOS-compatible - Adds iOS support to the example, via `flutter create`-ing a new iOS example and wiring it up to use the existing native unit test. (This was done instead of moving the example from `shared_preferences_ios` since it seemed better to have the example be in Swift as well now.) - Removes `shared_preferences_ios`. Once this lands and has been published, a follow-up will update `shared_preferences` to use this new package instead of the other two, and the old ones will be marked as deprecated on pub.dev. Part of https://github.com/flutter/flutter/issues/117941 * [various] Enable `avoid_dynamic_calls` (#6834) * Enable the option * Fix camera * Fix webview * Remove unnecessary 'call's from camera tests * Fix maps * Fix sign-in * fix image_picker * Fix IAP * Fix shared_preferences * Fix url_launcher_android * Version bumps * Fix tool * Re-apply webview test fix * Re-bump versions * Fix one new tool issue * == override parameters are non-nullable (#6900) * [espresso]: Bump espresso-accessibility and espresso-idling-resource from 3.1.0 to 3.5.1 in /packages/espresso/android (#6933) * [espresso]: Bump espresso-accessibility in /packages/espresso/android Bumps espresso-accessibility from 3.1.0 to 3.5.1. --- updated-dependencies: - dependency-name: androidx.test.espresso:espresso-accessibility dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Changelog * Also update espresso-idling-resource Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gary Qian * [file_selector] Switch to Pigeon for macOS (#6902) * Initial pigeon definition * Update Dart, and add Dart test coverage * Update Swift and native tests * Version bump * Format * Revert SDK change * [google_sign_in] Renames generated folder to js_interop. (#6915) * Renames generated to js_interop * Fix analyze errors * Fix format * Fix version check * Add comment for public_member_api_docs * [gh_actions]: Bump ossf/scorecard-action from 2.0.6 to 2.1.2 (#6882) Bumps [ossf/scorecard-action](https://github.com/ossf/scorecard-action) from 2.0.6 to 2.1.2. - [Release notes](https://github.com/ossf/scorecard-action/releases) - [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md) - [Commits](https://github.com/ossf/scorecard-action/compare/99c53751e09b9529366343771cc321ec74e9bd3d...e38b1902ae4f44df626f11ba0734b14fb91f8f86) --- updated-dependencies: - dependency-name: ossf/scorecard-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [in_app_pur]: Bump espresso-core from 3.4.0 to 3.5.1 in /packages/in_app_purchase/in_app_purchase_android/android (#6924) * [in_app_pur]: Bump espresso-core Bumps espresso-core from 3.4.0 to 3.5.1. --- updated-dependencies: - dependency-name: androidx.test.espresso:espresso-core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Changelog * Typo Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gary Qian * Roll Flutter from 507062032fa4 to 7ddf42eae5ee (5 revisions) (#6923) * 6d7c5077c Update docstring (flutter/flutter#118072) * e0f89e7b7 Fix out-of-sync ExpansionPanel animation (flutter/flutter#105024) * 0e555049b Roll Plugins from 320461910156 to 276cfd4b212d (2 revisions) (flutter/flutter#118099) * 71f920732 33d7f8a1b Remove single view assumptions from `window.dart` (flutter/engine#38453) (flutter/flutter#118069) * 7ddf42eae InteractiveViewer parameter to return to pre-3.3 trackpad/Magic Mouse behaviour (flutter/flutter#114280) * [shared_preferences] Switch to `shared_preferences_foundation` (#6940) * [shared_preferences] Switch to `shared_preferences_foundation` Switches to using the new combined `shared_prefences_foundation` for iOS and macOS. Part of flutter/flutter#117941 * Update build-all exclusion * not for landing - try no-op test * Revert "not for landing - try no-op test" This reverts commit bf2ad1edd917ceeedcde3a9df3c62f4eb68c75dd. * Try recreating the example app * [tool] Replace `flutter format` (#6946) `flutter format` is deprecated on `master`, and prints a warning saying to switch to `dart format` instead. This updates `format` to make that switch. * [gh_actions]: Bump actions/checkout from 3.1.0 to 3.3.0 (#6935) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.1.0 to 3.3.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8...ac593985615ec2ede58e132d2e21d2b1cbd6127c) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Roll Flutter from 7ddf42eae5ee to 0d91c0343bdc (58 revisions) (#6948) * 5335a96b5 0a0e3d205 Roll Flutter from 43b912090224 to 507062032fa4 (9 revisions) (flutter/plugins#6919) (flutter/flutter#118183) * 6c225ddac Roll Flutter Engine from 33d7f8a1b307 to 03609b420beb (6 revisions) (flutter/flutter#118125) * 478d1dae7 remove the unused check in selectable_text (flutter/flutter#117716) * c87cfd710 Roll Flutter Engine from 03609b420beb to b5513d7a442a (2 revisions) (flutter/flutter#118186) * 234452fef Roll Flutter Engine from b5513d7a442a to 5bdb04f33f99 (2 revisions) (flutter/flutter#118187) * 78416d874 51baed6e0 [fuchsia][scenic] Use infinite hit region (flutter/engine#38647) (flutter/flutter#118189) * a02b9d2bf Update to Xcode 14.2 (flutter/flutter#117507) * eaaacdcba Allow iOS and macOS plugins to share darwin directory (flutter/flutter#115337) * 5bf6357d3 Roll Flutter Engine from 51baed6e01b8 to 5df0072a0e63 (3 revisions) (flutter/flutter#118192) * 46e48ba3b Use program during attach if provided (flutter/flutter#118130) * 31da71212 eb5c6f0b4 iOS FlutterTextureRegistry should be a proxy, not the engine itself (flutter/engine#37666) (flutter/flutter#118197) * fa1a4eebf Update `ListTile` to support Material 3 (flutter/flutter#117965) * 02a8bbfaf 3a7d8862f Re-enable UIA text/range provider unit tests (flutter/engine#38718) (flutter/flutter#118201) * e9bbb1137 Fix path for require.js (flutter/flutter#118120) * 851611b38 ee0c4d26b Roll flutter/packages to 25454e (flutter/engine#38685) (flutter/flutter#118205) * 00a9db816 Roll Flutter Engine from ee0c4d26b0fa to 264aa032cf75 (2 revisions) (flutter/flutter#118208) * d6cd9c0ce 9c0b187a1 Roll Dart SDK from 853eff8b0faa to 418bee5da2e2 (4 revisions) (flutter/engine#38727) (flutter/flutter#118210) * 420535755 add closed/open focus traversal; use open on web (flutter/flutter#115961) * 466cb5463 Roll Flutter Engine from 9c0b187a1139 to 716bb9172c0d (3 revisions) (flutter/flutter#118220) * ad7322ddb Hide InkWell hover highlight when an hovered InkWell is disabled (flutter/flutter#118026) * 583a8122b Allow select cases to be numbers (flutter/flutter#116625) * 700fe3d2b [Impeller Scene] Add SceneC asset importing (flutter/flutter#118157) * 07bc24524 Add a comment about repeat event + fix typos (flutter/flutter#118095) * 5d96d619d Add MaterialStateProperty `overlayColor` & `mouseCursor` and fix hovering on thumbs behavior (flutter/flutter#116894) * f2c088b1c Roll Flutter Engine from 716bb9172c0d to 687e3cb0fbe2 (2 revisions) (flutter/flutter#118242) * 669a3d2b4 Roll Plugins from 0a0e3d205ca3 to 9fdc899b72ca (8 revisions) (flutter/flutter#118253) * cb62cfdd1 Manually mark Windows run_debug_test_windows as unflaky (flutter/flutter#118112) * 0cd8d8451 Marks Mac_arm64_android run_debug_test_android to be unflaky (flutter/flutter#117469) * 3cee38e80 Marks Mac_arm64_ios run_debug_test_macos to be unflaky (flutter/flutter#117990) * 1d2e62b76 remove unsound mode web test (flutter/flutter#118256) * 3d3f8e85a Update `CupertinoPicker` example (flutter/flutter#118248) * 594333b36 roll packages (flutter/flutter#118117) * a6f17e697 Add option for opting out of enter route snapshotting. (flutter/flutter#118086) * 01583b748 roll packages (flutter/flutter#118272) * cf529ec57 Roll Flutter Engine from 687e3cb0fbe2 to c1d61cf11da8 (6 revisions) (flutter/flutter#118274) * b7881e5b6 Align `flutter pub get/upgrade/add/remove/downgrade` (flutter/flutter#117896) * 12b43ed8a ae9e181e3 Roll Dart SDK from 5e344de60564 to 7b4d49402252 (1 revision) (flutter/engine#38756) (flutter/flutter#118287) * b13f83a2b Fix Finnish TimeOfDate format (flutter/flutter#118204) * 55e3894a8 Roll Flutter Engine from ae9e181e30c2 to 53bd4bbf9646 (3 revisions) (flutter/flutter#118289) * a40fb9bdd 9ade91c8b removed forbidden skia include (flutter/engine#38761) (flutter/flutter#118296) * 32b1ea3b5 8d7beac82 Roll Dart SDK from 7b4d49402252 to 23cbd61a1327 (1 revision) (flutter/engine#38764) (flutter/flutter#118297) * 957781a10 6256f05db Roll Fuchsia Mac SDK from 6xysoRPCXJ3cJX12x... to a9NpYJbjhDRX9P9u4... (flutter/engine#38767) (flutter/flutter#118300) * f10965f2d FIX: UnderlineInputBorder hashCode and equality by including borderRadius (flutter/flutter#118284) * 33c71beee Bump actions/upload-artifact from 3.1.1 to 3.1.2 (flutter/flutter#118116) * 2e0849e9d Bump actions/checkout from 3.1.0 to 3.3.0 (flutter/flutter#118052) * aabf146f3 Bump github/codeql-action from 2.1.35 to 2.1.37 (flutter/flutter#117104) * a50e2c8d6 6048f9110 Roll Dart SDK from 23cbd61a1327 to 22fa50e09ee8 (3 revisions) (flutter/engine#38776) (flutter/flutter#118320) * e697805bf Roll Plugins from 9fdc899b72ca to 620a059d62b2 (4 revisions) (flutter/flutter#118317) * b4a07de2a ee76ab71e Cleanup Skia includes in image_generator/descriptor (flutter/engine#38775) (flutter/flutter#118335) * c6be43a65 Roll Flutter Engine from ee76ab71e0a6 to cccaae2f3d8b (3 revisions) (flutter/flutter#118349) * 4b2d3eb0f 764a9e012 Roll Skia from e1f3980272f3 to dfb838747295 (48 revisions) (flutter/engine#38790) (flutter/flutter#118355) * b2b405043 Roll Flutter Engine from 764a9e01204d to 4a8d6866a1c0 (2 revisions) (flutter/flutter#118357) * 727e86079 Marks Mac_ios complex_layout_scroll_perf_bad_ios__timeline_summary to be unflaky (flutter/flutter#111570) * 13ebde6ff Marks Mac channels_integration_test to be unflaky (flutter/flutter#111571) * f8628b5cb Marks Mac_ios platform_views_scroll_perf_non_intersecting_impeller_ios__timeline_summary to be unflaky (flutter/flutter#116668) * 44f540338 Fix `SliverAppBar.large` and `SliverAppBar.medium` do not use `foregroundColor` (flutter/flutter#118322) * 51c2af56c docs: update docs about color property in material card (flutter/flutter#117263) * 0d91c0343 Fix M3 `Drawer` default shape in RTL (flutter/flutter#118185) * [path_provider] Switch to Pigeon for macOS (#6635) * [path_provider] Switch to Pigeon for macOS Converts from direct method channel use to the new experimental Swift generator. Also switches from a one-call-per-method approach to a single method with an enum, since it's easy to do that in a maintainable way with Pigeon (unlike manual method channels, where keeping enum indexes in sync across the language boundary is error-prone, and requires manual int/enum conversion). * Format * Analyzer * Update to latest version of Pigeon * [shared_preferences_foundation] Add Swift runtime search paths for Objective-C apps (#6952) * [tool] Fix false positives in update-exceprts (#6950) When determining whether or not to fail with `--fail-on-change`, only look at .md files. In some cases, running the necessary commands (e.g., `flutter pub get`) may change unrelated files, causing fales positive failures. Only changed documentation files should be flagged. Also log the specific files that were detected as changed, to aid in debugging any future false positives. Fixes https://github.com/flutter/flutter/issues/111592 Fixes https://github.com/flutter/flutter/issues/111590 * Roll Flutter from 0d91c0343bdc to 220169878e77 (28 revisions) (#6953) * f1a1f2726 [M3] Add error state support for side property of CheckBox (flutter/flutter#118386) * 27502f685 Roll Plugins from 620a059d62b2 to 39197f17ca59 (6 revisions) (flutter/flutter#118391) * 40bc6b55e Move debug error message from failed pub to logger.printTrace (flutter/flutter#118379) * 5630d531b [tool] Generate a binary version of the asset manifest (flutter/flutter#117233) * c7a3f0fed IconButtonTheme should be overridden by the AppBar/AppBarTheme's iconTheme and actionsIconTheme (flutter/flutter#118216) * ee1c59d46 reduce pub output from flutter create (flutter/flutter#118285) * 947b694f1 roll packages (flutter/flutter#118277) * 8f365a3bd [web] Update build to use generated JS runtime for Dart2Wasm. (flutter/flutter#118359) * ace4fb5c5 Roll Flutter Engine from 4a8d6866a1c0 to c01465a18f31 (9 revisions) (flutter/flutter#118409) * db8d1a441 Add MSYS2 detection on Windows Terminal (flutter/flutter#117612) * c905a09b0 Add documentation for drag/fling offset in WidgetController. (flutter/flutter#118288) * 4e85235f3 688015782 fixed glfw example for arm64 (flutter/engine#38426) (flutter/flutter#118413) * 9e11d4a10 Use correct API docs link in create --sample help message (flutter/flutter#118371) * 3e00520f1 Roll Flutter Engine from 688015782762 to 35cfe9158648 (2 revisions) (flutter/flutter#118415) * bd938b008 Fix tap/drag callbacks firing when TapAndDragGestureRecognizer has not won the arena (flutter/flutter#118342) * 19af46f75 8aa26baa9 Roll Dart SDK from edd406c07399 to 20cca507d98b (1 revision) (flutter/engine#38823) (flutter/flutter#118420) * f7b444e11 add generated_plugins.cmake (flutter/flutter#116581) * 40c96f17b Enable xcode cache cleanup for a few days. (flutter/flutter#118419) * 59d737e64 99509a7e4 Correct FrameTimingRecorder's raster start time. (flutter/engine#38674) (flutter/flutter#118425) * 9024a70f0 Roll Flutter Engine from 99509a7e4275 to f3f05368033b (2 revisions) (flutter/flutter#118429) * 0752af841 Add `allowedButtonsFilter` to prevent Draggable from appearing with secondary click. (flutter/flutter#111852) * 1578acb60 15d59792e Roll Skia from dfb838747295 to 9e51c2c9e231 (26 revisions) (flutter/engine#38827) (flutter/flutter#118432) * 07c47dcc3 a62d25326 Roll Skia from dfb838747295 to cc983d28f3bf (27 revisions) (flutter/engine#38830) (flutter/flutter#118435) * 17a855eab dfa0327f8 Roll Skia from cc983d28f3bf to fd54be29a3cc (3 revisions) (flutter/engine#38833) (flutter/flutter#118436) * b8960660f 07603c6d4 Roll Dart SDK from 20cca507d98b to 3d629d00a8d7 (2 revisions) (flutter/engine#38834) (flutter/flutter#118439) * ddad6f163 Fix copying/applying font fallback with package (flutter/flutter#118393) * 8889d49ea dec608917 Roll Fuchsia Mac SDK from nIPtQ59jG1pxyatOq... to 21nYb648VWbpxc36t... (flutter/engine#38839) (flutter/flutter#118445) * 220169878 970889b87 Roll Skia from fd54be29a3cc to c72c7bf7e45b (3 revisions) (flutter/engine#38840) (flutter/flutter#118448) * [tool] Check for search paths in Swift plugins (#6954) * Rename command, bump version * Update tests to write actual podspecs * Add new check * Analyzer fix * Unhdo file move * [shared_preferences] Revert recent iOS example changes (#6955) * Revert "[shared_preferences_foundation] Add Swift runtime search paths for Objective-C apps (#6952)" This reverts commit be2e3de7a7a605848d2ba74b572b4f4b4b411025. * Revert "[shared_preferences] Switch to `shared_preferences_foundation` (#6940)" This reverts commit 44098fe341cfae6c9f8ad331358d121f6284496e. * Re-apply the non-example parts of the reverted PRs * [android_webview_controller] Fixes bug where an `AndroidController` couldn't be reused (#6910) * the fix * change location of setting pageLoaded * destroy webview when removed from system * [webview_flutter_android] Fixes crash when the Java `InstanceManager` was used after plugin was removed from engine (#6943) * dont throw errors on all instance manager methods * tests * version bump * change to logging a warning and ignore calls in other methods * the * documentation * Roll Flutter from 220169878e77 to 68dd63d66ba6 (9 revisions) (#6956) * 8900bda1c a512cebdc Roll Dart SDK from 3d629d00a8d7 to 645fd748e79e (1 revision) (flutter/engine#38841) (flutter/flutter#118454) * fb1a1510a Roll Plugins from 39197f17ca59 to 92a5367d58df (4 revisions) (flutter/flutter#118457) * 3a181e495 Added LinearBorder, an OutlinedBorder like BoxBorder (flutter/flutter#116940) * a523f7923 Marks Mac_ios spell_check_test to be unflaky (flutter/flutter#117743) * cea55d99d [Linux] Add a 'flutter run' console output test (flutter/flutter#118279) * b4d72752b Add Info.plist from build directory as input path to Thin Binary build phase (flutter/flutter#118209) * 2fd825028 [flutter_tools] re-enable web shader compilation (flutter/flutter#118461) * 13a8dce22 Bump github/codeql-action from 2.1.37 to 2.1.38 (flutter/flutter#118482) * 68dd63d66 Mark Mac_arm64 tool_host_cross_arch_tests not flaky (flutter/flutter#118484) * Roll Flutter from 68dd63d66ba6 to 1220245b330c (4 revisions) (#6957) * da5f8cf90 Roll Flutter Engine from a512cebdcd30 to 7dc5e7efa66a (2 revisions) (flutter/flutter#118505) * baefeccbe 35479aa1c Roll Fuchsia Mac SDK from 21nYb648VWbpxc36t... to w0hr1ZMvYGJnWInwK... (flutter/engine#38880) (flutter/flutter#118509) * ca300ce57 25cb82272 Add include to make g3 happy (flutter/engine#38850) (flutter/flutter#118510) * 1220245b3 f79030440 Roll Skia from c72c7bf7e45b to c64a10d525d1 (7 revisions) (flutter/engine#38858) (flutter/flutter#118511) * Roll Flutter from 1220245b330c to 8c2fdb803e49 (2 revisions) (#6979) * 7188c3e62 Update documentation about accent color (flutter/flutter#116778) * 8c2fdb803 M3 Button padding adjustments (flutter/flutter#118449) * Roll Flutter from 8c2fdb803e49 to cc7845e71a9d (2 revisions) (#6983) * f22280a0c Revert "M3 Button padding adjustments (#118449)" (flutter/flutter#118598) * cc7845e71 Post a ToolEvent when selecting widget for inspection (flutter/flutter#118098) * [shared_pref]: Bump mockito-inline (#6976) Bumps [mockito-inline](https://github.com/mockito/mockito) from 4.7.0 to 5.0.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.7.0...v5.0.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-inline dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [camera] Allow logical cameras to use all physical cameras via zoom on android 11+ (#6150) * Adding support for the android 11+ Camera2 CONTROL_ZOOM_RATIO_RANGE camera preference. This preference is essential for supporting Logical cameras on the rear which after android 11 wrap multiple cameras (ultrawide, normal, superzoom, etc) into a single rear facing camera. This updates change how zoom functions as well as how min and max zoom is set to be compliant with those updates as long as the user device is on android 11+. * Adding updates to the changelog and pubspec to reflect the zoom and camera updates. * comment cleanup for min and max zoom ratio functions * Pull request fixes * Update packages/camera/camera_android/CHANGELOG.md Co-authored-by: Camille Simon <43054281+camsim99@users.noreply.github.com> * Update packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtils.java Co-authored-by: Camille Simon <43054281+camsim99@users.noreply.github.com> * Updating comments from PR * Fixing variable name formatting, and comment structures * Fixing variable name formatting, and comment structures * Autogormatter updates Co-authored-by: Camille Simon <43054281+camsim99@users.noreply.github.com> Co-authored-by: stuartmorgan * [camera] Use startVideoCapturing and expose concurrent stream/record (#6815) * Use startVideoCapturing and expose concurrent stream/record This uses the new startVideoCapturing implementation, that supports concurrent stream/record. * Ran dart formatter * retrigger checks * Account for version bump * Roll Flutter from cc7845e71a9d to 973cff40b402 (9 revisions) (#6987) * a3629a223 Roll Plugins from 92a5367d58df to 4e5cf2d2da27 (8 revisions) (flutter/flutter#118624) * ae7b99efb Rename `_*Marker` classes to be `_*Scope`, for consistency (flutter/flutter#118070) * 6fafbc33f Updated tokens to v0.152 (flutter/flutter#118594) * 4b3cf9bbd Add reference to HardwareKeyboard in RawKeyboard documentation (flutter/flutter#118607) * 0449030a9 Disable Xcode cache cleanup (flutter/flutter#118641) * f989d551c Devicelab android emulator (flutter/flutter#113472) * 0eaa83ad6 Fix some Focus related documentation typos (flutter/flutter#118576) * 780563ce0 Add const constructor to TextInputFormatter (flutter/flutter#116654) * 973cff40b [Re-land] Button padding m3 (flutter/flutter#118640) * [path_provider] Merge iOS and macOS implementations (#6988) * Rename directory; no changes * Exclude new plugin from build-all during the transition * Rename files and classes, update metadata * Make error messages non-macOS-specific * Move implementation to shared location * IGNORE IN REVIEW add symlinks * Add macOS symlink readme * Add iOS support with sharedDarwinSource * Add iOS example freshly created from template * Add iOS symlinks * Add missing library directory to example, for parity with iOS * Move unit test to shared location, and add iOS unit test target using it * Add the Swift search paths to podspec * Remove path_provider_ios * Update CODEOWNERS * Add copyrights to example files * [video_player]: Bump mockito-core (#6974) Bumps [mockito-core](https://github.com/mockito/mockito) from 4.7.0 to 5.0.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.7.0...v5.0.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [path_provider] Switch to `path_provider_foundation` (#6989) * [path_provider] Switch to `path_provider_foundation` Switches to using the new combined `path_provider_foundation` for iOS and macOS. Also updates the code documentation to make it less Android and iOS specific. The original goal was to make the documentation for the download directory not be actively wrong for the new implementation, but it seemed like a good time to fix 76427 more generally. (The fact that the docs are kind of a mess because the API itself is kind of a mess is now https://github.com/flutter/flutter/issues/118712.) Fixes flutter/flutter#117941 Fixes https://github.com/flutter/flutter/issues/76427 * Remove exclusion * Update test expectations and README * Update test expectations again, and update docs * [video_player] Expose `VideoScrubber` so it can be used to create custom video timelines (#6680) * Expose `VideoScrubber` so it can be used to make custom video timelines * Added documentation * Updated version * Cleanup * Formatting * Update CHANGELOG.md * [webview_flutter_android] Adds support for receiving Java callback `WebChromeClient.onShowFileChooser` (#6881) * some progress * more work * compiling code * dart side should be correct * maybe working code * fix plugin class * formatting * some docs and polish * foramtting tests and docs * version bump * doc improvements * flutterapi create test * working java test * unused imports * formatting and more tests * formatting and more tests * formatting * copy and tests * add more description to the custom method * formatting * change doc wording * more docs * null out result early * remove the open option * fix spelling * interface implementation * version bump * move webchromeclient to webview controller * tests * reference method in changelong * missing typed data import * undo changes * use stateerror instead * updated lints * [webview_flutter_android] Fix throwing `StateError` when `onShowFileChooser` was nonnull (#6995) * [various] More analysis_options alignment (#6949) * Change the way local changes are handled to match packages * only_throw_errors * Enable no_default_cases * Fix violations in camera * Fix violations in webview * Fix url_launcher violation * Fix violations in shared_preferences * Fix violations in maps * Version bumps * Fix image_picker violations * Fix video_player violations * New version bumps * Update excerpts * Address review feedback * [local_auth]: Bump mockito-inline (#6968) Bumps [mockito-inline](https://github.com/mockito/mockito) from 4.7.0 to 5.0.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.7.0...v5.0.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-inline dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [camera]: Bump mockito-inline (#6978) Bumps [mockito-inline](https://github.com/mockito/mockito) from 4.7.0 to 5.0.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.7.0...v5.0.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-inline dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [in_app_pur]: Bump mockito-core (#6961) Bumps [mockito-core](https://github.com/mockito/mockito) from 4.7.0 to 5.0.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.7.0...v5.0.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [video_player]: Bump mockito-inline (#6964) Bumps [mockito-inline](https://github.com/mockito/mockito) from 4.7.0 to 5.0.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.7.0...v5.0.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-inline dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [quick_actions]: Bump mockito-android (#6970) Bumps [mockito-android](https://github.com/mockito/mockito) from 4.7.0 to 5.0.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.7.0...v5.0.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-android dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [camera]: Bump mockito-inline in /packages/camera/camera_android/android (#6973) Bumps [mockito-inline](https://github.com/mockito/mockito) from 4.7.0 to 5.0.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.7.0...v5.0.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-inline dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [sign_in]: Bump mockito-inline (#6962) Bumps [mockito-inline](https://github.com/mockito/mockito) from 4.7.0 to 5.0.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.7.0...v5.0.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-inline dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [quick_actions]: Bump mockito-core (#6972) Bumps [mockito-core](https://github.com/mockito/mockito) from 4.7.0 to 5.0.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.7.0...v5.0.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [quick_actions]: Bump mockito-core (#6966) Bumps [mockito-core](https://github.com/mockito/mockito) from 4.7.0 to 5.0.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.7.0...v5.0.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [video_player]: Bump mockito-core (#6965) Bumps [mockito-core](https://github.com/mockito/mockito) from 4.7.0 to 5.0.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.7.0...v5.0.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Roll Flutter from 973cff40b402 to a07e8a6ac43d (60 revisions) (#7003) * 334898754 Add new macos target configured for flavors (flutter/flutter#117352) * 627752064 Roll Plugins from 4e5cf2d2da27 to 11361d01099d (4 revisions) (flutter/flutter#118682) * 997d43618 Fix applyBoxFit's handling of fitWidth and fitHeight. (flutter/flutter#117185) * 8a58ec5c3 Roll Flutter Engine from f79030440948 to c52b290813bd (29 revisions) (flutter/flutter#118720) * 374f09e1a [flutter_tools] No more implicit --no-sound-null-safety (flutter/flutter#118491) * ae1cc18c4 remove single-view assumption from `paintImage` (flutter/flutter#118721) * bb8b96a5d Fix path for require.js (flutter/flutter#118722) * c83a69855 update uikit view documentation (flutter/flutter#118715) * 2b3ca0dc4 Bump github/codeql-action from 2.1.38 to 2.1.39 (flutter/flutter#118735) * 666dccc85 [macOS] bringup new e2e_summary devicelab test (flutter/flutter#118717) * d07b88e4c Docs fix an=>a (flutter/flutter#118652) * 11d21e066 Add @pragma('vm:entry-point') to RestorableRouteBuilder arguments (flutter/flutter#118738) * 7d9eaab01 Appbar iconTheme override fix (flutter/flutter#118681) * 6f708305d Roll Flutter Engine from c52b290813bd to 290636c1cb6b (2 revisions) (flutter/flutter#118743) * b3059d2c0 Bump activesupport from 6.1.5 to 6.1.7.1 in /dev/ci/mac (flutter/flutter#118745) * ffcf63ae8 Add verbose flag to plugin_dependencies_test to debug flake (flutter/flutter#118755) * 2609212ca 2a11023c7 [ios_platform_view] more precision when determine if a clip rrect is necessary (flutter/engine#38965) (flutter/flutter#118751) * 21fb443a3 8ed6790b5 Bump chrome_and_driver version to 110. (flutter/engine#38986) (flutter/flutter#118758) * e5c9d065f Forgot to remove emulator flag. (flutter/flutter#118762) * 6a9b2db4a 95b0c151f Roll Dart SDK from 645fd748e79e to ddf70a598f27 (14 revisions) (flutter/engine#38990) (flutter/flutter#118763) * 0bbb5ec0c 40f7f0f09 Roll Fuchsia Mac SDK from P5QcCJU8I71xVXuMT... to tlYMsnCv86Fjt5LfF... (flutter/engine#38994) (flutter/flutter#118771) * d53cc4a10 [macOS] New e2e_summary benchmark fails without Cocoapods. (flutter/flutter#118754) * 3e71e0caf Updated `ListTile` documentation, add Material 3 example and other `ListTile` examples fixes. (flutter/flutter#118705) * 213b3cb3d Check whether slider is mounted before interaction, no-op if unmounted (flutter/flutter#113556) * 06909ccfa Update packages + fix tests for javascript mime change (flutter/flutter#118617) * 46c7fd14d 88e61d8bd Remove references to Observatory (flutter/engine#38919) (flutter/flutter#118793) * b9ab64049 Remove incorrect statement in documentation (flutter/flutter#118636) * ea36b3a5a Add focus detector to CupertinoSwitch (flutter/flutter#118345) * 9b5ea30a9 Switching over from iOS-15 to iOS-16 in .ci.yaml. (flutter/flutter#118807) * 67ffaef25 29a0582a1 Roll Fuchsia Mac SDK from tlYMsnCv86Fjt5LfF... to 6oiZwMyNsjucSxTHJ... (flutter/engine#39004) (flutter/flutter#118817) * 5cd2d4c61 Support iOS wireless debugging (flutter/flutter#118104) * cbf2e1689 Revert "Support iOS wireless debugging (#118104)" (flutter/flutter#118826) * 2258590a8 Do not run Mac_arm64_ios run_debug_test_macos in presubmit during iPhone 11 migration (flutter/flutter#118828) * 1dd7f45bf Add `build macos --config-only` option. (flutter/flutter#118649) * 22520f54d [macOS] Add timeline summary benchmarks (flutter/flutter#118748) * 99e4ca50c Roll Flutter Engine from 29a0582a1d5f to 78bbea005d27 (2 revisions) (flutter/flutter#118829) * c5ceff11d [flutter_tools] Ensure service worker starts caching assets since first load (flutter/flutter#116833) * 818bb4e65 Roll Flutter Engine from 78bbea005d27 to 26b6609c603b (3 revisions) (flutter/flutter#118839) * 09bd0f661 Support logging 'flutter run' communication to DAP clients (flutter/flutter#118674) * 73096fd96 [macos] add flavor options to commands in the `flutter_tool` (flutter/flutter#118421) * 030288d33 Revert "[macos] add flavor options to commands in the `flutter_tool` (#118421)" (flutter/flutter#118858) * 9acf34d0d Roll Flutter Engine from 26b6609c603b to 7d40e77d0035 (2 revisions) (flutter/flutter#118852) * ec51d3271 Remove unnecessary null checks in ‘dev/conductor’ (flutter/flutter#118843) * 54217bd4b Remove unnecessary null checks in `dev/benchmarks` (flutter/flutter#118840) * 98c18ca93 Remove unnecessary null checks in examples/ (flutter/flutter#118848) * 99b5262b2 Remove unnecessary null checks in dev/tools (flutter/flutter#118845) * 52d1205b8 Roll Flutter Engine from 7d40e77d0035 to 730e88fb6787 (3 revisions) (flutter/flutter#118869) * ee9c9b692 3876320cb Roll Skia from aedfc8695954 to 1b3aa8b6e1cc (43 revisions) (flutter/engine#39024) (flutter/flutter#118871) * 589f2eb9e d2436a536 Extract WideToUTF16String/UTF16StringToWide to FML (flutter/engine#39020) (flutter/flutter#118873) * 74645b43a Fix `NavigationBar` indicator ripple doesn't account for label height (flutter/flutter#117473) * f78b1f351 dfe67f4c7 Roll Skia from 1b3aa8b6e1cc to f6a5c806294d (11 revisions) (flutter/engine#39027) (flutter/flutter#118874) * 572f0a1a9 66e177a3d Roll Dart SDK from ddf70a598f27 to fbbfc122dba6 (9 revisions) (flutter/engine#39029) (flutter/flutter#118878) * 26472b59c ccccee513 [macos] Synthesize modifier keys events on pointer events (flutter/engine#37870) (flutter/flutter#118880) * 095b1abda Checkbox borderSide lerp bug fix (flutter/flutter#118728) * ec6ff90ab Roll Flutter Engine from ccccee513fb2 to d84b3dc74c9f (2 revisions) (flutter/flutter#118893) * 492d57262 Cleanup obsolete --compact-async compiler option (flutter/flutter#118894) * f291eb349 Remove unnecessary null checks in integration_test (flutter/flutter#118861) * ab3c82244 Remove unnecessary null checks in dev/devicelab (flutter/flutter#118842) * bf72f5ebf 58eb1061e Revert "Remove references to Observatory (#38919)" (flutter/engine#39035) (flutter/flutter#118899) * a07e8a6ac [reland] Support wireless debugging (flutter/flutter#118895) * Roll Flutter from a07e8a6ac43d to f33e8d3701b5 (24 revisions) (#7007) * 3c769effa Cupertino navbar ellipsis fix (flutter/flutter#118841) * d1be731c6 3fead63ba Roll Dart SDK from ac4c63168ff2 to 03d35455a8d8 (1 revision) (flutter/engine#39036) (flutter/flutter#118909) * c0ad6add2 Marks Mac plugin_test_macos to be unflaky (flutter/flutter#118706) * 83720015a Remove unnecessary null checks in flutter_test (flutter/flutter#118865) * 288a7733e Remove unnecessary null checks in flutter_driver (flutter/flutter#118864) * bb73121cb Remove unnecessary null checks in flutter/test (flutter/flutter#118905) * 15bc4e466 Marks Mac_android microbenchmarks to be flaky (flutter/flutter#118693) * 1cdaf9e33 e2c2e5009 [impeller] correct input order in ColorFilterContents::MakeBlend (flutter/engine#39038) (flutter/flutter#118913) * 49e025d8a Update android defines test to use emulator (flutter/flutter#118808) * bae4c1d24 Revert "Update android defines test to use emulator (#118808)" (flutter/flutter#118928) * 9837eb6fc Remove unnecessary null checks in flutter/rendering (flutter/flutter#118923) * 25843bdb5 Remove macOS impeller benchmarks (flutter/flutter#118917) * 70cecf6c9 Remove unnecessary null checks in dev/*_tests (flutter/flutter#118844) * c757df3bf Remove unnecessary null checks in dev/bots (flutter/flutter#118846) * 5d74b5cbf Remove unnecessary null checks in flutter/painting (flutter/flutter#118925) * 7272c809e Remove unnecessary null checks in `flutter/{animation,semantics,scheduler}` (flutter/flutter#118922) * 2baea2f62 7b68d71b8 Roll Dart SDK from 03d35455a8d8 to 807077cc5d1b (1 revision) (flutter/engine#39042) (flutter/flutter#118933) * 8d60a8c0b Roll Flutter Engine from 7b68d71b8d03 to 3a444b36657c (3 revisions) (flutter/flutter#118938) * 5ccdb8107 bb4e8dfe2 Roll Fuchsia Linux SDK from rPo4_TYHCtkoOfRup... to S6wQW1tLFe-YnReaZ... (flutter/engine#39048) (flutter/flutter#118942) * b1f4070d5 ef7b1856a Roll Dart SDK from 8c2eb20b5376 to 548678dd684c (1 revision) (flutter/engine#39049) (flutter/flutter#118944) * 80658873b Add transform flip (flutter/flutter#116705) * 68b6e720c 406dce64f Roll Fuchsia Mac SDK from ZTKDeVL1HDAwsZdhl... to l7jVM3Urw73TVWfee... (flutter/engine#39050) (flutter/flutter#118964) * cf628add4 aa194347a Roll Fuchsia Linux SDK from S6wQW1tLFe-YnReaZ... to l3c_b-vRr-o6ZFX_M... (flutter/engine#39055) (flutter/flutter#118968) * f33e8d370 2a2dfaafb Roll Fuchsia Mac SDK from l7jVM3Urw73TVWfee... to 5TQ9IL4-Yu3KHCR-H... (flutter/engine#39056) (flutter/flutter#118969) * [file_selector] add getDirectoryPaths implementation on Linux (#6573) * Add getDirectoriesPaths method to the file_selector_platform_interface Add getDirectoriesPaths to method channel. Increment version to 2.3.0 apply feedback extract assertion method * add getDirectoryPaths Linux implementation * apply rebase * update version to 0.9.1 Co-authored-by: eugerossetto * Roll Flutter from f33e8d3701b5 to bd7bee0f9eb8 (5 revisions) (#7010) * 044e344a7 a8522271c Roll Fuchsia Mac SDK from 5TQ9IL4-Yu3KHCR-H... to R4F4q-h902yt4s7ow... (flutter/engine#39058) (flutter/flutter#118984) * b974eac8e b3da52d8c Roll Fuchsia Linux SDK from l3c_b-vRr-o6ZFX_M... to f613tOkDB282hW2tA... (flutter/engine#39061) (flutter/flutter#118987) * 696a84b1e 1e4e11ad1 Add more flexible image loading API (flutter/engine#38905) (flutter/flutter#118989) * 6ae7ad72c 92313596d Roll Dart SDK from 548678dd684c to 608a0691a1d7 (1 revision) (flutter/engine#39063) (flutter/flutter#118990) * bd7bee0f9 Roll Flutter Engine from 92313596d77b to 8e7bc509e0d7 (3 revisions) (flutter/flutter#119004) * [sign_in]: Bump play-services-auth from 20.4.0 to 20.4.1 in /packages/google_sign_in/google_sign_in_android/android (#7008) * [sign_in]: Bump play-services-auth Bumps play-services-auth from 20.4.0 to 20.4.1. --- updated-dependencies: - dependency-name: com.google.android.gms:play-services-auth dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Bump version Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: camsim99 * [local_auth] Convert Windows to Pigeon (#7012) Updates `local_auth_windows` to use Pigeon, and moves to a more platform-tailored and Dart-centric implementation (rather than keeping the previous cross-platform method channel interface that the current implementation was duplicated from): - Eliminates `deviceSupportsBiometrics` from the platform interface, since it's always the same as `isDeviceSupported`, in favor of doing that mapping in Dart. - Eliminates `getEnrolledBiometrics` from the platform interface, since it was the same implementation as `isDeviceSupported` just with a different return value, in favor of doing that logic in Dart. - Moves throwing for the `biometricOnly` option to the Dart side, simplifying the native logic. Related changes: - Adds a significant amount of previously-missing Dart unit test coverage. - Removes the `biometricOnly` UI from the example app, since it will always fail. Part of https://github.com/flutter/flutter/issues/117912 * Roll Flutter from bd7bee0f9eb8 to c35efdaa6854 (24 revisions) (#7017) * 3bf79607d [web] Fix paths fetched by flutter.js (flutter/flutter#118684) * e71e8daa2 76998e529 Roll Fuchsia Linux SDK from f613tOkDB282hW2tA... to GLRbnjiO5SbZKX-Us... (flutter/engine#39067) (flutter/flutter#119009) * 71a42563d Revert "[Re-land] Button padding m3 (#118640)" (flutter/flutter#118962) * 90ffb1c65 94fc0728f Roll Dart SDK from c52810968747 to 107a1280a61f (1 revision) (flutter/engine#39069) (flutter/flutter#119010) * 224e6aa18 Remove unnecessary null checks in flutter/gestures (flutter/flutter#118926) * 6cd494554 Remove unnecessary null checks in flutter_web_plugins (flutter/flutter#118862) * a63e19ba0 Remove unnecessary null checks in flutter_localizations (flutter/flutter#118863) * 19dfde698 Remove unnecessary null checks in `flutter/{foundation,services,physics}` (flutter/flutter#118910) * 392dffeb0 Update the Linux Android defines test to use dimensions when selecting a build bot (flutter/flutter#118930) * 5e50ed972 Test Utf8FromUtf16 (flutter/flutter#118647) * edb571e49 Update README.md (flutter/flutter#118803) * 38630b6bd Remove unnecessary null checks in `flutter_tool` (flutter/flutter#118857) * 332aed9c8 Revert "Update the Linux Android defines test to use dimensions when selecting a build bot (#118930)" (flutter/flutter#119023) * 84071aa2a Add todo for linux defines test. (flutter/flutter#119035) * e8b7f4b20 [examples] Fix typo in `refresh_indicator` example (flutter/flutter#119000) * df4420835 Remove ThemeData.buttonColor references (flutter/flutter#118658) * 65486163a Remove animated_complex_opacity_perf_macos__e2e_summary bringup (flutter/flutter#118916) * 59767e5fc Remove unnecessary null checks in `flutter/material` (flutter/flutter#119022) * 1906ce5d4 7d3233d26 [web] Build multiple CanvasKit variants (using toolchain_args) (flutter/engine#38448) (flutter/flutter#119021) * 720bea026 Remove unnecessary null checks in `flutter/widgets` (flutter/flutter#119028) * 0de8bef74 Remove unnecessary null checks in flutter/cupertino (flutter/flutter#119020) * 2e8dd9dd6 Run integration_ui_test_test_macos in prod (flutter/flutter#118919) * 64b4c69bc roll pub deps and remove archive, crypto, typed_data from allow-list (flutter/flutter#119018) * c35efdaa6 Remove superfluous words. (flutter/flutter#119008) * Roll Flutter (stable) from 135454af3247 to b06b8b271095 (2551 revisions) (#7018) flutter/flutter@135454a...b06b8b2 * [ci] Update legacy Flutter version tests (#7019) * Update the N-1 and N-2 test configs * Update all minumim versions * changelog updates * Ignore deprecated member use on DecoderBufferCallback (#7014) * Ignore deprecated member use on DecoderBufferCallback In preparation for flutter/flutter#118966 * Bump version * [various] Enable use_build_context_synchronously (#6585) Enables the `use_build_context_synchronously` lint, and fixes violations. Part of https://github.com/flutter/flutter/issues/76229 * [path_provider] Fix iOS `getApplicationSupportDirectory` regression (#7026) When switching iOS to share the macOS implementation, the application support path for iOS accidentally changed because I forgot the macOS implementation added an extra subdirectory to it. This fixes that regression by returning iOS to the path `path_provider_ios` used. Fixes https://github.com/flutter/flutter/issues/119133 * [url_launcher] Convert Windows to Pigeon (#6991) * Initial definition matching current API * Rename, autoformat * Update native implementation and unit tests * Update Dart; remove unnecessary Pigeon test API * Version bump * autoformat * Adjust mock API setup * Improve comment * Roll Flutter from c35efdaa6854 to a815ee634202 (22 revisions) (#7025) * 373523184 Cleanup old Dart SDK layout compatibility (flutter/flutter#118819) * 4d250302a Add leak_tracker as dev_dependency. (flutter/flutter#118952) * e3c51a2f2 Add Windows unit tests to plugin template (flutter/flutter#118638) * d20dd9e4b Roll Flutter Engine from 7d3233d26d09 to 71ee5f19bc16 (15 revisions) (flutter/flutter#119081) * 5dabe102a Fix path name to discover debug apk on add2app builds (flutter/flutter#117999) * 50ed8a34b Enable `unnecessary_null_comparison` check (flutter/flutter#118849) * 455e6aca5 Test integration test apps' runner files against current template app (flutter/flutter#118646) * a788e1b31 Roll Flutter Engine from 71ee5f19bc16 to 59ea78bfabda (2 revisions) (flutter/flutter#119087) * c35370cf0 Roll Flutter Engine from 59ea78bfabda to 2499a5d9fca7 (2 revisions) (flutter/flutter#119089) * 2f0dd5673 Refactor highlight handling in FocusManager (flutter/flutter#119075) * 2759f3f0b Roll Flutter Engine from 2499a5d9fca7 to d98926c32ee7 (2 revisions) (flutter/flutter#119090) * 760fb2115 Roll Flutter Engine from d98926c32ee7 to bec40654a5d7 (2 revisions) (flutter/flutter#119093) * bbca694ef Roll Flutter Engine from bec40654a5d7 to 5405f2c26e85 (2 revisions) (flutter/flutter#119095) * 6414c3604 f1464b49c Manually roll ANGLE, vulkan-deps, SwiftShader (flutter/engine#38650) (flutter/flutter#119097) * 426cdd90c 55bb8deaf [Impeller] Linear sample atlas glyphs when the CTM isn't translation/scale only (flutter/engine#39112) (flutter/flutter#119098) * 83c3a61e3 Only emit image painting events in debug & profile modes. (flutter/flutter#118872) * b113df2dc bffb98352 Roll Skia from b72fececbdcc to 8ffd5c20d634 (3 revisions) (flutter/engine#39114) (flutter/flutter#119099) * 351466aea Add Decoding Flutter videos to API docs (flutter/flutter#116454) * 318f8758b Pass through magnifierConfiguration (flutter/flutter#118270) * eced23eab d39ab638b Roll Fuchsia Mac SDK from MUvFS0baOnigVUIND... to _H53AyDxR9Pm2TbwN... (flutter/engine#39122) (flutter/flutter#119126) * 29ab437e2 Add Material 3 `CheckboxListTile` example and update existing examples (flutter/flutter#118792) * a815ee634 8efc7183b Roll Skia from 8ffd5c20d634 to da5034f9d117 (4 revisions) (flutter/engine#39123) (flutter/flutter#119129) * [camerax] Adds functionality to bind UseCases to a lifecycle (#6939) * Copy over code from proof of concept * Add dart tests * Fix dart tests * Add java tests * Add me as owner and changelog change * Fix analyzer * Add instance manager fix * Update comment * Undo instance manager changes * Formatting * Fix analyze * Address review * Fix analyze * Add import * Fix assertion error * Remove unecessary this keywrod * Update packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.java Co-authored-by: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> Co-authored-by: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> * [tool/ci] Add minimum supported SDK validation (#7028) Adds options to `pubspec.yaml` to check that the minimum supported SDK range for Flutter/Dart is at least a given version, to add CI enforcement that we're updating all of our support claims when we update our tested versions (rather than it being something we have to remember to do), and enables it in CI. As part of enabling it, fixes some violations: - path_provider_foundation had been temporarily dropped back to 2.10 as part of pushing out a regression fix. - a number of examples were missing Flutter constraints even though they used Flutter. - the non-Flutter `plugin_platform_interface` package hadn't been update since I hadn't thought about Dart-only constraints in the past. * [camera] Add back Optional type for nullable CameraController orientations (#6911) * Add flag * Add missing comment * Add tests * Bump versions * Stage changelog changes * Revert "Fix examples analyze" This reverts commit 4db1858a29136f3fb07a223d94d7e68b6b8d4b7d. * Revert "[camera] Remove deprecated Optional type (#6870)" This reverts commit 3d8b73bf08bf746bcbcdd219eb87ced572cc529b. * Add back optional * Edit changelog * Fix semicolon * Add ) * [ci] Increase timeouts for platform_tests (#7036) `platform_tests` are our most time-consuming tests; 30 minutes isn't always enough to run them. Increase timeouts to 60 minutes so that we aren't getting timeouts from tests that are still running. Also disables Windows `platform_tests` for stable in presubmit, matching other platforms. * [google_sign_in] Add doc for iOS auth with SERVER_CLIENT_ID (#4747) * Add doc for iOS auth with SERVER_CLIENT_ID * Follow pub versioning guidelines * Remove incorrect documentation saying that `clientId` is only configurable on web. * Revert "Add doc for iOS auth with SERVER_CLIENT_ID" This reverts commit 477bd85693b4e546d97af46f01bd84a68d5876f0. * Adds documentation for iOS auth with SERVER_CLIENT_ID --------- Co-authored-by: stuartmorgan * Roll Flutter from a815ee634202 to 75680ae99e85 (58 revisions) (#7048) * a0f7c8cf7 6f806491e [web] use a render target instead of a new surface for Picture.toImage (flutter/engine#38573) (flutter/flutter#119143) * e85547b3c Roll Plugins from 11361d01099d to 8bab180a668a (28 revisions) (flutter/flutter#119115) * 81052a7d3 Add usage event to track when a iOS network device is used (flutter/flutter#118915) * cd34fa6d4 24aa324b8 Roll Skia from da5034f9d117 to c4b171fe5668 (1 revision) (flutter/engine#39127) (flutter/flutter#119159) * 6cd4fa45e Add --serve-observatory flag to run, attach, and test (flutter/flutter#118402) * 48cd95dd1 1e5efd144 [various] Enable use_build_context_synchronously (flutter/plugins#6585) (flutter/flutter#119162) * b907acdde Add the cupertino system colors mint, cyan, and brown (flutter/flutter#118971) * c6fa5d957 c54580138 Only build analyze_snapshot on Linux host (flutter/engine#39129) (flutter/flutter#119164) * f34ce86cf 7b72038ef Roll Fuchsia Linux SDK from E9m-Gk382PkB7_Nbp... to pGX7tanT1okL8XCg-... (flutter/engine#39130) (flutter/flutter#119169) * 0dd63d331 Export View (flutter/flutter#117475) * 1fd71de0c Remove superfluous words from comments (flutter/flutter#119055) * cef9cc717 2e7d6fa7b Remove unnecessary null checks (flutter/engine#39113) (flutter/flutter#119174) * 3be330aaf 30c02e4c8 [Impeller] Make text glyph offsets respect the current transform (flutter/engine#39119) (flutter/flutter#119179) * a45727d81 Add MediaQuery to View (flutter/flutter#118004) * 02a9c151f Fix lexer issue where select/plural/other/underscores cannot be in identifier names. (flutter/flutter#119190) * 766e4d28a Remove single-view assumption from material library (flutter/flutter#117486) * dcd367951 Roll Flutter Engine from 30c02e4c8b01 to 44362c90fcec (2 revisions) (flutter/flutter#119185) * 9037e3fe2 roll packages (flutter/flutter#119192) * e0e88da15 Roll Flutter Engine from 44362c90fcec to 308ce918f67f (2 revisions) (flutter/flutter#119201) * 202e90274 Roll Flutter Engine from 308ce918f67f to 8f1e5dc1b124 (4 revisions) (flutter/flutter#119208) * b319938ec Add more flexible image API (flutter/flutter#118966) * fc0270181 Marks Mac run_debug_test_macos to be unflaky (flutter/flutter#117470) * c9affdba9 Move windows-x64-flutter.zip to windows-x64-debug location. (flutter/flutter#119177) * 7d3b762df Fix: Added `margin` parameter for `MaterialBanner` class (flutter/flutter#119005) * 40bd82ef6 Roll Plugins from 1e5efd144f93 to e9406bc209a2 (4 revisions) (flutter/flutter#119249) * 07522b74e Roll Flutter Engine from 8f1e5dc1b124 to 04f22beebb42 (5 revisions) (flutter/flutter#119218) * 459c1b78b Marks Mac complex_layout_scroll_perf_macos__timeline_summary to be unflaky (flutter/flutter#119157) * 2b8f2d050 Add API for discovering assets (flutter/flutter#118410) * a04ab7129 Revert "Add API for discovering assets (#118410)" (flutter/flutter#119273) * 1da487dfb Roll Flutter Engine from 04f22beebb42 to 93901260098e (12 revisions) (flutter/flutter#119279) * 1b779b655 Roll Flutter Engine from 93901260098e to be0125bd5716 (2 revisions) (flutter/flutter#119283) * 42bd5f2bd Download platform-agnostic Flutter Web SDK in the flutter_tool (flutter/flutter#118654) * d52b6b989 Roll Flutter Engine from be0125bd5716 to d17004dd96d7 (2 revisions) (flutter/flutter#119287) * 4aed487ca Roll Flutter Engine from d17004dd96d7 to a63d98feb608 (3 revisions) (flutter/flutter#119299) * 05fc29fe7 Rename DeviceGestureSettings.fromWindow to DeviceGestureSettings.fromView (flutter/flutter#119291) * 86ab01d2b Revert "Add --serve-observatory flag to run, attach, and test (#118402)" (flutter/flutter#119302) * 8d03af342 Roll Flutter Engine from a63d98feb608 to 79c958fc7e9b (3 revisions) (flutter/flutter#119306) * 27f8ebdae ade610ec8 [fuchsia] Migrate to new RealmBuilder API (flutter/engine#39175) (flutter/flutter#119310) * c31856bc4 Roll Plugins from e9406bc209a2 to ff84c44a5ddb (2 revisions) (flutter/flutter#119335) * d939863a2 Roll Flutter Engine from ade610ec88b5 to 621e13cc9be3 (3 revisions) (flutter/flutter#119344) * 0b5759671 Run "flutter update-packages --force-upgrade" (flutter/flutter#119340) * 0417f6621 Fix nullability of TableRow.children (flutter/flutter#119285) * fc3e8243c Roll Flutter Engine from 621e13cc9be3 to 189a69d9918d (3 revisions) (flutter/flutter#119347) * ad1a44d0a Add `requestFocusOnTap` to `DropdownMenu` (flutter/flutter#117504) * 4dbb573ff [flutter_tools] remove usage of remap samplers arg (flutter/flutter#119346) * b2f2bf31c Marks Linux run_release_test_linux to be unflaky (flutter/flutter#119156) * 3f95befe5 Roll Flutter Engine from 189a69d9918d to b32fc7fef208 (3 revisions) (flutter/flutter#119358) * 2e8bebd17 Remove single window assumption from macrobenchmark (flutter/flutter#119368) * ab2232a18 Roll Flutter Engine from b32fc7fef208 to 8567d96993ed (5 revisions) (flutter/flutter#119369) * e9ca9cc14 Remove references to dart:ui's window singelton (flutter/flutter#119296) * da3d4bd0e Roll Flutter Engine from 8567d96993ed to 225ae87334a5 (2 revisions) (flutter/flutter#119376) * 018c1f84a e2e089ebb Use arm64 engine variant on simulators in iOS unit tests (flutter/engine#39213) (flutter/flutter#119387) * e349bdc1a 19651cb1d Roll Dart SDK from 2cd9b7ac95e8 to 135f4c51c9ff (3 revisions) (flutter/engine#39214) (flutter/flutter#119389) * 95345b516 77bee011d Roll Dart SDK from 2cd9b7ac95e8 to 135f4c51c9ff (3 revisions) (flutter/engine#39217) (flutter/flutter#119394) * de43ec977 Roll Flutter Engine from 77bee011dabf to 3394b84cc5d7 (3 revisions) (flutter/flutter#119405) * f8d4de488 3dd0fc13f Roll Fuchsia Linux SDK from 6c2H32X3EXOGlWIgb... to TiK_fVODtUaKOgxRf... (flutter/engine#39224) (flutter/flutter#119408) * 785641160 7c5c6c9c9 Roll Skia from 0b75650caf2a to 7df7a83f733d (13 revisions) (flutter/engine#39225) (flutter/flutter#119413) * 75680ae99 649362168 Roll Dart SDK from f9583e13e214 to 52dc94238144 (1 revision) (flutter/engine#39227) (flutter/flutter#119416) * [camera]: Bump camerax_version from 1.3.0-alpha02 to 1.3.0-alpha03 in /packages/camera/camera_android_camerax/android (#7061) * [camera]: Bump camerax_version Bumps `camerax_version` from 1.3.0-alpha02 to 1.3.0-alpha03. Updates `camera-core` from 1.3.0-alpha02 to 1.3.0-alpha03 Updates `camera-camera2` from 1.3.0-alpha02 to 1.3.0-alpha03 Updates `camera-lifecycle` from 1.3.0-alpha02 to 1.3.0-alpha03 --- updated-dependencies: - dependency-name: androidx.camera:camera-core dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: androidx.camera:camera-camera2 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: androidx.camera:camera-lifecycle dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update changelog * Bump Kotlin version --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: camsim99 * [ci] Add LUCI versions of macOS ARM tests (#6984) Adds a macOS arm64 configuration, and adds LUCI versions of the remaining Cirrus macOS-host tests (iOS build-all and macOS platform tests) in bringup mode, to begin testing a LUCI migration for the ARM tests in this repository. * [tool] Improve main-branch detection (#7038) * [tool] Improve main-branch detection Currently main-branch detection for `--packages-for-branch` looks at branch names, but this no longer works on LUCI which now seems to be checking out specific hashes rather than branches. This updates the behavior so that it will treat any hash that is an ancestor of `main` as being part of `main`, which should allow post-submit detection to work under LUCI. Fixes https://github.com/flutter/flutter/issues/119330 * Fix throw * Fix typos * Update comment * [in_app_purchase] Prep for more const widgets (#7030) * [ci] Switch remaining macOS host tests to LUCI (#7063) * [ci] Switch remaining macOS host tests to LUCI Enables the new LUCI versions of the remaining Cirrus macOS host tests, and removes the Cirrus versions. This completes the macOS LUCI transition for flutter/plugins, leaving only Linux on Cirrus. * standardize naming as macos_ * [ci] Part 1 of swapping iOS platform test arch (#7064) * [ci] Part 1 of swapping iOS task arch Adds Intel versions of iOS build-all and ARM versions of iOS platform tests, as part one of swapping them. Once the new tasks propagate, they will be brought out of bringup mode and the old versions removed. These were on the opposite architectures because of issues with running the platfor tests on Cirrus ARM VMs, and then were ported as-is from Cirrus to LUCI, but now that macOS ARM works on LUCI we can switch to the desired configuration. * Increase sharding * Renaming * Also do stable * [camerax] Add system services to plugin (#6986) * Add base code from proof of concept * Add pigeon types * Make corrections, add Dart tests * Add test files * Add java tests * Add docs * Add stop, docs, fix analyze * Fix comment * Add comment and remove literals * Formatting * Remove merge conflict reside * Fix test * Fix bad pasting job * [webview]: Bump mockito-inline (#7056) Bumps [mockito-inline](https://github.com/mockito/mockito) from 4.8.0 to 5.1.0. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.8.0...v5.1.0) --- updated-dependencies: - dependency-name: org.mockito:mockito-inline dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [webview_flutter_wkwebview][webview_flutter_android] Fixes bug where the `WebView`s could not be released (#6996) * fix and test non disposing webview * combine boolean * version bump * more pumpAndSettles * add more pumpAndSettles * update int tests * android test * fix version bump and unchange compile files * make _currentNavigationDelegate nullable * quick fix * accidental letter * small tests fixes * missing hashtag * [camerax] Allow instance manager to create identical objects (#7034) * Make changes amd add test * Update changelog * Add comment * Fix spelling * Update packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/InstanceManager.java Co-authored-by: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> --------- Co-authored-by: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> * [ci] Increase heavy workload memory (#7065) Android builds are increasingly running into OOM errors; this increases the machine configuration from 12GB to 16GB to hopefully avoid the issue. * [various] Update to use sharedDarwinSource (#7027) * Update shared_preferences * Update IAP * [various] Standardize the extension for Pigeon-generated Dart (#7029) * Standardize on .g.dart * Remove unused exclusion patterns * Mark pigeons/ as dev-only in the tooling * Version bumps * Add missed files * More new import fixes * Roll Flutter from 75680ae99e85 to 0a22a1dbf475 (3 revisions) (#7051) * e8c0e467d Roll Flutter Engine from 649362168faa to 153a33dad61b (2 revisions) (flutter/flutter#119433) * 1b2edb8aa dd3e975f3 Roll Fuchsia Mac SDK from 1TFy9RSFMfNy7JpQU... to 9y7C2oamTv6Py4JSC... (flutter/engine#39233) (flutter/flutter#119446) * 0a22a1dbf Roll Flutter Engine from dd3e975f3188 to 49ea2123a1a9 (2 revisions) (flutter/flutter#119454) * [tool] More main-branch detection improvement (#7067) Follow-up to https://github.com/flutter/plugins/pull/7038 to try to make it work on LUCI. Looking at the checkout steps of a LUCI run, it looks like we do a full `fetch` of `origin`, but likely only have a `master` branch locally. Rather than rely on a specific local branch name existing, this allows for checking `origin` (and just in case, since it's another common remote name, `upstream`). Hopefully fixes https://github.com/flutter/flutter/issues/119330 * [ci] Clean up analysis options (#7068) Removes some options that are no longer necessary, further aligning the options with flutter/packages. Part of https://github.com/flutter/flutter/issues/113764 * [in_app_puchase_storekit] handle `appStoreReceiptURL` is nil (#7069) * handle nil * versio update * prepare for TestDefaultBinaryMessengerBinding.instance becoming non-nullable (#6847) * Roll Flutter from 0a22a1dbf475 to d27880801435 (58 revisions) (#7078) * 472b887d5 0ec8e2802 Roll Fuchsia Mac SDK from 9y7C2oamTv6Py4JSC... to EAFnGijD0l5QxaPxF... (flutter/engine#39236) (flutter/flutter#119461) * 15cd00f1e a7bb0e410 Roll Fuchsia Linux SDK from 1D63BqURfJdG4r3CK... to xTXbcsPr5GJvFSLha... (flutter/engine#39238) (flutter/flutter#119482) * 530c3f2d1 [Re-land#2] Button padding M3 (flutter/flutter#119498) * 17eb2e8ae Ability to disable the browser's context menu on web (flutter/flutter#118194) * df8ad3d2c roll packages (flutter/flutter#119370) * b68cebd9c roll packages (flutter/flutter#119530) * 59d80dc87 [Android] Add explicit exported tag to Linux_android flavors test (flutter/flutter#117542) * 458b298f9 Refactoring to use `ver` command instead of `systeminfo` (flutter/flutter#119304) * 54405bfa3 fixes PointerEventConverter to handle malformed scrolling event (flutter/flutter#118124) * e69ea6dee Support flipping mouse scrolling axes through modifier keys (flutter/flutter#115610) * 92df6b4bc 396c7fd0b Reland "Remove references to Observatory (#38919)" (flutter/engine#39139) (flutter/flutter#119546) * 7477d7ac7 Reland "Add --serve-observatory flag to run, attach, and test (#118402)" (flutter/flutter#119529) * 6c12e3994 Introduce ParagraphBoundary subclass for text editing (flutter/flutter#116549) * b227df308 Hint text semantics to be excluded in a11y read out if hintText is not visible. (flutter/flutter#119198) * 18c7f8a27 Fix typo in --machine help text (flutter/flutter#119563) * 329f86a90 Make a few values non-nullable in cupertino (flutter/flutter#119478) * c4520bc8b b2efe0175 [web] Expose felt flag for building CanvasKit Chromium (flutter/engine#39201) (flutter/flutter#119567) * 8898f4f19 Marks Mac_android run_debug_test_android to be unflaky (flutter/flutter#117468) * 1f0b6fbd7 Remove deprecated AppBar/SliverAppBar/AppBarTheme.textTheme member (flutter/flutter#119253) * edaeec8ed Roll Flutter Engine from b2efe01754ef to 5011144c0b46 (3 revisions) (flutter/flutter#119578) * 865dc5c51 Roll Flutter Engine from 5011144c0b46 to daa8eeb7fc0b (2 revisions) (flutter/flutter#119584) * 1148a2a8b Migrate EditableTextState from addPostFrameCallbacks to compositionCallbacks (flutter/flutter#119359) * 234090253 Roll Flutter Engine from daa8eeb7fc0b to 77218818138f (3 revisions) (flutter/flutter#119586) * 65900b71b Remove deprecated AnimatedSize.vsync parameter (flutter/flutter#119186) * 5b6572f96 Add debug diagnostics to channels integration test (flutter/flutter#119579) * 504e5652f Roll Flutter Engine from 77218818138f to 9448f2966c11 (3 revisions) (flutter/flutter#119592) * 7ba440655 Revert "[Re-land#2] Button padding M3 (#119498)" (flutter/flutter#119597) * 2c34a88eb Roll Flutter Engine from 9448f2966c11 to 72abe0e4b828 (3 revisions) (flutter/flutter#119603) * df0ab40ec Roll Plugins from ff84c44a5ddb to 9da327ca39c4 (15 revisions) (flutter/flutter#119629) * 67d07a6de [flutter_tools] Fix parsing of existing DDS URIs from exceptions (flutter/flutter#119506) * d272a3ab8 Reland: [macos] add flavor options to tool commands (flutter/flutter#119564) * a16d82cba aa00da3c1 Roll Skia from fc31f43cc40a to 3c6eb76a683a (1 revision) (flutter/engine#39280) (flutter/flutter#119605) * f6b0c6dde Use first Dart VM Service found with mDNS if there are duplicates (flutter/flutter#119545) * d4c74858f Make Decoration.padding non-nullable (flutter/flutter#119581) * 2fccf4d47 Remove MediaQuery from WidgetsApp (flutter/flutter#119377) * 9b3b9cf08 Roll Flutter Engine from aa00da3c1612 to cd2e8885e491 (6 revisions) (flutter/flutter#119639) * 6a5405925 Make MultiChildRenderObjectWidget const (flutter/flutter#119195) * e2b3d89e7 Fix CupertinoNavigationBar should create a backward compatible Annota… (flutter/flutter#119515) * 7bf95f41e 1aaf3db31 Roll Dart SDK from 4fdbc7c28141 to 9bcc1773ebf0 (1 revision) (flutter/engine#39290) (flutter/flutter#119640) * 0e22aca78 Add support for image insertion on Android (flutter/flutter#110052) * ff22813b7 separatorBuilder can't return null (flutter/flutter#119566) * 60c1f293d 2471f430f Update buildroot to c02da5072d1bb2. (flutter/engine#39292) (flutter/flutter#119645) * fbe9ff33e Disable an inaccurate test assertion that will be fixed by an engine roll (flutter/flutter#119653) * 8f90e2a7d Roll Flutter Engine from 2471f430ff4b to bb7b7006f4a3 (2 revisions) (flutter/flutter#119655) * 388438141 Make gen-l10n error handling independent of logger state (flutter/flutter#119644) * 198a51ace Migrate the Material Date pickers to M3 Reprise (flutter/flutter#119033) * dc8656594 Roll Flutter Engine from bb7b7006f4a3 to 521b975449ba (4 revisions) (flutter/flutter#119670) * 82df23539 Undo making Flex,Row,Column const (flutter/flutter#119669) * 6f9a896d7 Roll Flutter Engine from 521b975449ba to 38913c5484cf (2 revisions) (flutter/flutter#119675) * 8d0af3679 🥅 Produce warning instead of error for storage base url overrides (flutter/flutter#119595) * 3894d2481 1703a3966 Roll Skia from c29211525dac to 654f4805e8b8 (21 revisions) (flutter/engine#39309) (flutter/flutter#119683) * a752c2f15 Expose enableIMEPersonalizedLearning on CupertinoSearchTextField (flutter/flutter#119439) * e1f0b1d14 d92e23cb5 Roll Skia from 654f4805e8b8 to da41cf18f651 (1 revision) (flutter/engine#39311) (flutter/flutter#119686) * 97d273ce1 CupertinoThemeData equality (flutter/flutter#119480) * 416783503 5b549950f Roll Fuchsia Linux SDK from 71lEeibIyrq0V8jId... to TFcelQ5SwrzkcYK2d... (flutter/engine#39312) (flutter/flutter#119688) * b4a6e349a 0d87b1562 Roll Dart SDK from 8b57d23a7246 to de03e1f41b50 (1 revision) (flutter/engine#39313) (flutter/flutter#119695) * 3af30ff59 Roll Flutter Engine from 0d87b156265c to c08a286d60e9 (3 revisions) (flutter/flutter#119706) * d27880801 [Re-land] Exposed tooltip longPress (flutter/flutter#118796) * [various] prepare for more const widgets (#7074) * Change google_sign_in_ios and image_picker_ios owners (#7070) * Update README.md (#7076) * [ci] More cirrus.yml pre-alignment with flutter/packages (#7079) * Rename CI steps/templates to match the more generic naming in flutter/packages * Move Dart unit tests to heavy workload, matching flutter/packages * Tweak outdated comment * Update LUCI script reference to moved file * [camera_android] Default to legacy recording profile when EncoderProfiles unavailable (#7073) * Make changes, start test * Bump versions * Add test * Formatting * Add issue * Fix test * Address review * Roll Flutter from d27880801435 to c5e8757fcb79 (54 revisions) (#7092) * 254a796bc Revert "Reland "Add --serve-observatory flag to run, attach, and test (#118402)" (#119529)" (flutter/flutter#119729) * 1573c1296 Marks Mac_android microbenchmarks to be unflaky (flutter/flutter#119727) * 5613ab010 remove unnecessary parens (flutter/flutter#119736) * 1305a509d Roll Flutter Engine from c08a286d60e9 to ba3adb74d952 (5 revisions) (flutter/flutter#119741) * 484d881f2 [conductor] update console link (flutter/flutter#118338) * 73124dcd3 Fix `ListTileThemeData.copyWith` doesn't override correct properties (flutter/flutter#119738) * bc8927c7d 2b8dd4e5c Use Windows high contrast black/white theme with `MaterialApp` themes (flutter/engine#39206) (flutter/flutter#119747) * 578edfc85 Catch errors thrown while handling pointer events (flutter/flutter#119577) * 8fd5d4ebb Remove deprecated SystemNavigator.routeUpdated method (flutter/flutter#119187) * 7a926dcb0 Deprecate MediaQuery[Data].fromWindow (flutter/flutter#119647) * cd118dada Update a test expectation that depended on an SkParagraph fix (flutter/flutter#119756) * 4ae2d3b6d 🔥 Do not format the messages file for `gen-l10n` (flutter/flutter#119596) * 8f5949eda Roll Flutter Engine from 2b8dd4e5c699 to a12d102773dd (2 revisions) (flutter/flutter#119758) * 4c99da6c5 Avoid printing blank lines between "Another exception was thrown:" messages. (flutter/flutter#119587) * 2ecf4ae96 Update the counter app to enable Material 3 (flutter/flutter#118835) * 0b8486dda 29d09845f Roll Dart SDK from b47964e5d575 to 1c219a91e637 (1 revision) (flutter/engine#39324) (flutter/flutter#119763) * ca7b7e3b0 Roll Flutter Engine from 29d09845f21e to 97d27ff59459 (2 revisions) (flutter/flutter#119765) * 475fc4ac9 Run Mac hostonly tests on any available arch (flutter/flutter#119762) * 322d10e1b 18118e10a Add iOS spring animation objc files (flutter/engine#38801) (flutter/flutter#119768) * f767f860e check if directory exists before listing content (flutter/flutter#119748) * 34730c79c Revert "Run Mac hostonly tests on any available arch (#119762)" (flutter/flutter#119784) * f9daa9aac Roll Flutter Engine from 18118e10a0a5 to 679c4b42e222 (4 revisions) (flutter/flutter#119783) * fd76ef0f2 Reland "Add API for discovering assets" (flutter/flutter#119277) * ca0596e41 Fix `pub get --unknown-flag` (flutter/flutter#119622) * 9abb6d707 Roll Plugins from 9da327ca39c4 to 9302d87ee545 (11 revisions) (flutter/flutter#119825) * 9eafbcc8b Roll Flutter Engine from 679c4b42e222 to 388890a98e5b (6 revisions) (flutter/flutter#119830) * 1ee87990d Revert "[Re-land] Exposed tooltip longPress (#118796)" (flutter/flutter#119832) * 07b51a0db Add missing variants and *new* indicators to `useMaterial3` docs (flutter/flutter#119799) * 201380ab2 e9e601c7c [web] Hide autofill overlay (flutter/engine#39294) (flutter/flutter#119833) * 1c0065c8d 27f55219d Roll Fuchsia Linux SDK from QxkjqmRgowkk_n2NZ... to pWloCaRzjLEAUvQEz... (flutter/engine#39339) (flutter/flutter#119838) * e7d934a01 Marks Linux_android android_choreographer_do_frame_test to be flaky (flutter/flutter#119721) * 3f986e423 Roll Flutter Engine from 27f55219df79 to ae38c9585a61 (2 revisions) (flutter/flutter#119840) * b0f1714b7 Make Flex,Row,Column const for real (flutter/flutter#119673) * d4b689847 [web] Put all index.html operations in one place (flutter/flutter#118188) * d63987f71 Parser machine logs (flutter/flutter#118707) * 22bbdf03e Android defines target update (flutter/flutter#119766) * 8387c2388 [flutter_tools] Use base DAP detach and ensure correct output (flutter/flutter#119076) * d820aec78 Manual pub roll with dwds fix (flutter/flutter#119575) * 72f9cf548 Roll Flutter Engine from ae38c9585a61 to 616ecd8be3de (3 revisions) (flutter/flutter#119859) * cfdc35859 roll packages (flutter/flutter#119865) * d875899a6 Bump test Chrome version for Mac arm support (flutter/flutter#119773) * 9b86a4853 Fix gets removedItem instead of its index (flutter/flutter#119638) * 66b2ca638 Roll Flutter Engine from 616ecd8be3de to 2871970337df (3 revisions) (flutter/flutter#119870) * c6264605d Make `_focusDebug` not interpolate in debug mode (flutter/flutter#119680) * a27802e2d flutter_tool: remove explicit length header in HTTP response (flutter/flutter#119869) * 3570cce58 Remove deprecated kind in GestureRecognizer et al (flutter/flutter#119572) * bc45b1858 2696fff87 Roll Skia from c2d81db3ef41 to 4f0166baf5a4 (13 revisions) (flutter/engine#39348) (flutter/flutter#119879) * f3effce63 Roll Flutter Engine from 2696fff8716d to e3fe6dade964 (3 revisions) (flutter/flutter#119892) * 69421c168 [framework] use shader tiling instead of repeated calls to drawImage (flutter/flutter#119495) * 6b83eff56 Roll Flutter Engine from e3fe6dade964 to 655530e3fd15 (5 revisions) (flutter/flutter#119905) * a5d8a4a72 67d35267c [Impeller] Use minimal coverage for stencil restores after overdraw prevention (flutter/engine#39358) (flutter/flutter#119910) * fc8ea5620 0fb48ce5b Roll Dart SDK from 69452c5012d9 to be795cc64bd7 (1 revision) (flutter/engine#39360) (flutter/flutter#119926) * e0b2138ba Dispose OverlayEntry in TooltipState. (flutter/flutter#117291) * c5e8757fc Add M3 support for iconbuttons in error state in TextFields (flutter/flutter#119925) * Roll Flutter (stable) from b06b8b271095 to 7048ed95a5ad (5 revisions) (#7091) * 7999584b9 [release] use current branch as opposed to master (flutter/flutter#119648) * 9271eeba1 Switching over from iOS-15 to iOS-16 in .ci.yaml. (#118807) (flutter/flutter#118963) * c96f14ad3 Support safe area and scrolling in the NavigationDrawer (#116995) (flutter/flutter#119555) * ff6e9d4fa CP: Throw error when plural case had undefined behavior (#116622) (flutter/flutter#119384) * 7048ed95a Update Engine revision to 800594f1f4a6674010a6f1603c07a919b4d7ebd7 for stable release 3.7.1 (flutter/flutter#119687) * [in_app_pur]: Bump billing from 5.0.0 to 5.1.0 in /packages/in_app_purchase/in_app_purchase_android/android (#6701) * [in_app_pur]: Bump billing Bumps billing from 5.0.0 to 5.1.0. --- updated-dependencies: - dependency-name: com.android.billingclient:billing dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Add annotation and modify changelog --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: camsim99 * Roll Flutter from c5e8757fcb79 to b8f5394a5ca6 (22 revisions) (#7105) * 7177c413a Add Material 3 `RadioListTile` example and update existing examples (flutter/flutter#119716) * 96c8c6974 Roll Flutter Engine from 0fb48ce5b118 to c39047ffb2a6 (2 revisions) (flutter/flutter#119939) * 00b0d550c Fix iOS context menu position when flipped below (flutter/flutter#119565) * b65ae62cf Roll Flutter Engine from c39047ffb2a6 to 745d7efb5736 (3 revisions) (flutter/flutter#119943) * be4c8c0eb 33d932efc Add gen_snapshot to windows flutter artifact. (flutter/engine#39353) (flutter/flutter#119951) * 9a7e18701 [flutter_tools] fix Cannot delete file ENOENT from fuchsia_asset_builder (flutter/flutter#119867) * 16f81e656 Roll Flutter Engine from 33d932efc68e to 110c643d6ac2 (3 revisions) (flutter/flutter#119957) * 51b05ac7e Add mac_benchmark ci.yaml property (flutter/flutter#119871) * 3f02d4b4c Tweak to floating-cursor-end behaviour (flutter/flutter#119893) * c52215e93 Run Mac hostonly tests on any available arch (flutter/flutter#119884) * c24904dde Run macOS benchmarks in prod pool to upload metrics (flutter/flutter#119963) * 0fbef4693 e1b265bb5 Roll Skia from 07a95bb37760 to 83a3d8b16c94 (5 revisions) (flutter/engine#39373) (flutter/flutter#119967) * 909dc3009 Verify Mac artifact codesigning on x64 and arm64 (flutter/flutter#119971) * 57fd50f84 Fix unable to find bundled Java version (flutter/flutter#119244) * f7c2bd05f Revert "Fix unable to find bundled Java version (#119244)" (flutter/flutter#119981) * c8e75a8df Do not run customer testing on release candidate branches. (flutter/flutter#119979) * 5241d38fa Roll Flutter Engine from e1b265bb52aa to 1b132e44194d (8 revisions) (flutter/flutter#119980) * 61f6a0bcd Roll Flutter Engine from 1b132e44194d to c7a4bbab0e75 (6 revisions) (flutter/flutter#119990) * f10e625eb De-flake adapter integration test (flutter/flutter#120016) * 5187b45e4 Roll Flutter Engine from c7a4bbab0e75 to 6bd500c38ea8 (2 revisions) (flutter/flutter#120018) * 2e39badf9 Roll Flutter Engine from 6bd500c38ea8 to 2a104cdfcdf8 (2 revisions) (flutter/flutter#120022) * b8f5394a5 [flutter_tools] Fix Future error handling ArgumentError in doctor --android-licenses (flutter/flutter#119977) * [gh_actions]: Bump actions/upload-artifact from 3.1.1 to 3.1.2 (#6936) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3.1.1 to 3.1.2. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/83fd05a356d7e2593de66fc9913b3002723633cb...0b7f8abb1508181956e8e162db84b466c27e18ce) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [image_picker] GIF files will animate without permissions. PNG and GIF files will retain their image type if missing permissions. (#7084) * Fix GIF files not animating if permission not given. Fix PNG files getting convert to JPG if permission not given * updated changelog and pubspec, added a comment * [camera] flip/change camera while recording - platform interface (#7011) * platform interface changes pr * changes version to 2.4 * fixes versioning --------- Co-authored-by: BradenBagby * Roll Flutter from b8f5394a5ca6 to 3c3c9a1bd98f (3 revisions) (#7107) * bbca7ff69 Add Material 3 `SwitchListTile` example and update existing examples (flutter/flutter#119714) * 47a067465 Reland "Add --serve-observatory flag to run, attach, and test (#118402)" (flutter/flutter#119737) * 3c3c9a1bd [M3] Add ListTile's iconColor property support for icon buttons (flutter/flutter#120075) * [gh_actions]: Bump github/codeql-action from 2.1.37 to 2.2.1 (#7059) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2.1.37 to 2.2.1. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/959cbb7472c4d4ad70cdfe6f4976053fe48ab394...3ebbd71c74ef574dbc558c82f70e52732c8b44fe) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [camerax] Wrap methods necessary for preview implementation (#7046) * Add code needed from proof of concept * Add test files, delete unecessary method * Add tests, remove unecessary code * Fix analyze * Update changelog * Cleanup: * Cleanup and add switch * Finish todo * Add onCameraError * Fix pigeon file * Add method for releasing flutter texture and cleanup surface logic * Add test for release method * Add dart test * Update changelog * Modify flutter api names to avoid stack overflow * Cleanup * Fix tests * Delete space * Address review 1 * Update switch * Add annotations and constants in tests * Reset verification behavior * [local_auth]: Bump core from 1.8.0 to 1.9.0 in /packages/local_auth/local_auth_android/android (#6393) * [local_auth]: Bump core Bumps core from 1.8.0 to 1.9.0. --- updated-dependencies: - dependency-name: androidx.core:core dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Bump compilesdkversion * Bump fragment and update changelog * Bump gradle version * Bump compileSdkVersion * Bump all plugins compilesdkversion * Bump gradle version of example apps * Update changelog --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: camsim99 * [webview_flutter_web] Avoids XHR when possible. (#7090) * update * Request body must be empty too to skip XHR request. Add test. * Add ContentType class to parse response headers. * Use content-type in response to encode iframe contents. * Attempt to run integration_tests. Do they ever fail? * Update docs. * Set Widget styles in a way the flutter engine likes. * Add bash to codeblocks in readme. --------- Co-authored-by: David Iglesias Teixeira * [Espresso] Update expressio dependencies (#7108) * Update gradle and gson dependencies * Update changelog and version * Modify tense to follow changelog style * [url_launcher_ios] Update minimum Flutter version to 3.3 and iOS 11 (#7110) * [url_launcher_ios] Update minimum Flutter version to 3.3 and iOS 11 * super * Redistribute ownership of cross-platform plugin components (#7093) * [local_auth_android] update java complie sdk version to green tree (#7121) * update java complie sdk version * again * Update release tooling to give a workaround for predictable failing case https://github.com/flutter/flutter/issues/120116 (#7111) * Roll Flutter from 3c3c9a1bd98f to e8eac0d047cd (21 revisions) (#7122) * 1bec87b4a Update Android TESTOWNERS (flutter/flutter#119960) * 7bf1e99ea Roll Plugins from 9302d87ee545 to d065e4e0a82a (6 revisions) (flutter/flutter#120084) * 40b5e4cb5 Added "insertAll" and "removeAll" methods to AnimatedList (flutter/flutter#115545) * ec524ed06 Fix flutter_tools stuck when using custom LLDB prompt (flutter/flutter#119443) * 575ced6c5 Fix context menu web examples (flutter/flutter#120104) * e627e8d84 Force web_tool_tests to run on x64 builders (flutter/flutter#120128) * e62abfae6 Remove unreachable_from_main linter rule (flutter/flutter#120110) * 71971f223 Run `verify_binaries_codesigned` task on release branches (flutter/flutter#120141) * 845f7bb42 Roll Flutter Engine from 2a104cdfcdf8 to 165126e7034c (13 revisions) (flutter/flutter#120150) * cf3fc0177 remove deprecated accentTextTheme and accentIconTheme from ThemeData (flutter/flutter#119360) * 1d0cbbb24 fix a [SelectableRegion] crash bug (flutter/flutter#120076) * a808ba054 39f5e4cba Roll Skia from 7e2c9f54c0fd to 419bb63e733d (1 revision) (flutter/engine#39447) (flutter/flutter#120159) * 7a6f1d81d M3 segmented buttons token fixes (flutter/flutter#120095) * 16441f4bf 5aadda2f4 Roll Skia from 419bb63e733d to 83da27e4cd3a (1 revision) (flutter/engine#39448) (flutter/flutter#120172) * a6ea64457 Fix cut button creation in 'buttonItemsForToolbarOptions' (flutter/flutter#119822) * d7f742e90 Roll Flutter Engine from 5aadda2f40b1 to e432b82f49f3 (3 revisions) (flutter/flutter#120191) * 3f5b105fc Roll Plugins from d065e4e0a82a to 6f985d57b04b (10 revisions) (flutter/flutter#120193) * e03029ef6 [web] Move JS content to its own `.js` files (flutter/flutter#117691) * f2e89755e b67690f69 Roll Skia from 6babb6a1afe6 to 3b1401c4870d (1 revision) (flutter/engine#39455) (flutter/flutter#120198) * da36bd6fc Stop recursively including assets from asset folders (flutter/flutter#120167) * e8eac0d04 Update `ExpansionTile` to support Material 3 & add an example (flutter/flutter#119712) * Manual roll Flutter from e8eac0d047cd to 2303f42250b1 (23 revisions) (#7132) * 1c225675c Update to v0.158 of the token database. (flutter/flutter#120149) * 0b0450fbf Web tab selection (flutter/flutter#119583) * 108958886 un-pin package:intl (flutter/flutter#119900) * 5be7f6639 f310ffd14 Roll Skia from 3b1401c4870d to 87dbc81b421f (4 revisions) (flutter/engine#39457) (flutter/flutter#120214) * aed9b4adc Revert "Revert "Fix unable to find bundled Java version (#119244)" (#119981)" (flutter/flutter#120107) * 98b3e48ed Fix hang on successful dev/bots/analyze.dart (flutter/flutter#117660) * cd125e1f7 Add test for RenderProxyBoxMixin; clarify doc, resolve TODO (flutter/flutter#117664) * f94fa7ea2 Roll Flutter Engine from f310ffd1461a to 7098858dc0a5 (9 revisions) (flutter/flutter#120251) * 99b6bd8c0 Add support for extending selection to paragraph on ctrl + shift + arrow up/down on Non-Apple platforms (flutter/flutter#120151) * 1e6e6d41e Revert "Roll Flutter Engine from f310ffd1461a to 7098858dc0a5 (9 revisions) (#120251)" (flutter/flutter#120257) * 6e7f58037 fix a TextFormField bug (flutter/flutter#120182) * 3f98c0f8f Add trackOutlineColor for Switch and SwitchListTile (flutter/flutter#120140) * 7f578fb01 Revert "Stop recursively including assets from asset folders (#120167)" (flutter/flutter#120283) * d8154fde7 Manual roll Flutter Engine from f310ffd1461a to bdc5b6b768f6 (12 revisions) (flutter/flutter#120261) * 75ca31b0e Correct Badge interpretation of its alignment parameter (flutter/flutter#119853) * 0588b925a Removed "if" on resolving text color at "SnackBarAction" (flutter/flutter#120050) * 0a97ef85c Fix BottomAppBar & BottomSheet M3 shadow (flutter/flutter#119819) * bfea22db5 Roll Plugins from 6f985d57b04b to f59c08db3f27 (3 revisions) (flutter/flutter#120303) * ec289f1eb Don't call `PlatformViewCreatedCallback`s after `AndroidViewController` is disposed (flutter/flutter#116854) * 3a514175d Remove Android spell check integration test (flutter/flutter#120144) * 51227a9a5 Add missing parameters to `RadioListTile` (flutter/flutter#120117) * 212bac80d Revert "Update `ExpansionTile` to support Material 3 & add an example (#119712)" (flutter/flutter#120300) * 2303f4225 Manual roll Flutter Engine from bdc5b6b768f6 to cc4ca6a06ab3 (8 revisions) (flutter/flutter#120309) * [ci] Complete architecture switch for iOS (#7066) Enables the new architecture tests for iOS and turns down the old ones. Platform tests are now run on ARM, and the build-all test is run on Intel for coverage of building on both architectures. * [google_maps_flutter_android] Fixes points losing precision when converting to LatLng (#7101) * fix points losing precision when converted to lat long * add changelog * fix format * fix format * fix name * fix format * add license * [camera] availableCameras() implementation for CameraX re-write (#6945) * Adding availableCameras to camerax camera implementation * Marking loop variables as nullable in availableCameras * adding testing for android_camerax availableCameras() * marking variable final * removing commented import * cleaning up tests for availableCamera camerax re-write * chaning import * respondingt to review comments * fixing tests * fixing presubmit issues * adding changelog message * formatting changes for presubmit tests * another formatting change for presubmit checks --------- Co-authored-by: Gray Mackall * [image_picker_android] Name picked files to match the original filenames where possible (#6096) * [image_picker_android] Name picked files to match the original filenames where possible. * [image_picker_android] Update CHANGELOG.md * [image_picker_android] Add license blocks * [image_picker_android] Clear imports, document FileUtils.getPathFromUri * [image_picker_android] Fix analysis issues * Update .cirrus.yml (#7134) * Update .cirrus.yml * Restart checks * [google_sign_in] Slight cleanup in GoogleSignInPlugin (#7013) * Slight cleanup in GoogleSignInPlugin 1) Use switch instead of if/else and show all possible states 2) Handle the case of synchronous failure in signInSilently() by checking isComplete() instead of isSuccessful() * Run formatter and fix some compilation errors * Version bump * Declare throws * Update error text expectation * Diagnose failing test * Code * Code * Fix test * Manual roll Flutter from 2303f42250b1 to e3471f08d1d3 (24 revisions) (#7147) * 4a9660881 Reland "Stop recursively including assets from asset directories" (flutter/flutter#120312) * 4ddf0a89e Manual roll Flutter Engine from cc4ca6a06ab3 to 89c8a1393d4b (6 revisions) (flutter/flutter#120319) * b4908f376 Update .cirrus.yml (flutter/flutter#120315) * ef854a3db [Tool] [Windows] Output build duration (flutter/flutter#120311) * 0fb4406c3 Revert "[web] Move JS content to its own `.js` files (#117691)" (flutter/flutter#120275) * b1c4d5686 Fix widget inspector null check (flutter/flutter#120143) * dee226ef8 Manual roll Flutter Engine from 89c8a1393d4b to 2f2e2e27cb28 (3 revisions) (flutter/flutter#120333) * 0e7c5a885 Roll Plugins from f59c08db3f27 to 73986f4cc857 (5 revisions) (flutter/flutter#120362) * 468e21c50 Manual roll Flutter Engine from 2f2e2e27cb28 to eb346ba63f69 (7 revisions) (flutter/flutter#120364) * cd3806337 Update gallery.dart (flutter/flutter#120366) * 999612674 Add proper disabled values for input chips (flutter/flutter#120192) * 5e506aeb6 Add missing parameters to `SwitchListTile` (flutter/flutter#120115) * 0521c60cd Support --local-engine=ios_debug_sim (flutter/flutter#119524) * 42b20cf95 Added ListTile.titleAlignment, ListTileThemeData.titleAlignment (flutter/flutter#119872) * 1546fa08d [flutter_tools] toolExit on sdkmanager exit during doctor --android-licenses (flutter/flutter#120330) * 3fdd6ee46 Reland "Overlay always applies clip (#113770)" (flutter/flutter#116674) * c8c862141 Clean up null safety messages (flutter/flutter#120350) * 91dc513a3 Add missing parameters to `CheckboxListTile` (flutter/flutter#120118) * 1faa95009 Roll Flutter Engine from eb346ba63f69 to 603fd71f4749 (2 revisions) (flutter/flutter#120381) * 2239f6c8a Roll Flutter Engine from 603fd71f4749 to 39c41c40a4bc (3 revisions) (flutter/flutter#120393) * 425ab5dca Remove test that verifies we can switch to stateless (flutter/flutter#120390) * fecd5c969 Roll Flutter Engine from 39c41c40a4bc to 40e17fb5244c (3 revisions) (flutter/flutter#120397) * f945ad99c Resolve dwarf paths to enable source-code mapping of stacktraces (flutter/flutter#114767) * e3471f08d Roll Flutter Engine from 40e17fb5244c to 9a40a384997d (3 revisions) (flutter/flutter#120403) * [file_selector] Relax xdg_directories constraint (#7157) `xdg_directories` 0.2.0 was update to 1.0 to reflect that it has a stable API, with no other changes. This updates the constraint to allow either 0.2.x or 1.x to avoid dependency conflicts in the future. * [various] Raise JVM limit in example builds (#7155) Speculative fix for the intermittent OOM build errors. * Update GCLOUD_FIREBASE_TESTLAB_KEY (#7176) * [camera] flip/change camera while recording (split out PR for cam_avfoundation and cam_android) (#7109) * setDescription in Camera platform interface * example app setup to change description mid recording * AVFoundationCamera method call to setDescription * FLTCam setup to setDescription * captureSession split into video and audio so we will be able to switch cameras without breaking the audio * renamed setDescription to setDescriptionWhileRecording since it can only be used while recording * integration tests fixed * set description while recording integration test * throws error if device not recording and setDescriptionWhileRecording is called * set description while recording test * example project setup * camera preview can be changed while recording * camera switches and keeps surface pointed to mediarecorder * small change to set autofocus when switching while recording * android video record goes through VideoRenderer to apply matrix after switching camera * switch camera uses VideoRenderer * dont use video renderer until user switches camera while recording * rotate based on initial recording direction * VideoRenderer cleanup * flutter results for setDescriptionWhileRecording * error if you setDescriptionWhileRecording while device is not recording * android tests * integration tests * method channel test * main package tests * setDescriptionWhileRecording called while no video was recording test * integration tests * dependency overrides * update readme and version * removed old TODO * removed accidental dev team ID commit * renamed local variables * use captureSessionQueue * fixed local variable name * setupCaptureVideoOutput function * createConnectionWithInput * simplified configureConnection function to re-use code on switching camera * formatting * example project dependency overrides * fixed versioning * formatting * fixed some ios native tests * fixed small bug * dont emit initialized when switching camera * ios formatting * dependency overrides for camera/example * android formatting * ios test formatted * android tests formatted * android format that I missed * other android formatting * final formatting with flutter tool * formatted android again * android license in new file * update-excerpts ran * fixed changelog * removed development team * renames configureConnection to createConnection * renames unimplemented error message * renames setDescriptionWhileRecording error to match android and the other errors * fixes formatting * removes override dependencies from camera_web and camera_windows * removes camera_web override dependency in camera package * Update packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java Co-authored-by: Camille Simon <43054281+camsim99@users.noreply.github.com> * Update packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java Co-authored-by: Camille Simon <43054281+camsim99@users.noreply.github.com> * Update packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java Co-authored-by: Camille Simon <43054281+camsim99@users.noreply.github.com> * Update packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java Co-authored-by: Camille Simon <43054281+camsim99@users.noreply.github.com> * reformats camera.java * VideoRenderer uses surface texture timestamp instead of current system time * formats VideoRenderer.java * fixes comments in VideoRenderer.java * Update packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart Co-authored-by: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> * Update packages/camera/camera/lib/src/camera_controller.dart Co-authored-by: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> * renames error typo * frees shaders after program linking * handles eglSwapBuffers errors * extension check guards eglPresentationTimeANDROID * cleans openGL resources * reverted timestamp to use uptimeMillis() * Tests for startPreviewWithVideoRendererStream * fixes exception not being caught * tests for correct rotation to be set * fixes versioning * tests method channel setDescriptionWhileRecording * adds forwarding getter on CameraController to its value's description * dummy commit to fix github test's not finding commit hash * adds override description for FakeController in camera tests * fixes versioning for avfoundation and android * fixes versioning * fixes pubspec versions * ios setDescription * setDescription * android setDescription * formatting * revert * nits and reverts * nits * fixes README * fixes other comments * fixes setDescription override in camera_preview_test * set description test * versions * removes changes on platform_interface_changes * points all packages to platform interface version 2.4 * points to the new platform interface * removes everything that isnt under camera_avfoundation and camera_android * removes dependency overrides in examples * removes version change on camera * removes camera changes that were missed * fixes android version --------- Co-authored-by: BradenBagby Co-authored-by: Camille Simon <43054281+camsim99@users.noreply.github.com> Co-authored-by: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> * Revert "[camera] flip/change camera while recording (split out PR for cam_avfoundation and cam_android) (#7109)" (#7181) This reverts commit 9c312d4d2f5fe37608e11fa68295ecb59b2a200e. * [webview_flutter_android][webview_flutter_wkwebview] Adds support to retrieve native `WebView` (#7071) * implementation of the webViewIdentifier field * change to external classes * formatting * update readmes * iOS * improve * hmmmm * add note about not using other apis * project changes * add external api tests to project * ordering * fix docs and use id * [google_maps_flutter_android] Fixes initial padding not working while map has not been created yet. (#7135) * fix initial padding not working * fix changelog * remove unused imports * removed visiblefortesting from google map * add private back * applied patch * replace 10 with padding * add line * remove line * [ci] Remove repo tooling (#7172) * Remove tooling * Remove CI testing of tooling * Switch invocations to published version * Update tool_runner.sh * Update Cirrus * Fix global run command * Update stale comment * Re-add a minimal stub * Roll forward * [cameraX] Add integration test for availableCameras (#7156) * Add integration test * Add integration test * Roll Flutter from e3471f08d1d3 to df41e58f6f4e (83 revisions) (#7184) * 001c4951b Roll Flutter Engine from 9a40a384997d to e1d0032029e4 (6 revisions) (flutter/flutter#120414) * 96823590e post submit only (flutter/flutter#120411) * 5dbd28101 Use String.codeUnitAt instead of String.codeUnits[] in ParagraphBoundary (flutter/flutter#120234) * f05a555bc Fix lerping for `NavigationRailThemeData` icon themes (flutter/flutter#120066) * b0d04ea49 add9e11ed Fix position of BackdropFilter above PlatformView (flutter/engine#39244) (flutter/flutter#120415) * 858f94cfa Roll Plugins from 73986f4cc857 to 02571ec0dd36 (3 revisions) (flutter/flutter#120443) * 298c874ea Fix classes that shouldn't be extended/instantiated/mixedin (flutter/flutter#120409) * 25c2c22d2 Delete Chrome temp cache after closing (flutter/flutter#119062) * b2e37c659 [conductor] Tag engine versions (flutter/flutter#120419) * 780c9a8de Remove deprecated SystemChrome.setEnabledSystemUIOverlays (flutter/flutter#119576) * 52ab29936 Roll Flutter Engine from add9e11edb66 to 4104eb5cbc40 (14 revisions) (flutter/flutter#120470) * 65fd924d8 [conductor] Remove CiYaml model (flutter/flutter#120458) * d5dbcb708 Revert "Revert "[web] Move JS content to its own `.js` files (#117691)" (#120275)" (flutter/flutter#120363) * 2f9abd20f Roll Flutter Engine from 4104eb5cbc40 to 6660300ea34f (6 revisions) (flutter/flutter#120487) * d63c54c9c roll packages (flutter/flutter#120493) * 941578ea9 Roll Flutter Engine from 6660300ea34f to 5e3ff1e5c9b3 (4 revisions) (flutter/flutter#120495) * b3613c4cf f737dc868 [macOS] Add XCode marks for TextInputPlugin (flutter/engine#39550) (flutter/flutter#120501) * 4ab2ffd58 Roll Flutter Engine from f737dc868a3e to 3ac3338489d2 (2 revisions) (flutter/flutter#120505) * 859d57b3c 8baee2164 Roll Skia from 5230650dc096 to 70a3a194ec98 (2 revisions) (flutter/engine#39554) (flutter/flutter#120507) * e42ab1cb0 c3dc68e0e Roll Skia from 70a3a194ec98 to 6c3097e6f833 (1 revision) (flutter/engine#39555) (flutter/flutter#120508) * 61e059f44 Roll Flutter Engine from c3dc68e0e263 to 363355af5158 (2 revisions) (flutter/flutter#120511) * d27de1de0 5efb42971 Roll Fuchsia Linux SDK from 482Njb1v72P7fNyj4... to MVMTNxWJaWdwPWstz... (flutter/engine#39559) (flutter/flutter#120512) * bbf8446d9 761891200 Roll Dart SDK from 1d26a1d57edf to b1836aacc08a (1 revision) (flutter/engine#39561) (flutter/flutter#120513) * 0346f4b18 d8e01097e Roll Fuchsia Mac SDK from 6nMZjuYXTcnD_VZQI... to FxFPRn_9rSWWAWFw0... (flutter/engine#39562) (flutter/flutter#120519) * 0db47bdd0 Revert "Fix BottomAppBar & BottomSheet M3 shadow (#119819)" (flutter/flutter#120492) * f04600bfb Roll Flutter Engine from d8e01097e66b to 2cba062f9f4c (2 revisions) (flutter/flutter#120538) * 274f6cb2d Roll Flutter Engine from 2cba062f9f4c to 05d81c0f2ebe (2 revisions) (flutter/flutter#120547) * d6ff0f2af fe56a45b4 Roll Dart SDK from c4255cea566a to 1caf3a9ad101 (1 revision) (flutter/engine#39571) (flutter/flutter#120552) * 7295d4fe9 9a19d7eea Roll Fuchsia Linux SDK from arbaBzyUE2ok1bGl5... to 8fdyKaKQqTPpjcp-L... (flutter/engine#39572) (flutter/flutter#120554) * 6d68eb7b8 0aa4fcbd2 Roll Skia from ec87ec6fd34f to 615965d545f4 (1 revision) (flutter/engine#39573) (flutter/flutter#120558) * 527977b6a b7e80ad6e Roll Fuchsia Mac SDK from y35kWL0rP5Nd06lTg... to KpTOXssqVhPv2OBZi... (flutter/engine#39574) (flutter/flutter#120559) * 238b0dbc0 Roll Flutter Engine from b7e80ad6ef51 to 1ff345ce5f63 (2 revisions) (flutter/flutter#120574) * 3e659cf71 1eef041d4 [Impeller] Source the pipeline color attachment pixel format from RenderPass textures (flutter/engine#39556) (flutter/flutter#120576) * 53fe8a3f9 4107a7b71 Roll Skia from 615965d545f4 to c6f1de2239fb (1 revision) (flutter/engine#39581) (flutter/flutter#120580) * b0c24e8d3 fix a Slider theme update bug (flutter/flutter#120432) * b33c76f01 1695b7bbc Bump github/codeql-action from 2.1.39 to 2.2.4 (flutter/engine#39584) (flutter/flutter#120588) * ce8efb439 ede2a0a3c Roll Skia from c6f1de2239fb to d85501fa487d (1 revision) (flutter/engine#39585) (flutter/flutter#120593) * 7fb8497b5 Roll Plugins from 02571ec0dd36 to f3bc6f1eb0c2 (2 revisions) (flutter/flutter#120601) * 7c2d5b9c2 60c8532d6 Roll Fuchsia Mac SDK from NZAnfCkpbswhYplty... to 6hbPQq6ED0PkuQiKM... (flutter/engine#39587) (flutter/flutter#120602) * 1f268f1d6 fb8840578 Roll Dart SDK from 1caf3a9ad101 to f80c5db8736a (1 revision) (flutter/engine#39588) (flutter/flutter#120606) * 957494d9f Marks Linux_android flavors_test to be unflaky (flutter/flutter#120299) * 0792c2795 Roll Flutter Engine from fb8840578156 to df0ffe42b33e (2 revisions) (flutter/flutter#120607) * 7bacc25ee Remove accentColorBrightness usage (flutter/flutter#120577) * 98576cef5 Avoid null terminating characters in strings from Utf8FromUtf16() (flutter/flutter#109729) * 00adf9a33 roll packages (flutter/flutter#120609) * 2df140f40 Remove references to Observatory (flutter/flutter#118577) * a819d6156 Remove `prefer_equal_for_default_values` lint rule (flutter/flutter#120533) * 73afc7ba3 Roll Flutter Engine from df0ffe42b33e to 97dcf3e6201e (4 revisions) (flutter/flutter#120617) * f858302a6 Remove `brightness` from `AppBar`/`SliverAppBar`/`AppBarTheme`/`AppBarTheme.copyWith` (flutter/flutter#120575) * 95fd821ab Force `Mac build_tests` to run on x64 bots (flutter/flutter#120620) * ed35c80d2 d28cbf402 [Impeller] Return entity from filters instead of a snapshot (flutter/engine#39560) (flutter/flutter#120625) * 778b3fa32 support updating dragDecices at runtime (flutter/flutter#120336) * ddebe833b Added integration test for wide gamut support. (flutter/flutter#119657) * becb6bd00 Fix message type inconsistency between locales (flutter/flutter#120129) * f4495f5d3 Roll Flutter Engine from d28cbf402904 to 31a4648cbe99 (2 revisions) (flutter/flutter#120630) * 402caec2e Fix `ListTile`'s default `iconColor` token used & update examples (flutter/flutter#120444) * 865422da2 Force `Mac tool_integration_tests` to run on x64 bots (flutter/flutter#120634) * 07c548c69 Apply BindingBase.checkInstance to TestDefaultBinaryMessengerBinding (flutter/flutter#116937) * b08cc8be7 Roll Flutter Engine from 31a4648cbe99 to c4f51bc78644 (7 revisions) (flutter/flutter#120656) * 6a94f25a9 Roll Flutter Engine from c4f51bc78644 to 17ab09d382e3 (5 revisions) (flutter/flutter#120664) * b0edf5829 Roll Flutter Engine from 17ab09d382e3 to cbb7fc020b00 (2 revisions) (flutter/flutter#120673) * 17b4c70ff [M3] Add customizable overflow property to Snackbar's action (flutter/flutter#120394) * b35e4a54f Roll Plugins from f3bc6f1eb0c2 to 9c312d4d2f5f (2 revisions) (flutter/flutter#120694) * 9fd34048f f4fcb911b Roll Skia from bb7b22f3f444 to 8de7f68a3661 (1 revision) (flutter/engine#39619) (flutter/flutter#120699) * b9b4d3e43 roll packages (flutter/flutter#120628) * ed5bd1779 Fix tree by updating dependencies (flutter/flutter#120707) * 480c54c37 Increase Linux docs_publish timeout (flutter/flutter#120718) * c102bf467 [integration_test] Fix link to integration test for web section in `README.md` (flutter/flutter#103422) * 577ad2ee8 Fix error when resetting configurations in tear down phase (flutter/flutter#114468) * 2cfca820a Force Mac plugin_test to run on x64 bots (flutter/flutter#120714) * fd2fd94e3 d86089252 Roll Fuchsia Mac SDK from OeUljRQOmJwgDhNOo... to EFcCpAxOuQllDqP0F... (flutter/engine#39621) (flutter/flutter#120702) * 9d94a51b6 Move linux-x64-flutter-gtk.zip to linux-x64-debug location. (flutter/flutter#120658) * d29668ddb Improve network resources doctor check (flutter/flutter#120417) * 378668db4 added MaterialStateColor support to TabBarTheme.labelColor (flutter/flutter#109541) * ba46cb8d5 Remove deprecated AppBar.color & AppBar.backwardsCompatibility (flutter/flutter#120618) * 5a3957f3b Revert "Fix error when resetting configurations in tear down phase" (flutter/flutter#120739) * fd01812f6 Add temporary default case to support new PointerSignalKind (flutter/flutter#120731) * 4b8ad1b00 Temporarily disable info-based analyzer unit tests. (flutter/flutter#120753) * 911b13784 Roll Flutter Engine from d860892528ff to 44e36c9c0d73 (20 revisions) (flutter/flutter#120761) * 4ae5252f8 Fix license page crash (flutter/flutter#120728) * 31c73fcfe Roll Flutter Engine from 44e36c9c0d73 to bf7d51586704 (2 revisions) (flutter/flutter#120772) * 624445a45 Roll Flutter Engine from bf7d51586704 to ec70b5aa96be (2 revisions) (flutter/flutter#120781) * df41e58f6 1328c4bc6 Roll Dart SDK from 0456c4011cb3 to c022d475e9d8 (1 revision) (flutter/engine#39646) (flutter/flutter#120784) * [ci] Update iOS simulator (#7131) Updates the iOS simulator used in CI from an iPhone 11 to an iPhone 13. Part of alignment with flutter/packages in preparation for merging repositories. Updates a Maps integration test for issues with the newer device. * Roll Flutter from df41e58f6f4e to 22e17bb71050 (28 revisions) (#7186) * 3ad7ea3c9 Roll Plugins from 9c312d4d2f5f to 2ce625f1a87e (5 revisions) (flutter/flutter#120793) * 786571368 Roll Flutter Engine from 1328c4bc6299 to 4db9673d48d6 (2 revisions) (flutter/flutter#120796) * 541a8bfd9 Fix switching from scrollable and non-scrollable tab bars throws (flutter/flutter#120771) * ab1390e0a Use black30 for CupertinoTabBar's border (flutter/flutter#119509) * a513d4e7b Fix `flutter_localizations` README references (flutter/flutter#120800) * a664f08a5 In test of --(no-)fatal-infos analyzer flags, pin missing_return to info (flutter/flutter#120797) * ef49f5661 Add Android unit tests to plugin template (flutter/flutter#120720) * a12e242c0 Improve CupertinoContextMenu to match native more (flutter/flutter#117698) * a9f43665c Fix the `flutter run -d linux` tests (flutter/flutter#120721) * dff09558d 09da59a5a Roll Dart SDK from c022d475e9d8 to 5d17a336bdfe (1 revision) (flutter/engine#39649) (flutter/flutter#120816) * f35de0c80 Adds wide gamut saveLayer integration test (flutter/flutter#120131) * 99dcaa2d9 Roll Flutter Engine from 09da59a5adcf to a8b3d1af55b6 (3 revisions) (flutter/flutter#120821) * 8d150833b Use the impellerc GLES output flag when compiling shaders for Android (flutter/flutter#120647) * c6b636fa5 [flutter_tools] Replace Future.catchError() with Future.then(onError: ...) (flutter/flutter#120637) * 2b7d709fd Add `@widgetFactory` annotation (flutter/flutter#117455) * e65dfba8e Add Linux unit tests to plugin template (flutter/flutter#120814) * dccec41d5 5de007b90 Remove "bringup: true" from "Linux Fuchsia FEMU" (flutter/engine#39651) (flutter/flutter#120826) * d6de6bc68 9f3b061b7 Roll buildroot to 64b0c3deecaff8e66c2deb74e2171e8297b2bfcd (flutter/engine#39653) (flutter/flutter#120830) * da2508c9f bb1ff84b6 Add a white background to app anatomy diagram (flutter/engine#39638) (flutter/flutter#120832) * 1f85497ef [flutter_tools] Add the NoProfile parameter to the PowerShell execution statement (flutter/flutter#120786) * 4ad47fb47 Fix `StretchingOverscrollIndicator` not handling directional changes correctly (flutter/flutter#116548) * 9a721c456 Update AndroidManifest.xml.tmpl (flutter/flutter#120527) * c0b7d2ddd Roll Flutter Engine from bb1ff84b6c4f to 02a379db1d38 (4 revisions) (flutter/flutter#120845) * a10e295a0 Added identical(a,b) short circuit to Material Library lerp methods (flutter/flutter#120829) * efde35081 Roll Flutter Engine from 02a379db1d38 to a966cf878ffd (2 revisions) (flutter/flutter#120846) * cc473e4f1 Roll Flutter Engine from a966cf878ffd to 3fc40ca5beb9 (3 revisions) (flutter/flutter#120850) * d1252428c Roll Flutter Engine from 3fc40ca5beb9 to 9fa2a5c3cfbd (2 revisions) (flutter/flutter#120856) * 22e17bb71 ea1d087c4 Roll Skia from b8b36146c7a0 to 7b3fb04bc3d4 (3 revisions) (flutter/engine#39673) (flutter/flutter#120860) * [ios_platform_images] Update minimum version to iOS 11 (#6874) * [ios_platform_images] Update minimum version to iOS 11 * README update * [in_app_purchase] Update minimum Flutter version to 3.3 and iOS 11 (#6873) * [in_app_purchase] Bump minimum Flutter version to 3.3 for iOS plugins * Bump Flutter version * super params * Format * [google_sign_in_web] Migrate to the GIS SDK. (#6921) * [google_sign_in_web] Migrate to GIS SDK. * include_granted_scopes in requestScopes call. * Remove the old JS-interop layer. * Introduce a mockable GisSdkClient for tests. * Split the people utils. * Delete tests for the old code. * Add some tests for the new code. * More utils_test.dart * Make jsifyAs reusable. * Ignore the tester in utils_test.dart * Make Clients overridable, and some renaming. * Test people.dart * Make autoDetectedClientId more testable. * Add mockito. * Comment about where to better split the code so GisSdkClient is testable too. * Add google_sign_in_web_test.dart (and its mocks) * dart format * Log only in debug. * Sync min sdk with package gis_web * Add migration notes to the README. * When the user is known upon signIn, remove friction. * Do not ask for user selection again in the authorization popup * Pass the email of the known user as a hint to the signIn method * Address PR comments / checks. * Update migration guide after comments from testers. * Update README.md * Remove package:jose from tests. * Rename to Vincent Adultman * _isJsSdkLoaded -> _jsSdkLoadedFuture * Remove idToken comment. * Link issue to split mocking better. * Remove dependency in package:jwt_decoder * Remove unneeded cast call. * [image_picker] Fix images changing to incorrect orientation (#7187) * fix orientation issue * update changelog * add test * fix formatting * Roll Flutter from 22e17bb71050 to 298d8c76ba78 (20 revisions) (#7190) * f85438bfa c8b1d2ffa Roll Fuchsia Mac SDK from YpQKlqmyn8r_snx06... to xl9Y8o-9FDyvPogki... (flutter/engine#39675) (flutter/flutter#120887) * 174a562a6 d699b4a91 Roll Flutter from e3471f08d1d3 to df41e58f6f4e (83 revisions) (flutter/plugins#7184) (flutter/flutter#120888) * 170539f83 Roll Flutter Engine from c8b1d2ffaec8 to 0d8d93306822 (2 revisions) (flutter/flutter#120891) * df98689c9 2be7253c9 Roll Fuchsia Linux SDK from q7u2WyX2BSRBIzyTW... to yT4JLKTCWWwbRwB0l... (flutter/engine#39679) (flutter/flutter#120898) * cacef57b6 [flutter_tools] Skip over "Resolving dependencies..." text in integration tests (flutter/flutter#120077) * 34102ca3b Migrate channels to pkg:integration _test (flutter/flutter#120833) * df13ea248 Roll Flutter Engine from 2be7253c9b10 to e4cb80e22ee1 (2 revisions) (flutter/flutter#120903) * a2e65f7c3 Roll Flutter Engine from e4cb80e22ee1 to 4a90fbcd6901 (2 revisions) (flutter/flutter#120911) * e00241a06 Enable Windows plugin tests (flutter/flutter#119345) * 09ad9f3cd Document ScrollPhysics invariant requiring ballistic motion (flutter/flutter#120400) * 6029de2fb Update switch template (flutter/flutter#120919) * 229d70ea3 Roll Flutter Engine from 4a90fbcd6901 to bddfc1c4dcaa (5 revisions) (flutter/flutter#120920) * 206c6ae99 roll packages (flutter/flutter#120922) * 9fcaaebb5 Roll Flutter Engine from bddfc1c4dcaa to 6602fc753525 (3 revisions) (flutter/flutter#120928) * 00c0a07fa Increase Linux docs_test timeout (flutter/flutter#120899) * e29a79975 946b29198 [dart:ui] Introduce `PlatformDispatcher.implicitView` (flutter/engine#39553) (flutter/flutter#120939) * 081cd5776 650db7a72 [macOS] Eliminate mirrors support (flutter/engine#39694) (flutter/flutter#120943) * 875e48c69 52a4fb4c5 Roll Skia from b1800a8b9595 to d0df677ffd5e (13 revisions) (flutter/engine#39699) (flutter/flutter#120947) * 78d058f46 6e92c0c28 Roll Fuchsia Mac SDK from xl9Y8o-9FDyvPogki... to haDvcC5VzWVdQs9Rs... (flutter/engine#39700) (flutter/flutter#120950) * 298d8c76b Revert "Remove references to Observatory (#118577)" (flutter/flutter#120929) * [camerax] Implement camera preview (#7112) * Add base code from proof of concept * Fix analyzer * Add example app and tests * Actually add tets and visible for testing annotation * Make methods private * Fix tests * Fix analyze: * Update changelog * Formatting * Update todos with links and modify camera controller * Format and add availableCameras * Fix mocks * Try bumping mockito version * Review documentation * Re-generate mocks * Fix typo * Fix another typo * Address review and fix bug * Fix analyze * Fix tests * Foramtting * Clear preview is paused * Add unknown lens * [image_picker] Update NSPhotoLibraryUsageDescription description in README (#6589) * Update `NSPhotoLibraryUsageDescription` description in README * Update text about App Store policy * Migrate these tests to the "new" API. (#7189) * Roll Flutter from 298d8c76ba78 to 0be7c3f30d64 (21 revisions) (#7194) * 674254c03 Always use the testbed in web_test.dart so `environment` is populated. (flutter/flutter#120984) * c4d40cc15 Modify the updateChildren method deep copy _children (flutter/flutter#120773) * 9367641ce clean up (flutter/flutter#120934) * 51712b90a Roll Plugins from d699b4a91381 to 8f3419be5e0e (7 revisions) (flutter/flutter#120993) * c3587c62e Add `InheritedTheme` support to `ScrollbarTheme` (flutter/flutter#120970) * 08b409ab0 Roll Flutter Engine from 6e92c0c28410 to bd37a3992b50 (16 revisions) (flutter/flutter#121004) * f78513685 [web] Temporarily disable a line boundary test (flutter/flutter#121005) * 9fe556705 Print sub process that failed to run in tool (flutter/flutter#120999) * 6205c110d Remove "note that" in our documentation (as per style guide) (flutter/flutter#120842) * 1daa0be4f Fix scrollable to clear inner semantics node if it does not use two p… (flutter/flutter#120996) * 7f19b7485 0a27673d7 Roll Skia from 02890036028e to 0e444e355607 (9 revisions) (flutter/engine#39723) (flutter/flutter#121008) * 48d2dfc72 e7fde3f72 [web] Make glassPaneElement and glassPaneShadow non-nullable (flutter/engine#39692) (flutter/flutter#121009) * 610450523 2b2780185 Roll Skia from 0e444e355607 to 4b79e398dfe0 (5 revisions) (flutter/engine#39725) (flutter/flutter#121016) * f99f47280 Remove the deprecated accentColor from ThemeData (flutter/flutter#120932) * 2b4c96088 Remove more references to dart:ui.window (flutter/flutter#120994) * 0fa652752 Roll Flutter Engine from 2b2780185dd5 to a37e27b77008 (2 revisions) (flutter/flutter#121020) * 9281114fb Roll Flutter Engine from a37e27b77008 to 2fdce9a96367 (2 revisions) (flutter/flutter#121023) * 4dd555d32 Roll Flutter Engine from 2fdce9a96367 to 9a3c3e462fce (3 revisions) (flutter/flutter#121025) * 66dce657f Roll Flutter Engine from 9a3c3e462fce to 3777ed51774f (2 revisions) (flutter/flutter#121029) * a5b53a6d2 a9db42c3e Roll Skia from 733a19f6a625 to 2f05923f825e (3 revisions) (flutter/engine#39744) (flutter/flutter#121030) * 0be7c3f30 Roll Flutter Engine from a9db42c3edc2 to c22c64812243 (2 revisions) (flutter/flutter#121041) * [google_sign_in] Endorses next web package. (#7191) Bump major version to 6.0.0. * [google_maps]: Bump org.mockito:mockito-core (#7099) Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 4.7.0 to 5.1.1. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.7.0...v5.1.1) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [image_picker]: Bump org.mockito:mockito-core (#7097) Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 4.8.0 to 5.1.1. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.8.0...v5.1.1) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [lifecycle]: Bump org.mockito:mockito-core (#7096) Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 4.7.0 to 5.1.1. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.7.0...v5.1.1) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [in_app_pur]: Bump org.mockito:mockito-core (#7094) Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 4.7.0 to 5.1.1. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.7.0...v5.1.1) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [url_launcher]: Bump org.mockito:mockito-core (#7098) Bumps [org.mockito:mockito-core](https://github.com/mockito/mockito) from 4.8.0 to 5.1.1. - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v4.8.0...v5.1.1) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update codeowners (#7188) * Add missing CODEOWNER (#7016) `shared_preferences_android` was accidentally missing a code owner. * [webview_flutter] Adds examples of accessing platform-specific features for each class (#7089) * start * more docs and version bump * fix main * Roll Flutter from 0be7c3f30d64 to 33e4d21f7c13 (5 revisions) (#7196) * 43e74c05e a7c28d085 Roll Fuchsia Linux SDK from hi7JwgHijuYYKAFUR... to Iykltk3-HtXqYplbg... (flutter/engine#39750) (flutter/flutter#121047) * ab39d076c Roll Flutter Engine from a7c28d0851dd to 8d13f3761460 (2 revisions) (flutter/flutter#121050) * 424538990 1b71ea81a Roll ICU from 266a46937f05 to c6b685223182 (4 revisions) (flutter/engine#39753) (flutter/flutter#121054) * 3c6a25d7d 4a58ff869 Roll Fuchsia Mac SDK from nvf4Ago0k-VS2JPxZ... to HtmcMFg6ZlyRkcNsB... (flutter/engine#39754) (flutter/flutter#121056) * 33e4d21f7 00ce81fdf Roll Fuchsia Linux SDK from Iykltk3-HtXqYplbg... to 7rgqQxifQPjH_2zXB... (flutter/engine#39755) (flutter/flutter#121058) * [plugins] Disables the AndroidGradlePluginVersion issue ID in all android packages (#7045) * Disables the AndroidGradlePluginVersion issue ID in all android packages. * Reverted changelog changes * Reverted all changelog changes. * Fixes changelog conflicts across android packages. --------- Co-authored-by: Stuart Morgan * [espresso] Update some dependencies (#7195) * [local_auth] Add Android theme compatibility documentation (#6875) Add documentation to README.md file from local_auth package: - Compatibility of Android theme for Android 8 and below. * Update READMEs for archiving (#7210) Adds notes to README and CONTRIBUTING pointing to flutter/packages. --------- Signed-off-by: dependabot[bot] Co-authored-by: hellohuanlin <41930132+hellohuanlin@users.noreply.github.com> Co-authored-by: Camille Simon <43054281+camsim99@users.noreply.github.com> Co-authored-by: Sergey Pronin <1236794+wannabehero@users.noreply.github.com> Co-authored-by: Chris Yang Co-authored-by: David Iglesias Co-authored-by: engine-flutter-autoroll Co-authored-by: adam-harwood <45190361+adam-harwood@users.noreply.github.com> Co-authored-by: stuartmorgan Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: camsim99 Co-authored-by: keyonghan <54558023+keyonghan@users.noreply.github.com> Co-authored-by: Jonah Williams Co-authored-by: Julius Bredemeyer <48645716+IVLIVS-III@users.noreply.github.com> Co-authored-by: David Martos Co-authored-by: Joonas Kerttula Co-authored-by: Jason Simmons Co-authored-by: Oreofe Solarin Co-authored-by: Collin Jackson Co-authored-by: Phil Quitslund Co-authored-by: Maurice Parrish <10687576+bparrishMines@users.noreply.github.com> Co-authored-by: LouiseHsu Co-authored-by: Jenn Magder Co-authored-by: Victoria Ashworth Co-authored-by: asaarnak Co-authored-by: Victoria Ashworth Co-authored-by: Sarah Zakarias Co-authored-by: Tiffaa <33839996+TiffApps@users.noreply.github.com> Co-authored-by: Gary Qian Co-authored-by: Sam Rawlins Co-authored-by: Peixin Li Co-authored-by: lucasoskorep Co-authored-by: Rexios Co-authored-by: Alejandro Pinola <57049542+adpinola@users.noreply.github.com> Co-authored-by: eugerossetto Co-authored-by: Todd Volkert Co-authored-by: Milvintsiss <38794405+Milvintsiss@users.noreply.github.com> Co-authored-by: Michael Goderbauer Co-authored-by: Ian Hickson Co-authored-by: Drew Roen <102626803+drewroengoogle@users.noreply.github.com> Co-authored-by: Braden Bagby <33461698+BradenBagby@users.noreply.github.com> Co-authored-by: BradenBagby Co-authored-by: Reid Baker Co-authored-by: Tarrin Neal Co-authored-by: Balvinder Singh Gambhir Co-authored-by: gmackall <34871572+gmackall@users.noreply.github.com> Co-authored-by: Gray Mackall Co-authored-by: Jakub Walusiak Co-authored-by: Preston Schwartz Co-authored-by: Abel1027 <64153497+Abel1027@users.noreply.github.com> --- .ci.yaml | 176 +- .ci/flutter_master.version | 2 +- .ci/flutter_stable.version | 2 +- .ci/scripts/build_all_plugins.sh | 3 +- .ci/scripts/build_examples_win32.sh | 2 +- .ci/scripts/create_all_plugins_app.sh | 4 +- .ci/scripts/create_simulator.sh | 2 +- .ci/scripts/drive_examples_win32.sh | 2 +- .ci/scripts/native_test_win32.sh | 2 +- .ci/scripts/prepare_tool.sh | 5 +- .ci/targets/ios_build_all_plugins.yaml | 11 + ...orm_tests.yaml => ios_platform_tests.yaml} | 4 +- ...gins.yaml => macos_build_all_plugins.yaml} | 4 +- ...podspecs.yaml => macos_lint_podspecs.yaml} | 4 +- .ci/targets/macos_platform_tests.yaml | 19 + .ci/targets/plugin_tools_tests.yaml | 5 - ...ns.yaml => windows_build_all_plugins.yaml} | 4 +- .cirrus.yml | 200 +- .github/workflows/release.yml | 7 +- .github/workflows/scorecards-analysis.yml | 8 +- CODEOWNERS | 34 +- CONTRIBUTING.md | 6 +- FlutterFire.md | 4 +- README.md | 8 +- analysis_options.yaml | 82 +- packages/camera/camera/CHANGELOG.md | 22 +- packages/camera/camera/README.md | 4 +- .../camera/example/android/gradle.properties | 2 +- .../example/integration_test/camera_test.dart | 4 - packages/camera/camera/example/lib/main.dart | 13 +- .../example/lib/readme_full_example.dart | 4 +- .../example/test_driver/integration_test.dart | 2 + .../camera/lib/src/camera_controller.dart | 145 +- packages/camera/camera/pubspec.yaml | 10 +- .../camera/test/camera_image_stream_test.dart | 67 +- .../camera/test/camera_preview_test.dart | 4 +- packages/camera/camera/test/camera_test.dart | 31 +- packages/camera/camera_android/CHANGELOG.md | 34 + .../camera_android/android/build.gradle | 7 +- .../io/flutter/plugins/camera/Camera.java | 103 +- .../plugins/camera/CameraPermissions.java | 4 +- .../plugins/camera/CameraProperties.java | 38 +- .../io/flutter/plugins/camera/CameraZoom.java | 52 - .../plugins/camera/MethodCallHandlerImpl.java | 5 +- .../resolution/ResolutionFeature.java | 33 +- .../features/zoomlevel/ZoomLevelFeature.java | 50 +- .../camera/features/zoomlevel/ZoomUtils.java | 11 +- .../camera/media/MediaRecorderBuilder.java | 2 +- .../plugins/camera/CameraPermissionsTest.java | 14 + .../camera/CameraPropertiesImplTest.java | 24 + .../plugins/camera/CameraZoomTest.java | 125 - .../resolution/ResolutionFeatureTest.java | 98 + .../zoomlevel/ZoomLevelFeatureTest.java | 55 +- .../features/zoomlevel/ZoomUtilsTest.java | 37 +- .../example/android/gradle.properties | 2 +- .../example/integration_test/camera_test.dart | 47 +- .../example/lib/camera_controller.dart | 123 +- .../camera_android/example/lib/main.dart | 14 +- .../camera_android/example/pubspec.yaml | 4 +- .../example/test_driver/integration_test.dart | 2 + .../lib/src/android_camera.dart | 90 +- .../camera/camera_android/lib/src/utils.dart | 9 +- packages/camera/camera_android/pubspec.yaml | 6 +- .../test/android_camera_test.dart | 63 +- .../test/method_channel_mock.dart | 10 +- .../camera_android_camerax/CHANGELOG.md | 8 + .../android/build.gradle | 4 +- .../android/src/main/AndroidManifest.xml | 5 + .../camerax/CameraAndroidCameraxPlugin.java | 30 +- .../plugins/camerax/CameraFlutterApiImpl.java | 22 + .../camerax/CameraInfoHostApiImpl.java | 4 +- .../camerax/CameraPermissionsManager.java | 120 + .../camerax/CameraSelectorHostApiImpl.java | 10 +- .../flutter/plugins/camerax/CameraXProxy.java | 38 + .../camerax/DeviceOrientationManager.java | 329 +++ .../camerax/GeneratedCameraXLibrary.java | 655 +++++ .../plugins/camerax/InstanceManager.java | 7 +- .../plugins/camerax/PreviewHostApiImpl.java | 149 ++ .../ProcessCameraProviderHostApiImpl.java | 83 +- .../camerax/SystemServicesFlutterApiImpl.java | 24 + .../camerax/SystemServicesHostApiImpl.java | 111 + .../camerax/CameraPermissionsManagerTest.java | 89 + .../flutter/plugins/camerax/CameraTest.java | 52 + .../camerax/DeviceOrientationManagerTest.java | 313 +++ .../plugins/camerax/InstanceManagerTest.java | 15 + .../flutter/plugins/camerax/PreviewTest.java | 221 ++ .../camerax/ProcessCameraProviderTest.java | 56 + .../plugins/camerax/SystemServicesTest.java | 138 + .../example/android/build.gradle | 2 +- .../example/android/gradle.properties | 2 +- .../integration_test/integration_test.dart | 18 +- .../example/lib/camera_controller.dart | 957 +++++++ .../example/lib/camera_image.dart | 177 ++ .../example/lib/camera_preview.dart | 81 + .../example/lib/main.dart | 1047 +++++++- .../example/pubspec.yaml | 1 + .../lib/src/android_camera_camerax.dart | 363 ++- ...roid_camera_camerax_flutter_api_impls.dart | 17 +- .../lib/src/camera.dart | 53 + .../lib/src/camera_info.dart | 2 +- .../lib/src/camera_selector.dart | 34 +- .../lib/src/camerax_library.g.dart | 855 +++++++ .../lib/src/camerax_library.pigeon.dart | 374 --- .../lib/src/instance_manager.dart | 20 +- .../lib/src/java_object.dart | 2 +- .../lib/src/preview.dart | 126 + .../lib/src/process_camera_provider.dart | 92 +- .../lib/src/surface.dart | 34 + .../lib/src/system_services.dart | 147 ++ .../lib/src/use_case.dart | 14 + .../pigeons/camerax_library.dart | 65 +- .../camera_android_camerax/pubspec.yaml | 6 +- .../test/android_camera_camerax_test.dart | 405 +++ .../android_camera_camerax_test.mocks.dart | 389 +++ .../test/camera_info_test.dart | 4 +- .../test/camera_info_test.mocks.dart | 12 +- .../test/camera_selector_test.dart | 12 +- .../test/camera_selector_test.mocks.dart | 38 +- .../test/camera_test.dart | 26 + .../test/preview_test.dart | 138 + .../test/preview_test.mocks.dart | 89 + .../test/process_camera_provider_test.dart | 113 +- .../process_camera_provider_test.mocks.dart | 62 +- .../test/system_services_test.dart | 110 + .../test/system_services_test.mocks.dart | 66 + .../test/test_camerax_library.g.dart | 475 ++++ .../test/test_camerax_library.pigeon.dart | 187 -- .../camera/camera_avfoundation/CHANGELOG.md | 21 + .../example/integration_test/camera_test.dart | 33 +- .../example/lib/camera_controller.dart | 122 +- .../camera_avfoundation/example/lib/main.dart | 14 +- .../camera_avfoundation/example/pubspec.yaml | 2 +- .../ios/Classes/CameraPlugin.m | 7 +- .../camera_avfoundation/ios/Classes/FLTCam.h | 9 + .../camera_avfoundation/ios/Classes/FLTCam.m | 9 + .../lib/src/avfoundation_camera.dart | 93 +- .../camera_avfoundation/lib/src/utils.dart | 9 +- .../camera/camera_avfoundation/pubspec.yaml | 6 +- .../test/avfoundation_camera_test.dart | 64 +- .../test/method_channel_mock.dart | 10 +- .../camera_platform_interface/CHANGELOG.md | 21 + .../method_channel/method_channel_camera.dart | 87 +- .../platform_interface/camera_platform.dart | 7 +- .../lib/src/types/exposure_mode.dart | 2 - .../lib/src/types/focus_mode.dart | 2 - .../lib/src/types/image_format_group.dart | 1 - .../lib/src/types/types.dart | 1 + .../lib/src/utils/utils.dart | 2 - .../camera_platform_interface/pubspec.yaml | 4 +- .../test/camera_platform_interface_test.dart | 46 + .../method_channel_camera_test.dart | 37 +- .../test/utils/method_channel_mock.dart | 10 +- packages/camera/camera_web/CHANGELOG.md | 12 + .../example/integration_test/camera_test.dart | 19 +- .../integration_test/camera_web_test.dart | 81 +- .../integration_test/helpers/mocks.dart | 2 + .../camera/camera_web/example/pubspec.yaml | 2 +- .../camera_web/lib/src/camera_service.dart | 8 +- .../camera/camera_web/lib/src/camera_web.dart | 18 +- packages/camera/camera_web/pubspec.yaml | 6 +- .../test/more_tests_exist_elsewhere_test.dart | 2 + packages/camera/camera_windows/CHANGELOG.md | 12 + .../example/integration_test/camera_test.dart | 12 +- .../camera_windows/example/pubspec.yaml | 2 +- .../camera_windows/lib/camera_windows.dart | 36 +- packages/camera/camera_windows/pubspec.yaml | 6 +- .../test/camera_windows_test.dart | 19 +- .../test/utils/method_channel_mock.dart | 10 +- packages/espresso/CHANGELOG.md | 14 + packages/espresso/android/build.gradle | 27 +- .../example/android/gradle.properties | 2 +- packages/espresso/example/pubspec.yaml | 2 +- packages/espresso/pubspec.yaml | 4 +- .../file_selector/file_selector/CHANGELOG.md | 5 + .../example/lib/get_directory_page.dart | 10 +- .../example/lib/open_image_page.dart | 10 +- .../lib/open_multiple_images_page.dart | 10 +- .../example/lib/open_text_page.dart | 10 +- .../example/lib/save_text_page.dart | 4 +- .../file_selector/example/pubspec.yaml | 1 + .../file_selector/file_selector/pubspec.yaml | 2 +- .../file_selector_ios/CHANGELOG.md | 5 + .../example/lib/open_image_page.dart | 10 +- .../lib/open_multiple_images_page.dart | 10 +- .../example/lib/open_text_page.dart | 10 +- .../file_selector_ios/example/pubspec.yaml | 3 +- .../file_selector_ios/pigeons/messages.dart | 2 +- .../file_selector_ios/pubspec.yaml | 2 +- .../test/file_selector_ios_test.dart | 2 +- .../test/file_selector_ios_test.mocks.dart | 2 +- .../test/{test_api.dart => test_api.g.dart} | 0 .../file_selector_linux/CHANGELOG.md | 9 + .../example/lib/get_directory_page.dart | 10 +- .../lib/get_multiple_directories_page.dart | 87 + .../example/lib/home_page.dart | 7 + .../file_selector_linux/example/lib/main.dart | 3 + .../example/lib/open_image_page.dart | 10 +- .../lib/open_multiple_images_page.dart | 10 +- .../example/lib/open_text_page.dart | 10 +- .../example/lib/save_text_page.dart | 4 +- .../file_selector_linux/example/pubspec.yaml | 5 +- .../lib/file_selector_linux.dart | 27 +- .../file_selector_linux/linux/.gitignore | 2 + .../linux/file_selector_plugin.cc | 6 +- .../linux/test/file_selector_plugin_test.cc | 14 + .../file_selector_linux/pubspec.yaml | 6 +- .../test/file_selector_linux_test.dart | 386 +-- .../file_selector_macos/CHANGELOG.md | 9 + .../example/lib/get_directory_page.dart | 10 +- .../example/lib/open_image_page.dart | 10 +- .../lib/open_multiple_images_page.dart | 10 +- .../example/lib/open_text_page.dart | 10 +- .../example/lib/save_text_page.dart | 4 +- .../macos/RunnerTests/RunnerTests.swift | 120 +- .../file_selector_macos/example/pubspec.yaml | 2 +- .../lib/file_selector_macos.dart | 105 +- .../lib/src/messages.g.dart | 227 ++ .../macos/Classes/FileSelectorPlugin.swift | 113 +- .../macos/Classes/messages.g.swift | 228 ++ .../pigeons/copyright.txt | 0 .../file_selector_macos/pigeons/messages.dart | 84 + .../file_selector_macos/pubspec.yaml | 7 +- .../test/file_selector_macos_test.dart | 335 +-- .../test/file_selector_macos_test.mocks.dart | 51 + .../test/messages_test.g.dart | 107 + .../CHANGELOG.md | 8 + .../method_channel_file_selector.dart | 17 +- .../file_selector_interface.dart | 16 +- .../pubspec.yaml | 4 +- ...file_selector_platform_interface_test.dart | 10 + .../method_channel_file_selector_test.dart | 277 +- .../file_selector_web/CHANGELOG.md | 4 + .../file_selector_web/example/pubspec.yaml | 2 +- .../file_selector_web/pubspec.yaml | 2 +- .../test/more_tests_exist_elsewhere_test.dart | 2 + .../file_selector_windows/CHANGELOG.md | 5 + .../example/lib/get_directory_page.dart | 10 +- .../example/lib/open_image_page.dart | 10 +- .../lib/open_multiple_images_page.dart | 10 +- .../example/lib/open_text_page.dart | 10 +- .../example/lib/save_text_page.dart | 4 +- .../example/pubspec.yaml | 2 +- .../pigeons/messages.dart | 2 +- .../file_selector_windows/pubspec.yaml | 2 +- .../test/file_selector_windows_test.dart | 2 +- .../file_selector_windows_test.mocks.dart | 2 +- .../test/{test_api.dart => test_api.g.dart} | 0 .../CHANGELOG.md | 2 +- .../android/build.gradle | 7 +- .../example/android/gradle.properties | 2 +- .../example/pubspec.yaml | 1 + .../pubspec.yaml | 2 +- .../google_maps_flutter/CHANGELOG.md | 10 + .../google_maps_flutter/README.md | 35 +- .../example/android/gradle.properties | 2 +- .../example/build.excerpt.yaml | 15 + .../example/lib/map_coordinates.dart | 2 +- .../example/lib/map_ui.dart | 2 +- .../example/lib/place_marker.dart | 2 +- .../example/lib/readme_sample.dart | 72 + .../google_maps_flutter/example/pubspec.yaml | 5 +- .../google_maps_flutter/pubspec.yaml | 4 +- .../test/circle_updates_test.dart | 17 +- .../test/fake_maps_controllers.dart | 25 +- .../test/google_map_test.dart | 14 +- .../test/map_creation_test.dart | 4 + .../test/marker_updates_test.dart | 18 +- .../test/polygon_updates_test.dart | 17 +- .../test/polyline_updates_test.dart | 17 +- .../test/tile_overlay_updates_test.dart | 14 +- .../google_maps_flutter_android/CHANGELOG.md | 26 + .../google_maps_flutter_android/README.md | 23 + .../android/build.gradle | 9 +- .../flutter/plugins/googlemaps/Convert.java | 6 +- .../googlemaps/GoogleMapController.java | 25 +- .../plugins/googlemaps/GoogleMapFactory.java | 6 +- .../googlemaps/GoogleMapInitializer.java | 109 + .../plugins/googlemaps/GoogleMapsPlugin.java | 7 +- .../plugins/googlemaps/ConvertTest.java | 29 + .../googlemaps/GoogleMapControllerTest.java | 21 + .../googlemaps/GoogleMapInitializerTest.java | 98 + .../example/android/gradle.properties | 2 +- ..._maps_test.dart => google_maps_tests.dart} | 8 +- .../latest_renderer_test.dart | 40 + .../legacy_renderer_test.dart | 40 + .../example/lib/map_coordinates.dart | 2 +- .../example/lib/map_ui.dart | 2 +- .../example/lib/place_marker.dart | 2 +- .../example/lib/readme_excerpts.dart | 41 +- .../example/pubspec.yaml | 4 +- .../lib/src/google_maps_flutter_android.dart | 132 +- .../google_maps_flutter_android/pubspec.yaml | 2 +- .../google_maps_flutter_android_test.dart | 25 +- .../google_maps_flutter_ios/CHANGELOG.md | 11 + .../example/ios/Podfile | 3 - .../ios/RunnerUITests/GoogleMapsUITests.m | 33 +- .../example/lib/map_coordinates.dart | 36 +- .../example/lib/map_ui.dart | 2 +- .../example/lib/place_marker.dart | 2 +- .../example/pubspec.yaml | 4 +- .../lib/src/google_maps_flutter_ios.dart | 56 +- .../google_maps_flutter_ios/pubspec.yaml | 4 +- .../test/google_maps_flutter_ios_test.dart | 25 +- .../CHANGELOG.md | 8 + .../method_channel_google_maps_flutter.dart | 56 +- .../pubspec.yaml | 4 +- ...thod_channel_google_maps_flutter_test.dart | 25 +- .../google_maps_flutter_web/CHANGELOG.md | 9 + .../example/pubspec.yaml | 2 +- .../lib/src/convert.dart | 48 +- .../google_maps_flutter_web/pubspec.yaml | 4 +- .../test/tests_exist_elsewhere_test.dart | 2 + .../google_sign_in/CHANGELOG.md | 21 + .../google_sign_in/google_sign_in/README.md | 16 +- .../example/android/gradle.properties | 2 +- .../google_sign_in/example/lib/main.dart | 10 +- .../google_sign_in/example/pubspec.yaml | 2 +- .../google_sign_in/lib/google_sign_in.dart | 5 +- .../google_sign_in/pubspec.yaml | 7 +- .../test/google_sign_in_test.dart | 2 +- .../google_sign_in_android/CHANGELOG.md | 21 + .../android/build.gradle | 11 +- .../googlesignin/GoogleSignInPlugin.java | 28 +- .../googlesignin/GoogleSignInTest.java | 26 + .../example/android/gradle.properties | 2 +- .../example/lib/main.dart | 2 +- .../example/pubspec.yaml | 2 +- .../lib/google_sign_in_android.dart | 1 + .../google_sign_in_android/pubspec.yaml | 4 +- .../test/google_sign_in_android_test.dart | 34 +- .../google_sign_in_ios/CHANGELOG.md | 5 + .../google_sign_in_ios/example/lib/main.dart | 2 +- .../google_sign_in_ios/example/pubspec.yaml | 2 +- .../lib/google_sign_in_ios.dart | 1 + .../google_sign_in_ios/pubspec.yaml | 4 +- .../test/google_sign_in_ios_test.dart | 36 +- .../CHANGELOG.md | 2 +- .../pubspec.yaml | 2 +- .../method_channel_google_sign_in_test.dart | 14 +- .../google_sign_in_web/CHANGELOG.md | 10 +- .../google_sign_in_web/README.md | 158 +- .../google_sign_in_web/example/build.yaml | 6 + .../auth2_legacy_init_test.dart | 223 -- .../example/integration_test/auth2_test.dart | 230 -- .../gapi_load_legacy_init_test.dart | 51 - .../integration_test/gapi_load_test.dart | 50 - .../gapi_mocks/gapi_mocks.dart | 13 - .../gapi_mocks/src/auth2_init.dart | 109 - .../integration_test/gapi_mocks/src/gapi.dart | 12 - .../gapi_mocks/src/google_user.dart | 30 - .../gapi_mocks/src/test_iife.dart | 15 - .../integration_test/gapi_utils_test.dart | 70 - .../google_sign_in_web_test.dart | 219 ++ .../google_sign_in_web_test.mocks.dart | 125 + .../example/integration_test/people_test.dart | 132 + .../example/integration_test/src/dom.dart | 59 + .../integration_test/src/jsify_as.dart | 10 + .../integration_test/src/jwt_examples.dart | 46 + .../example/integration_test/src/person.dart | 66 + .../integration_test/src/test_utils.dart | 10 - .../example/integration_test/utils_test.dart | 173 ++ .../google_sign_in_web/example/pubspec.yaml | 5 +- .../google_sign_in_web/example/regen_mocks.sh | 9 +- .../google_sign_in_web/example/run_test.sh | 7 +- .../lib/google_sign_in_web.dart | 178 +- .../lib/src/generated/gapi.dart | 54 - .../lib/src/generated/gapiauth2.dart | 503 ---- .../lib/src/gis_client.dart | 310 +++ .../google_sign_in_web/lib/src/load_gapi.dart | 59 - .../google_sign_in_web/lib/src/people.dart | 152 ++ .../google_sign_in_web/lib/src/utils.dart | 112 +- .../google_sign_in_web/pubspec.yaml | 8 +- .../test/tests_exist_elsewhere_test.dart | 2 + .../image_picker/image_picker/CHANGELOG.md | 10 + packages/image_picker/image_picker/README.md | 2 +- .../example/android/gradle.properties | 2 +- .../image_picker/example/lib/main.dart | 2 +- .../image_picker/example/pubspec.yaml | 2 +- .../image_picker/image_picker/pubspec.yaml | 4 +- .../image_picker_android/CHANGELOG.md | 13 + .../image_picker_android/android/build.gradle | 6 +- .../plugins/imagepicker/FileUtils.java | 79 +- .../imagepicker/ImagePickerDelegate.java | 16 +- .../plugins/imagepicker/FileUtilTest.java | 66 + .../imagepicker/ImagePickerDelegateTest.java | 23 + .../example/android/app/build.gradle | 2 + .../ImagePickerPickTest.java | 43 + .../android/app/src/debug/AndroidManifest.xml | 12 + .../DriverExtensionActivity.java | 16 + .../DummyContentProvider.java | 68 + .../app/src/main/res/raw/ic_launcher.png | Bin 0 -> 442 bytes .../example/android/gradle.properties | 2 +- .../example/lib/main.dart | 88 +- .../image_picker_android/example/pubspec.yaml | 6 +- .../image_picker_android/pubspec.yaml | 4 +- .../test/image_picker_android_test.dart | 91 +- .../image_picker_for_web/CHANGELOG.md | 4 + .../image_picker_for_web/example/pubspec.yaml | 2 +- .../image_picker_for_web/pubspec.yaml | 2 +- .../test/tests_exist_elsewhere_test.dart | 2 + .../image_picker_ios/CHANGELOG.md | 29 + .../ios/Runner.xcodeproj/project.pbxproj | 48 + .../xcshareddata/xcschemes/Runner.xcscheme | 10 - .../ios/RunnerTests/ImagePickerPluginTests.m | 175 +- .../example/ios/RunnerTests/ImageUtilTests.m | 17 +- .../ios/RunnerTests/PhotoAssetUtilTests.m | 21 +- .../PickerSaveImageToPathOperationTests.m | 240 +- .../ImagePickerFromGalleryUITests.m | 88 +- .../ImagePickerFromLimitedGalleryUITests.m | 81 +- .../example/ios/TestImages/bmpImage.bmp | Bin 0 -> 92 bytes .../example/ios/TestImages/heicImage.heic | Bin 0 -> 3207 bytes .../example/ios/TestImages/icnsImage.icns | Bin 0 -> 354 bytes .../example/ios/TestImages/icoImage.ico | Bin 0 -> 139 bytes .../jpgImageWithRightOrientation.jpg | Bin 0 -> 3375 bytes .../example/ios/TestImages/proRawImage.dng | Bin 0 -> 8100 bytes .../example/ios/TestImages/svgImage.svg | 3 + .../example/ios/TestImages/tiffImage.tiff | Bin 0 -> 4192 bytes .../image_picker_ios/example/pubspec.yaml | 2 +- .../ios/Classes/FLTImagePickerImageUtil.m | 6 +- .../ios/Classes/FLTImagePickerMetaDataUtil.m | 2 +- .../Classes/FLTImagePickerPhotoAssetUtil.m | 7 +- .../ios/Classes/FLTImagePickerPlugin.h | 8 +- .../ios/Classes/FLTImagePickerPlugin.m | 104 +- .../ios/Classes/FLTImagePickerPlugin_Test.h | 8 +- .../FLTPHPickerSaveImageToPathOperation.h | 9 +- .../FLTPHPickerSaveImageToPathOperation.m | 71 +- .../lib/image_picker_ios.dart | 18 +- .../image_picker_ios/pigeons/messages.dart | 2 +- .../image_picker_ios/pubspec.yaml | 4 +- .../test/image_picker_ios_test.dart | 2 +- .../test/{test_api.dart => test_api.g.dart} | 0 .../CHANGELOG.md | 4 + .../pubspec.yaml | 2 +- .../new_method_channel_image_picker_test.dart | 104 +- .../image_picker_windows/CHANGELOG.md | 5 + .../example/lib/main.dart | 32 +- .../image_picker_windows/example/pubspec.yaml | 2 +- .../image_picker_windows/pubspec.yaml | 4 +- .../test/image_picker_windows_test.dart | 64 +- .../in_app_purchase/CHANGELOG.md | 21 + .../in_app_purchase/in_app_purchase/README.md | 8 +- .../example/android/app/build.gradle | 2 +- .../example/android/gradle.properties | 2 +- .../ios/Flutter/AppFrameworkInfo.plist | 2 +- .../in_app_purchase/example/ios/Podfile | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 11 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../example/ios/Runner/Info.plist | 4 + .../in_app_purchase/example/lib/main.dart | 26 +- .../macos/Flutter/Flutter-Debug.xcconfig | 2 + .../macos/Flutter/Flutter-Release.xcconfig | 2 + .../in_app_purchase/example/macos/Podfile | 40 + .../macos/Runner.xcodeproj/project.pbxproj | 631 +++++ .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/xcschemes/Runner.xcscheme | 87 + .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../example/macos/Runner/AppDelegate.swift | 0 .../macos/Runner/Base.lproj/MainMenu.xib | 343 +++ .../macos/Runner/Configs/AppInfo.xcconfig | 14 + .../macos/Runner/Configs/Debug.xcconfig | 0 .../macos/Runner/Configs/Release.xcconfig | 0 .../macos/Runner/Configs/Warnings.xcconfig | 0 .../macos/Runner/DebugProfile.entitlements | 0 .../example/macos/Runner/Info.plist | 0 .../macos/Runner/MainFlutterWindow.swift | 0 .../example/macos/Runner/Release.entitlements | 0 .../in_app_purchase/example/pubspec.yaml | 4 +- .../in_app_purchase/lib/in_app_purchase.dart | 3 +- .../in_app_purchase/pubspec.yaml | 14 +- .../in_app_purchase_android/CHANGELOG.md | 22 + .../android/build.gradle | 11 +- .../example/android/app/build.gradle | 2 +- .../example/android/gradle.properties | 2 +- .../example/lib/main.dart | 4 + .../example/pubspec.yaml | 2 +- .../billing_client_wrapper.dart | 8 +- .../src/in_app_purchase_android_platform.dart | 2 +- .../in_app_purchase_android/pubspec.yaml | 4 +- .../billing_client_wrapper_test.dart | 11 +- ...rchase_android_platform_addition_test.dart | 10 +- ...in_app_purchase_android_platform_test.dart | 22 +- .../CHANGELOG.md | 4 + .../pubspec.yaml | 2 +- .../in_app_purchase_storekit/CHANGELOG.md | 27 +- .../in_app_purchase_storekit/README.md | 2 +- .../darwin/Classes/FIAObjectTranslator.h | 62 + .../darwin/Classes/FIAObjectTranslator.m | 297 +++ .../darwin/Classes/FIAPPaymentQueueDelegate.h | 21 + .../darwin/Classes/FIAPPaymentQueueDelegate.m | 80 + .../darwin/Classes/FIAPReceiptManager.h | 17 + .../darwin/Classes/FIAPReceiptManager.m | 45 + .../darwin/Classes/FIAPRequestHandler.h | 20 + .../darwin/Classes/FIAPRequestHandler.m | 55 + .../darwin/Classes/FIAPaymentQueueHandler.h | 132 + .../darwin/Classes/FIAPaymentQueueHandler.m | 236 ++ .../darwin/Classes/FIATransactionCache.h | 31 + .../darwin/Classes/FIATransactionCache.m | 40 + .../darwin/Classes/InAppPurchasePlugin.h | 21 + .../darwin/Classes/InAppPurchasePlugin.m | 451 ++++ .../darwin/in_app_purchase_storekit.podspec | 26 + .../ios/Flutter/AppFrameworkInfo.plist | 2 +- .../example/ios/Podfile | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 9 +- .../example/ios/Runner/Info.plist | 4 + .../RunnerTests/FIAPPaymentQueueDeleteTests.m | 121 +- .../RunnerTests/FIATransactionCacheTests.m | 64 +- .../RunnerTests/InAppPurchasePluginTests.m | 521 +--- .../example/ios/RunnerTests/Info.plist | 23 +- .../ios/RunnerTests/PaymentQueueTests.m | 421 +-- .../RunnerTests/ProductRequestHandlerTests.m | 90 +- .../example/ios/RunnerTests/Stubs.h | 72 +- .../example/ios/RunnerTests/Stubs.m | 331 +-- .../example/ios/RunnerTests/TranslatorTests.m | 417 +-- .../example/lib/main.dart | 2 + .../macos/Flutter/Flutter-Debug.xcconfig | 2 + .../macos/Flutter/Flutter-Release.xcconfig | 2 + .../example/macos/Podfile | 46 + .../macos/Runner.xcodeproj/project.pbxproj | 883 +++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/xcschemes/Runner.xcscheme | 98 + .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../example/macos/Runner/AppDelegate.swift | 0 .../macos/Runner/Base.lproj/MainMenu.xib | 343 +++ .../macos/Runner/Configs/AppInfo.xcconfig | 14 + .../macos/Runner/Configs/Debug.xcconfig | 1 + .../macos/Runner/Configs/Release.xcconfig | 1 + .../macos/Runner/DebugProfile.entitlements | 12 + .../example/macos/Runner/Info.plist | 0 .../macos/Runner/MainFlutterWindow.swift | 0 .../example/macos/Runner/Release.entitlements | 8 + .../RunnerTests/FIAPPaymentQueueDeleteTests.m | 1 + .../RunnerTests/FIATransactionCacheTests.m | 1 + .../RunnerTests/InAppPurchasePluginTests.m | 1 + .../example/macos/RunnerTests/Info.plist | 1 + .../macos/RunnerTests/PaymentQueueTests.m | 1 + .../RunnerTests/ProductRequestHandlerTests.m | 1 + .../example/macos/RunnerTests/Stubs.h | 1 + .../example/macos/RunnerTests/Stubs.m | 1 + .../macos/RunnerTests/TranslatorTests.m | 1 + .../example/pubspec.yaml | 2 +- .../RunnerTests/FIAPPaymentQueueDeleteTests.m | 125 + .../RunnerTests/FIATransactionCacheTests.m | 63 + .../RunnerTests/InAppPurchasePluginTests.m | 541 ++++ .../example/shared}/RunnerTests/Info.plist | 2 +- .../shared/RunnerTests/PaymentQueueTests.m | 420 +++ .../RunnerTests/ProductRequestHandlerTests.m | 89 + .../example/shared/RunnerTests/Stubs.h | 71 + .../example/shared/RunnerTests/Stubs.m | 330 +++ .../shared/RunnerTests/TranslatorTests.m | 414 +++ .../ios/Assets/.gitkeep | 0 .../ios/Classes/FIAObjectTranslator.h | 63 +- .../ios/Classes/FIAObjectTranslator.m | 298 +-- .../ios/Classes/FIAPPaymentQueueDelegate.h | 17 +- .../ios/Classes/FIAPPaymentQueueDelegate.m | 79 +- .../ios/Classes/FIAPReceiptManager.h | 18 +- .../ios/Classes/FIAPReceiptManager.m | 39 +- .../ios/Classes/FIAPRequestHandler.h | 21 +- .../ios/Classes/FIAPRequestHandler.m | 56 +- .../ios/Classes/FIAPaymentQueueHandler.h | 133 +- .../ios/Classes/FIAPaymentQueueHandler.m | 233 +- .../ios/Classes/FIATransactionCache.h | 32 +- .../ios/Classes/FIATransactionCache.m | 41 +- .../ios/Classes/InAppPurchasePlugin.h | 18 +- .../ios/Classes/InAppPurchasePlugin.m | 439 +--- .../ios/in_app_purchase_storekit.podspec | 25 +- ...p_purchase_storekit_platform_addition.dart | 2 + .../sk_payment_queue_delegate_wrapper.dart | 4 +- .../sk_payment_queue_wrapper.dart | 12 +- .../src/types/app_store_product_details.dart | 24 +- .../src/types/app_store_purchase_details.dart | 21 +- .../src/types/app_store_purchase_param.dart | 9 +- .../macos/Classes/FIAObjectTranslator.h | 1 + .../macos/Classes/FIAObjectTranslator.m | 1 + .../macos/Classes/FIAPPaymentQueueDelegate.h | 1 + .../macos/Classes/FIAPPaymentQueueDelegate.m | 1 + .../macos/Classes/FIAPReceiptManager.h | 1 + .../macos/Classes/FIAPReceiptManager.m | 1 + .../macos/Classes/FIAPRequestHandler.h | 1 + .../macos/Classes/FIAPRequestHandler.m | 1 + .../macos/Classes/FIAPaymentQueueHandler.h | 1 + .../macos/Classes/FIAPaymentQueueHandler.m | 1 + .../macos/Classes/FIATransactionCache.h | 1 + .../macos/Classes/FIATransactionCache.m | 1 + .../macos/Classes/InAppPurchasePlugin.h | 1 + .../macos/Classes/InAppPurchasePlugin.m | 1 + .../macos/in_app_purchase_storekit.podspec | 1 + .../in_app_purchase_storekit/pubspec.yaml | 12 +- .../test/fakes/fake_storekit_platform.dart | 36 +- ...rchase_storekit_platform_addtion_test.dart | 12 +- ...n_app_purchase_storekit_platform_test.dart | 12 +- .../sk_methodchannel_apis_test.dart | 18 +- .../sk_payment_queue_delegate_api_test.dart | 16 +- packages/ios_platform_images/CHANGELOG.md | 13 +- packages/ios_platform_images/README.md | 6 +- .../ios/Flutter/AppFrameworkInfo.plist | 2 +- .../ios_platform_images/example/ios/Podfile | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 13 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../example/ios/Runner/Info.plist | 4 + .../ios_platform_images/example/lib/main.dart | 1 + .../ios_platform_images/example/pubspec.yaml | 2 +- .../ios/ios_platform_images.podspec | 2 +- .../lib/ios_platform_images.dart | 21 +- packages/ios_platform_images/pubspec.yaml | 6 +- .../test/ios_platform_images_test.dart | 14 +- packages/local_auth/local_auth/CHANGELOG.md | 8 +- packages/local_auth/local_auth/README.md | 36 + .../example/android/app/build.gradle | 2 +- .../local_auth/example/android/build.gradle | 2 +- .../example/android/gradle.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../local_auth/example/lib/main.dart | 6 +- .../example/lib/readme_excerpts.dart | 2 +- .../local_auth/example/pubspec.yaml | 2 +- packages/local_auth/local_auth/pubspec.yaml | 5 +- .../local_auth_android/CHANGELOG.md | 14 + .../local_auth_android/android/build.gradle | 17 +- .../example/android/app/build.gradle | 2 +- .../example/android/build.gradle | 2 +- .../example/android/gradle.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../local_auth_android/example/lib/main.dart | 6 +- .../local_auth_android/example/pubspec.yaml | 2 +- .../local_auth_android/pubspec.yaml | 6 +- .../test/local_auth_test.dart | 10 +- .../local_auth/local_auth_ios/CHANGELOG.md | 12 + .../ios/RunnerTests/FLTLocalAuthPluginTests.m | 94 +- .../local_auth_ios/example/lib/main.dart | 6 +- .../local_auth_ios/example/pubspec.yaml | 2 +- .../ios/Classes/FLTLocalAuthPlugin.m | 13 +- .../local_auth/local_auth_ios/pubspec.yaml | 6 +- .../local_auth_ios/test/local_auth_test.dart | 10 +- .../CHANGELOG.md | 8 + .../pubspec.yaml | 5 +- .../default_method_channel_platform_test.dart | 18 +- .../local_auth_windows/CHANGELOG.md | 8 + .../local_auth_windows/example/lib/main.dart | 56 +- .../local_auth_windows/example/pubspec.yaml | 2 +- .../lib/local_auth_windows.dart | 66 +- .../lib/src/messages.g.dart | 81 + .../local_auth_windows/pigeons/copyright.txt | 3 + .../local_auth_windows/pigeons/messages.dart | 27 + .../local_auth_windows/pubspec.yaml | 5 +- .../test/local_auth_test.dart | 161 +- .../local_auth_windows/windows/CMakeLists.txt | 4 +- .../local_auth_windows/windows/local_auth.h | 33 +- .../windows/local_auth_plugin.cpp | 111 +- .../local_auth_windows/windows/messages.g.cpp | 110 + .../local_auth_windows/windows/messages.g.h | 93 + .../windows/test/local_auth_plugin_test.cpp | 150 +- .../local_auth_windows/windows/test/mocks.h | 19 - .../path_provider/path_provider/CHANGELOG.md | 6 + .../path_provider/path_provider/README.md | 2 +- .../example/android/gradle.properties | 2 +- .../integration_test/path_provider_test.dart | 15 +- .../path_provider/example/pubspec.yaml | 2 +- .../path_provider/lib/path_provider.dart | 96 +- .../path_provider/path_provider/pubspec.yaml | 13 +- .../path_provider_android/CHANGELOG.md | 8 + .../android/build.gradle | 6 +- .../pathprovider/PathProviderPlugin.java | 2 +- .../example/android/gradle.properties | 2 +- .../example/pubspec.yaml | 2 +- .../path_provider_android/pubspec.yaml | 4 +- .../.gitignore | 0 .../AUTHORS | 0 .../path_provider_foundation/CHANGELOG.md | 13 + .../LICENSE | 0 .../README.md | 4 +- .../darwin/Classes/PathProviderPlugin.swift | 67 + .../darwin/Classes/messages.g.swift | 60 + .../darwin}/RunnerTests/RunnerTests.swift | 65 +- .../darwin/path_provider_foundation.podspec | 25 + .../example/README.md | 0 .../integration_test/path_provider_test.dart | 0 .../example/ios/.gitignore | 2 + .../ios/Flutter/AppFrameworkInfo.plist | 6 +- .../example/ios/Flutter/Debug.xcconfig | 1 - .../example/ios/Flutter/Release.xcconfig | 1 - .../example/ios/Podfile | 6 +- .../ios/Runner.xcodeproj/project.pbxproj | 450 ++-- .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/WorkspaceSettings.xcsettings | 0 .../xcshareddata/xcschemes/Runner.xcscheme | 7 +- .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 0 .../example/ios/Runner/AppDelegate.swift | 0 .../AppIcon.appiconset/Contents.json | 6 + .../Icon-App-1024x1024@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin .../Icon-App-83.5x83.5@2x.png | Bin .../LaunchImage.imageset/Contents.json | 0 .../LaunchImage.imageset/LaunchImage.png | Bin .../LaunchImage.imageset/LaunchImage@2x.png | Bin .../LaunchImage.imageset/LaunchImage@3x.png | Bin .../LaunchImage.imageset/README.md | 0 .../Runner/Base.lproj/LaunchScreen.storyboard | 22 +- .../ios/Runner/Base.lproj/Main.storyboard | 0 .../example/ios/Runner/Info.plist | 18 +- .../ios/Runner/Runner-Bridging-Header.h | 0 .../example/lib/main.dart | 10 + .../macos/Flutter/Flutter-Debug.xcconfig | 0 .../macos/Flutter/Flutter-Release.xcconfig | 0 .../example/macos/Podfile | 0 .../macos/Runner.xcodeproj/project.pbxproj | 6 +- .../xcshareddata/xcschemes/Runner.xcscheme | 0 .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../example/macos/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 0 .../AppIcon.appiconset/app_icon_1024.png | Bin .../AppIcon.appiconset/app_icon_128.png | Bin .../AppIcon.appiconset/app_icon_16.png | Bin .../AppIcon.appiconset/app_icon_256.png | Bin .../AppIcon.appiconset/app_icon_32.png | Bin .../AppIcon.appiconset/app_icon_512.png | Bin .../AppIcon.appiconset/app_icon_64.png | Bin .../macos/Runner/Base.lproj/MainMenu.xib | 0 .../macos/Runner/Configs/AppInfo.xcconfig | 0 .../macos/Runner/Configs/Debug.xcconfig | 0 .../macos/Runner/Configs/Release.xcconfig | 0 .../macos/Runner/Configs/Warnings.xcconfig | 0 .../macos/Runner/DebugProfile.entitlements | 0 .../example/macos/Runner}/Info.plist | 16 +- .../macos/Runner/MainFlutterWindow.swift | 19 + .../example/macos/Runner/Release.entitlements | 0 .../example/macos}/RunnerTests/Info.plist | 0 .../example/pubspec.yaml | 6 +- .../example/test_driver/integration_test.dart | 0 .../ios/Classes/PathProviderPlugin.swift | 1 + .../ios/Classes/messages.g.swift | 1 + .../path_provider_foundation/ios/README.md | 4 + .../ios/path_provider_foundation.podspec | 1 + .../lib/messages.g.dart | 52 + .../lib/path_provider_foundation.dart} | 36 +- .../macos/Classes/PathProviderPlugin.swift | 1 + .../macos/Classes/messages.g.swift | 1 + .../path_provider_foundation/macos/README.md | 4 + .../macos/path_provider_foundation.podspec | 1 + .../pigeons/copyright.txt | 3 + .../pigeons/messages.dart | 17 +- .../pubspec.yaml | 20 +- .../test/messages_test.g.dart | 44 + .../test/path_provider_foundation_test.dart} | 118 +- .../path_provider_foundation_test.mocks.dart | 37 + .../path_provider_ios/CHANGELOG.md | 28 - .../path_provider/path_provider_ios/README.md | 11 - .../integration_test/path_provider_test.dart | 69 - .../example/ios/Flutter/Debug.xcconfig | 2 - .../example/ios/Flutter/Release.xcconfig | 2 - .../example/ios/Runner/AppDelegate.m | 16 - .../example/ios/Runner/main.m | 13 - .../ios/RunnerTests/PathProviderTests.m | 18 - .../path_provider_ios/example/lib/main.dart | 133 - .../path_provider_ios/example/pubspec.yaml | 30 - .../path_provider_ios/ios/Assets/.gitkeep | 0 .../ios/Classes/FLTPathProviderPlugin.m | 43 - .../ios/Classes/messages.g.h | 28 - .../ios/Classes/messages.g.m | 138 - .../ios/path_provider_ios.podspec | 22 - .../path_provider_ios/lib/messages.g.dart | 124 - .../path_provider_ios/pubspec.yaml | 34 - .../test/messages_test.g.dart | 88 - .../test/path_provider_ios_test.dart | 115 - .../path_provider_linux/CHANGELOG.md | 5 +- .../path_provider_linux/example/lib/main.dart | 12 +- .../path_provider_linux/example/pubspec.yaml | 2 +- .../path_provider_linux/pubspec.yaml | 6 +- .../path_provider_macos/CHANGELOG.md | 91 - .../lib/path_provider_macos.dart | 72 - .../macos/Classes/PathProviderPlugin.swift | 47 - .../macos/path_provider_macos.podspec | 22 - .../CHANGELOG.md | 4 + .../pubspec.yaml | 2 +- .../method_channel_path_provider_test.dart | 12 +- .../path_provider_windows/CHANGELOG.md | 4 + .../example/pubspec.yaml | 2 +- .../plugin_platform_interface/CHANGELOG.md | 4 + .../plugin_platform_interface/pubspec.yaml | 2 +- .../quick_actions/quick_actions/CHANGELOG.md | 4 + .../example/android/gradle.properties | 2 +- .../quick_actions/example/pubspec.yaml | 2 +- .../quick_actions/quick_actions/pubspec.yaml | 2 +- .../quick_actions_android/CHANGELOG.md | 4 + .../android/build.gradle | 7 +- .../example/android/app/build.gradle | 4 +- .../quickactionsexample/QuickActionsTest.java | 6 +- .../example/android/gradle.properties | 2 +- .../example/pubspec.yaml | 2 +- .../quick_actions_android/pubspec.yaml | 2 +- .../test/quick_actions_android_test.dart | 12 +- .../quick_actions_ios/CHANGELOG.md | 5 + .../quick_actions_ios/example/ios/Podfile | 1 - .../ios/Runner.xcodeproj/project.pbxproj | 42 +- .../DefaultShortcutItemParserTests.swift | 67 + .../RunnerTests/FLTQuickActionsPluginTests.m | 210 -- .../FLTShortcutStateManagerTests.m | 62 - .../RunnerTests/Mocks/MockMethodChannel.swift | 14 + .../Mocks/MockShortcutItemParser.swift | 16 + .../Mocks/MockShortcutItemProvider.swift} | 9 +- .../RunnerTests/QuickActionsPluginTests.swift | 294 +++ .../quick_actions_ios/example/pubspec.yaml | 2 +- .../ios/Classes/FLTShortcutStateManager.h | 18 - .../ios/Classes/FLTShortcutStateManager.m | 32 - .../ios/Classes/MethodChannel.swift | 16 + .../ios/Classes/QuickActionsPlugin.swift | 21 +- .../ios/Classes/ShortcutItemParser.swift | 46 + .../ios/Classes/ShortcutItemProviding.swift | 15 + .../ios/quick_actions_ios.podspec | 3 +- .../quick_actions_ios/pubspec.yaml | 4 +- .../test/quick_actions_ios_test.dart | 12 +- .../CHANGELOG.md | 4 + .../pubspec.yaml | 2 +- .../method_channel_quick_actions_test.dart | 12 +- .../shared_preferences/CHANGELOG.md | 10 + .../example/android/gradle.properties | 2 +- .../shared_preferences/example/lib/main.dart | 4 +- .../shared_preferences/example/pubspec.yaml | 2 +- .../shared_preferences/pubspec.yaml | 11 +- .../shared_preferences_android/CHANGELOG.md | 8 + .../android/build.gradle | 6 +- .../example/android/gradle.properties | 2 +- .../example/lib/main.dart | 4 +- .../example/pubspec.yaml | 2 +- .../shared_preferences_android/pubspec.yaml | 4 +- .../test/shared_preferences_android_test.dart | 33 +- .../shared_preferences_foundation}/AUTHORS | 0 .../CHANGELOG.md | 19 + .../shared_preferences_foundation}/LICENSE | 0 .../README.md | 4 +- .../Classes/SharedPreferencesPlugin.swift | 64 + .../darwin/Classes/messages.g.swift | 111 + .../darwin/Tests/RunnerTests.swift | 64 + .../shared_preferences_foundation.podspec} | 20 +- .../example/.gitignore | 4 +- .../example/README.md | 0 .../shared_preferences_test.dart | 2 +- .../example/ios/.gitignore | 34 + .../ios/Flutter/AppFrameworkInfo.plist | 6 +- .../example/ios/Flutter/Debug.xcconfig | 2 + .../example/ios/Flutter/Release.xcconfig | 2 + .../example/ios/Podfile | 5 +- .../ios/Runner.xcodeproj/project.pbxproj | 457 ++-- .../contents.xcworkspacedata | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 9 +- .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../example/ios/Runner/AppDelegate.swift | 17 + .../AppIcon.appiconset/Contents.json | 6 + .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 22 +- .../ios/Runner/Base.lproj/Main.storyboard | 0 .../example/ios/Runner/Info.plist | 18 +- .../ios/Runner/Runner-Bridging-Header.h} | 5 +- .../example/lib/main.dart | 4 +- .../macos/Flutter/Flutter-Debug.xcconfig | 0 .../macos/Flutter/Flutter-Release.xcconfig | 0 .../example/macos/Podfile | 0 .../macos/Runner.xcodeproj/project.pbxproj | 11 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../example/macos/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 0 .../AppIcon.appiconset/app_icon_1024.png | Bin .../AppIcon.appiconset/app_icon_128.png | Bin .../AppIcon.appiconset/app_icon_16.png | Bin .../AppIcon.appiconset/app_icon_256.png | Bin .../AppIcon.appiconset/app_icon_32.png | Bin .../AppIcon.appiconset/app_icon_512.png | Bin .../AppIcon.appiconset/app_icon_64.png | Bin .../macos/Runner/Base.lproj/MainMenu.xib | 0 .../macos/Runner/Configs/AppInfo.xcconfig | 0 .../macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 12 + .../example/macos/Runner/Info.plist | 32 + .../macos/Runner/MainFlutterWindow.swift | 19 + .../example/macos/Runner/Release.entitlements | 8 + .../example/macos/RunnerTests/Info.plist | 0 .../example/pubspec.yaml | 8 +- .../example/test_driver/integration_test.dart | 0 .../ios/Classes/SharedPreferencesPlugin.swift | 1 + .../ios/Classes/messages.g.swift | 1 + .../ios/README.md | 4 + .../ios/shared_preferences_foundation.podspec | 1 + .../lib/messages.g.dart | 124 +- .../lib/shared_preferences_foundation.dart} | 9 +- .../Classes/SharedPreferencesPlugin.swift | 1 + .../macos/Classes/messages.g.swift | 1 + .../macos/README.md | 4 + .../shared_preferences_foundation.podspec | 1 + .../pigeons/copyright_header.txt | 0 .../pigeons/messages.dart | 9 +- .../pubspec.yaml | 18 +- .../shared_preferences_foundation_test.dart} | 36 +- .../test/test_api.g.dart} | 36 +- .../shared_preferences_ios/AUTHORS | 66 - .../shared_preferences_ios/CHANGELOG.md | 25 - .../shared_preferences_ios/LICENSE | 25 - .../shared_preferences_ios/example/.metadata | 10 - .../shared_preferences_ios/example/README.md | 9 - .../shared_preferences_test.dart | 106 - .../example/ios/Runner/AppDelegate.m | 16 - .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 564 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 1283 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 1588 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 1025 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 1716 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 1920 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 1283 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 1895 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 2665 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 2665 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 3831 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 1888 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 3294 -> 0 bytes .../Icon-App-83.5x83.5@2x.png | Bin 3612 -> 0 bytes .../example/ios/Runner/main.m | 13 - .../ios/RunnerTests/SharedPreferencesTests.m | 18 - .../example/lib/main.dart | 93 - .../example/pubspec.yaml | 30 - .../ios/Assets/.gitkeep | 0 .../ios/Classes/FLTSharedPreferencesPlugin.m | 68 - .../ios/Classes/messages.g.h | 33 - .../ios/Classes/messages.g.m | 178 -- .../ios/shared_preferences_ios.podspec | 23 - .../shared_preferences_ios/pubspec.yaml | 27 - .../shared_preferences_linux/CHANGELOG.md | 9 + .../example/lib/main.dart | 4 +- .../example/pubspec.yaml | 2 +- .../lib/shared_preferences_linux.dart | 6 +- .../shared_preferences_linux/pubspec.yaml | 4 +- .../shared_preferences_macos/AUTHORS | 66 - .../shared_preferences_macos/CHANGELOG.md | 81 - .../shared_preferences_macos/LICENSE | 25 - .../shared_preferences_macos/README.md | 11 - .../example/README.md | 9 - .../macos/RunnerTests/RunnerTests.swift | 88 - .../example/test_driver/integration_test.dart | 7 - .../lib/shared_preferences_macos.dart | 53 - .../Classes/SharedPreferencesPlugin.swift | 61 - .../test/shared_preferences_macos_test.dart | 117 - .../CHANGELOG.md | 2 +- .../pubspec.yaml | 2 +- ...ethod_channel_shared_preferences_test.dart | 33 +- .../shared_preferences_web/CHANGELOG.md | 2 +- .../example/pubspec.yaml | 2 +- .../shared_preferences_web/pubspec.yaml | 2 +- .../test/tests_exist_elsewhere_test.dart | 2 + .../shared_preferences_windows/CHANGELOG.md | 9 + .../example/lib/main.dart | 4 +- .../example/pubspec.yaml | 2 +- .../lib/shared_preferences_windows.dart | 6 +- .../shared_preferences_windows/pubspec.yaml | 4 +- .../url_launcher/url_launcher/CHANGELOG.md | 13 + packages/url_launcher/url_launcher/README.md | 12 +- .../example/android/gradle.properties | 2 +- .../ios/Flutter/AppFrameworkInfo.plist | 2 +- .../url_launcher/example/ios/Podfile | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 11 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../example/ios/Runner/Info.plist | 4 + .../url_launcher/example/lib/basic.dart | 2 +- .../url_launcher/example/lib/encoding.dart | 6 + .../url_launcher/example/lib/files.dart | 4 +- .../url_launcher/example/lib/main.dart | 9 +- .../url_launcher/example/pubspec.yaml | 2 +- .../url_launcher/lib/src/legacy_api.dart | 5 +- .../lib/src/url_launcher_string.dart | 4 +- .../lib/src/url_launcher_uri.dart | 6 +- .../url_launcher/url_launcher/pubspec.yaml | 4 +- .../test/src/legacy_api_test.dart | 14 +- .../test/src/url_launcher_string_test.dart | 2 +- .../test/src/url_launcher_uri_test.dart | 2 +- .../url_launcher_android/CHANGELOG.md | 12 + .../url_launcher_android/android/build.gradle | 6 +- .../example/android/gradle.properties | 2 +- .../example/lib/main.dart | 9 +- .../url_launcher_android/example/pubspec.yaml | 2 +- .../lib/url_launcher_android.dart | 2 +- .../url_launcher_android/pubspec.yaml | 4 +- .../test/url_launcher_android_test.dart | 36 +- .../url_launcher_ios/CHANGELOG.md | 7 +- .../ios/Flutter/AppFrameworkInfo.plist | 2 +- .../url_launcher_ios/example/ios/Podfile | 2 +- .../ios/Runner.xcodeproj/project.pbxproj | 11 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../example/ios/Runner/Info.plist | 4 + .../url_launcher_ios/example/lib/main.dart | 15 +- .../url_launcher_ios/example/pubspec.yaml | 4 +- .../ios/Classes/FLTURLLauncherPlugin.m | 32 +- .../ios/url_launcher_ios.podspec | 3 +- .../url_launcher_ios/pubspec.yaml | 6 +- .../test/url_launcher_ios_test.dart | 10 +- .../url_launcher_linux/CHANGELOG.md | 5 + .../url_launcher_linux/example/lib/main.dart | 2 +- .../url_launcher_linux/example/pubspec.yaml | 2 +- .../url_launcher_linux/pubspec.yaml | 4 +- .../test/url_launcher_linux_test.dart | 10 +- .../url_launcher_macos/CHANGELOG.md | 5 + .../url_launcher_macos/example/lib/main.dart | 2 +- .../url_launcher_macos/example/pubspec.yaml | 2 +- .../url_launcher_macos/pubspec.yaml | 4 +- .../test/url_launcher_macos_test.dart | 10 +- .../CHANGELOG.md | 4 + .../lib/link.dart | 1 - .../pubspec.yaml | 2 +- .../method_channel_url_launcher_test.dart | 10 +- .../url_launcher_web/CHANGELOG.md | 5 + .../integration_test/link_widget_test.dart | 10 +- .../url_launcher_web/example/pubspec.yaml | 2 +- .../url_launcher_web/lib/src/link.dart | 8 +- .../url_launcher_web/pubspec.yaml | 4 +- .../test/tests_exist_elsewhere_test.dart | 2 + .../url_launcher_windows/CHANGELOG.md | 8 +- .../example/lib/main.dart | 2 +- .../url_launcher_windows/example/pubspec.yaml | 2 +- .../lib/src/messages.g.dart | 71 + .../lib/url_launcher_windows.dart | 34 +- .../pigeons/copyright.txt | 3 + .../pigeons/messages.dart | 18 + .../url_launcher_windows/pubspec.yaml | 5 +- .../test/url_launcher_windows_test.dart | 197 +- .../windows/CMakeLists.txt | 2 + .../windows/messages.g.cpp | 113 + .../url_launcher_windows/windows/messages.g.h | 86 + .../windows/system_apis.cpp | 4 +- .../windows/system_apis.h | 4 +- .../test/url_launcher_windows_test.cpp | 82 +- .../windows/url_launcher_plugin.cpp | 53 +- .../windows/url_launcher_plugin.h | 19 +- .../windows/url_launcher_windows.cpp | 2 +- .../video_player/video_player/CHANGELOG.md | 21 + .../example/android/app/build.gradle | 2 +- .../example/android/gradle.properties | 2 +- .../video_player/example/pubspec.yaml | 2 +- .../video_player/lib/video_player.dart | 47 +- .../video_player/video_player/pubspec.yaml | 6 +- .../video_player/test/video_player_test.dart | 58 +- .../video_player_android/CHANGELOG.md | 7 + .../video_player_android/android/build.gradle | 6 +- .../example/android/app/build.gradle | 2 +- .../example/android/gradle.properties | 2 +- .../example/lib/mini_controller.dart | 4 +- .../video_player_android/example/pubspec.yaml | 4 +- .../pigeons/messages.dart | 2 +- .../video_player_android/pubspec.yaml | 6 +- .../test/android_video_player_test.dart | 31 +- .../test/{test_api.dart => test_api.g.dart} | 0 .../video_player_avfoundation/CHANGELOG.md | 9 +- .../example/lib/mini_controller.dart | 4 +- .../example/pubspec.yaml | 4 +- .../pigeons/messages.dart | 2 +- .../video_player_avfoundation/pubspec.yaml | 6 +- .../test/avfoundation_video_player_test.dart | 27 +- .../test/{test_api.dart => test_api.g.dart} | 0 .../CHANGELOG.md | 8 + .../lib/video_player_platform_interface.dart | 2 +- .../pubspec.yaml | 4 +- .../video_player_web/CHANGELOG.md | 5 + .../video_player_web/example/pubspec.yaml | 4 +- .../video_player_web/pubspec.yaml | 6 +- .../test/tests_exist_elsewhere_test.dart | 2 + .../webview_flutter/CHANGELOG.md | 28 +- .../webview_flutter/webview_flutter/README.md | 238 +- .../example/android/gradle.properties | 2 +- .../example/build.excerpt.yaml | 15 + .../legacy/webview_flutter_test.dart | 1382 ++++++++++ .../webview_flutter_test.dart | 1288 +++------- .../webview_flutter/example/lib/main.dart | 602 ++--- .../example/lib/simple_example.dart | 59 + .../webview_flutter/example/pubspec.yaml | 8 +- .../example/test/main_test.dart | 103 +- .../{ => src/legacy}/platform_interface.dart | 2 +- .../lib/src/{ => legacy}/webview.dart | 16 +- .../lib/src/navigation_delegate.dart | 153 ++ .../src/v4/src/webview_cookie_manager.dart | 37 - .../lib/src/v4/src/webview_widget.dart | 64 - .../lib/src/v4/webview_flutter.dart | 12 - .../src/{v4/src => }/webview_controller.dart | 73 +- .../lib/src/webview_cookie_manager.dart | 89 + .../lib/src/webview_flutter_legacy.dart | 9 + .../lib/src/webview_widget.dart | 122 + .../webview_flutter/lib/webview_flutter.dart | 30 +- .../webview_flutter/pubspec.yaml | 15 +- .../test/legacy/webview_flutter_test.dart | 1367 ++++++++++ .../legacy/webview_flutter_test.mocks.dart | 346 +++ .../test/navigation_delegate_test.dart | 91 + .../test/navigation_delegate_test.mocks.dart | 231 ++ .../v4/webview_controller_test.mocks.dart | 203 -- .../test/v4/webview_widget_test.mocks.dart | 246 -- .../{v4 => }/webview_controller_test.dart | 29 +- .../test/webview_controller_test.mocks.dart | 417 +++ .../{v4 => }/webview_cookie_manager_test.dart | 4 +- .../webview_cookie_manager_test.mocks.dart | 47 +- .../test/webview_flutter_test.dart | 1393 +--------- .../test/webview_flutter_test.mocks.dart | 213 -- .../test/{v4 => }/webview_widget_test.dart | 7 +- .../test/webview_widget_test.mocks.dart | 396 +++ .../webview_flutter_android/CHANGELOG.md | 49 + .../webview_flutter_android/README.md | 41 + .../android/build.gradle | 6 +- .../DownloadListenerFlutterApiImpl.java | 14 - .../DownloadListenerHostApiImpl.java | 21 +- .../FileChooserParamsFlutterApiImpl.java | 74 + .../GeneratedAndroidWebView.java | 1041 ++++---- .../webviewflutter/InstanceManager.java | 69 +- .../webviewflutter/JavaObjectHostApiImpl.java | 4 + .../webviewflutter/JavaScriptChannel.java | 19 +- .../JavaScriptChannelFlutterApiImpl.java | 14 - .../plugins/webviewflutter/Releasable.java | 14 - .../WebChromeClientFlutterApiImpl.java | 35 +- .../WebChromeClientHostApiImpl.java | 104 +- .../WebSettingsHostApiImpl.java | 5 - .../WebViewClientFlutterApiImpl.java | 14 - .../WebViewClientHostApiImpl.java | 135 +- .../WebViewFlutterAndroidExternalApi.java | 50 + .../webviewflutter/WebViewFlutterPlugin.java | 22 +- .../webviewflutter/WebViewHostApiImpl.java | 216 +- .../webviewflutter/DownloadListenerTest.java | 9 - .../webviewflutter/FileChooserParamsTest.java | 74 + .../webviewflutter/InstanceManagerTest.java | 49 + .../webviewflutter/JavaScriptChannelTest.java | 7 - .../webviewflutter/WebChromeClientTest.java | 15 +- .../webviewflutter/WebViewClientTest.java | 48 +- ...WebViewFlutterAndroidExternalApiTest.java} | 23 +- .../plugins/webviewflutter/WebViewTest.java | 171 +- .../example/android/gradle.properties | 2 +- .../legacy/webview_flutter_test.dart | 1574 ++++++++++++ .../webview_flutter_test.dart | 1795 ++++++------- .../lib/{ => legacy}/navigation_decision.dart | 0 .../lib/{ => legacy}/navigation_request.dart | 0 .../example/lib/{ => legacy}/web_view.dart | 11 +- .../example/lib/main.dart | 655 +++-- .../example/pubspec.yaml | 3 +- .../lib/src/android_proxy.dart | 106 + .../lib/src/android_webview.dart | 302 ++- ...iew.pigeon.dart => android_webview.g.dart} | 1344 +++++----- .../lib/src/android_webview_api_impls.dart | 216 +- .../lib/src/android_webview_controller.dart | 914 +++++++ .../src/android_webview_cookie_manager.dart | 74 + .../lib/src/android_webview_platform.dart | 44 + .../lib/{ => src/legacy}/webview_android.dart | 5 +- .../webview_android_cookie_manager.dart | 5 +- .../legacy}/webview_android_widget.dart | 421 ++- .../legacy}/webview_surface_android.dart | 5 +- .../lib/src/platform_views_service_proxy.dart | 53 + .../lib/src/weak_reference_utils.dart | 34 + .../src/webview_flutter_android_legacy.dart} | 6 +- .../lib/webview_flutter_android.dart | 9 + .../pigeons/android_webview.dart | 79 +- .../webview_flutter_android/pubspec.yaml | 11 +- .../android_navigation_delegate_test.dart | 499 ++++ .../test/android_webview_controller_test.dart | 1018 ++++++++ ...android_webview_controller_test.mocks.dart | 2248 +++++++++++++++++ .../android_webview_cookie_manager_test.dart | 79 + ...id_webview_cookie_manager_test.mocks.dart} | 35 +- .../test/android_webview_test.dart | 311 ++- .../test/android_webview_test.mocks.dart | 1488 ++++++++--- .../{ => legacy}/surface_android_test.dart | 27 +- .../webview_android_cookie_manager_test.dart | 4 +- ...iew_android_cookie_manager_test.mocks.dart | 54 + .../webview_android_widget_test.dart | 346 +-- .../webview_android_widget_test.mocks.dart | 1026 ++++++++ ...igeon.dart => test_android_webview.g.dart} | 311 +-- .../webview_android_widget_test.mocks.dart | 572 ----- .../CHANGELOG.md | 15 + .../README.md | 4 +- .../javascript_channel_registry.dart | 0 .../platform_interface.dart | 0 .../webview_cookie_manager.dart | 0 .../platform_interface/webview_platform.dart | 0 .../webview_platform_callbacks_handler.dart | 0 .../webview_platform_controller.dart | 0 .../types/auto_media_playback_policy.dart | 0 .../{ => legacy}/types/creation_params.dart | 0 .../types/javascript_channel.dart | 0 .../src/legacy/types/javascript_message.dart | 14 + .../legacy}/types/javascript_mode.dart | 2 +- .../{v4/src => src/legacy}/types/types.dart | 11 +- .../src/legacy/types/web_resource_error.dart | 57 + .../types/web_resource_error_type.dart | 0 .../src/{ => legacy}/types/web_settings.dart | 0 .../legacy}/types/webview_cookie.dart | 32 +- .../{ => legacy}/types/webview_request.dart | 0 .../webview_method_channel.dart | 297 --- .../src/platform_navigation_delegate.dart | 31 +- .../src/platform_webview_controller.dart | 34 +- .../src/platform_webview_cookie_manager.dart | 7 + .../{v4 => }/src/platform_webview_widget.dart | 7 + .../lib/src/types/javascript_message.dart | 47 +- .../lib/src/types/javascript_mode.dart | 2 +- .../src/types/load_request_params.dart | 6 +- .../lib/src/types}/navigation_decision.dart | 0 .../lib/src/types/navigation_request.dart | 18 + ...m_navigation_delegate_creation_params.dart | 0 ...rm_webview_controller_creation_params.dart | 0 ...ebview_cookie_manager_creation_params.dart | 0 ...atform_webview_widget_creation_params.dart | 0 .../lib/src/types/types.dart | 13 +- .../lib/src/types/web_resource_error.dart | 138 +- .../lib/src/types/webview_cookie.dart | 32 +- ...ew_flutter_platform_interface_legacy.dart} | 6 +- .../lib/{v4 => }/src/webview_platform.dart | 2 +- .../lib/v4/src/types/javascript_message.dart | 51 - .../lib/v4/src/types/web_resource_error.dart | 119 - .../webview_flutter_platform_interface.dart | 10 - .../webview_flutter_platform_interface.dart | 7 +- .../pubspec.yaml | 4 +- .../javascript_channel_registry_test.dart | 3 +- .../webview_cookie_manager_test.dart | 3 +- .../types/javascript_channel_test.dart | 2 +- .../types/webview_cookie_test.dart | 2 +- .../types/webview_request_test.dart | 2 +- .../platform_navigation_delegate_test.dart | 5 +- .../platform_webview_controller_test.dart | 32 +- ...latform_webview_controller_test.mocks.dart | 106 + .../v4 => }/platform_webview_widget_test.dart | 4 +- .../webview_method_channel_test.dart | 746 ------ ...latform_webview_controller_test.mocks.dart | 72 - .../src/v4/webview_platform_test.mocks.dart | 78 - .../{src/v4 => }/webview_platform_test.dart | 66 +- .../test/webview_platform_test.mocks.dart | 146 ++ .../webview_flutter_web/CHANGELOG.md | 19 +- .../webview_flutter_web/README.md | 63 +- .../legacy/webview_flutter_test.dart | 72 + .../webview_flutter_test.dart | 80 +- .../example/lib/{ => legacy}/web_view.dart | 6 +- .../webview_flutter_web/example/lib/main.dart | 72 +- .../webview_flutter_web/example/pubspec.yaml | 3 +- .../webview_flutter_web/example/run_test.sh | 22 + .../lib/src/content_type.dart | 48 + .../lib/src/http_request_factory.dart | 81 + .../lib/{ => src}/shims/dart_ui.dart | 0 .../lib/{ => src}/shims/dart_ui_fake.dart | 0 .../lib/{ => src}/shims/dart_ui_real.dart | 0 .../lib/src/web_webview_controller.dart | 134 + .../lib/src/web_webview_platform.dart | 30 + .../lib/src/webview_flutter_web_legacy.dart | 220 ++ .../lib/webview_flutter_web.dart | 290 +-- .../webview_flutter_web/pubspec.yaml | 6 +- .../test/content_type_test.dart | 77 + .../test/legacy/webview_flutter_web_test.dart | 180 ++ .../webview_flutter_web_test.mocks.dart | 14 +- .../test/web_webview_controller_test.dart | 210 ++ .../web_webview_controller_test.mocks.dart | 360 +++ .../test/web_webview_widget_test.dart | 33 + .../test/webview_flutter_web_test.dart | 169 +- .../webview_flutter_wkwebview/CHANGELOG.md | 31 +- .../webview_flutter_wkwebview/README.md | 36 + .../legacy/webview_flutter_test.dart | 1311 ++++++++++ .../webview_flutter_test.dart | 1461 +++++------ .../ios/Runner.xcodeproj/project.pbxproj | 43 +- .../ios/RunnerTests/FWFDataConvertersTests.m | 3 + ...FWebViewFlutterWKWebViewExternalAPITests.m | 28 + .../lib/legacy/navigation_decision.dart | 12 + .../lib/{ => legacy}/navigation_request.dart | 0 .../example/lib/{ => legacy}/web_view.dart | 10 +- .../example/lib/main.dart | 670 ++--- .../example/pubspec.yaml | 3 +- .../ios/Classes/FLTWebViewFlutterPlugin.h | 5 + .../ios/Classes/FWFDataConverters.h | 9 + .../ios/Classes/FWFDataConverters.m | 20 +- .../ios/Classes/FWFGeneratedWebKitApis.h | 143 +- .../ios/Classes/FWFGeneratedWebKitApis.m | 772 +++--- .../ios/Classes/FWFUIViewHostApi.m | 2 +- .../FWFWebViewFlutterWKWebViewExternalAPI.h | 37 + .../FWFWebViewFlutterWKWebViewExternalAPI.m | 21 + .../ios/Classes/webview-umbrella.h | 1 + .../{web_kit.pigeon.dart => web_kit.g.dart} | 1218 ++++----- .../src/foundation/foundation_api_impls.dart | 2 +- .../{ => legacy}/web_kit_webview_widget.dart | 15 +- .../src/{ => legacy}/webview_cupertino.dart | 5 +- .../wkwebview_cookie_manager.dart | 7 +- .../lib/src/ui_kit/ui_kit_api_impls.dart | 2 +- .../lib/src/v4/webview_flutter_wkwebview.dart | 9 - .../lib/src/web_kit/web_kit.dart | 11 +- .../lib/src/web_kit/web_kit_api_impls.dart | 5 +- .../lib/src/{v4/src => }/webkit_proxy.dart | 21 +- .../src => }/webkit_webview_controller.dart | 234 +- .../webkit_webview_cookie_manager.dart | 6 +- .../{v4/src => }/webkit_webview_platform.dart | 7 +- .../webview_flutter_wkwebview_legacy.dart} | 8 +- .../lib/webview_flutter_wkwebview.dart | 7 +- .../pigeons/web_kit.dart | 41 +- .../webview_flutter_wkwebview/pubspec.yaml | 9 +- .../web_kit_cookie_manager_test.dart | 4 +- .../web_kit_cookie_manager_test.mocks.dart | 191 ++ .../web_kit_webview_widget_test.dart | 71 +- .../web_kit_webview_widget_test.mocks.dart | 1300 ++++++++++ ...eb_kit.pigeon.dart => test_web_kit.g.dart} | 226 +- .../test/src/foundation/foundation_test.dart | 4 +- .../src/foundation/foundation_test.mocks.dart | 61 +- .../test/src/ui_kit/ui_kit_test.dart | 2 +- .../test/src/ui_kit/ui_kit_test.mocks.dart | 407 ++- .../test/src/web_kit/web_kit_test.dart | 14 +- .../test/src/web_kit/web_kit_test.mocks.dart | 576 +++-- .../web_kit_cookie_manager_test.mocks.dart | 100 - .../web_kit_webview_widget_test.mocks.dart | 648 ----- .../webkit_webview_controller_test.mocks.dart | 414 --- ...kit_webview_cookie_manager_test.mocks.dart | 100 - .../webkit_navigation_delegate_test.dart | 85 +- ...webkit_navigation_delegate_test.mocks.dart | 308 +++ .../webkit_webview_controller_test.dart | 228 +- .../webkit_webview_controller_test.mocks.dart | 833 ++++++ .../webkit_webview_cookie_manager_test.dart | 6 +- ...kit_webview_cookie_manager_test.mocks.dart | 191 ++ .../{v4 => }/webkit_webview_widget_test.dart | 23 +- .../webkit_webview_widget_test.mocks.dart | 168 ++ ...app.yaml => exclude_all_packages_app.yaml} | 0 script/configs/temp_exclude_excerpt.yaml | 1 - script/install_chromium.sh | 36 +- script/tool/CHANGELOG.md | 28 +- script/tool/README.md | 196 +- script/tool/analysis_options.yaml | 5 + script/tool/lib/src/analyze_command.dart | 26 +- .../tool/lib/src/build_examples_command.dart | 314 --- script/tool/lib/src/common/cmake.dart | 118 - script/tool/lib/src/common/file_utils.dart | 20 - .../lib/src/common/git_version_finder.dart | 3 +- script/tool/lib/src/common/gradle.dart | 56 - .../tool/lib/src/common/package_command.dart | 52 +- .../src/common/package_looping_command.dart | 2 +- .../lib/src/common/package_state_utils.dart | 222 -- script/tool/lib/src/common/plugin_utils.dart | 119 - .../lib/src/common/pub_version_finder.dart | 103 - script/tool/lib/src/common/xcode.dart | 159 -- .../src/create_all_packages_app_command.dart | 348 --- script/tool/lib/src/custom_test_command.dart | 86 - .../lib/src/dependabot_check_command.dart | 114 - .../tool/lib/src/drive_examples_command.dart | 380 --- .../src/federation_safety_check_command.dart | 199 -- .../lib/src/firebase_test_lab_command.dart | 358 --- script/tool/lib/src/fix_command.dart | 51 - script/tool/lib/src/format_command.dart | 336 --- .../tool/lib/src/license_check_command.dart | 308 --- script/tool/lib/src/lint_android_command.dart | 67 - .../tool/lib/src/lint_podspecs_command.dart | 129 - script/tool/lib/src/list_command.dart | 68 - script/tool/lib/src/main.dart | 64 +- .../lib/src/make_deps_path_based_command.dart | 283 --- script/tool/lib/src/native_test_command.dart | 624 ----- .../tool/lib/src/publish_check_command.dart | 289 --- script/tool/lib/src/publish_command.dart | 456 ---- .../tool/lib/src/pubspec_check_command.dart | 322 --- script/tool/lib/src/readme_check_command.dart | 343 --- .../tool/lib/src/remove_dev_dependencies.dart | 58 - script/tool/lib/src/test_command.dart | 104 - .../tool/lib/src/update_excerpts_command.dart | 225 -- .../lib/src/update_release_info_command.dart | 310 --- .../tool/lib/src/version_check_command.dart | 592 ----- .../tool/lib/src/xcode_analyze_command.dart | 133 - script/tool/pubspec.yaml | 3 +- script/tool/test/analyze_command_test.dart | 425 ---- .../test/build_examples_command_test.dart | 634 ----- script/tool/test/common/file_utils_test.dart | 32 - .../test/common/git_version_finder_test.dart | 105 - script/tool/test/common/gradle_test.dart | 188 -- .../test/common/package_command_test.dart | 1069 -------- .../common/package_command_test.mocks.dart | 286 --- .../common/package_looping_command_test.dart | 947 ------- .../test/common/package_state_utils_test.dart | 341 --- .../tool/test/common/plugin_utils_test.dart | 256 -- .../test/common/pub_version_finder_test.dart | 89 - .../test/common/repository_package_test.dart | 220 -- script/tool/test/common/xcode_test.dart | 406 --- .../create_all_packages_app_command_test.dart | 256 -- .../tool/test/custom_test_command_test.dart | 328 --- .../test/dependabot_check_command_test.dart | 141 -- .../test/drive_examples_command_test.dart | 1257 --------- .../federation_safety_check_command_test.dart | 411 --- .../test/firebase_test_lab_command_test.dart | 795 ------ script/tool/test/fix_command_test.dart | 78 - script/tool/test/format_command_test.dart | 624 ----- .../tool/test/license_check_command_test.dart | 613 ----- .../tool/test/lint_android_command_test.dart | 205 -- .../tool/test/lint_podspecs_command_test.dart | 222 -- script/tool/test/list_command_test.dart | 197 -- .../make_deps_path_based_command_test.dart | 483 ---- script/tool/test/mocks.dart | 89 - .../tool/test/native_test_command_test.dart | 1796 ------------- .../tool/test/publish_check_command_test.dart | 464 ---- script/tool/test/publish_command_test.dart | 922 ------- .../tool/test/pubspec_check_command_test.dart | 982 ------- .../tool/test/readme_check_command_test.dart | 741 ------ .../test/remove_dev_dependencies_test.dart | 102 - script/tool/test/test_command_test.dart | 268 -- .../test/update_excerpts_command_test.dart | 280 -- .../update_release_info_command_test.dart | 645 ----- script/tool/test/util.dart | 471 ---- .../tool/test/version_check_command_test.dart | 1468 ----------- .../tool/test/xcode_analyze_command_test.dart | 484 ---- script/tool_runner.sh | 18 +- 1433 files changed, 56582 insertions(+), 54552 deletions(-) create mode 100644 .ci/targets/ios_build_all_plugins.yaml rename .ci/targets/{mac_ios_platform_tests.yaml => ios_platform_tests.yaml} (90%) rename .ci/targets/{mac_build_all_plugins.yaml => macos_build_all_plugins.yaml} (77%) rename .ci/targets/{mac_lint_podspecs.yaml => macos_lint_podspecs.yaml} (59%) create mode 100644 .ci/targets/macos_platform_tests.yaml delete mode 100644 .ci/targets/plugin_tools_tests.yaml rename .ci/targets/{build_all_plugins.yaml => windows_build_all_plugins.yaml} (76%) delete mode 100644 packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java delete mode 100644 packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java create mode 100644 packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraFlutterApiImpl.java create mode 100644 packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraPermissionsManager.java create mode 100644 packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java create mode 100644 packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java create mode 100644 packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesFlutterApiImpl.java create mode 100644 packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java create mode 100644 packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraPermissionsManagerTest.java create mode 100644 packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraTest.java create mode 100644 packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerTest.java create mode 100644 packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java create mode 100644 packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java create mode 100644 packages/camera/camera_android_camerax/example/lib/camera_controller.dart create mode 100644 packages/camera/camera_android_camerax/example/lib/camera_image.dart create mode 100644 packages/camera/camera_android_camerax/example/lib/camera_preview.dart create mode 100644 packages/camera/camera_android_camerax/lib/src/camera.dart create mode 100644 packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart delete mode 100644 packages/camera/camera_android_camerax/lib/src/camerax_library.pigeon.dart create mode 100644 packages/camera/camera_android_camerax/lib/src/preview.dart create mode 100644 packages/camera/camera_android_camerax/lib/src/surface.dart create mode 100644 packages/camera/camera_android_camerax/lib/src/system_services.dart create mode 100644 packages/camera/camera_android_camerax/lib/src/use_case.dart create mode 100644 packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart create mode 100644 packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart create mode 100644 packages/camera/camera_android_camerax/test/camera_test.dart create mode 100644 packages/camera/camera_android_camerax/test/preview_test.dart create mode 100644 packages/camera/camera_android_camerax/test/preview_test.mocks.dart create mode 100644 packages/camera/camera_android_camerax/test/system_services_test.dart create mode 100644 packages/camera/camera_android_camerax/test/system_services_test.mocks.dart create mode 100644 packages/camera/camera_android_camerax/test/test_camerax_library.g.dart delete mode 100644 packages/camera/camera_android_camerax/test/test_camerax_library.pigeon.dart rename packages/file_selector/file_selector_ios/test/{test_api.dart => test_api.g.dart} (100%) create mode 100644 packages/file_selector/file_selector_linux/example/lib/get_multiple_directories_page.dart create mode 100644 packages/file_selector/file_selector_linux/linux/.gitignore create mode 100644 packages/file_selector/file_selector_macos/lib/src/messages.g.dart create mode 100644 packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift rename packages/{path_provider/path_provider_ios => file_selector/file_selector_macos}/pigeons/copyright.txt (100%) create mode 100644 packages/file_selector/file_selector_macos/pigeons/messages.dart create mode 100644 packages/file_selector/file_selector_macos/test/file_selector_macos_test.mocks.dart create mode 100644 packages/file_selector/file_selector_macos/test/messages_test.g.dart rename packages/file_selector/file_selector_windows/test/{test_api.dart => test_api.g.dart} (100%) create mode 100644 packages/google_maps_flutter/google_maps_flutter/example/build.excerpt.yaml create mode 100644 packages/google_maps_flutter/google_maps_flutter/example/lib/readme_sample.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapInitializer.java create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapInitializerTest.java rename packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/{google_maps_test.dart => google_maps_tests.dart} (99%) create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/latest_renderer_test.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/legacy_renderer_test.dart create mode 100644 packages/google_sign_in/google_sign_in_web/example/build.yaml delete mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_legacy_init_test.dart delete mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart delete mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_legacy_init_test.dart delete mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_test.dart delete mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/gapi_mocks.dart delete mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart delete mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/gapi.dart delete mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/google_user.dart delete mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/test_iife.dart delete mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_utils_test.dart create mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart create mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.mocks.dart create mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/people_test.dart create mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/src/dom.dart create mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/src/jsify_as.dart create mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/src/jwt_examples.dart create mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/src/person.dart delete mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/src/test_utils.dart create mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/utils_test.dart rename .ci/scripts/plugin_tools_tests.sh => packages/google_sign_in/google_sign_in_web/example/regen_mocks.sh (56%) mode change 100644 => 100755 delete mode 100644 packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart delete mode 100644 packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart create mode 100644 packages/google_sign_in/google_sign_in_web/lib/src/gis_client.dart delete mode 100644 packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart create mode 100644 packages/google_sign_in/google_sign_in_web/lib/src/people.dart create mode 100644 packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerPickTest.java create mode 100644 packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/DriverExtensionActivity.java create mode 100644 packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/DummyContentProvider.java create mode 100755 packages/image_picker/image_picker_android/example/android/app/src/main/res/raw/ic_launcher.png create mode 100644 packages/image_picker/image_picker_ios/example/ios/TestImages/bmpImage.bmp create mode 100644 packages/image_picker/image_picker_ios/example/ios/TestImages/heicImage.heic create mode 100644 packages/image_picker/image_picker_ios/example/ios/TestImages/icnsImage.icns create mode 100644 packages/image_picker/image_picker_ios/example/ios/TestImages/icoImage.ico create mode 100644 packages/image_picker/image_picker_ios/example/ios/TestImages/jpgImageWithRightOrientation.jpg create mode 100644 packages/image_picker/image_picker_ios/example/ios/TestImages/proRawImage.dng create mode 100644 packages/image_picker/image_picker_ios/example/ios/TestImages/svgImage.svg create mode 100644 packages/image_picker/image_picker_ios/example/ios/TestImages/tiffImage.tiff rename packages/image_picker/image_picker_ios/test/{test_api.dart => test_api.g.dart} (100%) create mode 100644 packages/in_app_purchase/in_app_purchase/example/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 packages/in_app_purchase/in_app_purchase/example/macos/Flutter/Flutter-Release.xcconfig create mode 100644 packages/in_app_purchase/in_app_purchase/example/macos/Podfile create mode 100644 packages/in_app_purchase/in_app_purchase/example/macos/Runner.xcodeproj/project.pbxproj rename packages/{path_provider/path_provider_ios/example/ios/Runner.xcworkspace => in_app_purchase/in_app_purchase/example/macos/Runner.xcodeproj/project.xcworkspace}/xcshareddata/IDEWorkspaceChecks.plist (100%) create mode 100644 packages/in_app_purchase/in_app_purchase/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename packages/{path_provider/path_provider_ios/example/ios => in_app_purchase/in_app_purchase/example/macos}/Runner.xcworkspace/contents.xcworkspacedata (100%) rename packages/{path_provider/path_provider_macos => in_app_purchase/in_app_purchase}/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename packages/{path_provider/path_provider_macos => in_app_purchase/in_app_purchase}/example/macos/Runner/AppDelegate.swift (100%) create mode 100644 packages/in_app_purchase/in_app_purchase/example/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 packages/in_app_purchase/in_app_purchase/example/macos/Runner/Configs/AppInfo.xcconfig rename packages/{path_provider/path_provider_macos => in_app_purchase/in_app_purchase}/example/macos/Runner/Configs/Debug.xcconfig (100%) rename packages/{path_provider/path_provider_macos => in_app_purchase/in_app_purchase}/example/macos/Runner/Configs/Release.xcconfig (100%) rename packages/{path_provider/path_provider_macos => in_app_purchase/in_app_purchase}/example/macos/Runner/Configs/Warnings.xcconfig (100%) rename packages/{shared_preferences/shared_preferences_macos => in_app_purchase/in_app_purchase}/example/macos/Runner/DebugProfile.entitlements (100%) rename packages/{path_provider/path_provider_macos => in_app_purchase/in_app_purchase}/example/macos/Runner/Info.plist (100%) rename packages/{path_provider/path_provider_macos => in_app_purchase/in_app_purchase}/example/macos/Runner/MainFlutterWindow.swift (100%) rename packages/{shared_preferences/shared_preferences_macos => in_app_purchase/in_app_purchase}/example/macos/Runner/Release.entitlements (100%) create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAObjectTranslator.h create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAObjectTranslator.m create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPPaymentQueueDelegate.h create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPPaymentQueueDelegate.m create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPReceiptManager.h create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPReceiptManager.m create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPRequestHandler.h create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPRequestHandler.m create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPaymentQueueHandler.h create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPaymentQueueHandler.m create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIATransactionCache.h create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIATransactionCache.m create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.h create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.m create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit.podspec mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIATransactionCacheTests.m mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.m mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Info.plist mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/PaymentQueueTests.m mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/ProductRequestHandlerTests.m mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.h mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.m mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/example/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/example/macos/Flutter/Flutter-Release.xcconfig create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/example/macos/Podfile create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcodeproj/project.pbxproj rename packages/{shared_preferences/shared_preferences_ios/example/ios => in_app_purchase/in_app_purchase_storekit/example/macos}/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename packages/{path_provider/path_provider_macos => in_app_purchase/in_app_purchase_storekit}/example/macos/Runner.xcworkspace/contents.xcworkspacedata (100%) rename packages/{shared_preferences/shared_preferences_ios/example/ios => in_app_purchase/in_app_purchase_storekit/example/macos}/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename packages/{shared_preferences/shared_preferences_macos => in_app_purchase/in_app_purchase_storekit}/example/macos/Runner/AppDelegate.swift (100%) create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Configs/Debug.xcconfig create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Configs/Release.xcconfig create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/DebugProfile.entitlements rename packages/{shared_preferences/shared_preferences_macos => in_app_purchase/in_app_purchase_storekit}/example/macos/Runner/Info.plist (100%) rename packages/{shared_preferences/shared_preferences_macos => in_app_purchase/in_app_purchase_storekit}/example/macos/Runner/MainFlutterWindow.swift (100%) create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Release.entitlements create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/FIAPPaymentQueueDeleteTests.m create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/FIATransactionCacheTests.m create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/InAppPurchasePluginTests.m create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/Info.plist create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/PaymentQueueTests.m create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/ProductRequestHandlerTests.m create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/Stubs.h create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/Stubs.m create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/TranslatorTests.m create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/FIAPPaymentQueueDeleteTests.m create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/FIATransactionCacheTests.m create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchasePluginTests.m rename packages/{shared_preferences/shared_preferences_ios/example/ios => in_app_purchase/in_app_purchase_storekit/example/shared}/RunnerTests/Info.plist (93%) create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/PaymentQueueTests.m create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/ProductRequestHandlerTests.m create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/Stubs.h create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/Stubs.m create mode 100644 packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/TranslatorTests.m delete mode 100644 packages/in_app_purchase/in_app_purchase_storekit/ios/Assets/.gitkeep mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.h mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.h mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.m mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.h mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.m mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.h mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.m mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.h mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.m mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.h mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.m mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.h mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m mode change 100644 => 120000 packages/in_app_purchase/in_app_purchase_storekit/ios/in_app_purchase_storekit.podspec create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAObjectTranslator.h create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAObjectTranslator.m create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPPaymentQueueDelegate.h create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPPaymentQueueDelegate.m create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPReceiptManager.h create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPReceiptManager.m create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPRequestHandler.h create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPRequestHandler.m create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPaymentQueueHandler.h create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPaymentQueueHandler.m create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIATransactionCache.h create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIATransactionCache.m create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/InAppPurchasePlugin.h create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/InAppPurchasePlugin.m create mode 120000 packages/in_app_purchase/in_app_purchase_storekit/macos/in_app_purchase_storekit.podspec create mode 100644 packages/local_auth/local_auth_windows/lib/src/messages.g.dart create mode 100644 packages/local_auth/local_auth_windows/pigeons/copyright.txt create mode 100644 packages/local_auth/local_auth_windows/pigeons/messages.dart create mode 100644 packages/local_auth/local_auth_windows/windows/messages.g.cpp create mode 100644 packages/local_auth/local_auth_windows/windows/messages.g.h rename packages/path_provider/{path_provider_macos => path_provider_foundation}/.gitignore (100%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/AUTHORS (100%) create mode 100644 packages/path_provider/path_provider_foundation/CHANGELOG.md rename packages/path_provider/{path_provider_ios => path_provider_foundation}/LICENSE (100%) rename packages/path_provider/{path_provider_macos => path_provider_foundation}/README.md (78%) create mode 100644 packages/path_provider/path_provider_foundation/darwin/Classes/PathProviderPlugin.swift create mode 100644 packages/path_provider/path_provider_foundation/darwin/Classes/messages.g.swift rename packages/path_provider/{path_provider_macos/example/macos => path_provider_foundation/darwin}/RunnerTests/RunnerTests.swift (60%) create mode 100644 packages/path_provider/path_provider_foundation/darwin/path_provider_foundation.podspec rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/README.md (100%) rename packages/path_provider/{path_provider_macos => path_provider_foundation}/example/integration_test/path_provider_test.dart (100%) rename packages/{shared_preferences/shared_preferences_ios => path_provider/path_provider_foundation}/example/ios/.gitignore (95%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Flutter/AppFrameworkInfo.plist (86%) rename packages/{shared_preferences/shared_preferences_ios => path_provider/path_provider_foundation}/example/ios/Flutter/Debug.xcconfig (58%) rename packages/{shared_preferences/shared_preferences_ios => path_provider/path_provider_foundation}/example/ios/Flutter/Release.xcconfig (58%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Podfile (95%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner.xcodeproj/project.pbxproj (60%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) rename packages/{shared_preferences/shared_preferences_macos/example/macos/Runner.xcworkspace => path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/project.xcworkspace}/xcshareddata/IDEWorkspaceChecks.plist (100%) rename packages/{shared_preferences/shared_preferences_ios => path_provider/path_provider_foundation}/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (95%) rename packages/{shared_preferences/shared_preferences_ios => path_provider/path_provider_foundation}/example/ios/Runner.xcworkspace/contents.xcworkspacedata (100%) create mode 100644 packages/path_provider/path_provider_foundation/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename packages/{shared_preferences/shared_preferences_ios => path_provider/path_provider_foundation}/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%) rename packages/{shared_preferences/shared_preferences_ios => path_provider/path_provider_foundation}/example/ios/Runner/AppDelegate.swift (100%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (94%) rename packages/{shared_preferences/shared_preferences_ios => path_provider/path_provider_foundation}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png (100%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png (100%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png (100%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png (100%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png (100%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png (100%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png (100%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png (100%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png (100%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png (100%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png (100%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png (100%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png (100%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png (100%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png (100%) rename packages/{shared_preferences/shared_preferences_ios => path_provider/path_provider_foundation}/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json (100%) rename packages/{shared_preferences/shared_preferences_ios => path_provider/path_provider_foundation}/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png (100%) rename packages/{shared_preferences/shared_preferences_ios => path_provider/path_provider_foundation}/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png (100%) rename packages/{shared_preferences/shared_preferences_ios => path_provider/path_provider_foundation}/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png (100%) rename packages/{shared_preferences/shared_preferences_ios => path_provider/path_provider_foundation}/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md (100%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner/Base.lproj/LaunchScreen.storyboard (51%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner/Base.lproj/Main.storyboard (100%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/ios/Runner/Info.plist (78%) rename packages/{shared_preferences/shared_preferences_ios => path_provider/path_provider_foundation}/example/ios/Runner/Runner-Bridging-Header.h (100%) rename packages/path_provider/{path_provider_macos => path_provider_foundation}/example/lib/main.dart (88%) rename packages/path_provider/{path_provider_macos => path_provider_foundation}/example/macos/Flutter/Flutter-Debug.xcconfig (100%) rename packages/path_provider/{path_provider_macos => path_provider_foundation}/example/macos/Flutter/Flutter-Release.xcconfig (100%) rename packages/path_provider/{path_provider_macos => path_provider_foundation}/example/macos/Podfile (100%) rename packages/path_provider/{path_provider_macos => path_provider_foundation}/example/macos/Runner.xcodeproj/project.pbxproj (99%) rename packages/path_provider/{path_provider_macos => path_provider_foundation}/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (100%) rename packages/{shared_preferences/shared_preferences_macos => path_provider/path_provider_foundation}/example/macos/Runner.xcworkspace/contents.xcworkspacedata (100%) create mode 100644 packages/path_provider/path_provider_foundation/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/path_provider/path_provider_foundation/example/macos/Runner/AppDelegate.swift rename packages/path_provider/{path_provider_macos => path_provider_foundation}/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename packages/path_provider/{path_provider_macos => path_provider_foundation}/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png (100%) rename packages/path_provider/{path_provider_macos => path_provider_foundation}/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png (100%) rename packages/path_provider/{path_provider_macos => path_provider_foundation}/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png (100%) rename packages/path_provider/{path_provider_macos => path_provider_foundation}/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png (100%) rename packages/path_provider/{path_provider_macos => path_provider_foundation}/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png (100%) rename packages/path_provider/{path_provider_macos => path_provider_foundation}/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png (100%) rename packages/path_provider/{path_provider_macos => path_provider_foundation}/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png (100%) rename packages/path_provider/{path_provider_macos => path_provider_foundation}/example/macos/Runner/Base.lproj/MainMenu.xib (100%) rename packages/path_provider/{path_provider_macos => path_provider_foundation}/example/macos/Runner/Configs/AppInfo.xcconfig (100%) rename packages/{shared_preferences/shared_preferences_macos => path_provider/path_provider_foundation}/example/macos/Runner/Configs/Debug.xcconfig (100%) rename packages/{shared_preferences/shared_preferences_macos => path_provider/path_provider_foundation}/example/macos/Runner/Configs/Release.xcconfig (100%) rename packages/{shared_preferences/shared_preferences_macos => path_provider/path_provider_foundation}/example/macos/Runner/Configs/Warnings.xcconfig (100%) rename packages/path_provider/{path_provider_macos => path_provider_foundation}/example/macos/Runner/DebugProfile.entitlements (100%) rename packages/{shared_preferences/shared_preferences_macos/example/macos/RunnerTests => path_provider/path_provider_foundation/example/macos/Runner}/Info.plist (60%) create mode 100644 packages/path_provider/path_provider_foundation/example/macos/Runner/MainFlutterWindow.swift rename packages/path_provider/{path_provider_macos => path_provider_foundation}/example/macos/Runner/Release.entitlements (100%) rename packages/path_provider/{path_provider_ios/example/ios => path_provider_foundation/example/macos}/RunnerTests/Info.plist (100%) rename packages/path_provider/{path_provider_macos => path_provider_foundation}/example/pubspec.yaml (88%) rename packages/path_provider/{path_provider_ios => path_provider_foundation}/example/test_driver/integration_test.dart (100%) create mode 120000 packages/path_provider/path_provider_foundation/ios/Classes/PathProviderPlugin.swift create mode 120000 packages/path_provider/path_provider_foundation/ios/Classes/messages.g.swift create mode 100644 packages/path_provider/path_provider_foundation/ios/README.md create mode 120000 packages/path_provider/path_provider_foundation/ios/path_provider_foundation.podspec create mode 100644 packages/path_provider/path_provider_foundation/lib/messages.g.dart rename packages/path_provider/{path_provider_ios/lib/path_provider_ios.dart => path_provider_foundation/lib/path_provider_foundation.dart} (51%) create mode 120000 packages/path_provider/path_provider_foundation/macos/Classes/PathProviderPlugin.swift create mode 120000 packages/path_provider/path_provider_foundation/macos/Classes/messages.g.swift create mode 100644 packages/path_provider/path_provider_foundation/macos/README.md create mode 120000 packages/path_provider/path_provider_foundation/macos/path_provider_foundation.podspec create mode 100644 packages/path_provider/path_provider_foundation/pigeons/copyright.txt rename packages/path_provider/{path_provider_ios => path_provider_foundation}/pigeons/messages.dart (63%) rename packages/path_provider/{path_provider_macos => path_provider_foundation}/pubspec.yaml (52%) create mode 100644 packages/path_provider/path_provider_foundation/test/messages_test.g.dart rename packages/path_provider/{path_provider_macos/test/path_provider_macos_test.dart => path_provider_foundation/test/path_provider_foundation_test.dart} (50%) create mode 100644 packages/path_provider/path_provider_foundation/test/path_provider_foundation_test.mocks.dart delete mode 100644 packages/path_provider/path_provider_ios/CHANGELOG.md delete mode 100644 packages/path_provider/path_provider_ios/README.md delete mode 100644 packages/path_provider/path_provider_ios/example/integration_test/path_provider_test.dart delete mode 100644 packages/path_provider/path_provider_ios/example/ios/Flutter/Debug.xcconfig delete mode 100644 packages/path_provider/path_provider_ios/example/ios/Flutter/Release.xcconfig delete mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/AppDelegate.m delete mode 100644 packages/path_provider/path_provider_ios/example/ios/Runner/main.m delete mode 100644 packages/path_provider/path_provider_ios/example/ios/RunnerTests/PathProviderTests.m delete mode 100644 packages/path_provider/path_provider_ios/example/lib/main.dart delete mode 100644 packages/path_provider/path_provider_ios/example/pubspec.yaml delete mode 100644 packages/path_provider/path_provider_ios/ios/Assets/.gitkeep delete mode 100644 packages/path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.m delete mode 100644 packages/path_provider/path_provider_ios/ios/Classes/messages.g.h delete mode 100644 packages/path_provider/path_provider_ios/ios/Classes/messages.g.m delete mode 100644 packages/path_provider/path_provider_ios/ios/path_provider_ios.podspec delete mode 100644 packages/path_provider/path_provider_ios/lib/messages.g.dart delete mode 100644 packages/path_provider/path_provider_ios/pubspec.yaml delete mode 100644 packages/path_provider/path_provider_ios/test/messages_test.g.dart delete mode 100644 packages/path_provider/path_provider_ios/test/path_provider_ios_test.dart delete mode 100644 packages/path_provider/path_provider_macos/CHANGELOG.md delete mode 100644 packages/path_provider/path_provider_macos/lib/path_provider_macos.dart delete mode 100644 packages/path_provider/path_provider_macos/macos/Classes/PathProviderPlugin.swift delete mode 100644 packages/path_provider/path_provider_macos/macos/path_provider_macos.podspec create mode 100644 packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/DefaultShortcutItemParserTests.swift delete mode 100644 packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/FLTQuickActionsPluginTests.m delete mode 100644 packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/FLTShortcutStateManagerTests.m create mode 100644 packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/Mocks/MockMethodChannel.swift create mode 100644 packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/Mocks/MockShortcutItemParser.swift rename packages/{shared_preferences/shared_preferences_ios/example/ios/Runner/AppDelegate.h => quick_actions/quick_actions_ios/example/ios/RunnerTests/Mocks/MockShortcutItemProvider.swift} (51%) create mode 100644 packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/QuickActionsPluginTests.swift delete mode 100644 packages/quick_actions/quick_actions_ios/ios/Classes/FLTShortcutStateManager.h delete mode 100644 packages/quick_actions/quick_actions_ios/ios/Classes/FLTShortcutStateManager.m create mode 100644 packages/quick_actions/quick_actions_ios/ios/Classes/MethodChannel.swift create mode 100644 packages/quick_actions/quick_actions_ios/ios/Classes/ShortcutItemParser.swift create mode 100644 packages/quick_actions/quick_actions_ios/ios/Classes/ShortcutItemProviding.swift rename packages/{path_provider/path_provider_macos => shared_preferences/shared_preferences_foundation}/AUTHORS (100%) create mode 100644 packages/shared_preferences/shared_preferences_foundation/CHANGELOG.md rename packages/{path_provider/path_provider_macos => shared_preferences/shared_preferences_foundation}/LICENSE (100%) rename packages/shared_preferences/{shared_preferences_ios => shared_preferences_foundation}/README.md (77%) create mode 100644 packages/shared_preferences/shared_preferences_foundation/darwin/Classes/SharedPreferencesPlugin.swift create mode 100644 packages/shared_preferences/shared_preferences_foundation/darwin/Classes/messages.g.swift create mode 100644 packages/shared_preferences/shared_preferences_foundation/darwin/Tests/RunnerTests.swift rename packages/shared_preferences/{shared_preferences_macos/macos/shared_preferences_macos.podspec => shared_preferences_foundation/darwin/shared_preferences_foundation.podspec} (52%) rename packages/shared_preferences/{shared_preferences_ios => shared_preferences_foundation}/example/.gitignore (92%) rename packages/{path_provider/path_provider_macos => shared_preferences/shared_preferences_foundation}/example/README.md (100%) rename packages/shared_preferences/{shared_preferences_macos => shared_preferences_foundation}/example/integration_test/shared_preferences_test.dart (98%) create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/.gitignore rename packages/shared_preferences/{shared_preferences_ios => shared_preferences_foundation}/example/ios/Flutter/AppFrameworkInfo.plist (86%) create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Flutter/Debug.xcconfig create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Flutter/Release.xcconfig rename packages/shared_preferences/{shared_preferences_ios => shared_preferences_foundation}/example/ios/Podfile (95%) rename packages/shared_preferences/{shared_preferences_ios => shared_preferences_foundation}/example/ios/Runner.xcodeproj/project.pbxproj (59%) rename packages/shared_preferences/{shared_preferences_ios => shared_preferences_foundation}/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata (100%) create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename packages/shared_preferences/{shared_preferences_ios => shared_preferences_foundation}/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (94%) create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/AppDelegate.swift rename packages/shared_preferences/{shared_preferences_ios => shared_preferences_foundation}/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (94%) create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename packages/shared_preferences/{shared_preferences_ios => shared_preferences_foundation}/example/ios/Runner/Base.lproj/LaunchScreen.storyboard (51%) rename packages/shared_preferences/{shared_preferences_ios => shared_preferences_foundation}/example/ios/Runner/Base.lproj/Main.storyboard (100%) rename packages/shared_preferences/{shared_preferences_ios => shared_preferences_foundation}/example/ios/Runner/Info.plist (78%) rename packages/{path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.h => shared_preferences/shared_preferences_foundation/example/ios/Runner/Runner-Bridging-Header.h} (63%) rename packages/shared_preferences/{shared_preferences_macos => shared_preferences_foundation}/example/lib/main.dart (95%) rename packages/shared_preferences/{shared_preferences_macos => shared_preferences_foundation}/example/macos/Flutter/Flutter-Debug.xcconfig (100%) rename packages/shared_preferences/{shared_preferences_macos => shared_preferences_foundation}/example/macos/Flutter/Flutter-Release.xcconfig (100%) rename packages/shared_preferences/{shared_preferences_macos => shared_preferences_foundation}/example/macos/Podfile (100%) rename packages/shared_preferences/{shared_preferences_macos => shared_preferences_foundation}/example/macos/Runner.xcodeproj/project.pbxproj (99%) rename packages/shared_preferences/{shared_preferences_macos => shared_preferences_foundation}/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (99%) create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/AppDelegate.swift rename packages/shared_preferences/{shared_preferences_macos => shared_preferences_foundation}/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename packages/shared_preferences/{shared_preferences_macos => shared_preferences_foundation}/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png (100%) rename packages/shared_preferences/{shared_preferences_macos => shared_preferences_foundation}/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png (100%) rename packages/shared_preferences/{shared_preferences_macos => shared_preferences_foundation}/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png (100%) rename packages/shared_preferences/{shared_preferences_macos => shared_preferences_foundation}/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png (100%) rename packages/shared_preferences/{shared_preferences_macos => shared_preferences_foundation}/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png (100%) rename packages/shared_preferences/{shared_preferences_macos => shared_preferences_foundation}/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png (100%) rename packages/shared_preferences/{shared_preferences_macos => shared_preferences_foundation}/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png (100%) rename packages/shared_preferences/{shared_preferences_macos => shared_preferences_foundation}/example/macos/Runner/Base.lproj/MainMenu.xib (100%) rename packages/shared_preferences/{shared_preferences_macos => shared_preferences_foundation}/example/macos/Runner/Configs/AppInfo.xcconfig (100%) create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Configs/Debug.xcconfig create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Configs/Release.xcconfig create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Configs/Warnings.xcconfig create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/DebugProfile.entitlements create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Info.plist create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/MainFlutterWindow.swift create mode 100644 packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Release.entitlements rename packages/{path_provider/path_provider_macos => shared_preferences/shared_preferences_foundation}/example/macos/RunnerTests/Info.plist (100%) rename packages/shared_preferences/{shared_preferences_macos => shared_preferences_foundation}/example/pubspec.yaml (78%) rename packages/{path_provider/path_provider_macos => shared_preferences/shared_preferences_foundation}/example/test_driver/integration_test.dart (100%) create mode 120000 packages/shared_preferences/shared_preferences_foundation/ios/Classes/SharedPreferencesPlugin.swift create mode 120000 packages/shared_preferences/shared_preferences_foundation/ios/Classes/messages.g.swift create mode 100644 packages/shared_preferences/shared_preferences_foundation/ios/README.md create mode 120000 packages/shared_preferences/shared_preferences_foundation/ios/shared_preferences_foundation.podspec rename packages/shared_preferences/{shared_preferences_ios => shared_preferences_foundation}/lib/messages.g.dart (53%) rename packages/shared_preferences/{shared_preferences_ios/lib/shared_preferences_ios.dart => shared_preferences_foundation/lib/shared_preferences_foundation.dart} (85%) create mode 120000 packages/shared_preferences/shared_preferences_foundation/macos/Classes/SharedPreferencesPlugin.swift create mode 120000 packages/shared_preferences/shared_preferences_foundation/macos/Classes/messages.g.swift create mode 100644 packages/shared_preferences/shared_preferences_foundation/macos/README.md create mode 120000 packages/shared_preferences/shared_preferences_foundation/macos/shared_preferences_foundation.podspec rename packages/shared_preferences/{shared_preferences_ios => shared_preferences_foundation}/pigeons/copyright_header.txt (100%) rename packages/shared_preferences/{shared_preferences_ios => shared_preferences_foundation}/pigeons/messages.dart (63%) rename packages/shared_preferences/{shared_preferences_macos => shared_preferences_foundation}/pubspec.yaml (52%) rename packages/shared_preferences/{shared_preferences_ios/test/shared_preferences_ios_test.dart => shared_preferences_foundation/test/shared_preferences_foundation_test.dart} (65%) rename packages/shared_preferences/{shared_preferences_ios/test/messages.g.dart => shared_preferences_foundation/test/test_api.g.dart} (88%) delete mode 100644 packages/shared_preferences/shared_preferences_ios/AUTHORS delete mode 100644 packages/shared_preferences/shared_preferences_ios/CHANGELOG.md delete mode 100644 packages/shared_preferences/shared_preferences_ios/LICENSE delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/.metadata delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/README.md delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/integration_test/shared_preferences_test.dart delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/ios/Runner/AppDelegate.m delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/ios/Runner/main.m delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/ios/RunnerTests/SharedPreferencesTests.m delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/lib/main.dart delete mode 100644 packages/shared_preferences/shared_preferences_ios/example/pubspec.yaml delete mode 100644 packages/shared_preferences/shared_preferences_ios/ios/Assets/.gitkeep delete mode 100644 packages/shared_preferences/shared_preferences_ios/ios/Classes/FLTSharedPreferencesPlugin.m delete mode 100644 packages/shared_preferences/shared_preferences_ios/ios/Classes/messages.g.h delete mode 100644 packages/shared_preferences/shared_preferences_ios/ios/Classes/messages.g.m delete mode 100644 packages/shared_preferences/shared_preferences_ios/ios/shared_preferences_ios.podspec delete mode 100644 packages/shared_preferences/shared_preferences_ios/pubspec.yaml delete mode 100644 packages/shared_preferences/shared_preferences_macos/AUTHORS delete mode 100644 packages/shared_preferences/shared_preferences_macos/CHANGELOG.md delete mode 100644 packages/shared_preferences/shared_preferences_macos/LICENSE delete mode 100644 packages/shared_preferences/shared_preferences_macos/README.md delete mode 100644 packages/shared_preferences/shared_preferences_macos/example/README.md delete mode 100644 packages/shared_preferences/shared_preferences_macos/example/macos/RunnerTests/RunnerTests.swift delete mode 100644 packages/shared_preferences/shared_preferences_macos/example/test_driver/integration_test.dart delete mode 100644 packages/shared_preferences/shared_preferences_macos/lib/shared_preferences_macos.dart delete mode 100644 packages/shared_preferences/shared_preferences_macos/macos/Classes/SharedPreferencesPlugin.swift delete mode 100644 packages/shared_preferences/shared_preferences_macos/test/shared_preferences_macos_test.dart create mode 100644 packages/url_launcher/url_launcher_windows/lib/src/messages.g.dart create mode 100644 packages/url_launcher/url_launcher_windows/pigeons/copyright.txt create mode 100644 packages/url_launcher/url_launcher_windows/pigeons/messages.dart create mode 100644 packages/url_launcher/url_launcher_windows/windows/messages.g.cpp create mode 100644 packages/url_launcher/url_launcher_windows/windows/messages.g.h rename packages/video_player/video_player_android/test/{test_api.dart => test_api.g.dart} (100%) rename packages/video_player/video_player_avfoundation/test/{test_api.dart => test_api.g.dart} (100%) create mode 100644 packages/webview_flutter/webview_flutter/example/build.excerpt.yaml create mode 100644 packages/webview_flutter/webview_flutter/example/integration_test/legacy/webview_flutter_test.dart create mode 100644 packages/webview_flutter/webview_flutter/example/lib/simple_example.dart rename packages/webview_flutter/webview_flutter/lib/{ => src/legacy}/platform_interface.dart (89%) rename packages/webview_flutter/webview_flutter/lib/src/{ => legacy}/webview.dart (98%) create mode 100644 packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart delete mode 100644 packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_cookie_manager.dart delete mode 100644 packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_widget.dart delete mode 100644 packages/webview_flutter/webview_flutter/lib/src/v4/webview_flutter.dart rename packages/webview_flutter/webview_flutter/lib/src/{v4/src => }/webview_controller.dart (76%) create mode 100644 packages/webview_flutter/webview_flutter/lib/src/webview_cookie_manager.dart create mode 100644 packages/webview_flutter/webview_flutter/lib/src/webview_flutter_legacy.dart create mode 100644 packages/webview_flutter/webview_flutter/lib/src/webview_widget.dart create mode 100644 packages/webview_flutter/webview_flutter/test/legacy/webview_flutter_test.dart create mode 100644 packages/webview_flutter/webview_flutter/test/legacy/webview_flutter_test.mocks.dart create mode 100644 packages/webview_flutter/webview_flutter/test/navigation_delegate_test.dart create mode 100644 packages/webview_flutter/webview_flutter/test/navigation_delegate_test.mocks.dart delete mode 100644 packages/webview_flutter/webview_flutter/test/v4/webview_controller_test.mocks.dart delete mode 100644 packages/webview_flutter/webview_flutter/test/v4/webview_widget_test.mocks.dart rename packages/webview_flutter/webview_flutter/test/{v4 => }/webview_controller_test.dart (91%) create mode 100644 packages/webview_flutter/webview_flutter/test/webview_controller_test.mocks.dart rename packages/webview_flutter/webview_flutter/test/{v4 => }/webview_cookie_manager_test.dart (90%) rename packages/webview_flutter/webview_flutter/test/{v4 => }/webview_cookie_manager_test.mocks.dart (55%) delete mode 100644 packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart rename packages/webview_flutter/webview_flutter/test/{v4 => }/webview_widget_test.dart (90%) create mode 100644 packages/webview_flutter/webview_flutter/test/webview_widget_test.mocks.dart create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FileChooserParamsFlutterApiImpl.java delete mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/Releasable.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterAndroidExternalApi.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FileChooserParamsTest.java rename packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/{WebViewFlutterPluginTest.java => WebViewFlutterAndroidExternalApiTest.java} (58%) create mode 100644 packages/webview_flutter/webview_flutter_android/example/integration_test/legacy/webview_flutter_test.dart rename packages/webview_flutter/webview_flutter_android/example/lib/{ => legacy}/navigation_decision.dart (100%) rename packages/webview_flutter/webview_flutter_android/example/lib/{ => legacy}/navigation_request.dart (100%) rename packages/webview_flutter/webview_flutter_android/example/lib/{ => legacy}/web_view.dart (98%) create mode 100644 packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart rename packages/webview_flutter/webview_flutter_android/lib/src/{android_webview.pigeon.dart => android_webview.g.dart} (63%) create mode 100644 packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart create mode 100644 packages/webview_flutter/webview_flutter_android/lib/src/android_webview_cookie_manager.dart create mode 100644 packages/webview_flutter/webview_flutter_android/lib/src/android_webview_platform.dart rename packages/webview_flutter/webview_flutter_android/lib/{ => src/legacy}/webview_android.dart (94%) rename packages/webview_flutter/webview_flutter_android/lib/{ => src/legacy}/webview_android_cookie_manager.dart (85%) rename packages/webview_flutter/webview_flutter_android/lib/{ => src/legacy}/webview_android_widget.dart (67%) rename packages/webview_flutter/webview_flutter_android/lib/{ => src/legacy}/webview_surface_android.dart (96%) create mode 100644 packages/webview_flutter/webview_flutter_android/lib/src/platform_views_service_proxy.dart create mode 100644 packages/webview_flutter/webview_flutter_android/lib/src/weak_reference_utils.dart rename packages/{shared_preferences/shared_preferences_ios/example/test_driver/integration_test.dart => webview_flutter/webview_flutter_android/lib/src/webview_flutter_android_legacy.dart} (54%) create mode 100644 packages/webview_flutter/webview_flutter_android/lib/webview_flutter_android.dart create mode 100644 packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart create mode 100644 packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart create mode 100644 packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart create mode 100644 packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.dart rename packages/webview_flutter/webview_flutter_android/test/{webview_android_cookie_manager_test.mocks.dart => android_webview_cookie_manager_test.mocks.dart} (52%) rename packages/webview_flutter/webview_flutter_android/test/{ => legacy}/surface_android_test.dart (74%) rename packages/webview_flutter/webview_flutter_android/test/{ => legacy}/webview_android_cookie_manager_test.dart (89%) create mode 100644 packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.mocks.dart rename packages/webview_flutter/webview_flutter_android/test/{ => legacy}/webview_android_widget_test.dart (74%) create mode 100644 packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.mocks.dart rename packages/webview_flutter/webview_flutter_android/test/{test_android_webview.pigeon.dart => test_android_webview.g.dart} (91%) delete mode 100644 packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart rename packages/webview_flutter/webview_flutter_platform_interface/lib/src/{ => legacy}/platform_interface/javascript_channel_registry.dart (100%) rename packages/webview_flutter/webview_flutter_platform_interface/lib/src/{ => legacy}/platform_interface/platform_interface.dart (100%) rename packages/webview_flutter/webview_flutter_platform_interface/lib/src/{ => legacy}/platform_interface/webview_cookie_manager.dart (100%) rename packages/webview_flutter/webview_flutter_platform_interface/lib/src/{ => legacy}/platform_interface/webview_platform.dart (100%) rename packages/webview_flutter/webview_flutter_platform_interface/lib/src/{ => legacy}/platform_interface/webview_platform_callbacks_handler.dart (100%) rename packages/webview_flutter/webview_flutter_platform_interface/lib/src/{ => legacy}/platform_interface/webview_platform_controller.dart (100%) rename packages/webview_flutter/webview_flutter_platform_interface/lib/src/{ => legacy}/types/auto_media_playback_policy.dart (100%) rename packages/webview_flutter/webview_flutter_platform_interface/lib/src/{ => legacy}/types/creation_params.dart (100%) rename packages/webview_flutter/webview_flutter_platform_interface/lib/src/{ => legacy}/types/javascript_channel.dart (100%) create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/javascript_message.dart rename packages/webview_flutter/webview_flutter_platform_interface/lib/{v4/src => src/legacy}/types/javascript_mode.dart (94%) rename packages/webview_flutter/webview_flutter_platform_interface/lib/{v4/src => src/legacy}/types/types.dart (51%) create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/web_resource_error.dart rename packages/webview_flutter/webview_flutter_platform_interface/lib/src/{ => legacy}/types/web_resource_error_type.dart (100%) rename packages/webview_flutter/webview_flutter_platform_interface/lib/src/{ => legacy}/types/web_settings.dart (100%) rename packages/webview_flutter/webview_flutter_platform_interface/lib/{v4/src => src/legacy}/types/webview_cookie.dart (63%) rename packages/webview_flutter/webview_flutter_platform_interface/lib/src/{ => legacy}/types/webview_request.dart (100%) delete mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart rename packages/webview_flutter/webview_flutter_platform_interface/lib/{v4 => }/src/platform_navigation_delegate.dart (71%) rename packages/webview_flutter/webview_flutter_platform_interface/lib/{v4 => }/src/platform_webview_controller.dart (92%) rename packages/webview_flutter/webview_flutter_platform_interface/lib/{v4 => }/src/platform_webview_cookie_manager.dart (86%) rename packages/webview_flutter/webview_flutter_platform_interface/lib/{v4 => }/src/platform_webview_widget.dart (79%) rename packages/webview_flutter/webview_flutter_platform_interface/lib/{v4 => }/src/types/load_request_params.dart (94%) rename packages/webview_flutter/{webview_flutter_wkwebview/example/lib => webview_flutter_platform_interface/lib/src/types}/navigation_decision.dart (100%) create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/navigation_request.dart rename packages/webview_flutter/webview_flutter_platform_interface/lib/{v4 => }/src/types/platform_navigation_delegate_creation_params.dart (100%) rename packages/webview_flutter/webview_flutter_platform_interface/lib/{v4 => }/src/types/platform_webview_controller_creation_params.dart (100%) rename packages/webview_flutter/webview_flutter_platform_interface/lib/{v4 => }/src/types/platform_webview_cookie_manager_creation_params.dart (100%) rename packages/webview_flutter/webview_flutter_platform_interface/lib/{v4 => }/src/types/platform_webview_widget_creation_params.dart (100%) rename packages/{shared_preferences/shared_preferences_ios/ios/Classes/FLTSharedPreferencesPlugin.h => webview_flutter/webview_flutter_platform_interface/lib/src/webview_flutter_platform_interface_legacy.dart} (62%) rename packages/webview_flutter/webview_flutter_platform_interface/lib/{v4 => }/src/webview_platform.dart (98%) delete mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/javascript_message.dart delete mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/web_resource_error.dart delete mode 100644 packages/webview_flutter/webview_flutter_platform_interface/lib/v4/webview_flutter_platform_interface.dart rename packages/webview_flutter/webview_flutter_platform_interface/test/{src => legacy}/platform_interface/javascript_channel_registry_test.dart (94%) rename packages/webview_flutter/webview_flutter_platform_interface/test/{src => legacy}/platform_interface/webview_cookie_manager_test.dart (81%) rename packages/webview_flutter/webview_flutter_platform_interface/test/{src => legacy}/types/javascript_channel_test.dart (93%) rename packages/webview_flutter/webview_flutter_platform_interface/test/{src => legacy}/types/webview_cookie_test.dart (87%) rename packages/webview_flutter/webview_flutter_platform_interface/test/{src => legacy}/types/webview_request_test.dart (93%) rename packages/webview_flutter/webview_flutter_platform_interface/test/{src/v4 => }/platform_navigation_delegate_test.dart (95%) rename packages/webview_flutter/webview_flutter_platform_interface/test/{src/v4 => }/platform_webview_controller_test.dart (92%) create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.mocks.dart rename packages/webview_flutter/webview_flutter_platform_interface/test/{src/v4 => }/platform_webview_widget_test.dart (92%) delete mode 100644 packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart delete mode 100644 packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_controller_test.mocks.dart delete mode 100644 packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/webview_platform_test.mocks.dart rename packages/webview_flutter/webview_flutter_platform_interface/test/{src/v4 => }/webview_platform_test.dart (59%) create mode 100644 packages/webview_flutter/webview_flutter_platform_interface/test/webview_platform_test.mocks.dart create mode 100644 packages/webview_flutter/webview_flutter_web/example/integration_test/legacy/webview_flutter_test.dart rename packages/webview_flutter/webview_flutter_web/example/lib/{ => legacy}/web_view.dart (98%) create mode 100755 packages/webview_flutter/webview_flutter_web/example/run_test.sh create mode 100644 packages/webview_flutter/webview_flutter_web/lib/src/content_type.dart create mode 100644 packages/webview_flutter/webview_flutter_web/lib/src/http_request_factory.dart rename packages/webview_flutter/webview_flutter_web/lib/{ => src}/shims/dart_ui.dart (100%) rename packages/webview_flutter/webview_flutter_web/lib/{ => src}/shims/dart_ui_fake.dart (100%) rename packages/webview_flutter/webview_flutter_web/lib/{ => src}/shims/dart_ui_real.dart (100%) create mode 100644 packages/webview_flutter/webview_flutter_web/lib/src/web_webview_controller.dart create mode 100644 packages/webview_flutter/webview_flutter_web/lib/src/web_webview_platform.dart create mode 100644 packages/webview_flutter/webview_flutter_web/lib/src/webview_flutter_web_legacy.dart create mode 100644 packages/webview_flutter/webview_flutter_web/test/content_type_test.dart create mode 100644 packages/webview_flutter/webview_flutter_web/test/legacy/webview_flutter_web_test.dart rename packages/webview_flutter/webview_flutter_web/test/{ => legacy}/webview_flutter_web_test.mocks.dart (99%) create mode 100644 packages/webview_flutter/webview_flutter_web/test/web_webview_controller_test.dart create mode 100644 packages/webview_flutter/webview_flutter_web/test/web_webview_controller_test.mocks.dart create mode 100644 packages/webview_flutter/webview_flutter_web/test/web_webview_widget_test.dart create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/legacy/webview_flutter_test.dart create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewFlutterWKWebViewExternalAPITests.m create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/example/lib/legacy/navigation_decision.dart rename packages/webview_flutter/webview_flutter_wkwebview/example/lib/{ => legacy}/navigation_request.dart (100%) rename packages/webview_flutter/webview_flutter_wkwebview/example/lib/{ => legacy}/web_view.dart (98%) create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewFlutterWKWebViewExternalAPI.h create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewFlutterWKWebViewExternalAPI.m rename packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/{web_kit.pigeon.dart => web_kit.g.dart} (70%) rename packages/webview_flutter/webview_flutter_wkwebview/lib/src/{ => legacy}/web_kit_webview_widget.dart (98%) rename packages/webview_flutter/webview_flutter_wkwebview/lib/src/{ => legacy}/webview_cupertino.dart (92%) rename packages/webview_flutter/webview_flutter_wkwebview/lib/src/{ => legacy}/wkwebview_cookie_manager.dart (89%) delete mode 100644 packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/webview_flutter_wkwebview.dart rename packages/webview_flutter/webview_flutter_wkwebview/lib/src/{v4/src => }/webkit_proxy.dart (82%) rename packages/webview_flutter/webview_flutter_wkwebview/lib/src/{v4/src => }/webkit_webview_controller.dart (74%) rename packages/webview_flutter/webview_flutter_wkwebview/lib/src/{v4/src => }/webkit_webview_cookie_manager.dart (94%) rename packages/webview_flutter/webview_flutter_wkwebview/lib/src/{v4/src => }/webkit_webview_platform.dart (80%) rename packages/{path_provider/path_provider_ios/example/ios/Runner/AppDelegate.h => webview_flutter/webview_flutter_wkwebview/lib/src/webview_flutter_wkwebview_legacy.dart} (61%) rename packages/webview_flutter/webview_flutter_wkwebview/test/{src => legacy}/web_kit_cookie_manager_test.dart (93%) create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_cookie_manager_test.mocks.dart rename packages/webview_flutter/webview_flutter_wkwebview/test/{src => legacy}/web_kit_webview_widget_test.dart (95%) create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_webview_widget_test.mocks.dart rename packages/webview_flutter/webview_flutter_wkwebview/test/src/common/{test_web_kit.pigeon.dart => test_web_kit.g.dart} (94%) delete mode 100644 packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.mocks.dart delete mode 100644 packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.mocks.dart delete mode 100644 packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.mocks.dart delete mode 100644 packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_cookie_manager_test.mocks.dart rename packages/webview_flutter/webview_flutter_wkwebview/test/{v4 => }/webkit_navigation_delegate_test.dart (69%) create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.mocks.dart rename packages/webview_flutter/webview_flutter_wkwebview/test/{v4 => }/webkit_webview_controller_test.dart (79%) create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.mocks.dart rename packages/webview_flutter/webview_flutter_wkwebview/test/{v4 => }/webkit_webview_cookie_manager_test.dart (93%) create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_cookie_manager_test.mocks.dart rename packages/webview_flutter/webview_flutter_wkwebview/test/{v4 => }/webkit_webview_widget_test.dart (66%) create mode 100644 packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart rename script/configs/{exclude_all_plugins_app.yaml => exclude_all_packages_app.yaml} (100%) create mode 100644 script/tool/analysis_options.yaml delete mode 100644 script/tool/lib/src/build_examples_command.dart delete mode 100644 script/tool/lib/src/common/cmake.dart delete mode 100644 script/tool/lib/src/common/file_utils.dart delete mode 100644 script/tool/lib/src/common/gradle.dart delete mode 100644 script/tool/lib/src/common/package_state_utils.dart delete mode 100644 script/tool/lib/src/common/plugin_utils.dart delete mode 100644 script/tool/lib/src/common/pub_version_finder.dart delete mode 100644 script/tool/lib/src/common/xcode.dart delete mode 100644 script/tool/lib/src/create_all_packages_app_command.dart delete mode 100644 script/tool/lib/src/custom_test_command.dart delete mode 100644 script/tool/lib/src/dependabot_check_command.dart delete mode 100644 script/tool/lib/src/drive_examples_command.dart delete mode 100644 script/tool/lib/src/federation_safety_check_command.dart delete mode 100644 script/tool/lib/src/firebase_test_lab_command.dart delete mode 100644 script/tool/lib/src/fix_command.dart delete mode 100644 script/tool/lib/src/format_command.dart delete mode 100644 script/tool/lib/src/license_check_command.dart delete mode 100644 script/tool/lib/src/lint_android_command.dart delete mode 100644 script/tool/lib/src/lint_podspecs_command.dart delete mode 100644 script/tool/lib/src/list_command.dart delete mode 100644 script/tool/lib/src/make_deps_path_based_command.dart delete mode 100644 script/tool/lib/src/native_test_command.dart delete mode 100644 script/tool/lib/src/publish_check_command.dart delete mode 100644 script/tool/lib/src/publish_command.dart delete mode 100644 script/tool/lib/src/pubspec_check_command.dart delete mode 100644 script/tool/lib/src/readme_check_command.dart delete mode 100644 script/tool/lib/src/remove_dev_dependencies.dart delete mode 100644 script/tool/lib/src/test_command.dart delete mode 100644 script/tool/lib/src/update_excerpts_command.dart delete mode 100644 script/tool/lib/src/update_release_info_command.dart delete mode 100644 script/tool/lib/src/version_check_command.dart delete mode 100644 script/tool/lib/src/xcode_analyze_command.dart delete mode 100644 script/tool/test/analyze_command_test.dart delete mode 100644 script/tool/test/build_examples_command_test.dart delete mode 100644 script/tool/test/common/file_utils_test.dart delete mode 100644 script/tool/test/common/git_version_finder_test.dart delete mode 100644 script/tool/test/common/gradle_test.dart delete mode 100644 script/tool/test/common/package_command_test.dart delete mode 100644 script/tool/test/common/package_command_test.mocks.dart delete mode 100644 script/tool/test/common/package_looping_command_test.dart delete mode 100644 script/tool/test/common/package_state_utils_test.dart delete mode 100644 script/tool/test/common/plugin_utils_test.dart delete mode 100644 script/tool/test/common/pub_version_finder_test.dart delete mode 100644 script/tool/test/common/repository_package_test.dart delete mode 100644 script/tool/test/common/xcode_test.dart delete mode 100644 script/tool/test/create_all_packages_app_command_test.dart delete mode 100644 script/tool/test/custom_test_command_test.dart delete mode 100644 script/tool/test/dependabot_check_command_test.dart delete mode 100644 script/tool/test/drive_examples_command_test.dart delete mode 100644 script/tool/test/federation_safety_check_command_test.dart delete mode 100644 script/tool/test/firebase_test_lab_command_test.dart delete mode 100644 script/tool/test/fix_command_test.dart delete mode 100644 script/tool/test/format_command_test.dart delete mode 100644 script/tool/test/license_check_command_test.dart delete mode 100644 script/tool/test/lint_android_command_test.dart delete mode 100644 script/tool/test/lint_podspecs_command_test.dart delete mode 100644 script/tool/test/list_command_test.dart delete mode 100644 script/tool/test/make_deps_path_based_command_test.dart delete mode 100644 script/tool/test/mocks.dart delete mode 100644 script/tool/test/native_test_command_test.dart delete mode 100644 script/tool/test/publish_check_command_test.dart delete mode 100644 script/tool/test/publish_command_test.dart delete mode 100644 script/tool/test/pubspec_check_command_test.dart delete mode 100644 script/tool/test/readme_check_command_test.dart delete mode 100644 script/tool/test/remove_dev_dependencies_test.dart delete mode 100644 script/tool/test/test_command_test.dart delete mode 100644 script/tool/test/update_excerpts_command_test.dart delete mode 100644 script/tool/test/update_release_info_command_test.dart delete mode 100644 script/tool/test/util.dart delete mode 100644 script/tool/test/version_check_command_test.dart delete mode 100644 script/tool/test/xcode_analyze_command_test.dart diff --git a/.ci.yaml b/.ci.yaml index dffc7364e65a..6cc325b985fe 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -25,6 +25,17 @@ platform_properties: ] device_type: none os: Windows + mac_arm64: + properties: + dependencies: >- + [ + {"dependency": "xcode", "version": "14a5294e"}, + {"dependency": "gems", "version": "v3.3.14"} + ] + os: Mac-12 + device_type: none + cpu: arm64 + xcode: 14a5294e # xcode 14.0 beta 5 mac_x64: properties: dependencies: >- @@ -36,141 +47,190 @@ platform_properties: device_type: none cpu: x86 xcode: 14a5294e # xcode 14.0 beta 5 - + targets: ### 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 # pod's arch exclusions, so fails to build. + # When moving it, rename the task and file to check_podspecs - name: Mac_x64 lint_podspecs recipe: plugins/plugins - bringup: true # New target: https://github.com/flutter/plugins/pull/6637 timeout: 30 properties: add_recipes_cq: "true" version_file: flutter_master.version - target_file: mac_lint_podspecs.yaml + target_file: macos_lint_podspecs.yaml ### macOS desktop tasks ### - # macos-platform_tests builds all the plugins on M1, so this build is run + # macos_platform_tests builds all the plugins on ARM, so this build is run # on Intel to give us build coverage of both host types. - name: Mac_x64 build_all_plugins master recipe: plugins/plugins - bringup: true # New target: https://github.com/flutter/plugins/pull/6671 timeout: 30 properties: add_recipes_cq: "true" version_file: flutter_master.version - target_file: mac_build_all_plugins.yaml + target_file: macos_build_all_plugins.yaml channel: master - name: Mac_x64 build_all_plugins stable recipe: plugins/plugins - bringup: true # New target: https://github.com/flutter/plugins/pull/6671 timeout: 30 properties: add_recipes_cq: "true" version_file: flutter_stable.version - target_file: mac_build_all_plugins.yaml + target_file: macos_build_all_plugins.yaml + channel: stable + + - name: Mac_arm64 macos_platform_tests master + recipe: plugins/plugins + timeout: 60 + properties: + channel: master + add_recipes_cq: "true" + version_file: flutter_master.version + target_file: macos_platform_tests.yaml + + - name: Mac_arm64 macos_platform_tests stable + recipe: plugins/plugins + presubmit: false + timeout: 60 + properties: channel: stable + add_recipes_cq: "true" + version_file: flutter_stable.version + target_file: macos_platform_tests.yaml ### iOS tasks ### - # TODO(stuartmorgan): Swap this and ios-build_all_plugins once simulator - # tests are reliable on the ARM infrastructure. See discussion at - # https://github.com/flutter/plugins/pull/5693#issuecomment-1126011089 - - name: Mac_x64 ios_platform_tests_1_of_4 master + # ios_platform_tests builds all the plugins on ARM, so this build is run + # on Intel to give us build coverage of both host types. + - name: Mac_x64 ios_build_all_plugins master recipe: plugins/plugins - bringup: true # New target: https://github.com/flutter/plugins/pull/6682 timeout: 30 properties: + channel: master add_recipes_cq: "true" version_file: flutter_master.version - target_file: mac_ios_platform_tests.yaml - package_sharding: "--shardIndex 0 --shardCount 4" + target_file: ios_build_all_plugins.yaml - - name: Mac_x64 ios_platform_tests_2_of_4 master + - name: Mac_x64 ios_build_all_plugins stable recipe: plugins/plugins - bringup: true # New target: https://github.com/flutter/plugins/pull/6682 timeout: 30 + properties: + channel: stable + add_recipes_cq: "true" + version_file: flutter_stable.version + target_file: ios_build_all_plugins.yaml + + # TODO(stuartmorgan): Change all of the ios_platform_tests_* task timeouts + # to 60 minutes once https://github.com/flutter/flutter/issues/119750 is + # fixed. + - name: Mac_arm64 ios_platform_tests_shard_1 master - plugins + recipe: plugins/plugins + timeout: 120 properties: add_recipes_cq: "true" version_file: flutter_master.version - target_file: mac_ios_platform_tests.yaml - package_sharding: "--shardIndex 1 --shardCount 4" + target_file: ios_platform_tests.yaml + package_sharding: "--shardIndex 0 --shardCount 5" - - name: Mac_x64 ios_platform_tests_3_of_4 master + - name: Mac_arm64 ios_platform_tests_shard_2 master - plugins recipe: plugins/plugins - bringup: true # New target: https://github.com/flutter/plugins/pull/6682 - timeout: 30 + timeout: 120 properties: add_recipes_cq: "true" version_file: flutter_master.version - target_file: mac_ios_platform_tests.yaml - package_sharding: "--shardIndex 2 --shardCount 4" + target_file: ios_platform_tests.yaml + package_sharding: "--shardIndex 1 --shardCount 5" - - name: Mac_x64 ios_platform_tests_4_of_4 master + - name: Mac_arm64 ios_platform_tests_shard_3 master - plugins recipe: plugins/plugins - bringup: true # New target: https://github.com/flutter/plugins/pull/6682 - timeout: 30 + timeout: 120 + properties: + add_recipes_cq: "true" + version_file: flutter_master.version + target_file: ios_platform_tests.yaml + package_sharding: "--shardIndex 2 --shardCount 5" + + - name: Mac_arm64 ios_platform_tests_shard_4 master - plugins + recipe: plugins/plugins + timeout: 120 + properties: + add_recipes_cq: "true" + version_file: flutter_master.version + target_file: ios_platform_tests.yaml + package_sharding: "--shardIndex 3 --shardCount 5" + + - name: Mac_arm64 ios_platform_tests_shard_5 master - plugins + recipe: plugins/plugins + timeout: 120 properties: add_recipes_cq: "true" version_file: flutter_master.version - target_file: mac_ios_platform_tests.yaml - package_sharding: "--shardIndex 3 --shardCount 4" + target_file: ios_platform_tests.yaml + package_sharding: "--shardIndex 4 --shardCount 5" # Don't run full platform tests on both channels in pre-submit. - - name: Mac_x64 ios_platform_tests_1_of_4 stable + - name: Mac_arm64 ios_platform_tests_shard_1 stable - plugins recipe: plugins/plugins presubmit: false - bringup: true # New target: https://github.com/flutter/plugins/pull/6682 - timeout: 30 + timeout: 120 properties: channel: stable add_recipes_cq: "true" version_file: flutter_stable.version - target_file: mac_ios_platform_tests.yaml - package_sharding: "--shardIndex 0 --shardCount 4" + target_file: ios_platform_tests.yaml + package_sharding: "--shardIndex 0 --shardCount 5" - - name: Mac_x64 ios_platform_tests_2_of_4 stable + - name: Mac_arm64 ios_platform_tests_shard_2 stable - plugins recipe: plugins/plugins presubmit: false - bringup: true # New target: https://github.com/flutter/plugins/pull/6682 - timeout: 30 + timeout: 120 properties: channel: stable add_recipes_cq: "true" version_file: flutter_stable.version - target_file: mac_ios_platform_tests.yaml - package_sharding: "--shardIndex 1 --shardCount 4" + target_file: ios_platform_tests.yaml + package_sharding: "--shardIndex 1 --shardCount 5" - - name: Mac_x64 ios_platform_tests_3_of_4 stable + - name: Mac_arm64 ios_platform_tests_shard_3 stable - plugins recipe: plugins/plugins presubmit: false - bringup: true # New target: https://github.com/flutter/plugins/pull/6682 - timeout: 30 + timeout: 120 properties: channel: stable add_recipes_cq: "true" version_file: flutter_stable.version - target_file: mac_ios_platform_tests.yaml - package_sharding: "--shardIndex 2 --shardCount 4" + target_file: ios_platform_tests.yaml + package_sharding: "--shardIndex 2 --shardCount 5" - - name: Mac_x64 ios_platform_tests_4_of_4 stable + - name: Mac_arm64 ios_platform_tests_shard_4 stable - plugins recipe: plugins/plugins presubmit: false - bringup: true # New target: https://github.com/flutter/plugins/pull/6682 - timeout: 30 + timeout: 120 + properties: + channel: stable + add_recipes_cq: "true" + version_file: flutter_stable.version + target_file: ios_platform_tests.yaml + package_sharding: "--shardIndex 3 --shardCount 5" + + - name: Mac_arm64 ios_platform_tests_shard_5 stable - plugins + recipe: plugins/plugins + presubmit: false + timeout: 120 properties: channel: stable add_recipes_cq: "true" version_file: flutter_stable.version - target_file: mac_ios_platform_tests.yaml - package_sharding: "--shardIndex 3 --shardCount 4" + target_file: ios_platform_tests.yaml + package_sharding: "--shardIndex 4 --shardCount 5" - name: Windows win32-platform_tests master recipe: plugins/plugins - timeout: 30 + timeout: 60 properties: add_recipes_cq: "true" target_file: windows_build_and_platform_tests.yaml @@ -183,7 +243,8 @@ targets: - name: Windows win32-platform_tests stable recipe: plugins/plugins - timeout: 30 + presubmit: false + timeout: 60 properties: add_recipes_cq: "true" target_file: windows_build_and_platform_tests.yaml @@ -199,7 +260,7 @@ targets: timeout: 30 properties: add_recipes_cq: "true" - target_file: build_all_plugins.yaml + target_file: windows_build_all_plugins.yaml channel: master version_file: flutter_master.version dependencies: > @@ -212,7 +273,7 @@ targets: timeout: 30 properties: add_recipes_cq: "true" - target_file: build_all_plugins.yaml + target_file: windows_build_all_plugins.yaml channel: stable version_file: flutter_stable.version dependencies: > @@ -220,15 +281,6 @@ targets: {"dependency": "vs_build", "version": "version:vs2019"} ] - - name: Windows plugin_tools_tests - recipe: plugins/plugins - timeout: 30 - properties: - add_recipes_cq: "true" - target_file: plugin_tools_tests.yaml - channel: master - version_file: flutter_master.version - - name: Linux ci_yaml plugins roller recipe: infra/ci_yaml timeout: 30 diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index 3fbdedb3a1d3..ec9a0909f40f 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -61e927d22fe6d82c94c368d62aa81f733bd9218d +33e4d21f7c13e02a7c92c7272309afbff792a864 diff --git a/.ci/flutter_stable.version b/.ci/flutter_stable.version index b19aa25587e7..542569bcfd31 100644 --- a/.ci/flutter_stable.version +++ b/.ci/flutter_stable.version @@ -1 +1 @@ -52b3dc25f6471c27b2144594abb11c741cb88f57 +7048ed95a5ad3e43d697e0c397464193991fc230 diff --git a/.ci/scripts/build_all_plugins.sh b/.ci/scripts/build_all_plugins.sh index 89dab629fd52..c22b9832ff22 100644 --- a/.ci/scripts/build_all_plugins.sh +++ b/.ci/scripts/build_all_plugins.sh @@ -5,5 +5,6 @@ platform="$1" build_mode="$2" +shift 2 cd all_packages -flutter build "$platform" --"$build_mode" +flutter build "$platform" --"$build_mode" "$@" diff --git a/.ci/scripts/build_examples_win32.sh b/.ci/scripts/build_examples_win32.sh index bcf57a4b311f..ff30ca93eec1 100644 --- a/.ci/scripts/build_examples_win32.sh +++ b/.ci/scripts/build_examples_win32.sh @@ -3,5 +3,5 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -dart ./script/tool/bin/flutter_plugin_tools.dart build-examples --windows \ +dart pub global run flutter_plugin_tools build-examples --windows \ --packages-for-branch --log-timing diff --git a/.ci/scripts/create_all_plugins_app.sh b/.ci/scripts/create_all_plugins_app.sh index 100e8aca804a..8c45a351bef4 100644 --- a/.ci/scripts/create_all_plugins_app.sh +++ b/.ci/scripts/create_all_plugins_app.sh @@ -3,5 +3,5 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -dart ./script/tool/bin/flutter_plugin_tools.dart create-all-packages-app \ - --output-dir=. --exclude script/configs/exclude_all_plugins_app.yaml +dart pub global run flutter_plugin_tools create-all-packages-app \ + --output-dir=. --exclude script/configs/exclude_all_packages_app.yaml diff --git a/.ci/scripts/create_simulator.sh b/.ci/scripts/create_simulator.sh index 3d86739051f1..98bfb6573593 100644 --- a/.ci/scripts/create_simulator.sh +++ b/.ci/scripts/create_simulator.sh @@ -3,7 +3,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -device=com.apple.CoreSimulator.SimDeviceType.iPhone-11 +device=com.apple.CoreSimulator.SimDeviceType.iPhone-13 os=com.apple.CoreSimulator.SimRuntime.iOS-16-0 xcrun simctl list diff --git a/.ci/scripts/drive_examples_win32.sh b/.ci/scripts/drive_examples_win32.sh index c3e2e7bc5447..d06c192ab551 100644 --- a/.ci/scripts/drive_examples_win32.sh +++ b/.ci/scripts/drive_examples_win32.sh @@ -3,5 +3,5 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -dart ./script/tool/bin/flutter_plugin_tools.dart drive-examples --windows \ +dart pub global run flutter_plugin_tools drive-examples --windows \ --exclude=script/configs/exclude_integration_win32.yaml --packages-for-branch --log-timing diff --git a/.ci/scripts/native_test_win32.sh b/.ci/scripts/native_test_win32.sh index 37cf54e55c5c..7bfe84022487 100644 --- a/.ci/scripts/native_test_win32.sh +++ b/.ci/scripts/native_test_win32.sh @@ -3,5 +3,5 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -dart ./script/tool/bin/flutter_plugin_tools.dart native-test --windows \ +dart pub global run flutter_plugin_tools native-test --windows \ --no-integration --packages-for-branch --log-timing diff --git a/.ci/scripts/prepare_tool.sh b/.ci/scripts/prepare_tool.sh index f93694bf1ff6..aced1517760c 100755 --- a/.ci/scripts/prepare_tool.sh +++ b/.ci/scripts/prepare_tool.sh @@ -6,5 +6,6 @@ # To set FETCH_HEAD for "git merge-base" to work git fetch origin main -cd script/tool -dart pub get +# Pinned version of the plugin tools, to avoid breakage in this repository +# when pushing updates from flutter/packages. +dart pub global activate flutter_plugin_tools 0.13.4+3 diff --git a/.ci/targets/ios_build_all_plugins.yaml b/.ci/targets/ios_build_all_plugins.yaml new file mode 100644 index 000000000000..7b5b88d9c9ff --- /dev/null +++ b/.ci/targets/ios_build_all_plugins.yaml @@ -0,0 +1,11 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + - name: create all_plugins app + script: .ci/scripts/create_all_plugins_app.sh + - name: build all_plugins for iOS debug + script: .ci/scripts/build_all_plugins.sh + args: ["ios", "debug", "--no-codesign"] + - name: build all_plugins for iOS release + script: .ci/scripts/build_all_plugins.sh + args: ["ios", "release", "--no-codesign"] diff --git a/.ci/targets/mac_ios_platform_tests.yaml b/.ci/targets/ios_platform_tests.yaml similarity index 90% rename from .ci/targets/mac_ios_platform_tests.yaml rename to .ci/targets/ios_platform_tests.yaml index ed63f226eaec..692b83dcb285 100644 --- a/.ci/targets/mac_ios_platform_tests.yaml +++ b/.ci/targets/ios_platform_tests.yaml @@ -1,4 +1,6 @@ tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh - name: create simulator script: .ci/scripts/create_simulator.sh - name: build examples @@ -13,7 +15,7 @@ tasks: args: ["xcode-analyze", "--ios", "--ios-min-version=13.0"] - name: native test script: script/tool_runner.sh - args: ["native-test", "--ios", "--ios-destination", "platform=iOS Simulator,name=iPhone 11,OS=latest"] + args: ["native-test", "--ios", "--ios-destination", "platform=iOS Simulator,name=iPhone 13,OS=latest"] - name: drive examples # `drive-examples` contains integration tests, which changes the UI of the application. # This UI change sometimes affects `xctest`. diff --git a/.ci/targets/mac_build_all_plugins.yaml b/.ci/targets/macos_build_all_plugins.yaml similarity index 77% rename from .ci/targets/mac_build_all_plugins.yaml rename to .ci/targets/macos_build_all_plugins.yaml index 4dd324e8b3f0..e6eb8ac2c315 100644 --- a/.ci/targets/mac_build_all_plugins.yaml +++ b/.ci/targets/macos_build_all_plugins.yaml @@ -3,9 +3,9 @@ tasks: script: .ci/scripts/prepare_tool.sh - name: create all_plugins app script: .ci/scripts/create_all_plugins_app.sh - - name: build all_plugins debug + - name: build all_plugins for macOS debug script: .ci/scripts/build_all_plugins.sh args: ["macos", "debug"] - - name: build all_plugins release + - name: build all_plugins for macOS release script: .ci/scripts/build_all_plugins.sh args: ["macos", "release"] diff --git a/.ci/targets/mac_lint_podspecs.yaml b/.ci/targets/macos_lint_podspecs.yaml similarity index 59% rename from .ci/targets/mac_lint_podspecs.yaml rename to .ci/targets/macos_lint_podspecs.yaml index 02a904ee3d85..0b2217325635 100644 --- a/.ci/targets/mac_lint_podspecs.yaml +++ b/.ci/targets/macos_lint_podspecs.yaml @@ -1,6 +1,6 @@ tasks: - name: prepare tool script: .ci/scripts/prepare_tool.sh - - name: lint iOS and macOS podspecs + - name: validate iOS and macOS podspecs script: script/tool_runner.sh - args: ["podspecs"] + args: ["podspec-check"] diff --git a/.ci/targets/macos_platform_tests.yaml b/.ci/targets/macos_platform_tests.yaml new file mode 100644 index 000000000000..4b2ee4eac1fe --- /dev/null +++ b/.ci/targets/macos_platform_tests.yaml @@ -0,0 +1,19 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + - name: build examples + script: script/tool_runner.sh + args: ["build-examples", "--macos"] + - name: xcode analyze + script: script/tool_runner.sh + args: ["xcode-analyze", "--macos"] + - name: xcode analyze deprecation + # Ensure we don't accidentally introduce deprecated code. + script: script/tool_runner.sh + args: ["xcode-analyze", "--macos", "--macos-min-version=12.3"] + - name: native test + script: script/tool_runner.sh + args: ["native-test", "--macos"] + - name: drive examples + script: script/tool_runner.sh + args: ["drive-examples", "--macos", "--exclude=script/configs/exclude_integration_macos.yaml"] diff --git a/.ci/targets/plugin_tools_tests.yaml b/.ci/targets/plugin_tools_tests.yaml deleted file mode 100644 index 265e74bdd06b..000000000000 --- a/.ci/targets/plugin_tools_tests.yaml +++ /dev/null @@ -1,5 +0,0 @@ -tasks: - - name: prepare tool - script: .ci/scripts/prepare_tool.sh - - name: tool unit tests - script: .ci/scripts/plugin_tools_tests.sh diff --git a/.ci/targets/build_all_plugins.yaml b/.ci/targets/windows_build_all_plugins.yaml similarity index 76% rename from .ci/targets/build_all_plugins.yaml rename to .ci/targets/windows_build_all_plugins.yaml index 0ffbdfcce376..53d6b99e2444 100644 --- a/.ci/targets/build_all_plugins.yaml +++ b/.ci/targets/windows_build_all_plugins.yaml @@ -3,9 +3,9 @@ tasks: script: .ci/scripts/prepare_tool.sh - name: create all_plugins app script: .ci/scripts/create_all_plugins_app.sh - - name: build all_plugins debug + - name: build all_plugins for Windows debug script: .ci/scripts/build_all_plugins.sh args: ["windows", "debug"] - - name: build all_plugins release + - name: build all_plugins for Windows release script: .ci/scripts/build_all_plugins.sh args: ["windows", "release"] diff --git a/.cirrus.yml b/.cirrus.yml index 5ff275e4c053..e9d513bf5d45 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,30 +1,27 @@ -gcp_credentials: ENCRYPTED[!9e38557f08108136b3625b7e62c64cc9eccc50365ffeaaa55c6be52f1d8fd6225af5badc69983ca08484274f02f34424!] +gcp_credentials: ENCRYPTED[!3a93d98d7c95a41f5033834ef30e50928fc5d81239dc632b153c2628200a8187f3811cb01ce338b1ab3b6505a7a65c37!] # Run on PRs and main branch post submit only. Don't run tests when tagging. only_if: $CIRRUS_TAG == '' && ($CIRRUS_PR != '' || $CIRRUS_BRANCH == 'main') env: CHANNEL: "master" # Default to master when not explicitly set by a task. - PLUGIN_TOOL_COMMAND: "dart ./script/tool/bin/flutter_plugin_tools.dart" + PLUGIN_TOOL_COMMAND: "dart pub global run flutter_plugin_tools" + +install_chrome_linux_template: &INSTALL_CHROME_LINUX + env: + CHROME_NO_SANDBOX: true + CHROME_DOWNLOAD_DIR: /tmp/chromium + CHROME_EXECUTABLE: $CHROME_DOWNLOAD_DIR/chrome-linux/chrome + CHROMEDRIVER_EXECUTABLE: $CHROME_DOWNLOAD_DIR/chromedriver/chromedriver + PATH: $PATH:$CHROME_DOWNLOAD_DIR/chrome-linux + install_chromium_script: + # Install a pinned version of Chromium and its corresponding ChromeDriver. + # Setting CHROME_EXECUTABLE above causes this version to be used for tests. + - ./script/install_chromium.sh 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_intel_template: &MACOS_INTEL_TEMPLATE - << : *MACOS_TEMPLATE - osx_instance: - image: big-sur-xcode-13 - -macos_arm_template: &MACOS_ARM_TEMPLATE - << : *MACOS_TEMPLATE - macos_instance: - image: ghcr.io/cirruslabs/macos-ventura-xcode:14 - flutter_upgrade_template: &FLUTTER_UPGRADE_TEMPLATE upgrade_flutter_script: # Channels that are part of our normal test matrix use a pinned, @@ -50,17 +47,19 @@ flutter_upgrade_template: &FLUTTER_UPGRADE_TEMPLATE - flutter doctor -v << : *TOOL_SETUP_TEMPLATE -build_all_plugins_app_template: &BUILD_ALL_PLUGINS_APP_TEMPLATE - create_all_plugins_app_script: - - $PLUGIN_TOOL_COMMAND create-all-packages-app --output-dir=. --exclude script/configs/exclude_all_plugins_app.yaml - build_all_plugins_debug_script: +# Ensures that the latest versions of all of the 1P packages can be used +# together. See script/configs/exclude_all_packages_app.yaml for exceptions. +build_all_packages_app_template: &BUILD_ALL_PACKAGES_APP_TEMPLATE + create_all_packages_app_script: + - $PLUGIN_TOOL_COMMAND create-all-packages-app --output-dir=. --exclude script/configs/exclude_all_packages_app.yaml + build_all_packages_debug_script: - cd all_packages - if [[ "$BUILD_ALL_ARGS" == "web" ]]; then - echo "Skipping; web does not support debug builds" - else - flutter build $BUILD_ALL_ARGS --debug - fi - build_all_plugins_release_script: + build_all_packages_release_script: - cd all_packages - flutter build $BUILD_ALL_ARGS --release @@ -78,10 +77,6 @@ task: namespace: default matrix: ### Platform-agnostic tasks ### - - name: Linux plugin_tools_tests - script: - - cd script/tool - - dart pub run test # Repository rules and best-practice enforcement. # Only channel-agnostic tests should go here since it is only run once # (on Flutter master). @@ -89,7 +84,9 @@ task: always: format_script: ./script/tool_runner.sh format --fail-on-change license_script: $PLUGIN_TOOL_COMMAND license-check - pubspec_script: ./script/tool_runner.sh pubspec-check + # The major and minor versions here should match the lowest version + # analyzed in legacy_version_analyze. + pubspec_script: ./script/tool_runner.sh pubspec-check --min-min-flutter-version=3.0.0 --min-min-dart-version=2.17.0 readme_script: - ./script/tool_runner.sh readme-check # Re-run with --require-excerpts, skipping packages that still need @@ -118,21 +115,11 @@ task: - else - echo "Only run in presubmit" - fi - - name: dart_unit_tests - env: - matrix: - CHANNEL: "master" - CHANNEL: "stable" - unit_test_script: - - ./script/tool_runner.sh test - name: analyze env: matrix: CHANNEL: "master" CHANNEL: "stable" - analyze_tool_script: - - cd script/tool - - dart analyze --fatal-infos analyze_script: # DO NOT change the custom-analysis argument here without changing the Dart repo. # See the comment in script/configs/custom_analysis.yaml for details. @@ -159,12 +146,13 @@ task: - name: legacy_version_analyze depends_on: analyze matrix: + # Change the arguments to pubspec-check when changing these values. env: CHANNEL: "3.0.5" DART_VERSION: "2.17.6" env: - CHANNEL: "2.10.5" - DART_VERSION: "2.16.2" + CHANNEL: "3.3.10" + DART_VERSION: "2.18.6" package_prep_script: # Allow analyzing packages that use a dev dependency with a higher # minimum Flutter/Dart version than the package itself. @@ -186,21 +174,21 @@ task: CIRRUS_CLONE_SUBMODULES: true script: ./script/tool_runner.sh update-excerpts --fail-on-change ### Web tasks ### - - name: web-build_all_plugins + - name: web-build_all_packages env: BUILD_ALL_ARGS: "web" matrix: CHANNEL: "master" CHANNEL: "stable" - << : *BUILD_ALL_PLUGINS_APP_TEMPLATE + << : *BUILD_ALL_PACKAGES_APP_TEMPLATE ### Linux desktop tasks ### - - name: linux-build_all_plugins + - name: linux-build_all_packages env: BUILD_ALL_ARGS: "linux" matrix: CHANNEL: "master" CHANNEL: "stable" - << : *BUILD_ALL_PLUGINS_APP_TEMPLATE + << : *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' @@ -228,8 +216,16 @@ task: zone: us-central1-a namespace: default cpu: 4 - memory: 12G + memory: 16G matrix: + ### Platform-agnostic tasks ### + - name: dart_unit_tests + env: + matrix: + CHANNEL: "master" + CHANNEL: "stable" + unit_test_script: + - ./script/tool_runner.sh test ### Android tasks ### - name: android-platform_tests # Don't run full platform tests on both channels in pre-submit. @@ -245,22 +241,20 @@ task: CHANNEL: "master" CHANNEL: "stable" MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] - GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[4457646586de940f49e054de7d82e60078b205ac627f11a89d077e63f639c9ba1002541d9209a9ee7777e159e97b43d0] + GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[df5cf97036c09184e386edbf4ab1e741189e0ac5ca7e4c73673c4bf02d8709c9ac733597e8f5b6511b51eafb52e4027f] build_script: - ./script/tool_runner.sh build-examples --apk lint_script: - ./script/tool_runner.sh lint-android # must come after build-examples native_unit_test_script: - # Native integration tests are handled by firebase-test-lab below, so + # Native integration tests are handled by Firebase Test Lab below, so # only run unit tests. # Must come after build-examples. - ./script/tool_runner.sh native-test --android --no-integration --exclude script/configs/exclude_native_unit_android.yaml firebase_test_lab_script: - if [[ -n "$GCLOUD_FIREBASE_TESTLAB_KEY" ]]; then - echo $GCLOUD_FIREBASE_TESTLAB_KEY > ${HOME}/gcloud-service-key.json - # (TODO)cyanglaz: add --device model=starqlteue,version=26 back when the device issue is fixed in FTL. - # https://github.com/flutter/flutter/issues/114535 - - ./script/tool_runner.sh firebase-test-lab --device model=redfin,version=30 --exclude=script/configs/exclude_integration_android.yaml + - ./script/tool_runner.sh firebase-test-lab --device model=redfin,version=30 --device model=starqlteue,version=26 --exclude=script/configs/exclude_integration_android.yaml - else - echo "This user does not have permission to run Firebase Test Lab tests." - fi @@ -270,13 +264,13 @@ task: path: "**/reports/lint-results-debug.xml" type: text/xml format: android-lint - - name: android-build_all_plugins + - name: android-build_all_packages env: BUILD_ALL_ARGS: "apk" matrix: CHANNEL: "master" CHANNEL: "stable" - << : *BUILD_ALL_PLUGINS_APP_TEMPLATE + << : *BUILD_ALL_PACKAGES_APP_TEMPLATE ### Web tasks ### - name: web-platform_tests env: @@ -286,110 +280,10 @@ task: matrix: CHANNEL: "master" CHANNEL: "stable" - CHROME_NO_SANDBOX: true - CHROME_DIR: /tmp/web_chromium/ - CHROME_EXECUTABLE: $CHROME_DIR/chrome-linux/chrome - install_script: - # Install a pinned version of Chromium and its corresponding ChromeDriver. - # Setting CHROME_EXECUTABLE above causes this version to be used for tests. - - ./script/install_chromium.sh "$CHROME_DIR" + << : *INSTALL_CHROME_LINUX chromedriver_background_script: - - cd "$CHROME_DIR" - - ./chromedriver/chromedriver --port=4444 + - $CHROMEDRIVER_EXECUTABLE --port=4444 build_script: - ./script/tool_runner.sh build-examples --web drive_script: - ./script/tool_runner.sh drive-examples --web --exclude=script/configs/exclude_integration_web.yaml - -# ARM macOS tasks. -task: - << : *MACOS_ARM_TEMPLATE - << : *FLUTTER_UPGRADE_TEMPLATE - matrix: - ### iOS tasks ### - - name: ios-build_all_plugins - env: - BUILD_ALL_ARGS: "ios --no-codesign" - matrix: - CHANNEL: "master" - CHANNEL: "stable" - << : *BUILD_ALL_PLUGINS_APP_TEMPLATE - ### macOS desktop tasks ### - - name: macos-platform_tests - # Don't run full platform tests on both channels in pre-submit. - skip: $CIRRUS_PR != '' && $CHANNEL == 'stable' - env: - matrix: - CHANNEL: "master" - CHANNEL: "stable" - PATH: $PATH:/usr/local/bin - build_script: - - ./script/tool_runner.sh build-examples --macos - xcode_analyze_script: - - ./script/tool_runner.sh xcode-analyze --macos - xcode_analyze_deprecation_script: - # Ensure we don't accidentally introduce deprecated code. - - ./script/tool_runner.sh xcode-analyze --macos --macos-min-version=12.3 - native_test_script: - - ./script/tool_runner.sh native-test --macos - drive_script: - - ./script/tool_runner.sh drive-examples --macos --exclude=script/configs/exclude_integration_macos.yaml - -# Intel macOS tasks. -task: - << : *MACOS_INTEL_TEMPLATE - << : *FLUTTER_UPGRADE_TEMPLATE - matrix: - ### 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 - # pod's arch exclusions, so fails to build. - - name: darwin-lint_podspecs - script: - - ./script/tool_runner.sh podspecs - ### iOS tasks ### - # TODO(stuartmorgan): Swap this and ios-build_all_plugins once simulator - # tests are reliable on the ARM infrastructure. See discussion at - # https://github.com/flutter/plugins/pull/5693#issuecomment-1126011089 - - name: ios-platform_tests - # Don't run full platform tests on both channels in pre-submit. - skip: $CIRRUS_PR != '' && $CHANNEL == 'stable' - env: - PATH: $PATH:/usr/local/bin - matrix: - PACKAGE_SHARDING: "--shardIndex 0 --shardCount 4" - PACKAGE_SHARDING: "--shardIndex 1 --shardCount 4" - PACKAGE_SHARDING: "--shardIndex 2 --shardCount 4" - PACKAGE_SHARDING: "--shardIndex 3 --shardCount 4" - matrix: - CHANNEL: "master" - CHANNEL: "stable" - SIMCTL_CHILD_MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] - create_simulator_script: - - xcrun simctl list - - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-11 com.apple.CoreSimulator.SimRuntime.iOS-15-0 | xargs xcrun simctl boot - build_script: - - ./script/tool_runner.sh build-examples --ios - xcode_analyze_script: - - ./script/tool_runner.sh xcode-analyze --ios - xcode_analyze_deprecation_script: - # Ensure we don't accidentally introduce deprecated code. - - ./script/tool_runner.sh xcode-analyze --ios --ios-min-version=13.0 - native_test_script: - - ./script/tool_runner.sh native-test --ios --ios-destination "platform=iOS Simulator,name=iPhone 11,OS=latest" - drive_script: - # `drive-examples` contains integration tests, which changes the UI of the application. - # This UI change sometimes affects `xctest`. - # So we run `drive-examples` after `native-test`; changing the order will result ci failure. - - ./script/tool_runner.sh drive-examples --ios --exclude=script/configs/exclude_integration_ios.yaml - ### macOS desktop tasks ### - # macos-platform_tests builds all the plugins on M1, so this build is run - # on Intel to give us build coverage of both host types. - - name: macos-build_all_plugins - env: - BUILD_ALL_ARGS: "macos" - matrix: - CHANNEL: "master" - CHANNEL: "stable" - setup_script: - << : *BUILD_ALL_PLUGINS_APP_TEMPLATE diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 292db55dede7..532987f931df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,12 +27,11 @@ jobs: cd $GITHUB_WORKSPACE # Checks out a copy of the repo. - name: Check out code - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c with: fetch-depth: 0 # Fetch all history so the tool can get all the tags to determine version. - name: Set up tools - run: dart pub get - working-directory: ${{ github.workspace }}/script/tool + run: dart pub global activate flutter_plugin_tools 0.13.4+3 # This workflow should be the last to run. So wait for all the other tests to succeed. - name: Wait on all tests @@ -50,5 +49,5 @@ jobs: run: | git config --global user.name ${{ secrets.USER_NAME }} git config --global user.email ${{ secrets.USER_EMAIL }} - dart ./script/tool/lib/src/main.dart publish --all-changed --base-sha=HEAD~ --skip-confirmation --remote=origin + dart pub global run flutter_plugin_tools publish --all-changed --base-sha=HEAD~ --skip-confirmation --remote=origin env: {PUB_CREDENTIALS: "${{ secrets.PUB_CREDENTIALS }}"} diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml index 55fa7f14591e..f0f36ab9d96c 100644 --- a/.github/workflows/scorecards-analysis.yml +++ b/.github/workflows/scorecards-analysis.yml @@ -22,12 +22,12 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@e363bfca00e752f91de7b7d2a77340e2e523cb18 + uses: ossf/scorecard-action@e38b1902ae4f44df626f11ba0734b14fb91f8f86 with: results_file: results.sarif results_format: sarif @@ -42,7 +42,7 @@ jobs: # Upload the results as artifacts (optional). - name: "Upload artifact" - uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce with: name: SARIF file path: results.sarif @@ -50,6 +50,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@807578363a7869ca324a79039e6db9c843e0e100 + uses: github/codeql-action/upload-sarif@3ebbd71c74ef574dbc558c82f70e52732c8b44fe with: sarif_file: results.sarif diff --git a/CODEOWNERS b/CODEOWNERS index 04cc2b1c4468..603e4a24fcc0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -9,15 +9,15 @@ packages/camera/** @bparrishMines packages/file_selector/** @stuartmorgan packages/google_maps_flutter/** @stuartmorgan packages/google_sign_in/** @stuartmorgan -packages/image_picker/** @stuartmorgan +packages/image_picker/** @tarrinneal packages/in_app_purchase/** @bparrishMines packages/local_auth/** @stuartmorgan -packages/path_provider/** @gaaclarke +packages/path_provider/** @stuartmorgan packages/plugin_platform_interface/** @stuartmorgan -packages/quick_actions/** @stuartmorgan +packages/quick_actions/** @bparrishMines packages/shared_preferences/** @tarrinneal packages/url_launcher/** @stuartmorgan -packages/video_player/** @gaaclarke +packages/video_player/** @tarrinneal packages/webview_flutter/** @bparrishMines # Sub-package-level rules. These should stay last, since the last matching @@ -28,30 +28,32 @@ packages/**/*_web/** @ditman # - Android packages/camera/camera_android/** @camsim99 -packages/espresso/** @GaryQian -packages/flutter_plugin_android_lifecycle/** @GaryQian -packages/google_maps_flutter/google_maps_flutter_android/** @GaryQian +packages/camera/camera_android_camerax/** @camsim99 +packages/espresso/** @reidbaker +packages/flutter_plugin_android_lifecycle/** @reidbaker +packages/google_maps_flutter/google_maps_flutter_android/** @reidbaker packages/google_sign_in/google_sign_in_android/** @camsim99 -packages/image_picker/image_picker_android/** @GaryQian -packages/in_app_purchase/in_app_purchase_android/** @GaryQian +packages/image_picker/image_picker_android/** @gmackall +packages/in_app_purchase/in_app_purchase_android/** @gmackall packages/local_auth/local_auth_android/** @camsim99 packages/path_provider/path_provider_android/** @camsim99 packages/quick_actions/quick_actions_android/** @camsim99 -packages/url_launcher/url_launcher_android/** @GaryQian +packages/shared_preferences/shared_preferences_android/** @reidbaker +packages/url_launcher/url_launcher_android/** @gmackall packages/video_player/video_player_android/** @camsim99 # - iOS packages/camera/camera_avfoundation/** @hellohuanlin packages/file_selector/file_selector_ios/** @jmagman packages/google_maps_flutter/google_maps_flutter_ios/** @cyanglaz -packages/google_sign_in/google_sign_in_ios/** @jmagman -packages/image_picker/image_picker_ios/** @cyanglaz +packages/google_sign_in/google_sign_in_ios/** @vashworth +packages/image_picker/image_picker_ios/** @vashworth packages/in_app_purchase/in_app_purchase_storekit/** @cyanglaz packages/ios_platform_images/ios/** @jmagman -packages/local_auth/local_auth_ios/** @hellohuanlin -packages/path_provider/path_provider_ios/** @jmagman +packages/local_auth/local_auth_ios/** @louisehsu +packages/path_provider/path_provider_foundation/** @jmagman packages/quick_actions/quick_actions_ios/** @hellohuanlin -packages/shared_preferences/shared_preferences_ios/** @cyanglaz +packages/shared_preferences/shared_preferences_foundation/** @cyanglaz packages/url_launcher/url_launcher_ios/** @jmagman packages/video_player/video_player_avfoundation/** @hellohuanlin packages/webview_flutter/webview_flutter_wkwebview/** @cyanglaz @@ -64,8 +66,6 @@ packages/url_launcher/url_launcher_linux/** @cbracken # - macOS packages/file_selector/file_selector_macos/** @cbracken -packages/path_provider/path_provider_macos/** @cbracken -packages/shared_preferences/shared_preferences_macos/** @cbracken packages/url_launcher/url_launcher_macos/** @cbracken # - Windows diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2d44d50049b..8441f06a5884 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,10 @@ # Contributing to Flutter Plugins -[![Build Status](https://api.cirrus-ci.com/github/flutter/plugins.svg)](https://cirrus-ci.com/github/flutter/plugins/main) +| **ARCHIVED** | +|--------------| +| This repository is no longer in use; all source has moved to [flutter/packages](https://github.com/flutter/packages) and future development work will be done there. | + +## ARCHIVED CONTENT _See also: [Flutter's code of conduct](https://github.com/flutter/flutter/blob/master/CODE_OF_CONDUCT.md)_ diff --git a/FlutterFire.md b/FlutterFire.md index 00326167e7ab..551d9b642c31 100644 --- a/FlutterFire.md +++ b/FlutterFire.md @@ -1,6 +1,6 @@ # FlutterFire - MOVED -The FlutterFire family of plugins has moved to the FirebaseExtended organization on GitHub. This makes it easier for us to collaborate with the Firebase team. We want to build the best integration we can! +The FlutterFire family of plugins has moved to the Firebase organization on GitHub. This makes it easier for us to collaborate with the Firebase team. We want to build the best integration we can! Visit FlutterFire at its new home: -https://github.com/FirebaseExtended/flutterfire \ No newline at end of file +https://github.com/firebase/flutterfire diff --git a/README.md b/README.md index eb898e0b1e5e..50f2d42fb600 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,11 @@ The following changes were necessary to adapt the code to our needs: # Flutter plugins -[![Build Status](https://api.cirrus-ci.com/github/flutter/plugins.svg)](https://cirrus-ci.com/github/flutter/plugins/main) -[![Release Status](https://github.com/flutter/plugins/actions/workflows/release.yml/badge.svg)](https://github.com/flutter/plugins/actions/workflows/release.yml) -[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/flutter/plugins/badge)](https://api.securityscorecards.dev/projects/github.com/flutter/plugins) +| **ARCHIVED** | +|--------------| +| This repository is no longer in use; all source has moved to [flutter/packages](https://github.com/flutter/packages) and future development work will be done there. | + +## ARCHIVED CONTENT This repo is a companion repo to the main [flutter repo](https://github.com/flutter/flutter). It contains the source code for diff --git a/analysis_options.yaml b/analysis_options.yaml index b12af6cf11e6..498d19dfb4ae 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,55 +1,24 @@ -# This is a copy (as of March 2021) of flutter/flutter's analysis_options file, -# with minimal changes for this repository. The goal is to move toward using a -# shared set of analysis options as much as possible, and eventually a shared -# file. - # Specify analysis options. # -# For a list of lints, see: http://dart-lang.github.io/linter/lints/ -# See the configuration guide for more -# https://github.com/dart-lang/sdk/tree/main/pkg/analyzer#configuring-the-analyzer -# -# There are other similar analysis options files in the flutter repos, -# which should be kept in sync with this file: -# -# - analysis_options.yaml (this file) -# - packages/flutter/lib/analysis_options_user.yaml -# - https://github.com/flutter/flutter/blob/master/analysis_options.yaml -# - https://github.com/flutter/engine/blob/main/analysis_options.yaml -# - https://github.com/flutter/packages/blob/main/analysis_options.yaml -# -# This file contains the analysis options used for code in the flutter/plugins -# repository. +# This file is a copy of analysis_options.yaml from flutter repo +# as of 2022-07-27, but with some modifications marked with +# "DIFFERENT FROM FLUTTER/FLUTTER" below. The file is expected to +# be kept in sync with the master file from the flutter repo. analyzer: language: strict-casts: true strict-raw-types: true errors: - # treat missing required parameters as a warning (not a hint) - missing_required_param: warning - # treat missing returns as a warning (not a hint) - missing_return: warning - # allow having TODO comments in the code - todo: ignore # allow self-reference to deprecated members (we do this because otherwise we have # to annotate every member in every test, assert, etc, when we deprecate something) deprecated_member_use_from_same_package: ignore - # Ignore analyzer hints for updating pubspecs when using Future or - # Stream and not importing dart:async - # Please see https://github.com/flutter/flutter/pull/24528 for details. - sdk_version_async_exported_from_core: ignore # Turned off until null-safe rollout is complete. unnecessary_null_comparison: ignore - ### Local flutter/plugins changes ### - # Allow null checks for as long as mixed mode is officially supported. - always_require_non_null_named_parameters: false # not needed with nnbd - exclude: + exclude: # DIFFERENT FROM FLUTTER/FLUTTER # Ignore generated files - '**/*.g.dart' - - 'lib/src/generated/*.dart' - '**/*.mocks.dart' # Mockito @GenerateMocks - - '**/*.pigeon.dart' # Pigeon generated file linter: rules: @@ -68,26 +37,26 @@ linter: # - avoid_catching_errors # blocked on https://github.com/dart-lang/linter/issues/3023 - avoid_classes_with_only_static_members - avoid_double_and_int_checks - # - avoid_dynamic_calls # LOCAL CHANGE - Needs to be enabled and violations fixed. + - avoid_dynamic_calls - avoid_empty_else - avoid_equals_and_hash_code_on_mutable_classes - avoid_escaping_inner_quotes - avoid_field_initializers_in_const_classes # - avoid_final_parameters # incompatible with prefer_final_parameters - avoid_function_literals_in_foreach_calls - # - avoid_implementing_value_types # LOCAL CHANGE - Needs to be enabled and violations fixed. + - avoid_implementing_value_types - avoid_init_to_null - avoid_js_rounded_ints # - avoid_multiple_declarations_per_line # seems to be a stylistic choice we don't subscribe to - avoid_null_checks_in_equality_operators # - avoid_positional_boolean_parameters # would have been nice to enable this but by now there's too many places that break it - # - avoid_print # LOCAL CHANGE - Needs to be enabled and violations fixed. + - avoid_print # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) - avoid_redundant_argument_values - avoid_relative_lib_imports - avoid_renaming_method_parameters - avoid_return_types_on_setters - # - avoid_returning_null # still violated by some pre-nnbd code that we haven't yet migrated + - avoid_returning_null - avoid_returning_null_for_future - avoid_returning_null_for_void # - avoid_returning_this # there are enough valid reasons to return `this` that this lint ends up with too many false positives @@ -109,15 +78,17 @@ linter: # - cascade_invocations # doesn't match the typical style of this repo - cast_nullable_to_non_nullable # - close_sinks # not reliable enough + # - combinators_ordering # DIFFERENT FROM FLUTTER/FLUTTER: This isn't available on stable yet. # - comment_references # blocked on https://github.com/dart-lang/linter/issues/1142 - # - conditional_uri_does_not_exist # not yet tested + - conditional_uri_does_not_exist # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204 - control_flow_in_finally - # - curly_braces_in_flow_control_structures # not required by flutter style + - curly_braces_in_flow_control_structures - depend_on_referenced_packages - deprecated_consistency # - diagnostic_describe_all_properties # enabled only at the framework level (packages/flutter/lib) - directives_ordering + # - discarded_futures # not yet tested # - do_not_use_environment # there are appropriate times to use the environment, especially in our tests and build logic - empty_catches - empty_constructor_bodies @@ -128,7 +99,6 @@ linter: - flutter_style_todos - hash_and_equals - implementation_imports - # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811 - iterable_contains_unrelated_type # - join_return_with_assignment # not required by flutter style - leading_newlines_in_multiline_strings @@ -140,19 +110,19 @@ linter: # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/linter/issues/453 - missing_whitespace_between_adjacent_strings - no_adjacent_strings_in_list - # - no_default_cases # LOCAL CHANGE - Needs to be enabled and violations fixed. + - no_default_cases - no_duplicate_case_values - no_leading_underscores_for_library_prefixes - no_leading_underscores_for_local_identifiers - no_logic_in_create_state - # - no_runtimeType_toString # ok in tests; we enable this only in packages/ + - no_runtimeType_toString # DIFFERENT FROM FLUTTER/FLUTTER - non_constant_identifier_names - noop_primitive_operations - null_check_on_nullable_type_parameter - null_closures # - omit_local_variable_types # opposite of always_specify_types # - one_member_abstracts # too many false positives - # - only_throw_errors # this does get disabled in a few places where we have legacy code that uses strings et al # LOCAL CHANGE - Needs to be enabled and violations fixed. + - only_throw_errors # this does get disabled in a few places where we have legacy code that uses strings et al - overridden_fields - package_api_docs - package_names @@ -199,16 +169,16 @@ linter: - prefer_typing_uninitialized_variables - prefer_void_to_null - provide_deprecation_message - # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml + - public_member_api_docs # DIFFERENT FROM FLUTTER/FLUTTER - recursive_getters # - require_trailing_commas # blocked on https://github.com/dart-lang/sdk/issues/47441 - secure_pubspec_urls - # - sized_box_for_whitespace # LOCAL CHANGE - Needs to be enabled and violations fixed. + - sized_box_for_whitespace # - sized_box_shrink_expand # not yet tested - slash_for_doc_comments - sort_child_properties_last - sort_constructors_first - # - sort_pub_dependencies # prevents separating pinned transitive dependencies + - sort_pub_dependencies # DIFFERENT FROM FLUTTER/FLUTTER: Flutter's use case for not sorting does not apply to this repository. - sort_unnamed_constructors_first - test_types_in_equals - throw_in_finally @@ -216,7 +186,7 @@ linter: # - type_annotate_public_apis # subset of always_specify_types - type_init_formals # - unawaited_futures # too many false positives, especially with the way AnimationController works - # - unnecessary_await_in_return # LOCAL CHANGE - Needs to be enabled and violations fixed. + - unnecessary_await_in_return - unnecessary_brace_in_string_interps - unnecessary_const - unnecessary_constructor_name @@ -226,6 +196,7 @@ linter: - unnecessary_late - unnecessary_new - unnecessary_null_aware_assignments + - unnecessary_null_aware_operator_on_extension_on_nullable - unnecessary_null_checks - unnecessary_null_in_if_null_operators - unnecessary_nullable_for_final_variable_declarations @@ -236,9 +207,10 @@ linter: - unnecessary_string_escapes - unnecessary_string_interpolations - unnecessary_this + - unnecessary_to_list_in_spreads - unrelated_type_equality_checks - unsafe_html - # - use_build_context_synchronously # LOCAL CHANGE - Needs to be enabled and violations fixed. + - use_build_context_synchronously # - use_colored_box # not yet tested # - use_decorated_box # not yet tested # - use_enums # not yet tested @@ -258,11 +230,3 @@ linter: # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review - valid_regexps - void_checks - ### Local flutter/plugins additions ### - # These are from flutter/flutter/packages, so will need to be preserved - # separately when moving to a shared file. - - no_runtimeType_toString # use objectRuntimeType from package:foundation - - public_member_api_docs # see https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#documentation-dartdocs-javadocs-etc - # Flutter has a specific use case for dependencies that are intentionally - # not sorted, which doesn't apply to this repo. - - sort_pub_dependencies diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 127da4561bc3..13c00402449a 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,23 @@ +## 0.10.3 + +* Adds back use of Optional type. + +## 0.10.2+1 + +* Updates code for stricter lint checks. + +## 0.10.2 + +* Implements option to also stream when recording a video. + +## 0.10.1 + +* Remove usage of deprecated quiver Optional type. + +## 0.10.0+5 + +* Updates code for stricter lint checks. + ## 0.10.0+4 * Removes usage of `_ambiguate` method in example. @@ -19,7 +39,7 @@ ## 0.10.0 * **Breaking Change** Bumps default camera_web package version, which updates permission exception code from `cameraPermission` to `CameraAccessDenied`. -* **Breaking Change** Bumps default camera_android package version, which updates permission exception code from `cameraPermission` to +* **Breaking Change** Bumps default camera_android package version, which updates permission exception code from `cameraPermission` to `CameraAccessDenied` and `AudioAccessDenied`. * Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/106316). diff --git a/packages/camera/camera/README.md b/packages/camera/camera/README.md index 4d7c3d90791a..86b0355b8bcc 100644 --- a/packages/camera/camera/README.md +++ b/packages/camera/camera/README.md @@ -141,10 +141,10 @@ class _CameraAppState extends State { if (e is CameraException) { switch (e.code) { case 'CameraAccessDenied': - print('User denied camera access.'); + // Handle access errors here. break; default: - print('Handle other errors.'); + // Handle other errors here. break; } } diff --git a/packages/camera/camera/example/android/gradle.properties b/packages/camera/camera/example/android/gradle.properties index b253d8e5f746..d0448f163e41 100644 --- a/packages/camera/camera/example/android/gradle.properties +++ b/packages/camera/camera/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=false android.enableR8=true diff --git a/packages/camera/camera/example/integration_test/camera_test.dart b/packages/camera/camera/example/integration_test/camera_test.dart index b233d4958c8a..f0cc67f0c06c 100644 --- a/packages/camera/camera/example/integration_test/camera_test.dart +++ b/packages/camera/camera/example/integration_test/camera_test.dart @@ -55,8 +55,6 @@ void main() { Future testCaptureImageResolution( CameraController controller, ResolutionPreset preset) async { final Size expectedSize = presetExpectedSizes[preset]!; - print( - 'Capturing photo at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}'); // Take Picture final XFile file = await controller.takePicture(); @@ -104,8 +102,6 @@ void main() { Future testCaptureVideoResolution( CameraController controller, ResolutionPreset preset) async { final Size expectedSize = presetExpectedSizes[preset]!; - print( - 'Capturing video at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}'); // Take Video await controller.startVideoRecording(); diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 860263edf2d3..b343b6da9d89 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -31,17 +31,16 @@ IconData getCameraLensIcon(CameraLensDirection direction) { return Icons.camera_front; case CameraLensDirection.external: return Icons.camera; - default: - throw ArgumentError('Unknown lens direction'); } + // This enum is from a different package, so a new value could be added at + // any time. The example should keep working if that happens. + // ignore: dead_code + return Icons.camera; } void _logError(String code, String? message) { - if (message != null) { - print('Error: $code\nError Message: $message'); - } else { - print('Error: $code'); - } + // ignore: avoid_print + print('Error: $code${message == null ? '' : '\nError Message: $message'}'); } class _CameraExampleHomeState extends State diff --git a/packages/camera/camera/example/lib/readme_full_example.dart b/packages/camera/camera/example/lib/readme_full_example.dart index a3c232ec44f7..20bfe78c30fc 100644 --- a/packages/camera/camera/example/lib/readme_full_example.dart +++ b/packages/camera/camera/example/lib/readme_full_example.dart @@ -40,10 +40,10 @@ class _CameraAppState extends State { if (e is CameraException) { switch (e.code) { case 'CameraAccessDenied': - print('User denied camera access.'); + // Handle access errors here. break; default: - print('Handle other errors.'); + // Handle other errors here. break; } } diff --git a/packages/camera/camera/example/test_driver/integration_test.dart b/packages/camera/camera/example/test_driver/integration_test.dart index 4ec97e66d36c..aa57599f3165 100644 --- a/packages/camera/camera/example/test_driver/integration_test.dart +++ b/packages/camera/camera/example/test_driver/integration_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: avoid_print + import 'dart:async'; import 'dart:convert'; import 'dart:io'; diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index ed1c951925d8..7a396c1589f9 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -3,13 +3,13 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:collection'; import 'dart:math'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:quiver/core.dart'; import '../camera.dart'; @@ -459,12 +459,6 @@ class CameraController extends ValueNotifier { assert(defaultTargetPlatform == TargetPlatform.android || defaultTargetPlatform == TargetPlatform.iOS); _throwIfNotInitialized('stopImageStream'); - if (value.isRecordingVideo) { - throw CameraException( - 'A video recording is already started.', - 'stopImageStream was called while a video is being recorded.', - ); - } if (!value.isStreamingImages) { throw CameraException( 'No camera is streaming images', @@ -483,9 +477,13 @@ class CameraController extends ValueNotifier { /// Start a video recording. /// + /// You may optionally pass an [onAvailable] callback to also have the + /// video frames streamed to this callback. + /// /// The video is returned as a [XFile] after calling [stopVideoRecording]. /// Throws a [CameraException] if the capture fails. - Future startVideoRecording() async { + Future startVideoRecording( + {onLatestImageAvailable? onAvailable}) async { _throwIfNotInitialized('startVideoRecording'); if (value.isRecordingVideo) { throw CameraException( @@ -493,20 +491,23 @@ class CameraController extends ValueNotifier { 'startVideoRecording was called when a recording is already started.', ); } - if (value.isStreamingImages) { - throw CameraException( - 'A camera has started streaming images.', - 'startVideoRecording was called while a camera was streaming images.', - ); + + Function(CameraImageData image)? streamCallback; + if (onAvailable != null) { + streamCallback = (CameraImageData imageData) { + onAvailable(CameraImage.fromPlatformInterface(imageData)); + }; } try { - await CameraPlatform.instance.startVideoRecording(_cameraId); + await CameraPlatform.instance.startVideoCapturing( + VideoCaptureOptions(_cameraId, streamCallback: streamCallback)); value = value.copyWith( isRecordingVideo: true, isRecordingPaused: false, recordingOrientation: Optional.of( - value.lockedCaptureOrientation ?? value.deviceOrientation)); + value.lockedCaptureOrientation ?? value.deviceOrientation), + isStreamingImages: onAvailable != null); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } @@ -523,6 +524,11 @@ class CameraController extends ValueNotifier { 'stopVideoRecording was called when no video is recording.', ); } + + if (value.isStreamingImages) { + stopImageStream(); + } + try { final XFile file = await CameraPlatform.instance.stopVideoRecording(_cameraId); @@ -840,3 +846,112 @@ class CameraController extends ValueNotifier { } } } + +/// A value that might be absent. +/// +/// Used to represent [DeviceOrientation]s that are optional but also able +/// to be cleared. +@immutable +class Optional extends IterableBase { + /// Constructs an empty Optional. + const Optional.absent() : _value = null; + + /// Constructs an Optional of the given [value]. + /// + /// Throws [ArgumentError] if [value] is null. + Optional.of(T value) : _value = value { + // TODO(cbracken): Delete and make this ctor const once mixed-mode + // execution is no longer around. + ArgumentError.checkNotNull(value); + } + + /// Constructs an Optional of the given [value]. + /// + /// If [value] is null, returns [absent()]. + const Optional.fromNullable(T? value) : _value = value; + + final T? _value; + + /// True when this optional contains a value. + bool get isPresent => _value != null; + + /// True when this optional contains no value. + bool get isNotPresent => _value == null; + + /// Gets the Optional value. + /// + /// Throws [StateError] if [value] is null. + T get value { + if (_value == null) { + throw StateError('value called on absent Optional.'); + } + return _value!; + } + + /// Executes a function if the Optional value is present. + void ifPresent(void Function(T value) ifPresent) { + if (isPresent) { + ifPresent(_value as T); + } + } + + /// Execution a function if the Optional value is absent. + void ifAbsent(void Function() ifAbsent) { + if (!isPresent) { + ifAbsent(); + } + } + + /// Gets the Optional value with a default. + /// + /// The default is returned if the Optional is [absent()]. + /// + /// Throws [ArgumentError] if [defaultValue] is null. + T or(T defaultValue) { + return _value ?? defaultValue; + } + + /// Gets the Optional value, or `null` if there is none. + T? get orNull => _value; + + /// Transforms the Optional value. + /// + /// If the Optional is [absent()], returns [absent()] without applying the transformer. + /// + /// The transformer must not return `null`. If it does, an [ArgumentError] is thrown. + Optional transform(S Function(T value) transformer) { + return _value == null + ? Optional.absent() + : Optional.of(transformer(_value as T)); + } + + /// Transforms the Optional value. + /// + /// If the Optional is [absent()], returns [absent()] without applying the transformer. + /// + /// Returns [absent()] if the transformer returns `null`. + Optional transformNullable(S? Function(T value) transformer) { + return _value == null + ? Optional.absent() + : Optional.fromNullable(transformer(_value as T)); + } + + @override + Iterator get iterator => + isPresent ? [_value as T].iterator : Iterable.empty().iterator; + + /// Delegates to the underlying [value] hashCode. + @override + int get hashCode => _value.hashCode; + + /// Delegates to the underlying [value] operator==. + @override + bool operator ==(Object o) => o is Optional && o._value == _value; + + @override + String toString() { + return _value == null + ? 'Optional { absent }' + : 'Optional { value: $_value }'; + } +} diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 0f75d10c36cd..1b902ab61f0a 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for controlling the camera. Supports previewing Dart. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.10.0+4 +version: 0.10.3 environment: sdk: ">=2.14.0 <3.0.0" @@ -21,10 +21,10 @@ flutter: default_package: camera_web dependencies: - camera_android: ^0.10.0 - camera_avfoundation: ^0.9.7+1 - camera_platform_interface: ^2.2.0 - camera_web: ^0.3.0 + camera_android: ^0.10.1 + camera_avfoundation: ^0.9.9 + camera_platform_interface: ^2.3.2 + camera_web: ^0.3.1 flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.2 diff --git a/packages/camera/camera/test/camera_image_stream_test.dart b/packages/camera/camera/test/camera_image_stream_test.dart index a9320e46dfb5..29b5cceaa49a 100644 --- a/packages/camera/camera/test/camera_image_stream_test.dart +++ b/packages/camera/camera/test/camera_image_stream_test.dart @@ -130,7 +130,7 @@ void main() { ); }); - test('stopImageStream() throws $CameraException when recording videos', + test('stopImageStream() throws $CameraException when not streaming images', () async { final CameraController cameraController = CameraController( const CameraDescription( @@ -140,20 +140,16 @@ void main() { ResolutionPreset.max); await cameraController.initialize(); - await cameraController.startImageStream((CameraImage image) => null); - cameraController.value = - cameraController.value.copyWith(isRecordingVideo: true); expect( cameraController.stopImageStream, throwsA(isA().having( (CameraException error) => error.description, - 'A video recording is already started.', - 'stopImageStream was called while a video is being recorded.', + 'No camera is streaming images', + 'stopImageStream was called when no camera is streaming images.', ))); }); - test('stopImageStream() throws $CameraException when not streaming images', - () async { + test('stopImageStream() intended behaviour', () async { final CameraController cameraController = CameraController( const CameraDescription( name: 'cam', @@ -161,29 +157,44 @@ void main() { sensorOrientation: 90), ResolutionPreset.max); await cameraController.initialize(); + await cameraController.startImageStream((CameraImage image) => null); + await cameraController.stopImageStream(); + + expect(mockPlatform.streamCallLog, + ['onStreamedFrameAvailable', 'listen', 'cancel']); + }); + + test('startVideoRecording() can stream images', () async { + final CameraController cameraController = CameraController( + const CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + + cameraController.startVideoRecording( + onAvailable: (CameraImage image) => null); expect( - cameraController.stopImageStream, - throwsA(isA().having( - (CameraException error) => error.description, - 'No camera is streaming images', - 'stopImageStream was called when no camera is streaming images.', - ))); + mockPlatform.streamCallLog.contains('startVideoCapturing with stream'), + isTrue); }); - test('stopImageStream() intended behaviour', () async { + test('startVideoRecording() by default does not stream', () async { final CameraController cameraController = CameraController( const CameraDescription( name: 'cam', lensDirection: CameraLensDirection.back, sensorOrientation: 90), ResolutionPreset.max); + await cameraController.initialize(); - await cameraController.startImageStream((CameraImage image) => null); - await cameraController.stopImageStream(); - expect(mockPlatform.streamCallLog, - ['onStreamedFrameAvailable', 'listen', 'cancel']); + cameraController.startVideoRecording(); + + expect(mockPlatform.streamCallLog.contains('startVideoCapturing'), isTrue); }); } @@ -203,6 +214,24 @@ class MockStreamingCameraPlatform extends MockCameraPlatform { return _streamController!.stream; } + @override + Future startVideoRecording(int cameraId, + {Duration? maxVideoDuration}) { + streamCallLog.add('startVideoRecording'); + return super + .startVideoRecording(cameraId, maxVideoDuration: maxVideoDuration); + } + + @override + Future startVideoCapturing(VideoCaptureOptions options) { + if (options.streamCallback == null) { + streamCallLog.add('startVideoCapturing'); + } else { + streamCallLog.add('startVideoCapturing with stream'); + } + return super.startVideoCapturing(options); + } + void _onFrameStreamListen() { streamCallLog.add('listen'); } diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index bedb0ea8e01f..6677fcf90393 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -8,7 +8,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:quiver/core.dart'; class FakeController extends ValueNotifier implements CameraController { @@ -98,7 +97,8 @@ class FakeController extends ValueNotifier Future startImageStream(onLatestImageAvailable onAvailable) async {} @override - Future startVideoRecording() async {} + Future startVideoRecording( + {onLatestImageAvailable? onAvailable}) async {} @override Future stopImageStream() async {} diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index 3c12648f13b9..ab8354f7ba05 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -13,7 +13,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:quiver/core.dart'; List get mockAvailableCameras => [ const CameraDescription( @@ -336,30 +335,6 @@ void main() { ))); }); - test( - 'startVideoRecording() throws $CameraException when already streaming images', - () async { - final CameraController cameraController = CameraController( - const CameraDescription( - name: 'cam', - lensDirection: CameraLensDirection.back, - sensorOrientation: 90), - ResolutionPreset.max); - - await cameraController.initialize(); - - cameraController.value = - cameraController.value.copyWith(isStreamingImages: true); - - expect( - cameraController.startVideoRecording(), - throwsA(isA().having( - (CameraException error) => error.description, - 'A camera has started streaming images.', - 'startVideoRecording was called while a camera was streaming images.', - ))); - }); - test('getMaxZoomLevel() throws $CameraException when uninitialized', () async { final CameraController cameraController = CameraController( @@ -1459,6 +1434,12 @@ class MockCameraPlatform extends Mock {Duration? maxVideoDuration}) => Future.value(mockVideoRecordingXFile); + @override + Future startVideoCapturing(VideoCaptureOptions options) { + return startVideoRecording(options.cameraId, + maxVideoDuration: options.maxDuration); + } + @override Future lockCaptureOrientation( int? cameraId, DeviceOrientation? orientation) async => diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md index a62d3169e409..4609b402058a 100644 --- a/packages/camera/camera_android/CHANGELOG.md +++ b/packages/camera/camera_android/CHANGELOG.md @@ -1,3 +1,37 @@ +## 0.10.4 + +* Temporarily fixes issue with requested video profiles being null by falling back to deprecated behavior in that case. + +## 0.10.3 + +* Adds back use of Optional type. +* Updates minimum Flutter version to 3.0. + +## 0.10.2+3 + +* Updates code for stricter lint checks. + +## 0.10.2+2 + +* Fixes zoom computation for virtual cameras hiding physical cameras in Android 11+. +* Removes the unused CameraZoom class from the codebase. + +## 0.10.2+1 + +* Updates code for stricter lint checks. + +## 0.10.2 + +* Remove usage of deprecated quiver Optional type. + +## 0.10.1 + +* Implements an option to also stream when recording a video. + +## 0.10.0+5 + +* Fixes `ArrayIndexOutOfBoundsException` when the permission request is interrupted. + ## 0.10.0+4 * Upgrades `androidx.annotation` version to 1.5.0. diff --git a/packages/camera/camera_android/android/build.gradle b/packages/camera/camera_android/android/build.gradle index 4fbb2270b556..9c403e02bbd4 100644 --- a/packages/camera/camera_android/android/build.gradle +++ b/packages/camera/camera_android/android/build.gradle @@ -35,10 +35,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - // TODO(stuartmorgan): Enable when gradle is updated. - // disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' baseline file("lint-baseline.xml") } compileOptions { @@ -63,7 +60,7 @@ android { dependencies { implementation 'androidx.annotation:annotation:1.5.0' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-inline:4.7.0' + testImplementation 'org.mockito:mockito-inline:5.0.0' testImplementation 'androidx.test:core:1.4.0' testImplementation 'org.robolectric:robolectric:4.5' } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java index 3d2df98b60da..b02d6864b5b7 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -258,8 +258,11 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { MediaRecorderBuilder mediaRecorderBuilder; - if (Build.VERSION.SDK_INT >= 31) { - mediaRecorderBuilder = new MediaRecorderBuilder(getRecordingProfile(), outputFilePath); + // TODO(camsim99): Revert changes that allow legacy code to be used when recordingProfile is null + // once this has largely been fixed on the Android side. https://github.com/flutter/flutter/issues/119668 + EncoderProfiles recordingProfile = getRecordingProfile(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && recordingProfile != null) { + mediaRecorderBuilder = new MediaRecorderBuilder(recordingProfile, outputFilePath); } else { mediaRecorderBuilder = new MediaRecorderBuilder(getRecordingProfileLegacy(), outputFilePath); } @@ -522,6 +525,21 @@ private void refreshPreviewCaptureSession( } } + private void startCapture(boolean record, boolean stream) throws CameraAccessException { + List surfaces = new ArrayList<>(); + Runnable successCallback = null; + if (record) { + surfaces.add(mediaRecorder.getSurface()); + successCallback = () -> mediaRecorder.start(); + } + if (stream) { + surfaces.add(imageStreamReader.getSurface()); + } + + createCaptureSession( + CameraDevice.TEMPLATE_RECORD, successCallback, surfaces.toArray(new Surface[0])); + } + public void takePicture(@NonNull final Result result) { // Only take one picture at a time. if (cameraCaptureCallback.getCameraState() != CameraState.STATE_PREVIEW) { @@ -731,29 +749,17 @@ private void unlockAutoFocus() { dartMessenger.error(flutterResult, errorCode, errorMessage, null)); } - public void startVideoRecording(@NonNull Result result) { - final File outputDir = applicationContext.getCacheDir(); - try { - captureFile = File.createTempFile("REC", ".mp4", outputDir); - } catch (IOException | SecurityException e) { - result.error("cannotCreateFile", e.getMessage(), null); - return; - } - try { - prepareMediaRecorder(captureFile.getAbsolutePath()); - } catch (IOException e) { - recordingVideo = false; - captureFile = null; - result.error("videoRecordingFailed", e.getMessage(), null); - return; + public void startVideoRecording( + @NonNull Result result, @Nullable EventChannel imageStreamChannel) { + prepareRecording(result); + + if (imageStreamChannel != null) { + setStreamHandler(imageStreamChannel); } - // Re-create autofocus feature so it's using video focus mode now. - cameraFeatures.setAutoFocus( - cameraFeatureFactory.createAutoFocusFeature(cameraProperties, true)); + recordingVideo = true; try { - createCaptureSession( - CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); + startCapture(true, imageStreamChannel != null); result.success(null); } catch (CameraAccessException e) { recordingVideo = false; @@ -1073,21 +1079,10 @@ public void startPreview() throws CameraAccessException { public void startPreviewWithImageStream(EventChannel imageStreamChannel) throws CameraAccessException { - createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); - Log.i(TAG, "startPreviewWithImageStream"); - - imageStreamChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object o, EventChannel.EventSink imageStreamSink) { - setImageStreamImageAvailableListener(imageStreamSink); - } + setStreamHandler(imageStreamChannel); - @Override - public void onCancel(Object o) { - imageStreamReader.setOnImageAvailableListener(null, backgroundHandler); - } - }); + startCapture(false, true); + Log.i(TAG, "startPreviewWithImageStream"); } /** @@ -1117,6 +1112,42 @@ public void onError(String errorCode, String errorMessage) { cameraCaptureCallback.setCameraState(CameraState.STATE_PREVIEW); } + private void prepareRecording(@NonNull Result result) { + final File outputDir = applicationContext.getCacheDir(); + try { + captureFile = File.createTempFile("REC", ".mp4", outputDir); + } catch (IOException | SecurityException e) { + result.error("cannotCreateFile", e.getMessage(), null); + return; + } + try { + prepareMediaRecorder(captureFile.getAbsolutePath()); + } catch (IOException e) { + recordingVideo = false; + captureFile = null; + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } + // Re-create autofocus feature so it's using video focus mode now. + cameraFeatures.setAutoFocus( + cameraFeatureFactory.createAutoFocusFeature(cameraProperties, true)); + } + + private void setStreamHandler(EventChannel imageStreamChannel) { + imageStreamChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object o, EventChannel.EventSink imageStreamSink) { + setImageStreamImageAvailableListener(imageStreamSink); + } + + @Override + public void onCancel(Object o) { + imageStreamReader.setOnImageAvailableListener(null, backgroundHandler); + } + }); + } + private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { imageStreamReader.setOnImageAvailableListener( reader -> { diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java index 4441751e19cf..ee8fa5a71a16 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java @@ -105,7 +105,9 @@ public boolean onRequestPermissionsResult(int id, String[] permissions, int[] gr } alreadyCalled = true; - if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { + // grantResults could be empty if the permissions request with the user is interrupted + // https://developer.android.com/reference/android/app/Activity#onRequestPermissionsResult(int,%20java.lang.String[],%20int[]) + if (grantResults.length == 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED) { callback.onResult(CAMERA_ACCESS_DENIED, CAMERA_ACCESS_DENIED_MESSAGE); } else if (grantResults.length > 1 && grantResults[1] != PackageManager.PERMISSION_GRANTED) { callback.onResult(AUDIO_ACCESS_DENIED, AUDIO_ACCESS_DENIED_MESSAGE); diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java index 95efebbf6488..a69bae43ee17 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java @@ -150,10 +150,34 @@ public interface CameraProperties { * android.hardware.camera2.CameraCharacteristics#SCALER_AVAILABLE_MAX_DIGITAL_ZOOM key. * * @return Float Maximum ratio between both active area width and crop region width, and active - * area height and crop region height + * area height and crop region height. */ Float getScalerAvailableMaxDigitalZoom(); + /** + * Returns the minimum ratio between the default camera zoom setting and all of the available + * zoom. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#CONTROL_ZOOM_RATIO_RANGE key's lower value. + * + * @return Float Minimum ratio between the default zoom ratio and the minimum possible zoom. + */ + @RequiresApi(api = VERSION_CODES.R) + Float getScalerMinZoomRatio(); + + /** + * Returns the maximum ratio between the default camera zoom setting and all of the available + * zoom. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#CONTROL_ZOOM_RATIO_RANGE key's upper value. + * + * @return Float Maximum ratio between the default zoom ratio and the maximum possible zoom. + */ + @RequiresApi(api = VERSION_CODES.R) + Float getScalerMaxZoomRatio(); + /** * Returns the area of the image sensor which corresponds to active pixels after any geometric * distortion correction has been applied. @@ -315,6 +339,18 @@ public Float getScalerAvailableMaxDigitalZoom() { return cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); } + @RequiresApi(api = VERSION_CODES.R) + @Override + public Float getScalerMaxZoomRatio() { + return cameraCharacteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE).getUpper(); + } + + @RequiresApi(api = VERSION_CODES.R) + @Override + public Float getScalerMinZoomRatio() { + return cameraCharacteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE).getLower(); + } + @Override public Rect getSensorInfoActiveArraySize() { return cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java deleted file mode 100644 index 42ad6d76dcfc..000000000000 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java +++ /dev/null @@ -1,52 +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. - -package io.flutter.plugins.camera; - -import android.graphics.Rect; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.math.MathUtils; - -public final class CameraZoom { - public static final float DEFAULT_ZOOM_FACTOR = 1.0f; - - @NonNull private final Rect cropRegion = new Rect(); - @Nullable private final Rect sensorSize; - - public final float maxZoom; - public final boolean hasSupport; - - public CameraZoom(@Nullable final Rect sensorArraySize, final Float maxZoom) { - this.sensorSize = sensorArraySize; - - if (this.sensorSize == null) { - this.maxZoom = DEFAULT_ZOOM_FACTOR; - this.hasSupport = false; - return; - } - - this.maxZoom = - ((maxZoom == null) || (maxZoom < DEFAULT_ZOOM_FACTOR)) ? DEFAULT_ZOOM_FACTOR : maxZoom; - - this.hasSupport = (Float.compare(this.maxZoom, DEFAULT_ZOOM_FACTOR) > 0); - } - - public Rect computeZoom(final float zoom) { - if (sensorSize == null || !this.hasSupport) { - return null; - } - - final float newZoom = MathUtils.clamp(zoom, DEFAULT_ZOOM_FACTOR, this.maxZoom); - - final int centerX = this.sensorSize.width() / 2; - final int centerY = this.sensorSize.height() / 2; - final int deltaX = (int) ((0.5f * this.sensorSize.width()) / newZoom); - final int deltaY = (int) ((0.5f * this.sensorSize.height()) / newZoom); - - this.cropRegion.set(centerX - deltaX, centerY - deltaY, centerX + deltaX, centerY + deltaY); - - return cropRegion; - } -} diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index 38201e1136c9..432344ade8cd 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -26,6 +26,7 @@ import io.flutter.view.TextureRegistry; import java.util.HashMap; import java.util.Map; +import java.util.Objects; final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { private final Activity activity; @@ -118,7 +119,9 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) } case "startVideoRecording": { - camera.startVideoRecording(result); + camera.startVideoRecording( + result, + Objects.equals(call.argument("enableStream"), true) ? imageStreamChannel : null); break; } case "stopVideoRecording": diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java index afbd7c3758a6..0ec2fbef87de 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java @@ -114,19 +114,23 @@ static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) if (preset.ordinal() > ResolutionPreset.high.ordinal()) { preset = ResolutionPreset.high; } - if (Build.VERSION.SDK_INT >= 31) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { EncoderProfiles profile = getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset); List videoProfiles = profile.getVideoProfiles(); EncoderProfiles.VideoProfile defaultVideoProfile = videoProfiles.get(0); - return new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight()); - } else { - @SuppressWarnings("deprecation") - CamcorderProfile profile = - getBestAvailableCamcorderProfileForResolutionPresetLegacy(cameraId, preset); - return new Size(profile.videoFrameWidth, profile.videoFrameHeight); + if (defaultVideoProfile != null) { + return new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight()); + } } + + @SuppressWarnings("deprecation") + // TODO(camsim99): Suppression is currently safe because legacy code is used as a fallback for SDK >= S. + // This should be removed when reverting that fallback behavior: https://github.com/flutter/flutter/issues/119668. + CamcorderProfile profile = + getBestAvailableCamcorderProfileForResolutionPresetLegacy(cameraId, preset); + return new Size(profile.videoFrameWidth, profile.videoFrameHeight); } /** @@ -234,15 +238,24 @@ private void configureResolution(ResolutionPreset resolutionPreset, int cameraId if (!checkIsSupported()) { return; } + boolean captureSizeCalculated = false; - if (Build.VERSION.SDK_INT >= 31) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + recordingProfileLegacy = null; recordingProfile = getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset); List videoProfiles = recordingProfile.getVideoProfiles(); EncoderProfiles.VideoProfile defaultVideoProfile = videoProfiles.get(0); - captureSize = new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight()); - } else { + + if (defaultVideoProfile != null) { + captureSizeCalculated = true; + captureSize = new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight()); + } + } + + if (!captureSizeCalculated) { + recordingProfile = null; @SuppressWarnings("deprecation") CamcorderProfile camcorderProfile = getBestAvailableCamcorderProfileForResolutionPresetLegacy(cameraId, resolutionPreset); diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java index 736fad4d92dc..2ac70822eb77 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java @@ -6,16 +6,18 @@ import android.graphics.Rect; import android.hardware.camera2.CaptureRequest; +import android.os.Build; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.features.CameraFeature; /** Controls the zoom configuration on the {@link android.hardware.camera2} API. */ public class ZoomLevelFeature extends CameraFeature { - private static final float MINIMUM_ZOOM_LEVEL = 1.0f; + private static final Float DEFAULT_ZOOM_LEVEL = 1.0f; private final boolean hasSupport; private final Rect sensorArraySize; - private Float currentSetting = MINIMUM_ZOOM_LEVEL; - private Float maximumZoomLevel = MINIMUM_ZOOM_LEVEL; + private Float currentSetting = DEFAULT_ZOOM_LEVEL; + private Float minimumZoomLevel = currentSetting; + private Float maximumZoomLevel; /** * Creates a new instance of the {@link ZoomLevelFeature}. @@ -28,18 +30,24 @@ public ZoomLevelFeature(CameraProperties cameraProperties) { sensorArraySize = cameraProperties.getSensorInfoActiveArraySize(); if (sensorArraySize == null) { - maximumZoomLevel = MINIMUM_ZOOM_LEVEL; + maximumZoomLevel = minimumZoomLevel; hasSupport = false; return; } + // On Android 11+ CONTROL_ZOOM_RATIO_RANGE should be use to get the zoom ratio directly as minimum zoom does not have to be 1.0f. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + minimumZoomLevel = cameraProperties.getScalerMinZoomRatio(); + maximumZoomLevel = cameraProperties.getScalerMaxZoomRatio(); + } else { + minimumZoomLevel = DEFAULT_ZOOM_LEVEL; + Float maxDigitalZoom = cameraProperties.getScalerAvailableMaxDigitalZoom(); + maximumZoomLevel = + ((maxDigitalZoom == null) || (maxDigitalZoom < minimumZoomLevel)) + ? minimumZoomLevel + : maxDigitalZoom; + } - Float maxDigitalZoom = cameraProperties.getScalerAvailableMaxDigitalZoom(); - maximumZoomLevel = - ((maxDigitalZoom == null) || (maxDigitalZoom < MINIMUM_ZOOM_LEVEL)) - ? MINIMUM_ZOOM_LEVEL - : maxDigitalZoom; - - hasSupport = (Float.compare(maximumZoomLevel, MINIMUM_ZOOM_LEVEL) > 0); + hasSupport = (Float.compare(maximumZoomLevel, minimumZoomLevel) > 0); } @Override @@ -67,11 +75,19 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { if (!checkIsSupported()) { return; } - - final Rect computedZoom = - ZoomUtils.computeZoom( - currentSetting, sensorArraySize, MINIMUM_ZOOM_LEVEL, maximumZoomLevel); - requestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); + // On Android 11+ CONTROL_ZOOM_RATIO can be set to a zoom ratio and the camera feed will compute + // how to zoom on its own accounting for multiple logical cameras. + // Prior the image cropping window must be calculated and set manually. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + requestBuilder.set( + CaptureRequest.CONTROL_ZOOM_RATIO, + ZoomUtils.computeZoomRatio(currentSetting, minimumZoomLevel, maximumZoomLevel)); + } else { + final Rect computedZoom = + ZoomUtils.computeZoomRect( + currentSetting, sensorArraySize, minimumZoomLevel, maximumZoomLevel); + requestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); + } } /** @@ -80,7 +96,7 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) { * @return The minimum zoom level. */ public float getMinimumZoomLevel() { - return MINIMUM_ZOOM_LEVEL; + return minimumZoomLevel; } /** diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtils.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtils.java index a4890b952cff..af9e48ff135a 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtils.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtils.java @@ -18,7 +18,8 @@ final class ZoomUtils { * Computes an image sensor area based on the supplied zoom settings. * *

The returned image sensor area can be applied to the {@link android.hardware.camera2} API in - * order to control zoom levels. + * order to control zoom levels. This method of zoom should only be used for Android versions <= + * 11 as past that, the newer {@link #computeZoomRatio()} functional can be used. * * @param zoom The desired zoom level. * @param sensorArraySize The current area of the image sensor. @@ -26,9 +27,9 @@ final class ZoomUtils { * @param maximumZoomLevel The maximim supported zoom level. * @return An image sensor area based on the supplied zoom settings */ - static Rect computeZoom( + static Rect computeZoomRect( float zoom, @NonNull Rect sensorArraySize, float minimumZoomLevel, float maximumZoomLevel) { - final float newZoom = MathUtils.clamp(zoom, minimumZoomLevel, maximumZoomLevel); + final float newZoom = computeZoomRatio(zoom, minimumZoomLevel, maximumZoomLevel); final int centerX = sensorArraySize.width() / 2; final int centerY = sensorArraySize.height() / 2; @@ -37,4 +38,8 @@ static Rect computeZoom( return new Rect(centerX - deltaX, centerY - deltaY, centerX + deltaX, centerY + deltaY); } + + static Float computeZoomRatio(float zoom, float minimumZoomLevel, float maximumZoomLevel) { + return MathUtils.clamp(zoom, minimumZoomLevel, maximumZoomLevel); + } } 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 0aebfee39e0a..1f9f6200bb99 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 @@ -75,7 +75,7 @@ public MediaRecorder build() throws IOException, NullPointerException, IndexOutO if (enableAudio) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); - if (Build.VERSION.SDK_INT >= 31) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && encoderProfiles != null) { EncoderProfiles.VideoProfile videoProfile = encoderProfiles.getVideoProfiles().get(0); EncoderProfiles.AudioProfile audioProfile = encoderProfiles.getAudioProfiles().get(0); diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraPermissionsTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraPermissionsTest.java index d734a63b15ca..575ec8c1caad 100644 --- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraPermissionsTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraPermissionsTest.java @@ -72,4 +72,18 @@ public void callback_doesNotRespond() { verify(fakeResultCallback, never()) .onResult("AudioAccessDenied", "Audio access permission was denied."); } + + @Test + public void callback_respondsWithCameraAccessDeniedWhenEmptyResult() { + // Handles the case where the grantResults array is empty + + ResultCallback fakeResultCallback = mock(ResultCallback.class); + CameraRequestPermissionsListener permissionsListener = + new CameraRequestPermissionsListener(fakeResultCallback); + + permissionsListener.onRequestPermissionsResult(9796, null, new int[] {}); + + verify(fakeResultCallback) + .onResult("CameraAccessDenied", "Camera access permission was denied."); + } } diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java index 40db12ee0fc3..c61be04465ab 100644 --- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java @@ -201,6 +201,30 @@ public void getScalerAvailableMaxDigitalZoomTest() { assertEquals(actualDigitalZoom, expectedDigitalZoom); } + @Test + public void getScalerGetScalerMinZoomRatioTest() { + Range zoomRange = mock(Range.class); + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE)) + .thenReturn(zoomRange); + + Float minZoom = cameraProperties.getScalerMinZoomRatio(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE); + assertEquals(zoomRange.getLower(), minZoom); + } + + @Test + public void getScalerGetScalerMaxZoomRatioTest() { + Range zoomRange = mock(Range.class); + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE)) + .thenReturn(zoomRange); + + Float maxZoom = cameraProperties.getScalerMaxZoomRatio(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE); + assertEquals(zoomRange.getUpper(), maxZoom); + } + @Test public void getSensorInfoActiveArraySizeTest() { Rect expectedArraySize = mock(Rect.class); diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java deleted file mode 100644 index d3e495551608..000000000000 --- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java +++ /dev/null @@ -1,125 +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. - -package io.flutter.plugins.camera; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -import android.graphics.Rect; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class CameraZoomTest { - - @Test - public void ctor_whenParametersAreValid() { - final Rect sensorSize = new Rect(0, 0, 0, 0); - final Float maxZoom = 4.0f; - final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); - - assertNotNull(cameraZoom); - assertTrue(cameraZoom.hasSupport); - assertEquals(4.0f, cameraZoom.maxZoom, 0); - assertEquals(1.0f, CameraZoom.DEFAULT_ZOOM_FACTOR, 0); - } - - @Test - public void ctor_whenSensorSizeIsNull() { - final Rect sensorSize = null; - final Float maxZoom = 4.0f; - final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); - - assertNotNull(cameraZoom); - assertFalse(cameraZoom.hasSupport); - assertEquals(cameraZoom.maxZoom, 1.0f, 0); - } - - @Test - public void ctor_whenMaxZoomIsNull() { - final Rect sensorSize = new Rect(0, 0, 0, 0); - final Float maxZoom = null; - final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); - - assertNotNull(cameraZoom); - assertFalse(cameraZoom.hasSupport); - assertEquals(cameraZoom.maxZoom, 1.0f, 0); - } - - @Test - public void ctor_whenMaxZoomIsSmallerThenDefaultZoomFactor() { - final Rect sensorSize = new Rect(0, 0, 0, 0); - final Float maxZoom = 0.5f; - final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); - - assertNotNull(cameraZoom); - assertFalse(cameraZoom.hasSupport); - assertEquals(cameraZoom.maxZoom, 1.0f, 0); - } - - @Test - public void setZoom_whenNoSupportShouldNotSetScalerCropRegion() { - final CameraZoom cameraZoom = new CameraZoom(null, null); - final Rect computedZoom = cameraZoom.computeZoom(2f); - - assertNull(computedZoom); - } - - @Test - public void setZoom_whenSensorSizeEqualsZeroShouldReturnCropRegionOfZero() { - final Rect sensorSize = new Rect(0, 0, 0, 0); - final CameraZoom cameraZoom = new CameraZoom(sensorSize, 20f); - final Rect computedZoom = cameraZoom.computeZoom(18f); - - assertNotNull(computedZoom); - assertEquals(computedZoom.left, 0); - assertEquals(computedZoom.top, 0); - assertEquals(computedZoom.right, 0); - assertEquals(computedZoom.bottom, 0); - } - - @Test - public void setZoom_whenSensorSizeIsValidShouldReturnCropRegion() { - final Rect sensorSize = new Rect(0, 0, 100, 100); - final CameraZoom cameraZoom = new CameraZoom(sensorSize, 20f); - final Rect computedZoom = cameraZoom.computeZoom(18f); - - assertNotNull(computedZoom); - assertEquals(computedZoom.left, 48); - assertEquals(computedZoom.top, 48); - assertEquals(computedZoom.right, 52); - assertEquals(computedZoom.bottom, 52); - } - - @Test - public void setZoom_whenZoomIsGreaterThenMaxZoomClampToMaxZoom() { - final Rect sensorSize = new Rect(0, 0, 100, 100); - final CameraZoom cameraZoom = new CameraZoom(sensorSize, 10f); - final Rect computedZoom = cameraZoom.computeZoom(25f); - - assertNotNull(computedZoom); - assertEquals(computedZoom.left, 45); - assertEquals(computedZoom.top, 45); - assertEquals(computedZoom.right, 55); - assertEquals(computedZoom.bottom, 55); - } - - @Test - public void setZoom_whenZoomIsSmallerThenMinZoomClampToMinZoom() { - final Rect sensorSize = new Rect(0, 0, 100, 100); - final CameraZoom cameraZoom = new CameraZoom(sensorSize, 10f); - final Rect computedZoom = cameraZoom.computeZoom(0.5f); - - assertNotNull(computedZoom); - assertEquals(computedZoom.left, 0); - assertEquals(computedZoom.top, 0); - assertEquals(computedZoom.right, 100); - assertEquals(computedZoom.bottom, 100); - } -} 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 957b57a66435..dbc352d697a4 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 @@ -5,20 +5,27 @@ package io.flutter.plugins.camera.features.resolution; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; import android.media.CamcorderProfile; import android.media.EncoderProfiles; +import android.util.Size; import io.flutter.plugins.camera.CameraProperties; +import java.util.ArrayList; import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockedStatic; +import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @@ -329,4 +336,95 @@ public void computeBestPreviewSize_shouldUseQVGAWhenResolutionPresetLow() { mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_QVGA)); } + + @Config(minSdk = 31) + @Test + public void computeBestPreviewSize_shouldUseLegacyBehaviorWhenEncoderProfilesNull() { + try (MockedStatic mockedResolutionFeature = + mockStatic(ResolutionFeature.class)) { + mockedResolutionFeature + .when( + () -> + ResolutionFeature.getBestAvailableCamcorderProfileForResolutionPreset( + anyInt(), any(ResolutionPreset.class))) + .thenAnswer( + (Answer) + invocation -> { + EncoderProfiles mockEncoderProfiles = mock(EncoderProfiles.class); + List videoProfiles = + new ArrayList() { + { + add(null); + } + }; + when(mockEncoderProfiles.getVideoProfiles()).thenReturn(videoProfiles); + return mockEncoderProfiles; + }); + mockedResolutionFeature + .when( + () -> + ResolutionFeature.getBestAvailableCamcorderProfileForResolutionPresetLegacy( + anyInt(), any(ResolutionPreset.class))) + .thenAnswer( + (Answer) + invocation -> { + CamcorderProfile mockCamcorderProfile = mock(CamcorderProfile.class); + mockCamcorderProfile.videoFrameWidth = 10; + mockCamcorderProfile.videoFrameHeight = 50; + return mockCamcorderProfile; + }); + mockedResolutionFeature + .when(() -> ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.max)) + .thenCallRealMethod(); + + Size testPreviewSize = ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.max); + assertEquals(testPreviewSize.getWidth(), 10); + assertEquals(testPreviewSize.getHeight(), 50); + } + } + + @Config(minSdk = 31) + @Test + public void resolutionFeatureShouldUseLegacyBehaviorWhenEncoderProfilesNull() { + beforeLegacy(); + try (MockedStatic mockedResolutionFeature = + mockStatic(ResolutionFeature.class)) { + mockedResolutionFeature + .when( + () -> + ResolutionFeature.getBestAvailableCamcorderProfileForResolutionPreset( + anyInt(), any(ResolutionPreset.class))) + .thenAnswer( + (Answer) + invocation -> { + EncoderProfiles mockEncoderProfiles = mock(EncoderProfiles.class); + List videoProfiles = + new ArrayList() { + { + add(null); + } + }; + when(mockEncoderProfiles.getVideoProfiles()).thenReturn(videoProfiles); + return mockEncoderProfiles; + }); + mockedResolutionFeature + .when( + () -> + ResolutionFeature.getBestAvailableCamcorderProfileForResolutionPresetLegacy( + anyInt(), any(ResolutionPreset.class))) + .thenAnswer( + (Answer) + invocation -> { + CamcorderProfile mockCamcorderProfile = mock(CamcorderProfile.class); + return mockCamcorderProfile; + }); + + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ResolutionFeature resolutionFeature = + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); + + assertNotNull(resolutionFeature.getRecordingProfileLegacy()); + assertNull(resolutionFeature.getRecordingProfile()); + } + } } diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java index 9f05cc255a8b..4d5826967009 100644 --- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java @@ -18,7 +18,10 @@ import android.graphics.Rect; import android.hardware.camera2.CaptureRequest; +import android.os.Build; import io.flutter.plugins.camera.CameraProperties; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -40,7 +43,7 @@ public void before() { mockSensorArray = mock(Rect.class); mockedStaticCameraZoom - .when(() -> ZoomUtils.computeZoom(anyFloat(), any(), anyFloat(), anyFloat())) + .when(() -> ZoomUtils.computeZoomRect(anyFloat(), any(), anyFloat(), anyFloat())) .thenReturn(mockZoomArea); } @@ -147,6 +150,22 @@ public void updateBuilder_shouldSetScalarCropRegionWhenCheckIsSupportIsTrue() { verify(mockBuilder, times(1)).set(CaptureRequest.SCALER_CROP_REGION, mockZoomArea); } + @Test + public void updateBuilder_shouldControlZoomRatioWhenCheckIsSupportIsTrue() throws Exception { + setSdkVersion(Build.VERSION_CODES.R); + when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(mockSensorArray); + when(mockCameraProperties.getScalerMaxZoomRatio()).thenReturn(42f); + when(mockCameraProperties.getScalerMinZoomRatio()).thenReturn(1.0f); + + ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + + zoomLevelFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_ZOOM_RATIO, 0.0f); + } + @Test public void getMinimumZoomLevel() { ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); @@ -163,4 +182,38 @@ public void getMaximumZoomLevel() { assertEquals(42f, zoomLevelFeature.getMaximumZoomLevel(), 0); } + + @Test + public void checkZoomLevelFeature_callsMaxDigitalZoomOnAndroidQ() throws Exception { + setSdkVersion(Build.VERSION_CODES.Q); + + when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(mockSensorArray); + + new ZoomLevelFeature(mockCameraProperties); + + verify(mockCameraProperties, times(0)).getScalerMaxZoomRatio(); + verify(mockCameraProperties, times(0)).getScalerMinZoomRatio(); + verify(mockCameraProperties, times(1)).getScalerAvailableMaxDigitalZoom(); + } + + @Test + public void checkZoomLevelFeature_callsScalarMaxZoomRatioOnAndroidR() throws Exception { + setSdkVersion(Build.VERSION_CODES.R); + when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(mockSensorArray); + + new ZoomLevelFeature(mockCameraProperties); + + verify(mockCameraProperties, times(1)).getScalerMaxZoomRatio(); + verify(mockCameraProperties, times(1)).getScalerMinZoomRatio(); + verify(mockCameraProperties, times(0)).getScalerAvailableMaxDigitalZoom(); + } + + static void setSdkVersion(int sdkVersion) throws Exception { + Field sdkInt = Build.VERSION.class.getField("SDK_INT"); + sdkInt.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(sdkInt, sdkInt.getModifiers() & ~Modifier.FINAL); + sdkInt.set(null, sdkVersion); + } } diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtilsTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtilsTest.java index 28160ff30714..2f6160816d15 100644 --- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtilsTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtilsTest.java @@ -15,9 +15,9 @@ @RunWith(RobolectricTestRunner.class) public class ZoomUtilsTest { @Test - public void setZoom_whenSensorSizeEqualsZeroShouldReturnCropRegionOfZero() { + public void setZoomRect_whenSensorSizeEqualsZeroShouldReturnCropRegionOfZero() { final Rect sensorSize = new Rect(0, 0, 0, 0); - final Rect computedZoom = ZoomUtils.computeZoom(18f, sensorSize, 1f, 20f); + final Rect computedZoom = ZoomUtils.computeZoomRect(18f, sensorSize, 1f, 20f); assertNotNull(computedZoom); assertEquals(computedZoom.left, 0); @@ -27,9 +27,9 @@ public void setZoom_whenSensorSizeEqualsZeroShouldReturnCropRegionOfZero() { } @Test - public void setZoom_whenSensorSizeIsValidShouldReturnCropRegion() { + public void setZoomRect_whenSensorSizeIsValidShouldReturnCropRegion() { final Rect sensorSize = new Rect(0, 0, 100, 100); - final Rect computedZoom = ZoomUtils.computeZoom(18f, sensorSize, 1f, 20f); + final Rect computedZoom = ZoomUtils.computeZoomRect(18f, sensorSize, 1f, 20f); assertNotNull(computedZoom); assertEquals(computedZoom.left, 48); @@ -39,9 +39,9 @@ public void setZoom_whenSensorSizeIsValidShouldReturnCropRegion() { } @Test - public void setZoom_whenZoomIsGreaterThenMaxZoomClampToMaxZoom() { + public void setZoomRect_whenZoomIsGreaterThenMaxZoomClampToMaxZoom() { final Rect sensorSize = new Rect(0, 0, 100, 100); - final Rect computedZoom = ZoomUtils.computeZoom(25f, sensorSize, 1f, 10f); + final Rect computedZoom = ZoomUtils.computeZoomRect(25f, sensorSize, 1f, 10f); assertNotNull(computedZoom); assertEquals(computedZoom.left, 45); @@ -51,9 +51,9 @@ public void setZoom_whenZoomIsGreaterThenMaxZoomClampToMaxZoom() { } @Test - public void setZoom_whenZoomIsSmallerThenMinZoomClampToMinZoom() { + public void setZoomRect_whenZoomIsSmallerThenMinZoomClampToMinZoom() { final Rect sensorSize = new Rect(0, 0, 100, 100); - final Rect computedZoom = ZoomUtils.computeZoom(0.5f, sensorSize, 1f, 10f); + final Rect computedZoom = ZoomUtils.computeZoomRect(0.5f, sensorSize, 1f, 10f); assertNotNull(computedZoom); assertEquals(computedZoom.left, 0); @@ -61,4 +61,25 @@ public void setZoom_whenZoomIsSmallerThenMinZoomClampToMinZoom() { assertEquals(computedZoom.right, 100); assertEquals(computedZoom.bottom, 100); } + + @Test + public void setZoomRatio_whenNewZoomGreaterThanMaxZoomClampToMaxZoom() { + final Float computedZoom = ZoomUtils.computeZoomRatio(21f, 1f, 20f); + assertNotNull(computedZoom); + assertEquals(computedZoom, 20f, 0.0f); + } + + @Test + public void setZoomRatio_whenNewZoomLesserThanMinZoomClampToMinZoom() { + final Float computedZoom = ZoomUtils.computeZoomRatio(0.7f, 1f, 20f); + assertNotNull(computedZoom); + assertEquals(computedZoom, 1f, 0.0f); + } + + @Test + public void setZoomRatio_whenNewZoomValidReturnNewZoom() { + final Float computedZoom = ZoomUtils.computeZoomRatio(2.0f, 1f, 20f); + assertNotNull(computedZoom); + assertEquals(computedZoom, 2.0f, 0.0f); + } } diff --git a/packages/camera/camera_android/example/android/gradle.properties b/packages/camera/camera_android/example/android/gradle.properties index b253d8e5f746..d0448f163e41 100644 --- a/packages/camera/camera_android/example/android/gradle.properties +++ b/packages/camera/camera_android/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=false android.enableR8=true diff --git a/packages/camera/camera_android/example/integration_test/camera_test.dart b/packages/camera/camera_android/example/integration_test/camera_test.dart index 3b1aae6aec51..e499872da5f3 100644 --- a/packages/camera/camera_android/example/integration_test/camera_test.dart +++ b/packages/camera/camera_android/example/integration_test/camera_test.dart @@ -55,8 +55,6 @@ void main() { Future testCaptureImageResolution( CameraController controller, ResolutionPreset preset) async { final Size expectedSize = presetExpectedSizes[preset]!; - print( - 'Capturing photo at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}'); // Take Picture final XFile file = await controller.takePicture(); @@ -105,8 +103,6 @@ void main() { Future testCaptureVideoResolution( CameraController controller, ResolutionPreset preset) async { final Size expectedSize = presetExpectedSizes[preset]!; - print( - 'Capturing video at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}'); // Take Video await controller.startVideoRecording(); @@ -245,4 +241,47 @@ void main() { await controller.dispose(); }, ); + + testWidgets( + 'recording with image stream', + (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.isEmpty) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + bool isDetecting = false; + + await controller.startVideoRecording( + streamCallback: (CameraImageData image) { + if (isDetecting) { + return; + } + + isDetecting = true; + + expectLater(image, isNotNull); + }); + + expect(controller.value.isStreamingImages, true); + + // Stopping recording before anything is recorded will throw, per + // https://developer.android.com/reference/android/media/MediaRecorder.html#stop() + // so delay long enough to ensure that some data is recorded. + await Future.delayed(const Duration(seconds: 2)); + + await controller.stopVideoRecording(); + await controller.dispose(); + + expect(controller.value.isStreamingImages, false); + }, + ); } diff --git a/packages/camera/camera_android/example/lib/camera_controller.dart b/packages/camera/camera_android/example/lib/camera_controller.dart index 09441cc5449c..8139dcdb0220 100644 --- a/packages/camera/camera_android/example/lib/camera_controller.dart +++ b/packages/camera/camera_android/example/lib/camera_controller.dart @@ -3,12 +3,12 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:collection'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:quiver/core.dart'; /// The state of a [CameraController]. class CameraValue { @@ -306,11 +306,14 @@ class CameraController extends ValueNotifier { /// /// The video is returned as a [XFile] after calling [stopVideoRecording]. /// Throws a [CameraException] if the capture fails. - Future startVideoRecording() async { - await CameraPlatform.instance.startVideoRecording(_cameraId); + Future startVideoRecording( + {Function(CameraImageData image)? streamCallback}) async { + await CameraPlatform.instance.startVideoCapturing( + VideoCaptureOptions(_cameraId, streamCallback: streamCallback)); value = value.copyWith( isRecordingVideo: true, isRecordingPaused: false, + isStreamingImages: streamCallback != null, recordingOrientation: Optional.of( value.lockedCaptureOrientation ?? value.deviceOrientation)); } @@ -319,10 +322,15 @@ class CameraController extends ValueNotifier { /// /// Throws a [CameraException] if the capture failed. Future stopVideoRecording() async { + if (value.isStreamingImages) { + await stopImageStream(); + } + final XFile file = await CameraPlatform.instance.stopVideoRecording(_cameraId); value = value.copyWith( isRecordingVideo: false, + isRecordingPaused: false, recordingOrientation: const Optional.absent(), ); return file; @@ -435,3 +443,112 @@ class CameraController extends ValueNotifier { } } } + +/// A value that might be absent. +/// +/// Used to represent [DeviceOrientation]s that are optional but also able +/// to be cleared. +@immutable +class Optional extends IterableBase { + /// Constructs an empty Optional. + const Optional.absent() : _value = null; + + /// Constructs an Optional of the given [value]. + /// + /// Throws [ArgumentError] if [value] is null. + Optional.of(T value) : _value = value { + // TODO(cbracken): Delete and make this ctor const once mixed-mode + // execution is no longer around. + ArgumentError.checkNotNull(value); + } + + /// Constructs an Optional of the given [value]. + /// + /// If [value] is null, returns [absent()]. + const Optional.fromNullable(T? value) : _value = value; + + final T? _value; + + /// True when this optional contains a value. + bool get isPresent => _value != null; + + /// True when this optional contains no value. + bool get isNotPresent => _value == null; + + /// Gets the Optional value. + /// + /// Throws [StateError] if [value] is null. + T get value { + if (_value == null) { + throw StateError('value called on absent Optional.'); + } + return _value!; + } + + /// Executes a function if the Optional value is present. + void ifPresent(void Function(T value) ifPresent) { + if (isPresent) { + ifPresent(_value as T); + } + } + + /// Execution a function if the Optional value is absent. + void ifAbsent(void Function() ifAbsent) { + if (!isPresent) { + ifAbsent(); + } + } + + /// Gets the Optional value with a default. + /// + /// The default is returned if the Optional is [absent()]. + /// + /// Throws [ArgumentError] if [defaultValue] is null. + T or(T defaultValue) { + return _value ?? defaultValue; + } + + /// Gets the Optional value, or `null` if there is none. + T? get orNull => _value; + + /// Transforms the Optional value. + /// + /// If the Optional is [absent()], returns [absent()] without applying the transformer. + /// + /// The transformer must not return `null`. If it does, an [ArgumentError] is thrown. + Optional transform(S Function(T value) transformer) { + return _value == null + ? Optional.absent() + : Optional.of(transformer(_value as T)); + } + + /// Transforms the Optional value. + /// + /// If the Optional is [absent()], returns [absent()] without applying the transformer. + /// + /// Returns [absent()] if the transformer returns `null`. + Optional transformNullable(S? Function(T value) transformer) { + return _value == null + ? Optional.absent() + : Optional.fromNullable(transformer(_value as T)); + } + + @override + Iterator get iterator => + isPresent ? [_value as T].iterator : Iterable.empty().iterator; + + /// Delegates to the underlying [value] hashCode. + @override + int get hashCode => _value.hashCode; + + /// Delegates to the underlying [value] operator==. + @override + bool operator ==(Object o) => o is Optional && o._value == _value; + + @override + String toString() { + return _value == null + ? 'Optional { absent }' + : 'Optional { value: $_value }'; + } +} diff --git a/packages/camera/camera_android/example/lib/main.dart b/packages/camera/camera_android/example/lib/main.dart index 9ebc27e4be5b..4d98aed9a4c2 100644 --- a/packages/camera/camera_android/example/lib/main.dart +++ b/packages/camera/camera_android/example/lib/main.dart @@ -35,17 +35,16 @@ IconData getCameraLensIcon(CameraLensDirection direction) { return Icons.camera_front; case CameraLensDirection.external: return Icons.camera; - default: - throw ArgumentError('Unknown lens direction'); } + // This enum is from a different package, so a new value could be added at + // any time. The example should keep working if that happens. + // ignore: dead_code + return Icons.camera; } void _logError(String code, String? message) { - if (message != null) { - print('Error: $code\nError Message: $message'); - } else { - print('Error: $code'); - } + // ignore: avoid_print + print('Error: $code${message == null ? '' : '\nError Message: $message'}'); } class _CameraExampleHomeState extends State @@ -1092,5 +1091,4 @@ Future main() async { /// /// We use this so that APIs that have become non-nullable can still be used /// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. T? _ambiguate(T? value) => value; diff --git a/packages/camera/camera_android/example/pubspec.yaml b/packages/camera/camera_android/example/pubspec.yaml index 2e530e02ca71..e23e31a886de 100644 --- a/packages/camera/camera_android/example/pubspec.yaml +++ b/packages/camera/camera_android/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: camera_android: @@ -14,7 +14,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: ../ - camera_platform_interface: ^2.2.0 + camera_platform_interface: ^2.3.1 flutter: sdk: flutter path_provider: ^2.0.0 diff --git a/packages/camera/camera_android/example/test_driver/integration_test.dart b/packages/camera/camera_android/example/test_driver/integration_test.dart index 4ec97e66d36c..aa57599f3165 100644 --- a/packages/camera/camera_android/example/test_driver/integration_test.dart +++ b/packages/camera/camera_android/example/test_driver/integration_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: avoid_print + import 'dart:async'; import 'dart:convert'; import 'dart:io'; diff --git a/packages/camera/camera_android/lib/src/android_camera.dart b/packages/camera/camera_android/lib/src/android_camera.dart index 36077eac8eed..9ab9b578616a 100644 --- a/packages/camera/camera_android/lib/src/android_camera.dart +++ b/packages/camera/camera_android/lib/src/android_camera.dart @@ -145,6 +145,7 @@ class AndroidCamera extends CameraPlatform { // ignore: body_might_complete_normally_catch_error (Object error, StackTrace stackTrace) { if (error is! PlatformException) { + // ignore: only_throw_errors throw error; } completer.completeError( @@ -248,13 +249,25 @@ class AndroidCamera extends CameraPlatform { @override Future startVideoRecording(int cameraId, {Duration? maxVideoDuration}) async { + return startVideoCapturing( + VideoCaptureOptions(cameraId, maxDuration: maxVideoDuration)); + } + + @override + Future startVideoCapturing(VideoCaptureOptions options) async { await _channel.invokeMethod( 'startVideoRecording', { - 'cameraId': cameraId, - 'maxVideoDuration': maxVideoDuration?.inMilliseconds, + 'cameraId': options.cameraId, + 'maxVideoDuration': options.maxDuration?.inMilliseconds, + 'enableStream': options.streamCallback != null, }, ); + + if (options.streamCallback != null) { + _installStreamController().stream.listen(options.streamCallback); + _startStreamListener(); + } } @override @@ -290,13 +303,19 @@ class AndroidCamera extends CameraPlatform { @override Stream onStreamedFrameAvailable(int cameraId, {CameraImageStreamOptions? options}) { + _installStreamController(onListen: _onFrameStreamListen); + return _frameStreamController!.stream; + } + + StreamController _installStreamController( + {Function()? onListen}) { _frameStreamController = StreamController( - onListen: _onFrameStreamListen, + onListen: onListen ?? () {}, onPause: _onFrameStreamPauseResume, onResume: _onFrameStreamPauseResume, onCancel: _onFrameStreamCancel, ); - return _frameStreamController!.stream; + return _frameStreamController!; } void _onFrameStreamListen() { @@ -305,6 +324,10 @@ class AndroidCamera extends CameraPlatform { Future _startPlatformStream() async { await _channel.invokeMethod('startImageStream'); + _startStreamListener(); + } + + void _startStreamListener() { const EventChannel cameraEventChannel = EventChannel('plugins.flutter.io/camera_android/imageStream'); _platformImageStreamSubscription = @@ -498,9 +521,14 @@ class AndroidCamera extends CameraPlatform { return 'always'; case FlashMode.torch: return 'torch'; - default: - throw ArgumentError('Unknown FlashMode value'); } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return 'off'; } /// Returns the resolution preset as a String. @@ -518,18 +546,23 @@ class AndroidCamera extends CameraPlatform { return 'medium'; case ResolutionPreset.low: return 'low'; - default: - throw ArgumentError('Unknown ResolutionPreset value'); } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return 'max'; } /// Converts messages received from the native platform into device events. Future _handleDeviceMethodCall(MethodCall call) async { switch (call.method) { case 'orientation_changed': + final Map arguments = _getArgumentDictionary(call); _deviceEventStreamController.add(DeviceOrientationChangedEvent( - deserializeDeviceOrientation( - call.arguments['orientation']! as String))); + deserializeDeviceOrientation(arguments['orientation']! as String))); break; default: throw MissingPluginException(); @@ -544,21 +577,23 @@ class AndroidCamera extends CameraPlatform { Future handleCameraMethodCall(MethodCall call, int cameraId) async { switch (call.method) { case 'initialized': + final Map arguments = _getArgumentDictionary(call); cameraEventStreamController.add(CameraInitializedEvent( cameraId, - call.arguments['previewWidth']! as double, - call.arguments['previewHeight']! as double, - deserializeExposureMode(call.arguments['exposureMode']! as String), - call.arguments['exposurePointSupported']! as bool, - deserializeFocusMode(call.arguments['focusMode']! as String), - call.arguments['focusPointSupported']! as bool, + arguments['previewWidth']! as double, + arguments['previewHeight']! as double, + deserializeExposureMode(arguments['exposureMode']! as String), + arguments['exposurePointSupported']! as bool, + deserializeFocusMode(arguments['focusMode']! as String), + arguments['focusPointSupported']! as bool, )); break; case 'resolution_changed': + final Map arguments = _getArgumentDictionary(call); cameraEventStreamController.add(CameraResolutionChangedEvent( cameraId, - call.arguments['captureWidth']! as double, - call.arguments['captureHeight']! as double, + arguments['captureWidth']! as double, + arguments['captureHeight']! as double, )); break; case 'camera_closing': @@ -567,23 +602,32 @@ class AndroidCamera extends CameraPlatform { )); break; case 'video_recorded': + final Map arguments = _getArgumentDictionary(call); cameraEventStreamController.add(VideoRecordedEvent( cameraId, - XFile(call.arguments['path']! as String), - call.arguments['maxVideoDuration'] != null - ? Duration( - milliseconds: call.arguments['maxVideoDuration']! as int) + XFile(arguments['path']! as String), + arguments['maxVideoDuration'] != null + ? Duration(milliseconds: arguments['maxVideoDuration']! as int) : null, )); break; case 'error': + final Map arguments = _getArgumentDictionary(call); cameraEventStreamController.add(CameraErrorEvent( cameraId, - call.arguments['description']! as String, + arguments['description']! as String, )); break; default: throw MissingPluginException(); } } + + /// Returns the arguments of [call] as typed string-keyed Map. + /// + /// This does not do any type validation, so is only safe to call if the + /// arguments are known to be a map. + Map _getArgumentDictionary(MethodCall call) { + return (call.arguments as Map).cast(); + } } diff --git a/packages/camera/camera_android/lib/src/utils.dart b/packages/camera/camera_android/lib/src/utils.dart index 663ec6da7a97..8d58f7fe1297 100644 --- a/packages/camera/camera_android/lib/src/utils.dart +++ b/packages/camera/camera_android/lib/src/utils.dart @@ -29,9 +29,14 @@ String serializeDeviceOrientation(DeviceOrientation orientation) { return 'landscapeRight'; case DeviceOrientation.landscapeLeft: return 'landscapeLeft'; - default: - throw ArgumentError('Unknown DeviceOrientation value'); } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return 'portraitUp'; } /// Returns the device orientation for a given String. diff --git a/packages/camera/camera_android/pubspec.yaml b/packages/camera/camera_android/pubspec.yaml index 6f1b667670e8..fb3371912911 100644 --- a/packages/camera/camera_android/pubspec.yaml +++ b/packages/camera/camera_android/pubspec.yaml @@ -2,11 +2,11 @@ name: camera_android description: Android implementation of the camera plugin. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.10.0+4 +version: 0.10.4 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -18,7 +18,7 @@ flutter: dartPluginClass: AndroidCamera dependencies: - camera_platform_interface: ^2.2.0 + camera_platform_interface: ^2.3.1 flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.2 diff --git a/packages/camera/camera_android/test/android_camera_test.dart b/packages/camera/camera_android/test/android_camera_test.dart index 3e50e6918648..d80bd9cac7a3 100644 --- a/packages/camera/camera_android/test/android_camera_test.dart +++ b/packages/camera/camera_android/test/android_camera_test.dart @@ -32,14 +32,15 @@ void main() { // registerWith is called very early in initialization the bindings won't // have been initialized. While registerWith could intialize them, that // could slow down startup, so instead the handler should be set up lazily. - final ByteData? response = await TestDefaultBinaryMessengerBinding - .instance!.defaultBinaryMessenger - .handlePlatformMessage( - AndroidCamera.deviceEventChannelName, - const StandardMethodCodec().encodeMethodCall(const MethodCall( - 'orientation_changed', - {'orientation': 'portraitDown'})), - (ByteData? data) {}); + final ByteData? response = + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .handlePlatformMessage( + AndroidCamera.deviceEventChannelName, + const StandardMethodCodec().encodeMethodCall(const MethodCall( + 'orientation_changed', + {'orientation': 'portraitDown'})), + (ByteData? data) {}); expect(response, null); }); @@ -421,7 +422,8 @@ void main() { const DeviceOrientationChangedEvent event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); for (int i = 0; i < 3; i++) { - await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( AndroidCamera.deviceEventChannelName, const StandardMethodCodec().encodeMethodCall( @@ -504,11 +506,13 @@ void main() { ]); expect(cameras.length, returnData.length); for (int i = 0; i < returnData.length; i++) { + final Map typedData = + (returnData[i] as Map).cast(); final CameraDescription cameraDescription = CameraDescription( - name: returnData[i]['name']! as String, + name: typedData['name']! as String, lensDirection: - parseCameraLensDirection(returnData[i]['lensFacing']! as String), - sensorOrientation: returnData[i]['sensorOrientation']! as int, + parseCameraLensDirection(typedData['lensFacing']! as String), + sensorOrientation: typedData['sensorOrientation']! as int, ); expect(cameras[i], cameraDescription); } @@ -587,6 +591,7 @@ void main() { isMethodCall('startVideoRecording', arguments: { 'cameraId': cameraId, 'maxVideoDuration': null, + 'enableStream': false, }), ]); }); @@ -609,7 +614,33 @@ void main() { expect(channel.log, [ isMethodCall('startVideoRecording', arguments: { 'cameraId': cameraId, - 'maxVideoDuration': 10000 + 'maxVideoDuration': 10000, + 'enableStream': false, + }), + ]); + }); + + test( + 'Should pass enableStream if callback is passed when starting recording a video', + () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'startVideoRecording': null}, + ); + + // Act + await camera.startVideoCapturing( + VideoCaptureOptions(cameraId, + streamCallback: (CameraImageData imageData) {}), + ); + + // Assert + expect(channel.log, [ + isMethodCall('startVideoRecording', arguments: { + 'cameraId': cameraId, + 'maxVideoDuration': null, + 'enableStream': true, }), ]); }); @@ -1092,3 +1123,9 @@ void main() { }); }); } + +/// 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/camera/camera_android/test/method_channel_mock.dart b/packages/camera/camera_android/test/method_channel_mock.dart index 413c10633cc1..f26d12a3688a 100644 --- a/packages/camera/camera_android/test/method_channel_mock.dart +++ b/packages/camera/camera_android/test/method_channel_mock.dart @@ -11,7 +11,9 @@ class MethodChannelMock { this.delay, required this.methods, }) : methodChannel = MethodChannel(channelName) { - methodChannel.setMockMethodCallHandler(_handler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, _handler); } final Duration? delay; @@ -37,3 +39,9 @@ class MethodChannelMock { }); } } + +/// 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/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index ce2fb9046c69..9e6c5a901fc9 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -4,3 +4,11 @@ * Adds CameraInfo class and removes unnecessary code from plugin. * Adds CameraSelector class. * Adds ProcessCameraProvider class. +* Bump CameraX version to 1.3.0-alpha02. +* Adds Camera and UseCase classes, along with methods for binding UseCases to a lifecycle with the ProcessCameraProvider. +* Bump CameraX version to 1.3.0-alpha03 and Kotlin version to 1.8.0. +* Changes instance manager to allow the separate creation of identical objects. +* Adds Preview and Surface classes, along with other methods needed to implement camera preview. +* Adds implementation of availableCameras(). +* Implements camera preview, createCamera, initializeCamera, onCameraError, onDeviceOrientationChanged, and onCameraInitialized. +* Adds integration test to plugin. diff --git a/packages/camera/camera_android_camerax/android/build.gradle b/packages/camera/camera_android_camerax/android/build.gradle index bbc06317a23e..822c3f6e318e 100644 --- a/packages/camera/camera_android_camerax/android/build.gradle +++ b/packages/camera/camera_android_camerax/android/build.gradle @@ -56,13 +56,13 @@ android { dependencies { // CameraX core library using the camera2 implementation must use same version number. - def camerax_version = "1.2.0-rc01" + def camerax_version = "1.3.0-alpha03" implementation "androidx.camera:camera-core:${camerax_version}" implementation "androidx.camera:camera-camera2:${camerax_version}" implementation "androidx.camera:camera-lifecycle:${camerax_version}" implementation 'com.google.guava:guava:31.1-android' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-inline:4.7.0' + testImplementation 'org.mockito:mockito-inline:5.0.0' testImplementation 'androidx.test:core:1.4.0' testImplementation 'org.robolectric:robolectric:4.8' } diff --git a/packages/camera/camera_android_camerax/android/src/main/AndroidManifest.xml b/packages/camera/camera_android_camerax/android/src/main/AndroidManifest.xml index ea4275c757cf..52012aaa6915 100644 --- a/packages/camera/camera_android_camerax/android/src/main/AndroidManifest.xml +++ b/packages/camera/camera_android_camerax/android/src/main/AndroidManifest.xml @@ -1,3 +1,8 @@ + + + + diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java index b8fbaf539c32..b61e7ac72224 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java @@ -6,16 +6,19 @@ import android.content.Context; import androidx.annotation.NonNull; +import androidx.lifecycle.LifecycleOwner; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.view.TextureRegistry; /** Platform implementation of the camera_plugin implemented with the CameraX library. */ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, ActivityAware { private InstanceManager instanceManager; private FlutterPluginBinding pluginBinding; private ProcessCameraProviderHostApiImpl processCameraProviderHostApi; + public SystemServicesHostApiImpl systemServicesHostApi; /** * Initialize this within the {@code #configureFlutterEngine} of a Flutter activity or fragment. @@ -24,7 +27,7 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity */ public CameraAndroidCameraxPlugin() {} - void setUp(BinaryMessenger binaryMessenger, Context context) { + void setUp(BinaryMessenger binaryMessenger, Context context, TextureRegistry textureRegistry) { // Set up instance manager. instanceManager = InstanceManager.open( @@ -36,23 +39,23 @@ void setUp(BinaryMessenger binaryMessenger, Context context) { // Set up Host APIs. GeneratedCameraXLibrary.CameraInfoHostApi.setup( binaryMessenger, new CameraInfoHostApiImpl(instanceManager)); - GeneratedCameraXLibrary.JavaObjectHostApi.setup( - binaryMessenger, new JavaObjectHostApiImpl(instanceManager)); GeneratedCameraXLibrary.CameraSelectorHostApi.setup( binaryMessenger, new CameraSelectorHostApiImpl(binaryMessenger, instanceManager)); + GeneratedCameraXLibrary.JavaObjectHostApi.setup( + binaryMessenger, new JavaObjectHostApiImpl(instanceManager)); processCameraProviderHostApi = new ProcessCameraProviderHostApiImpl(binaryMessenger, instanceManager, context); GeneratedCameraXLibrary.ProcessCameraProviderHostApi.setup( binaryMessenger, processCameraProviderHostApi); + systemServicesHostApi = new SystemServicesHostApiImpl(binaryMessenger, instanceManager); + GeneratedCameraXLibrary.SystemServicesHostApi.setup(binaryMessenger, systemServicesHostApi); + GeneratedCameraXLibrary.PreviewHostApi.setup( + binaryMessenger, new PreviewHostApiImpl(binaryMessenger, instanceManager, textureRegistry)); } @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { pluginBinding = flutterPluginBinding; - (new CameraAndroidCameraxPlugin()) - .setUp( - flutterPluginBinding.getBinaryMessenger(), - flutterPluginBinding.getApplicationContext()); } @Override @@ -66,7 +69,16 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { @Override public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBinding) { - updateContext(activityPluginBinding.getActivity()); + setUp( + pluginBinding.getBinaryMessenger(), + pluginBinding.getApplicationContext(), + pluginBinding.getTextureRegistry()); + updateContext(pluginBinding.getApplicationContext()); + processCameraProviderHostApi.setLifecycleOwner( + (LifecycleOwner) activityPluginBinding.getActivity()); + systemServicesHostApi.setActivity(activityPluginBinding.getActivity()); + systemServicesHostApi.setPermissionsRegistry( + activityPluginBinding::addRequestPermissionsResultListener); } @Override @@ -89,7 +101,7 @@ public void onDetachedFromActivity() { * Updates context that is used to fetch the corresponding instance of a {@code * ProcessCameraProvider}. */ - private void updateContext(Context context) { + public void updateContext(Context context) { if (processCameraProviderHostApi != null) { processCameraProviderHostApi.setContext(context); } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraFlutterApiImpl.java new file mode 100644 index 000000000000..a03548399485 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraFlutterApiImpl.java @@ -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. + +package io.flutter.plugins.camerax; + +import androidx.camera.core.Camera; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraFlutterApi; + +public class CameraFlutterApiImpl extends CameraFlutterApi { + private final InstanceManager instanceManager; + + public CameraFlutterApiImpl(BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + void create(Camera camera, Reply reply) { + create(instanceManager.addHostCreatedInstance(camera), reply); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoHostApiImpl.java index 7daba0d38d6a..d960b7fff70a 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoHostApiImpl.java @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; import androidx.camera.core.CameraInfo; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraInfoHostApi; +import java.util.Objects; public class CameraInfoHostApiImpl implements CameraInfoHostApi { private final InstanceManager instanceManager; @@ -17,7 +18,8 @@ public CameraInfoHostApiImpl(InstanceManager instanceManager) { @Override public Long getSensorRotationDegrees(@NonNull Long identifier) { - CameraInfo cameraInfo = (CameraInfo) instanceManager.getInstance(identifier); + CameraInfo cameraInfo = + (CameraInfo) Objects.requireNonNull(instanceManager.getInstance(identifier)); return Long.valueOf(cameraInfo.getSensorRotationDegrees()); } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraPermissionsManager.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraPermissionsManager.java new file mode 100644 index 000000000000..19b1ee569a9b --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraPermissionsManager.java @@ -0,0 +1,120 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import android.Manifest; +import android.Manifest.permission; +import android.app.Activity; +import android.content.pm.PackageManager; +import androidx.annotation.VisibleForTesting; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +final class CameraPermissionsManager { + interface PermissionsRegistry { + @SuppressWarnings("deprecation") + void addListener( + io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener handler); + } + + interface ResultCallback { + void onResult(String errorCode, String errorDescription); + } + + /** + * Camera access permission errors handled when camera is created. See {@code MethodChannelCamera} + * in {@code camera/camera_platform_interface} for details. + */ + private static final String CAMERA_PERMISSIONS_REQUEST_ONGOING = + "CameraPermissionsRequestOngoing"; + + private static final String CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE = + "Another request is ongoing and multiple requests cannot be handled at once."; + private static final String CAMERA_ACCESS_DENIED = "CameraAccessDenied"; + private static final String CAMERA_ACCESS_DENIED_MESSAGE = "Camera access permission was denied."; + private static final String AUDIO_ACCESS_DENIED = "AudioAccessDenied"; + private static final String AUDIO_ACCESS_DENIED_MESSAGE = "Audio access permission was denied."; + + private static final int CAMERA_REQUEST_ID = 9796; + @VisibleForTesting boolean ongoing = false; + + void requestPermissions( + Activity activity, + PermissionsRegistry permissionsRegistry, + boolean enableAudio, + ResultCallback callback) { + if (ongoing) { + callback.onResult( + CAMERA_PERMISSIONS_REQUEST_ONGOING, CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE); + return; + } + if (!hasCameraPermission(activity) || (enableAudio && !hasAudioPermission(activity))) { + permissionsRegistry.addListener( + new CameraRequestPermissionsListener( + (String errorCode, String errorDescription) -> { + ongoing = false; + callback.onResult(errorCode, errorDescription); + })); + ongoing = true; + ActivityCompat.requestPermissions( + activity, + enableAudio + ? new String[] {Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO} + : new String[] {Manifest.permission.CAMERA}, + CAMERA_REQUEST_ID); + } else { + // Permissions already exist. Call the callback with success. + callback.onResult(null, null); + } + } + + private boolean hasCameraPermission(Activity activity) { + return ContextCompat.checkSelfPermission(activity, permission.CAMERA) + == PackageManager.PERMISSION_GRANTED; + } + + private boolean hasAudioPermission(Activity activity) { + return ContextCompat.checkSelfPermission(activity, permission.RECORD_AUDIO) + == PackageManager.PERMISSION_GRANTED; + } + + @VisibleForTesting + @SuppressWarnings("deprecation") + static final class CameraRequestPermissionsListener + implements io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener { + + // There's no way to unregister permission listeners in the v1 embedding, so we'll be called + // duplicate times in cases where the user denies and then grants a permission. Keep track of if + // we've responded before and bail out of handling the callback manually if this is a repeat + // call. + boolean alreadyCalled = false; + + final ResultCallback callback; + + @VisibleForTesting + CameraRequestPermissionsListener(ResultCallback callback) { + this.callback = callback; + } + + @Override + public boolean onRequestPermissionsResult(int id, String[] permissions, int[] grantResults) { + if (alreadyCalled || id != CAMERA_REQUEST_ID) { + return false; + } + + alreadyCalled = true; + // grantResults could be empty if the permissions request with the user is interrupted + // https://developer.android.com/reference/android/app/Activity#onRequestPermissionsResult(int,%20java.lang.String[],%20int[]) + if (grantResults.length == 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED) { + callback.onResult(CAMERA_ACCESS_DENIED, CAMERA_ACCESS_DENIED_MESSAGE); + } else if (grantResults.length > 1 && grantResults[1] != PackageManager.PERMISSION_GRANTED) { + callback.onResult(AUDIO_ACCESS_DENIED, AUDIO_ACCESS_DENIED_MESSAGE); + } else { + callback.onResult(null, null); + } + return true; + } + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraSelectorHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraSelectorHostApiImpl.java index 9c559a72e63c..603f7cf78def 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraSelectorHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraSelectorHostApiImpl.java @@ -12,6 +12,7 @@ import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraSelectorHostApi; import java.util.ArrayList; import java.util.List; +import java.util.Objects; public class CameraSelectorHostApiImpl implements CameraSelectorHostApi { private final BinaryMessenger binaryMessenger; @@ -41,23 +42,22 @@ public void create(@NonNull Long identifier, Long lensFacing) { @Override public List filter(@NonNull Long identifier, @NonNull List cameraInfoIds) { - CameraSelector cameraSelector = (CameraSelector) instanceManager.getInstance(identifier); + CameraSelector cameraSelector = + (CameraSelector) Objects.requireNonNull(instanceManager.getInstance(identifier)); List cameraInfosForFilter = new ArrayList(); for (Number cameraInfoAsNumber : cameraInfoIds) { Long cameraInfoId = cameraInfoAsNumber.longValue(); - CameraInfo cameraInfo = (CameraInfo) instanceManager.getInstance(cameraInfoId); + CameraInfo cameraInfo = + (CameraInfo) Objects.requireNonNull(instanceManager.getInstance(cameraInfoId)); cameraInfosForFilter.add(cameraInfo); } List filteredCameraInfos = cameraSelector.filter(cameraInfosForFilter); - final CameraInfoFlutterApiImpl cameraInfoFlutterApiImpl = - new CameraInfoFlutterApiImpl(binaryMessenger, instanceManager); List filteredCameraInfosIds = new ArrayList(); for (CameraInfo cameraInfo : filteredCameraInfos) { - cameraInfoFlutterApiImpl.create(cameraInfo, result -> {}); Long filteredCameraInfoId = instanceManager.getIdentifierForStrongReference(cameraInfo); filteredCameraInfosIds.add(filteredCameraInfoId); } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java index 8063866d2fc6..4a3d277a4dc3 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java @@ -4,10 +4,48 @@ package io.flutter.plugins.camerax; +import android.app.Activity; +import android.graphics.SurfaceTexture; +import android.view.Surface; +import androidx.annotation.NonNull; import androidx.camera.core.CameraSelector; +import androidx.camera.core.Preview; +import io.flutter.plugin.common.BinaryMessenger; +/** Utility class used to create CameraX-related objects primarily for testing purposes. */ public class CameraXProxy { public CameraSelector.Builder createCameraSelectorBuilder() { return new CameraSelector.Builder(); } + + public CameraPermissionsManager createCameraPermissionsManager() { + return new CameraPermissionsManager(); + } + + public DeviceOrientationManager createDeviceOrientationManager( + @NonNull Activity activity, + @NonNull Boolean isFrontFacing, + @NonNull int sensorOrientation, + @NonNull DeviceOrientationManager.DeviceOrientationChangeCallback callback) { + return new DeviceOrientationManager(activity, isFrontFacing, sensorOrientation, callback); + } + + public Preview.Builder createPreviewBuilder() { + return new Preview.Builder(); + } + + public Surface createSurface(@NonNull SurfaceTexture surfaceTexture) { + return new Surface(surfaceTexture); + } + + /** + * Creates an instance of the {@code SystemServicesFlutterApiImpl}. + * + *

Included in this class to utilize the callback methods it provides, e.g. {@code + * onCameraError(String)}. + */ + public SystemServicesFlutterApiImpl createSystemServicesFlutterApiImpl( + @NonNull BinaryMessenger binaryMessenger) { + return new SystemServicesFlutterApiImpl(binaryMessenger); + } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java new file mode 100644 index 000000000000..ebcb86433f65 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java @@ -0,0 +1,329 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.view.Display; +import android.view.Surface; +import android.view.WindowManager; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; + +/** + * Support class to help to determine the media orientation based on the orientation of the device. + */ +public class DeviceOrientationManager { + + interface DeviceOrientationChangeCallback { + void onChange(DeviceOrientation newOrientation); + } + + private static final IntentFilter orientationIntentFilter = + new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); + + private final Activity activity; + private final boolean isFrontFacing; + private final int sensorOrientation; + private final DeviceOrientationChangeCallback deviceOrientationChangeCallback; + private PlatformChannel.DeviceOrientation lastOrientation; + private BroadcastReceiver broadcastReceiver; + + DeviceOrientationManager( + @NonNull Activity activity, + boolean isFrontFacing, + int sensorOrientation, + DeviceOrientationChangeCallback callback) { + this.activity = activity; + this.isFrontFacing = isFrontFacing; + this.sensorOrientation = sensorOrientation; + this.deviceOrientationChangeCallback = callback; + } + + /** + * Starts listening to the device's sensors or UI for orientation updates. + * + *

When orientation information is updated, the callback method of the {@link + * DeviceOrientationChangeCallback} is called with the new orientation. This latest value can also + * be retrieved through the {@link #getVideoOrientation()} accessor. + * + *

If the device's ACCELEROMETER_ROTATION setting is enabled the {@link + * DeviceOrientationManager} will report orientation updates based on the sensor information. If + * the ACCELEROMETER_ROTATION is disabled the {@link DeviceOrientationManager} will fallback to + * the deliver orientation updates based on the UI orientation. + */ + public void start() { + if (broadcastReceiver != null) { + return; + } + broadcastReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + handleUIOrientationChange(); + } + }; + activity.registerReceiver(broadcastReceiver, orientationIntentFilter); + broadcastReceiver.onReceive(activity, null); + } + + /** Stops listening for orientation updates. */ + public void stop() { + if (broadcastReceiver == null) { + return; + } + activity.unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; + } + + /** + * Returns the device's photo orientation in degrees based on the sensor orientation and the last + * known UI orientation. + * + *

Returns one of 0, 90, 180 or 270. + * + * @return The device's photo orientation in degrees. + */ + public int getPhotoOrientation() { + return this.getPhotoOrientation(this.lastOrientation); + } + + /** + * Returns the device's photo orientation in degrees based on the sensor orientation and the + * supplied {@link PlatformChannel.DeviceOrientation} value. + * + *

Returns one of 0, 90, 180 or 270. + * + * @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted + * into degrees. + * @return The device's photo orientation in degrees. + */ + public int getPhotoOrientation(PlatformChannel.DeviceOrientation orientation) { + int angle = 0; + // Fallback to device orientation when the orientation value is null. + if (orientation == null) { + orientation = getUIOrientation(); + } + + switch (orientation) { + case PORTRAIT_UP: + angle = 90; + break; + case PORTRAIT_DOWN: + angle = 270; + break; + case LANDSCAPE_LEFT: + angle = isFrontFacing ? 180 : 0; + break; + case LANDSCAPE_RIGHT: + angle = isFrontFacing ? 0 : 180; + break; + } + + // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X). + // This has to be taken into account so the JPEG is rotated properly. + // For devices with orientation of 90, this simply returns the mapping from ORIENTATIONS. + // For devices with orientation of 270, the JPEG is rotated 180 degrees instead. + return (angle + sensorOrientation + 270) % 360; + } + + /** + * Returns the device's video orientation in clockwise degrees based on the sensor orientation and + * the last known UI orientation. + * + *

Returns one of 0, 90, 180 or 270. + * + * @return The device's video orientation in clockwise degrees. + */ + public int getVideoOrientation() { + return this.getVideoOrientation(this.lastOrientation); + } + + /** + * Returns the device's video orientation in clockwise degrees based on the sensor orientation and + * the supplied {@link PlatformChannel.DeviceOrientation} value. + * + *

Returns one of 0, 90, 180 or 270. + * + *

More details can be found in the official Android documentation: + * https://developer.android.com/reference/android/media/MediaRecorder#setOrientationHint(int) + * + *

See also: + * https://developer.android.com/training/camera2/camera-preview-large-screens#orientation_calculation + * + * @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted + * into degrees. + * @return The device's video orientation in clockwise degrees. + */ + public int getVideoOrientation(PlatformChannel.DeviceOrientation orientation) { + int angle = 0; + + // Fallback to device orientation when the orientation value is null. + if (orientation == null) { + orientation = getUIOrientation(); + } + + switch (orientation) { + case PORTRAIT_UP: + angle = 0; + break; + case PORTRAIT_DOWN: + angle = 180; + break; + case LANDSCAPE_LEFT: + angle = 270; + break; + case LANDSCAPE_RIGHT: + angle = 90; + break; + } + + if (isFrontFacing) { + angle *= -1; + } + + return (angle + sensorOrientation + 360) % 360; + } + + /** @return the last received UI orientation. */ + public PlatformChannel.DeviceOrientation getLastUIOrientation() { + return this.lastOrientation; + } + + /** + * Handles orientation changes based on change events triggered by the OrientationIntentFilter. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + */ + @VisibleForTesting + void handleUIOrientationChange() { + PlatformChannel.DeviceOrientation orientation = getUIOrientation(); + handleOrientationChange(orientation, lastOrientation, deviceOrientationChangeCallback); + lastOrientation = orientation; + } + + /** + * Handles orientation changes coming from either the device's sensors or the + * OrientationIntentFilter. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + */ + @VisibleForTesting + static void handleOrientationChange( + DeviceOrientation newOrientation, + DeviceOrientation previousOrientation, + DeviceOrientationChangeCallback callback) { + if (!newOrientation.equals(previousOrientation)) { + callback.onChange(newOrientation); + } + } + + /** + * Gets the current user interface orientation. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + * + * @return The current user interface orientation. + */ + @VisibleForTesting + PlatformChannel.DeviceOrientation getUIOrientation() { + final int rotation = getDisplay().getRotation(); + final int orientation = activity.getResources().getConfiguration().orientation; + + switch (orientation) { + case Configuration.ORIENTATION_PORTRAIT: + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } else { + return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; + } + case Configuration.ORIENTATION_LANDSCAPE: + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { + return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; + } else { + return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; + } + default: + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } + } + + /** + * Calculates the sensor orientation based on the supplied angle. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + * + * @param angle Orientation angle. + * @return The sensor orientation based on the supplied angle. + */ + @VisibleForTesting + PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) { + final int tolerance = 45; + angle += tolerance; + + // Orientation is 0 in the default orientation mode. This is portrait-mode for phones + // and landscape for tablets. We have to compensate for this by calculating the default + // orientation, and apply an offset accordingly. + int defaultDeviceOrientation = getDeviceDefaultOrientation(); + if (defaultDeviceOrientation == Configuration.ORIENTATION_LANDSCAPE) { + angle += 90; + } + // Determine the orientation + angle = angle % 360; + return new PlatformChannel.DeviceOrientation[] { + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + } + [angle / 90]; + } + + /** + * Gets the default orientation of the device. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + * + * @return The default orientation of the device. + */ + @VisibleForTesting + int getDeviceDefaultOrientation() { + Configuration config = activity.getResources().getConfiguration(); + int rotation = getDisplay().getRotation(); + if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) + && config.orientation == Configuration.ORIENTATION_LANDSCAPE) + || ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) + && config.orientation == Configuration.ORIENTATION_PORTRAIT)) { + return Configuration.ORIENTATION_LANDSCAPE; + } else { + return Configuration.ORIENTATION_PORTRAIT; + } + } + + /** + * Gets an instance of the Android {@link android.view.Display}. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + * + * @return An instance of the Android {@link android.view.Display}. + */ + @SuppressWarnings("deprecation") + @VisibleForTesting + Display getDisplay() { + return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java index 041564c3bfcb..1e61ea699292 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java @@ -13,6 +13,8 @@ import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MessageCodec; import io.flutter.plugin.common.StandardMessageCodec; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -23,6 +25,154 @@ @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"}) public class GeneratedCameraXLibrary { + /** Generated class from Pigeon that represents data sent in messages. */ + public static class ResolutionInfo { + private @NonNull Long width; + + public @NonNull Long getWidth() { + return width; + } + + public void setWidth(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"width\" is null."); + } + this.width = setterArg; + } + + private @NonNull Long height; + + public @NonNull Long getHeight() { + return height; + } + + public void setHeight(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"height\" is null."); + } + this.height = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private ResolutionInfo() {} + + public static final class Builder { + private @Nullable Long width; + + public @NonNull Builder setWidth(@NonNull Long setterArg) { + this.width = setterArg; + return this; + } + + private @Nullable Long height; + + public @NonNull Builder setHeight(@NonNull Long setterArg) { + this.height = setterArg; + return this; + } + + public @NonNull ResolutionInfo build() { + ResolutionInfo pigeonReturn = new ResolutionInfo(); + pigeonReturn.setWidth(width); + pigeonReturn.setHeight(height); + return pigeonReturn; + } + } + + @NonNull + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("width", width); + toMapResult.put("height", height); + return toMapResult; + } + + static @NonNull ResolutionInfo fromMap(@NonNull Map map) { + ResolutionInfo pigeonResult = new ResolutionInfo(); + Object width = map.get("width"); + pigeonResult.setWidth( + (width == null) ? null : ((width instanceof Integer) ? (Integer) width : (Long) width)); + Object height = map.get("height"); + pigeonResult.setHeight( + (height == null) + ? null + : ((height instanceof Integer) ? (Integer) height : (Long) height)); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class CameraPermissionsErrorData { + private @NonNull String errorCode; + + public @NonNull String getErrorCode() { + return errorCode; + } + + public void setErrorCode(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"errorCode\" is null."); + } + this.errorCode = setterArg; + } + + private @NonNull String description; + + public @NonNull String getDescription() { + return description; + } + + public void setDescription(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"description\" is null."); + } + this.description = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private CameraPermissionsErrorData() {} + + public static final class Builder { + private @Nullable String errorCode; + + public @NonNull Builder setErrorCode(@NonNull String setterArg) { + this.errorCode = setterArg; + return this; + } + + private @Nullable String description; + + public @NonNull Builder setDescription(@NonNull String setterArg) { + this.description = setterArg; + return this; + } + + public @NonNull CameraPermissionsErrorData build() { + CameraPermissionsErrorData pigeonReturn = new CameraPermissionsErrorData(); + pigeonReturn.setErrorCode(errorCode); + pigeonReturn.setDescription(description); + return pigeonReturn; + } + } + + @NonNull + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("errorCode", errorCode); + toMapResult.put("description", description); + return toMapResult; + } + + static @NonNull CameraPermissionsErrorData fromMap(@NonNull Map map) { + CameraPermissionsErrorData pigeonResult = new CameraPermissionsErrorData(); + Object errorCode = map.get("errorCode"); + pigeonResult.setErrorCode((String) errorCode); + Object description = map.get("description"); + pigeonResult.setDescription((String) description); + return pigeonResult; + } + } + public interface Result { void success(T result); @@ -332,6 +482,16 @@ public interface ProcessCameraProviderHostApi { @NonNull List getAvailableCameraInfos(@NonNull Long identifier); + @NonNull + Long bindToLifecycle( + @NonNull Long identifier, + @NonNull Long cameraSelectorIdentifier, + @NonNull List useCaseIds); + + void unbind(@NonNull Long identifier, @NonNull List useCaseIds); + + void unbindAll(@NonNull Long identifier); + /** The codec used by ProcessCameraProviderHostApi. */ static MessageCodec getCodec() { return ProcessCameraProviderHostApiCodec.INSTANCE; @@ -405,6 +565,107 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.ProcessCameraProviderHostApi.bindToLifecycle", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + if (identifierArg == null) { + throw new NullPointerException("identifierArg unexpectedly null."); + } + Number cameraSelectorIdentifierArg = (Number) args.get(1); + if (cameraSelectorIdentifierArg == null) { + throw new NullPointerException( + "cameraSelectorIdentifierArg unexpectedly null."); + } + List useCaseIdsArg = (List) args.get(2); + if (useCaseIdsArg == null) { + throw new NullPointerException("useCaseIdsArg unexpectedly null."); + } + Long output = + api.bindToLifecycle( + (identifierArg == null) ? null : identifierArg.longValue(), + (cameraSelectorIdentifierArg == null) + ? null + : cameraSelectorIdentifierArg.longValue(), + useCaseIdsArg); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.ProcessCameraProviderHostApi.unbind", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + if (identifierArg == null) { + throw new NullPointerException("identifierArg unexpectedly null."); + } + List useCaseIdsArg = (List) args.get(1); + if (useCaseIdsArg == null) { + throw new NullPointerException("useCaseIdsArg unexpectedly null."); + } + api.unbind( + (identifierArg == null) ? null : identifierArg.longValue(), useCaseIdsArg); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.ProcessCameraProviderHostApi.unbindAll", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + if (identifierArg == null) { + throw new NullPointerException("identifierArg unexpectedly null."); + } + api.unbindAll((identifierArg == null) ? null : identifierArg.longValue()); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } } } @@ -445,6 +706,400 @@ public void create(@NonNull Long identifierArg, Reply callback) { } } + private static class CameraFlutterApiCodec extends StandardMessageCodec { + public static final CameraFlutterApiCodec INSTANCE = new CameraFlutterApiCodec(); + + private CameraFlutterApiCodec() {} + } + + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class CameraFlutterApi { + private final BinaryMessenger binaryMessenger; + + public CameraFlutterApi(BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + public interface Reply { + void reply(T reply); + } + + static MessageCodec getCodec() { + return CameraFlutterApiCodec.INSTANCE; + } + + public void create(@NonNull Long identifierArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.CameraFlutterApi.create", getCodec()); + channel.send( + new ArrayList(Arrays.asList(identifierArg)), + channelReply -> { + callback.reply(null); + }); + } + } + + private static class SystemServicesHostApiCodec extends StandardMessageCodec { + public static final SystemServicesHostApiCodec INSTANCE = new SystemServicesHostApiCodec(); + + private SystemServicesHostApiCodec() {} + + @Override + protected Object readValueOfType(byte type, ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return CameraPermissionsErrorData.fromMap((Map) readValue(buffer)); + + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(ByteArrayOutputStream stream, Object value) { + if (value instanceof CameraPermissionsErrorData) { + stream.write(128); + writeValue(stream, ((CameraPermissionsErrorData) value).toMap()); + } else { + super.writeValue(stream, value); + } + } + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface SystemServicesHostApi { + void requestCameraPermissions( + @NonNull Boolean enableAudio, Result result); + + void startListeningForDeviceOrientationChange( + @NonNull Boolean isFrontFacing, @NonNull Long sensorOrientation); + + void stopListeningForDeviceOrientationChange(); + + /** The codec used by SystemServicesHostApi. */ + static MessageCodec getCodec() { + return SystemServicesHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `SystemServicesHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, SystemServicesHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SystemServicesHostApi.requestCameraPermissions", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Boolean enableAudioArg = (Boolean) args.get(0); + if (enableAudioArg == null) { + throw new NullPointerException("enableAudioArg unexpectedly null."); + } + Result resultCallback = + new Result() { + public void success(CameraPermissionsErrorData result) { + wrapped.put("result", result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + wrapped.put("error", wrapError(error)); + reply.reply(wrapped); + } + }; + + api.requestCameraPermissions(enableAudioArg, resultCallback); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + reply.reply(wrapped); + } + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SystemServicesHostApi.startListeningForDeviceOrientationChange", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Boolean isFrontFacingArg = (Boolean) args.get(0); + if (isFrontFacingArg == null) { + throw new NullPointerException("isFrontFacingArg unexpectedly null."); + } + Number sensorOrientationArg = (Number) args.get(1); + if (sensorOrientationArg == null) { + throw new NullPointerException("sensorOrientationArg unexpectedly null."); + } + api.startListeningForDeviceOrientationChange( + isFrontFacingArg, + (sensorOrientationArg == null) ? null : sensorOrientationArg.longValue()); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SystemServicesHostApi.stopListeningForDeviceOrientationChange", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + api.stopListeningForDeviceOrientationChange(); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + private static class SystemServicesFlutterApiCodec extends StandardMessageCodec { + public static final SystemServicesFlutterApiCodec INSTANCE = + new SystemServicesFlutterApiCodec(); + + private SystemServicesFlutterApiCodec() {} + } + + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class SystemServicesFlutterApi { + private final BinaryMessenger binaryMessenger; + + public SystemServicesFlutterApi(BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + public interface Reply { + void reply(T reply); + } + + static MessageCodec getCodec() { + return SystemServicesFlutterApiCodec.INSTANCE; + } + + public void onDeviceOrientationChanged(@NonNull String orientationArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SystemServicesFlutterApi.onDeviceOrientationChanged", + getCodec()); + channel.send( + new ArrayList(Arrays.asList(orientationArg)), + channelReply -> { + callback.reply(null); + }); + } + + public void onCameraError(@NonNull String errorDescriptionArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError", + getCodec()); + channel.send( + new ArrayList(Arrays.asList(errorDescriptionArg)), + channelReply -> { + callback.reply(null); + }); + } + } + + private static class PreviewHostApiCodec extends StandardMessageCodec { + public static final PreviewHostApiCodec INSTANCE = new PreviewHostApiCodec(); + + private PreviewHostApiCodec() {} + + @Override + protected Object readValueOfType(byte type, ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return ResolutionInfo.fromMap((Map) readValue(buffer)); + + case (byte) 129: + return ResolutionInfo.fromMap((Map) readValue(buffer)); + + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(ByteArrayOutputStream stream, Object value) { + if (value instanceof ResolutionInfo) { + stream.write(128); + writeValue(stream, ((ResolutionInfo) value).toMap()); + } else if (value instanceof ResolutionInfo) { + stream.write(129); + writeValue(stream, ((ResolutionInfo) value).toMap()); + } else { + super.writeValue(stream, value); + } + } + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface PreviewHostApi { + void create( + @NonNull Long identifier, + @Nullable Long rotation, + @Nullable ResolutionInfo targetResolution); + + @NonNull + Long setSurfaceProvider(@NonNull Long identifier); + + void releaseFlutterSurfaceTexture(); + + @NonNull + ResolutionInfo getResolutionInfo(@NonNull Long identifier); + + /** The codec used by PreviewHostApi. */ + static MessageCodec getCodec() { + return PreviewHostApiCodec.INSTANCE; + } + + /** Sets up an instance of `PreviewHostApi` to handle messages through the `binaryMessenger`. */ + static void setup(BinaryMessenger binaryMessenger, PreviewHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PreviewHostApi.create", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + if (identifierArg == null) { + throw new NullPointerException("identifierArg unexpectedly null."); + } + Number rotationArg = (Number) args.get(1); + ResolutionInfo targetResolutionArg = (ResolutionInfo) args.get(2); + api.create( + (identifierArg == null) ? null : identifierArg.longValue(), + (rotationArg == null) ? null : rotationArg.longValue(), + targetResolutionArg); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + if (identifierArg == null) { + throw new NullPointerException("identifierArg unexpectedly null."); + } + Long output = + api.setSurfaceProvider( + (identifierArg == null) ? null : identifierArg.longValue()); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.PreviewHostApi.releaseFlutterSurfaceTexture", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + api.releaseFlutterSurfaceTexture(); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PreviewHostApi.getResolutionInfo", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + if (identifierArg == null) { + throw new NullPointerException("identifierArg unexpectedly null."); + } + ResolutionInfo output = + api.getResolutionInfo( + (identifierArg == null) ? null : identifierArg.longValue()); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + private static Map wrapError(Throwable exception) { Map errorMap = new HashMap<>(); errorMap.put("message", exception.toString()); diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/InstanceManager.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/InstanceManager.java index 9b549d7bd1ea..8212d1267a19 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/InstanceManager.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/InstanceManager.java @@ -122,16 +122,15 @@ public void addDartCreatedInstance(Object instance, long identifier) { /** * Adds a new instance that was instantiated from the host platform. * - *

If an instance has already been added, the identifier of the instance will be returned. + *

If an instance has already been added, this will replace it. {@code #containsInstance} can + * be used to check if the object has already been added to avoid this. * * @param instance the instance to be stored. * @return the unique identifier stored with instance. */ public long addHostCreatedInstance(Object instance) { assertManagerIsNotClosed(); - if (containsInstance(instance)) { - return getIdentifierForStrongReference(instance); - } + final long identifier = nextIdentifier++; addInstance(instance, identifier); return identifier; diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java new file mode 100644 index 000000000000..838f0b3d656c --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java @@ -0,0 +1,149 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import android.graphics.SurfaceTexture; +import android.util.Size; +import android.view.Surface; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.camera.core.Preview; +import androidx.camera.core.SurfaceRequest; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.PreviewHostApi; +import io.flutter.view.TextureRegistry; +import java.util.Objects; +import java.util.concurrent.Executors; + +public class PreviewHostApiImpl implements PreviewHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + private final TextureRegistry textureRegistry; + + @VisibleForTesting public CameraXProxy cameraXProxy = new CameraXProxy(); + @VisibleForTesting public TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture; + + public PreviewHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, + @NonNull InstanceManager instanceManager, + @NonNull TextureRegistry textureRegistry) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + this.textureRegistry = textureRegistry; + } + + /** Creates a {@link Preview} with the target rotation and resolution if specified. */ + @Override + public void create( + @NonNull Long identifier, + @Nullable Long rotation, + @Nullable GeneratedCameraXLibrary.ResolutionInfo targetResolution) { + Preview.Builder previewBuilder = cameraXProxy.createPreviewBuilder(); + if (rotation != null) { + previewBuilder.setTargetRotation(rotation.intValue()); + } + if (targetResolution != null) { + previewBuilder.setTargetResolution( + new Size( + targetResolution.getWidth().intValue(), targetResolution.getHeight().intValue())); + } + Preview preview = previewBuilder.build(); + instanceManager.addDartCreatedInstance(preview, identifier); + } + + /** + * Sets the {@link Preview.SurfaceProvider} that will be used to provide a {@code Surface} backed + * by a Flutter {@link TextureRegistry.SurfaceTextureEntry} used to build the {@link Preview}. + */ + @Override + public Long setSurfaceProvider(@NonNull Long identifier) { + Preview preview = (Preview) Objects.requireNonNull(instanceManager.getInstance(identifier)); + flutterSurfaceTexture = textureRegistry.createSurfaceTexture(); + SurfaceTexture surfaceTexture = flutterSurfaceTexture.surfaceTexture(); + Preview.SurfaceProvider surfaceProvider = createSurfaceProvider(surfaceTexture); + preview.setSurfaceProvider(surfaceProvider); + + return flutterSurfaceTexture.id(); + } + + /** + * Creates a {@link Preview.SurfaceProvider} that specifies how to provide a {@link Surface} to a + * {@code Preview} that is backed by a Flutter {@link TextureRegistry.SurfaceTextureEntry}. + */ + @VisibleForTesting + public Preview.SurfaceProvider createSurfaceProvider(@NonNull SurfaceTexture surfaceTexture) { + return new Preview.SurfaceProvider() { + @Override + public void onSurfaceRequested(SurfaceRequest request) { + surfaceTexture.setDefaultBufferSize( + request.getResolution().getWidth(), request.getResolution().getHeight()); + Surface flutterSurface = cameraXProxy.createSurface(surfaceTexture); + request.provideSurface( + flutterSurface, + Executors.newSingleThreadExecutor(), + (result) -> { + // See https://developer.android.com/reference/androidx/camera/core/SurfaceRequest.Result for documentation. + // Always attempt a release. + flutterSurface.release(); + int resultCode = result.getResultCode(); + switch (resultCode) { + case SurfaceRequest.Result.RESULT_REQUEST_CANCELLED: + case SurfaceRequest.Result.RESULT_WILL_NOT_PROVIDE_SURFACE: + case SurfaceRequest.Result.RESULT_SURFACE_ALREADY_PROVIDED: + case SurfaceRequest.Result.RESULT_SURFACE_USED_SUCCESSFULLY: + // Only need to release, do nothing. + break; + case SurfaceRequest.Result.RESULT_INVALID_SURFACE: // Intentional fall through. + default: + // Release and send error. + SystemServicesFlutterApiImpl systemServicesFlutterApi = + cameraXProxy.createSystemServicesFlutterApiImpl(binaryMessenger); + systemServicesFlutterApi.sendCameraError( + getProvideSurfaceErrorDescription(resultCode), reply -> {}); + break; + } + }); + }; + }; + } + + /** + * Returns an error description for each {@link SurfaceRequest.Result} that represents an error + * with providing a surface. + */ + private String getProvideSurfaceErrorDescription(@Nullable int resultCode) { + switch (resultCode) { + case SurfaceRequest.Result.RESULT_INVALID_SURFACE: + return resultCode + ": Provided surface could not be used by the camera."; + default: + return resultCode + ": Attempt to provide a surface resulted with unrecognizable code."; + } + } + + /** + * Releases the Flutter {@link TextureRegistry.SurfaceTextureEntry} if used to provide a surface + * for a {@link Preview}. + */ + @Override + public void releaseFlutterSurfaceTexture() { + if (flutterSurfaceTexture != null) { + flutterSurfaceTexture.release(); + } + } + + /** Returns the resolution information for the specified {@link Preview}. */ + @Override + public GeneratedCameraXLibrary.ResolutionInfo getResolutionInfo(@NonNull Long identifier) { + Preview preview = (Preview) Objects.requireNonNull(instanceManager.getInstance(identifier)); + Size resolution = preview.getResolutionInfo().getResolution(); + + GeneratedCameraXLibrary.ResolutionInfo.Builder resolutionInfo = + new GeneratedCameraXLibrary.ResolutionInfo.Builder() + .setWidth(Long.valueOf(resolution.getWidth())) + .setHeight(Long.valueOf(resolution.getHeight())); + return resolutionInfo.build(); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.java index 19c5eb5b3f70..e7036e7090c1 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.java @@ -6,20 +6,26 @@ import android.content.Context; import androidx.annotation.NonNull; +import androidx.camera.core.Camera; import androidx.camera.core.CameraInfo; +import androidx.camera.core.CameraSelector; +import androidx.camera.core.UseCase; import androidx.camera.lifecycle.ProcessCameraProvider; import androidx.core.content.ContextCompat; +import androidx.lifecycle.LifecycleOwner; import com.google.common.util.concurrent.ListenableFuture; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ProcessCameraProviderHostApi; import java.util.ArrayList; import java.util.List; +import java.util.Objects; public class ProcessCameraProviderHostApiImpl implements ProcessCameraProviderHostApi { private final BinaryMessenger binaryMessenger; private final InstanceManager instanceManager; private Context context; + private LifecycleOwner lifecycleOwner; public ProcessCameraProviderHostApiImpl( BinaryMessenger binaryMessenger, InstanceManager instanceManager, Context context) { @@ -28,6 +34,10 @@ public ProcessCameraProviderHostApiImpl( this.context = context; } + public void setLifecycleOwner(LifecycleOwner lifecycleOwner) { + this.lifecycleOwner = lifecycleOwner; + } + /** * Sets the context that the {@code ProcessCameraProvider} will use to attach the lifecycle of the * camera to. @@ -40,8 +50,8 @@ public void setContext(Context context) { } /** - * Returns the instance of the ProcessCameraProvider to manage the lifecycle of the camera for the - * current {@code Context}. + * Returns the instance of the {@code ProcessCameraProvider} to manage the lifecycle of the camera + * for the current {@code Context}. */ @Override public void getInstance(GeneratedCameraXLibrary.Result result) { @@ -54,9 +64,9 @@ public void getInstance(GeneratedCameraXLibrary.Result result) { // Camera provider is now guaranteed to be available. ProcessCameraProvider processCameraProvider = processCameraProviderFuture.get(); + final ProcessCameraProviderFlutterApiImpl flutterApi = + new ProcessCameraProviderFlutterApiImpl(binaryMessenger, instanceManager); if (!instanceManager.containsInstance(processCameraProvider)) { - final ProcessCameraProviderFlutterApiImpl flutterApi = - new ProcessCameraProviderFlutterApiImpl(binaryMessenger, instanceManager); flutterApi.create(processCameraProvider, reply -> {}); } result.success(instanceManager.getIdentifierForStrongReference(processCameraProvider)); @@ -67,11 +77,11 @@ public void getInstance(GeneratedCameraXLibrary.Result result) { ContextCompat.getMainExecutor(context)); } - /** Returns cameras available to the ProcessCameraProvider. */ + /** Returns cameras available to the {@code ProcessCameraProvider}. */ @Override public List getAvailableCameraInfos(@NonNull Long identifier) { ProcessCameraProvider processCameraProvider = - (ProcessCameraProvider) instanceManager.getInstance(identifier); + (ProcessCameraProvider) Objects.requireNonNull(instanceManager.getInstance(identifier)); List availableCameras = processCameraProvider.getAvailableCameraInfos(); List availableCamerasIds = new ArrayList(); @@ -79,9 +89,68 @@ public List getAvailableCameraInfos(@NonNull Long identifier) { new CameraInfoFlutterApiImpl(binaryMessenger, instanceManager); for (CameraInfo cameraInfo : availableCameras) { - cameraInfoFlutterApi.create(cameraInfo, result -> {}); + if (!instanceManager.containsInstance(cameraInfo)) { + cameraInfoFlutterApi.create(cameraInfo, result -> {}); + } availableCamerasIds.add(instanceManager.getIdentifierForStrongReference(cameraInfo)); } return availableCamerasIds; } + + /** + * Binds specified {@code UseCase}s to the lifecycle of the {@code LifecycleOwner} that + * corresponds to this instance and returns the instance of the {@code Camera} whose lifecycle + * that {@code LifecycleOwner} reflects. + */ + @Override + public Long bindToLifecycle( + @NonNull Long identifier, + @NonNull Long cameraSelectorIdentifier, + @NonNull List useCaseIds) { + ProcessCameraProvider processCameraProvider = + (ProcessCameraProvider) Objects.requireNonNull(instanceManager.getInstance(identifier)); + CameraSelector cameraSelector = + (CameraSelector) + Objects.requireNonNull(instanceManager.getInstance(cameraSelectorIdentifier)); + UseCase[] useCases = new UseCase[useCaseIds.size()]; + for (int i = 0; i < useCaseIds.size(); i++) { + useCases[i] = + (UseCase) + Objects.requireNonNull( + instanceManager.getInstance(((Number) useCaseIds.get(i)).longValue())); + } + + Camera camera = + processCameraProvider.bindToLifecycle( + (LifecycleOwner) lifecycleOwner, cameraSelector, useCases); + + final CameraFlutterApiImpl cameraFlutterApi = + new CameraFlutterApiImpl(binaryMessenger, instanceManager); + if (!instanceManager.containsInstance(camera)) { + cameraFlutterApi.create(camera, result -> {}); + } + + return instanceManager.getIdentifierForStrongReference(camera); + } + + @Override + public void unbind(@NonNull Long identifier, @NonNull List useCaseIds) { + ProcessCameraProvider processCameraProvider = + (ProcessCameraProvider) Objects.requireNonNull(instanceManager.getInstance(identifier)); + UseCase[] useCases = new UseCase[useCaseIds.size()]; + for (int i = 0; i < useCaseIds.size(); i++) { + useCases[i] = + (UseCase) + Objects.requireNonNull( + instanceManager.getInstance(((Number) useCaseIds.get(i)).longValue())); + } + processCameraProvider.unbind(useCases); + } + + @Override + public void unbindAll(@NonNull Long identifier) { + ProcessCameraProvider processCameraProvider = + (ProcessCameraProvider) Objects.requireNonNull(instanceManager.getInstance(identifier)); + processCameraProvider.unbindAll(); + } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesFlutterApiImpl.java new file mode 100644 index 000000000000..63158974f43a --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesFlutterApiImpl.java @@ -0,0 +1,24 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesFlutterApi; + +public class SystemServicesFlutterApiImpl extends SystemServicesFlutterApi { + public SystemServicesFlutterApiImpl(@NonNull BinaryMessenger binaryMessenger) { + super(binaryMessenger); + } + + public void sendDeviceOrientationChangedEvent( + @NonNull String orientation, @NonNull Reply reply) { + super.onDeviceOrientationChanged(orientation, reply); + } + + public void sendCameraError(@NonNull String errorDescription, @NonNull Reply reply) { + super.onCameraError(errorDescription, reply); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java new file mode 100644 index 000000000000..a6985811531f --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java @@ -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. + +package io.flutter.plugins.camerax; + +import android.app.Activity; +import androidx.annotation.VisibleForTesting; +import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.CameraPermissionsManager.PermissionsRegistry; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraPermissionsErrorData; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.Result; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesHostApi; + +public class SystemServicesHostApiImpl implements SystemServicesHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + + @VisibleForTesting public CameraXProxy cameraXProxy = new CameraXProxy(); + @VisibleForTesting public DeviceOrientationManager deviceOrientationManager; + @VisibleForTesting public SystemServicesFlutterApiImpl systemServicesFlutterApi; + + private Activity activity; + private PermissionsRegistry permissionsRegistry; + + public SystemServicesHostApiImpl( + BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + this.systemServicesFlutterApi = new SystemServicesFlutterApiImpl(binaryMessenger); + } + + public void setActivity(Activity activity) { + this.activity = activity; + } + + public void setPermissionsRegistry(PermissionsRegistry permissionsRegistry) { + this.permissionsRegistry = permissionsRegistry; + } + + /** + * Requests camera permissions using an instance of a {@link CameraPermissionsManager}. + * + *

Will result with {@code null} if permissions were approved or there were no errors; + * otherwise, it will result with the error data explaining what went wrong. + */ + @Override + public void requestCameraPermissions( + Boolean enableAudio, Result result) { + CameraPermissionsManager cameraPermissionsManager = + cameraXProxy.createCameraPermissionsManager(); + cameraPermissionsManager.requestPermissions( + activity, + permissionsRegistry, + enableAudio, + (String errorCode, String description) -> { + if (errorCode == null) { + result.success(null); + } else { + // If permissions are ongoing or denied, error data will be sent to be handled. + CameraPermissionsErrorData errorData = + new CameraPermissionsErrorData.Builder() + .setErrorCode(errorCode) + .setDescription(description) + .build(); + result.success(errorData); + } + }); + } + + /** + * Starts listening for device orientation changes using an instace of a {@link + * DeviceOrientationManager}. + * + *

Whenever a change in device orientation is detected by the {@code DeviceOrientationManager}, + * the {@link SystemServicesFlutterApi} will be used to notify the Dart side. + */ + @Override + public void startListeningForDeviceOrientationChange( + Boolean isFrontFacing, Long sensorOrientation) { + deviceOrientationManager = + cameraXProxy.createDeviceOrientationManager( + activity, + isFrontFacing, + sensorOrientation.intValue(), + (DeviceOrientation newOrientation) -> { + systemServicesFlutterApi.sendDeviceOrientationChangedEvent( + serializeDeviceOrientation(newOrientation), reply -> {}); + }); + deviceOrientationManager.start(); + } + + /** Serializes {@code DeviceOrientation} into a String that the Dart side is able to recognize. */ + String serializeDeviceOrientation(DeviceOrientation orientation) { + return orientation.toString(); + } + + /** + * Tells the {@code deviceOrientationManager} to stop listening for orientation updates. + * + *

Has no effect if the {@code deviceOrientationManager} was never created to listen for device + * orientation updates. + */ + @Override + public void stopListeningForDeviceOrientationChange() { + if (deviceOrientationManager != null) { + deviceOrientationManager.stop(); + } + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraPermissionsManagerTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraPermissionsManagerTest.java new file mode 100644 index 000000000000..d90bde953306 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraPermissionsManagerTest.java @@ -0,0 +1,89 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import static junit.framework.TestCase.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.content.pm.PackageManager; +import io.flutter.plugins.camerax.CameraPermissionsManager.CameraRequestPermissionsListener; +import io.flutter.plugins.camerax.CameraPermissionsManager.ResultCallback; +import org.junit.Test; + +public class CameraPermissionsManagerTest { + @Test + public void listener_respondsOnce() { + final int[] calledCounter = {0}; + CameraRequestPermissionsListener permissionsListener = + new CameraRequestPermissionsListener((String code, String desc) -> calledCounter[0]++); + + permissionsListener.onRequestPermissionsResult( + 9796, null, new int[] {PackageManager.PERMISSION_DENIED}); + permissionsListener.onRequestPermissionsResult( + 9796, null, new int[] {PackageManager.PERMISSION_GRANTED}); + + assertEquals(1, calledCounter[0]); + } + + @Test + public void callback_respondsWithCameraAccessDenied() { + ResultCallback fakeResultCallback = mock(ResultCallback.class); + CameraRequestPermissionsListener permissionsListener = + new CameraRequestPermissionsListener(fakeResultCallback); + + permissionsListener.onRequestPermissionsResult( + 9796, null, new int[] {PackageManager.PERMISSION_DENIED}); + + verify(fakeResultCallback) + .onResult("CameraAccessDenied", "Camera access permission was denied."); + } + + @Test + public void callback_respondsWithAudioAccessDenied() { + ResultCallback fakeResultCallback = mock(ResultCallback.class); + CameraRequestPermissionsListener permissionsListener = + new CameraRequestPermissionsListener(fakeResultCallback); + + permissionsListener.onRequestPermissionsResult( + 9796, + null, + new int[] {PackageManager.PERMISSION_GRANTED, PackageManager.PERMISSION_DENIED}); + + verify(fakeResultCallback).onResult("AudioAccessDenied", "Audio access permission was denied."); + } + + @Test + public void callback_doesNotRespond() { + ResultCallback fakeResultCallback = mock(ResultCallback.class); + CameraRequestPermissionsListener permissionsListener = + new CameraRequestPermissionsListener(fakeResultCallback); + + permissionsListener.onRequestPermissionsResult( + 9796, + null, + new int[] {PackageManager.PERMISSION_GRANTED, PackageManager.PERMISSION_GRANTED}); + + verify(fakeResultCallback, never()) + .onResult("CameraAccessDenied", "Camera access permission was denied."); + verify(fakeResultCallback, never()) + .onResult("AudioAccessDenied", "Audio access permission was denied."); + } + + @Test + public void callback_respondsWithCameraAccessDeniedWhenEmptyResult() { + // Handles the case where the grantResults array is empty + + ResultCallback fakeResultCallback = mock(ResultCallback.class); + CameraRequestPermissionsListener permissionsListener = + new CameraRequestPermissionsListener(fakeResultCallback); + + permissionsListener.onRequestPermissionsResult(9796, null, new int[] {}); + + verify(fakeResultCallback) + .onResult("CameraAccessDenied", "Camera access permission was denied."); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraTest.java new file mode 100644 index 000000000000..e2135b3945b0 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraTest.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.camerax; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import androidx.camera.core.Camera; +import io.flutter.plugin.common.BinaryMessenger; +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 CameraTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public Camera camera; + + InstanceManager testInstanceManager; + + @Before + public void setUp() { + testInstanceManager = InstanceManager.open(identifier -> {}); + } + + @After + public void tearDown() { + testInstanceManager.close(); + } + + @Test + public void flutterApiCreateTest() { + final CameraFlutterApiImpl spyFlutterApi = + spy(new CameraFlutterApiImpl(mockBinaryMessenger, testInstanceManager)); + + spyFlutterApi.create(camera, reply -> {}); + + final long identifier = + Objects.requireNonNull(testInstanceManager.getIdentifierForStrongReference(camera)); + verify(spyFlutterApi).create(eq(identifier), any()); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerTest.java new file mode 100644 index 000000000000..1e2bfba714c7 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerTest.java @@ -0,0 +1,313 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.provider.Settings; +import android.view.Display; +import android.view.Surface; +import android.view.WindowManager; +import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; +import io.flutter.plugins.camerax.DeviceOrientationManager.DeviceOrientationChangeCallback; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class DeviceOrientationManagerTest { + private Activity mockActivity; + private DeviceOrientationChangeCallback mockDeviceOrientationChangeCallback; + private WindowManager mockWindowManager; + private Display mockDisplay; + private DeviceOrientationManager deviceOrientationManager; + + @Before + @SuppressWarnings("deprecation") + public void before() { + mockActivity = mock(Activity.class); + mockDisplay = mock(Display.class); + mockWindowManager = mock(WindowManager.class); + mockDeviceOrientationChangeCallback = mock(DeviceOrientationChangeCallback.class); + + when(mockActivity.getSystemService(Context.WINDOW_SERVICE)).thenReturn(mockWindowManager); + when(mockWindowManager.getDefaultDisplay()).thenReturn(mockDisplay); + + deviceOrientationManager = + new DeviceOrientationManager(mockActivity, false, 0, mockDeviceOrientationChangeCallback); + } + + @Test + public void getVideoOrientation_whenNaturalScreenOrientationEqualsPortraitUp() { + int degreesPortraitUp = + deviceOrientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + deviceOrientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + deviceOrientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + deviceOrientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + + assertEquals(0, degreesPortraitUp); + assertEquals(270, degreesLandscapeLeft); + assertEquals(180, degreesPortraitDown); + assertEquals(90, degreesLandscapeRight); + } + + @Test + public void getVideoOrientation_whenNaturalScreenOrientationEqualsLandscapeLeft() { + DeviceOrientationManager orientationManager = + new DeviceOrientationManager(mockActivity, false, 90, mockDeviceOrientationChangeCallback); + + int degreesPortraitUp = orientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + orientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + orientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + orientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + + assertEquals(90, degreesPortraitUp); + assertEquals(0, degreesLandscapeLeft); + assertEquals(270, degreesPortraitDown); + assertEquals(180, degreesLandscapeRight); + } + + @Test + public void getVideoOrientation_fallbackToPortraitSensorOrientationWhenOrientationIsNull() { + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + + int degrees = deviceOrientationManager.getVideoOrientation(null); + + assertEquals(0, degrees); + } + + @Test + public void getVideoOrientation_fallbackToLandscapeSensorOrientationWhenOrientationIsNull() { + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + + DeviceOrientationManager orientationManager = + new DeviceOrientationManager(mockActivity, false, 90, mockDeviceOrientationChangeCallback); + + int degrees = orientationManager.getVideoOrientation(null); + + assertEquals(0, degrees); + } + + @Test + public void getPhotoOrientation_whenNaturalScreenOrientationEqualsPortraitUp() { + int degreesPortraitUp = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + + assertEquals(0, degreesPortraitUp); + assertEquals(90, degreesLandscapeRight); + assertEquals(180, degreesPortraitDown); + assertEquals(270, degreesLandscapeLeft); + } + + @Test + public void getPhotoOrientation_whenNaturalScreenOrientationEqualsLandscapeLeft() { + DeviceOrientationManager orientationManager = + new DeviceOrientationManager(mockActivity, false, 90, mockDeviceOrientationChangeCallback); + + int degreesPortraitUp = orientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + orientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + orientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + orientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + + assertEquals(90, degreesPortraitUp); + assertEquals(180, degreesLandscapeRight); + assertEquals(270, degreesPortraitDown); + assertEquals(0, degreesLandscapeLeft); + } + + @Test + public void getPhotoOrientation_shouldFallbackToCurrentOrientationWhenOrientationIsNull() { + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + + int degrees = deviceOrientationManager.getPhotoOrientation(null); + + assertEquals(270, degrees); + } + + @Test + public void handleUIOrientationChange_shouldSendMessageWhenSensorAccessIsAllowed() { + try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { + mockedSystem + .when( + () -> + Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) + .thenReturn(0); + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + + deviceOrientationManager.handleUIOrientationChange(); + } + + verify(mockDeviceOrientationChangeCallback, times(1)) + .onChange(DeviceOrientation.LANDSCAPE_LEFT); + } + + @Test + public void handleOrientationChange_shouldSendMessageWhenOrientationIsUpdated() { + DeviceOrientation previousOrientation = DeviceOrientation.PORTRAIT_UP; + DeviceOrientation newOrientation = DeviceOrientation.LANDSCAPE_LEFT; + + DeviceOrientationManager.handleOrientationChange( + newOrientation, previousOrientation, mockDeviceOrientationChangeCallback); + + verify(mockDeviceOrientationChangeCallback, times(1)).onChange(newOrientation); + } + + @Test + public void handleOrientationChange_shouldNotSendMessageWhenOrientationIsNotUpdated() { + DeviceOrientation previousOrientation = DeviceOrientation.PORTRAIT_UP; + DeviceOrientation newOrientation = DeviceOrientation.PORTRAIT_UP; + + DeviceOrientationManager.handleOrientationChange( + newOrientation, previousOrientation, mockDeviceOrientationChangeCallback); + + verify(mockDeviceOrientationChangeCallback, never()).onChange(any()); + } + + @Test + public void getUIOrientation() { + // Orientation portrait and rotation of 0 should translate to "PORTRAIT_UP". + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + DeviceOrientation uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_UP, uiOrientation); + + // Orientation portrait and rotation of 90 should translate to "PORTRAIT_UP". + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_90); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_UP, uiOrientation); + + // Orientation portrait and rotation of 180 should translate to "PORTRAIT_DOWN". + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_180); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_DOWN, uiOrientation); + + // Orientation portrait and rotation of 270 should translate to "PORTRAIT_DOWN". + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_270); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_DOWN, uiOrientation); + + // Orientation landscape and rotation of 0 should translate to "LANDSCAPE_LEFT". + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.LANDSCAPE_LEFT, uiOrientation); + + // Orientation landscape and rotation of 90 should translate to "LANDSCAPE_LEFT". + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_90); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.LANDSCAPE_LEFT, uiOrientation); + + // Orientation landscape and rotation of 180 should translate to "LANDSCAPE_RIGHT". + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_180); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.LANDSCAPE_RIGHT, uiOrientation); + + // Orientation landscape and rotation of 270 should translate to "LANDSCAPE_RIGHT". + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_270); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.LANDSCAPE_RIGHT, uiOrientation); + + // Orientation undefined should default to "PORTRAIT_UP". + setUpUIOrientationMocks(Configuration.ORIENTATION_UNDEFINED, Surface.ROTATION_0); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_UP, uiOrientation); + } + + @Test + public void getDeviceDefaultOrientation() { + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + int orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_PORTRAIT, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_180); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_PORTRAIT, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_90); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_LANDSCAPE, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_270); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_LANDSCAPE, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_LANDSCAPE, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_180); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_LANDSCAPE, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_90); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_PORTRAIT, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_270); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_PORTRAIT, orientation); + } + + @Test + public void calculateSensorOrientation() { + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + DeviceOrientation orientation = deviceOrientationManager.calculateSensorOrientation(0); + assertEquals(DeviceOrientation.PORTRAIT_UP, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + orientation = deviceOrientationManager.calculateSensorOrientation(90); + assertEquals(DeviceOrientation.LANDSCAPE_LEFT, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + orientation = deviceOrientationManager.calculateSensorOrientation(180); + assertEquals(DeviceOrientation.PORTRAIT_DOWN, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + orientation = deviceOrientationManager.calculateSensorOrientation(270); + assertEquals(DeviceOrientation.LANDSCAPE_RIGHT, orientation); + } + + private void setUpUIOrientationMocks(int orientation, int rotation) { + Resources mockResources = mock(Resources.class); + Configuration mockConfiguration = mock(Configuration.class); + + when(mockDisplay.getRotation()).thenReturn(rotation); + + mockConfiguration.orientation = orientation; + when(mockActivity.getResources()).thenReturn(mockResources); + when(mockResources.getConfiguration()).thenReturn(mockConfiguration); + } + + @Test + public void getDisplayTest() { + Display display = deviceOrientationManager.getDisplay(); + + assertEquals(mockDisplay, display); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/InstanceManagerTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/InstanceManagerTest.java index 3878e05a40e8..e2e012dc35fb 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/InstanceManagerTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/InstanceManagerTest.java @@ -5,6 +5,7 @@ package io.flutter.plugins.camerax; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -40,6 +41,20 @@ public void addHostCreatedInstance() { instanceManager.close(); } + @Test + public void addHostCreatedInstance_createsSameInstanceTwice() { + final InstanceManager instanceManager = InstanceManager.open(identifier -> {}); + + final Object object = new Object(); + long firstIdentifier = instanceManager.addHostCreatedInstance(object); + long secondIdentifier = instanceManager.addHostCreatedInstance(object); + + assertNotEquals(firstIdentifier, secondIdentifier); + assertTrue(instanceManager.containsInstance(object)); + + instanceManager.close(); + } + @Test public void remove() { final InstanceManager instanceManager = InstanceManager.open(identifier -> {}); diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java new file mode 100644 index 000000000000..9cb4e910dbb8 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java @@ -0,0 +1,221 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.graphics.SurfaceTexture; +import android.util.Size; +import android.view.Surface; +import androidx.camera.core.Preview; +import androidx.camera.core.SurfaceRequest; +import androidx.core.util.Consumer; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionInfo; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesFlutterApi.Reply; +import io.flutter.view.TextureRegistry; +import java.util.concurrent.Executor; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class PreviewTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public Preview mockPreview; + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public TextureRegistry mockTextureRegistry; + @Mock public CameraXProxy mockCameraXProxy; + + InstanceManager testInstanceManager; + + @Before + public void setUp() { + testInstanceManager = spy(InstanceManager.open(identifier -> {})); + } + + @After + public void tearDown() { + testInstanceManager.close(); + } + + @Test + public void create_createsPreviewWithCorrectConfiguration() { + final PreviewHostApiImpl previewHostApi = + new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry); + final Preview.Builder mockPreviewBuilder = mock(Preview.Builder.class); + final int targetRotation = 90; + final int targetResolutionWidth = 10; + final int targetResolutionHeight = 50; + final Long previewIdentifier = 3L; + final GeneratedCameraXLibrary.ResolutionInfo resolutionInfo = + new GeneratedCameraXLibrary.ResolutionInfo.Builder() + .setWidth(Long.valueOf(targetResolutionWidth)) + .setHeight(Long.valueOf(targetResolutionHeight)) + .build(); + + previewHostApi.cameraXProxy = mockCameraXProxy; + when(mockCameraXProxy.createPreviewBuilder()).thenReturn(mockPreviewBuilder); + when(mockPreviewBuilder.build()).thenReturn(mockPreview); + + final ArgumentCaptor sizeCaptor = ArgumentCaptor.forClass(Size.class); + + previewHostApi.create(previewIdentifier, Long.valueOf(targetRotation), resolutionInfo); + + verify(mockPreviewBuilder).setTargetRotation(targetRotation); + verify(mockPreviewBuilder).setTargetResolution(sizeCaptor.capture()); + assertEquals(sizeCaptor.getValue().getWidth(), targetResolutionWidth); + assertEquals(sizeCaptor.getValue().getHeight(), targetResolutionHeight); + verify(mockPreviewBuilder).build(); + verify(testInstanceManager).addDartCreatedInstance(mockPreview, previewIdentifier); + } + + @Test + public void setSurfaceProviderTest_createsSurfaceProviderAndReturnsTextureEntryId() { + final PreviewHostApiImpl previewHostApi = + spy(new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry)); + final TextureRegistry.SurfaceTextureEntry mockSurfaceTextureEntry = + mock(TextureRegistry.SurfaceTextureEntry.class); + final SurfaceTexture mockSurfaceTexture = mock(SurfaceTexture.class); + final Long previewIdentifier = 5L; + final Long surfaceTextureEntryId = 120L; + + previewHostApi.cameraXProxy = mockCameraXProxy; + testInstanceManager.addDartCreatedInstance(mockPreview, previewIdentifier); + + when(mockTextureRegistry.createSurfaceTexture()).thenReturn(mockSurfaceTextureEntry); + when(mockSurfaceTextureEntry.surfaceTexture()).thenReturn(mockSurfaceTexture); + when(mockSurfaceTextureEntry.id()).thenReturn(surfaceTextureEntryId); + + final ArgumentCaptor surfaceProviderCaptor = + ArgumentCaptor.forClass(Preview.SurfaceProvider.class); + final ArgumentCaptor surfaceCaptor = ArgumentCaptor.forClass(Surface.class); + final ArgumentCaptor consumerCaptor = ArgumentCaptor.forClass(Consumer.class); + + // Test that surface provider was set and the surface texture ID was returned. + assertEquals(previewHostApi.setSurfaceProvider(previewIdentifier), surfaceTextureEntryId); + verify(mockPreview).setSurfaceProvider(surfaceProviderCaptor.capture()); + verify(previewHostApi).createSurfaceProvider(mockSurfaceTexture); + } + + @Test + public void createSurfaceProvider_createsExpectedPreviewSurfaceProvider() { + final PreviewHostApiImpl previewHostApi = + new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry); + final SurfaceTexture mockSurfaceTexture = mock(SurfaceTexture.class); + final Surface mockSurface = mock(Surface.class); + final SurfaceRequest mockSurfaceRequest = mock(SurfaceRequest.class); + final SurfaceRequest.Result mockSurfaceRequestResult = mock(SurfaceRequest.Result.class); + final SystemServicesFlutterApiImpl mockSystemServicesFlutterApi = + mock(SystemServicesFlutterApiImpl.class); + final int resolutionWidth = 200; + final int resolutionHeight = 500; + + previewHostApi.cameraXProxy = mockCameraXProxy; + when(mockCameraXProxy.createSurface(mockSurfaceTexture)).thenReturn(mockSurface); + when(mockSurfaceRequest.getResolution()) + .thenReturn(new Size(resolutionWidth, resolutionHeight)); + when(mockCameraXProxy.createSystemServicesFlutterApiImpl(mockBinaryMessenger)) + .thenReturn(mockSystemServicesFlutterApi); + + final ArgumentCaptor surfaceCaptor = ArgumentCaptor.forClass(Surface.class); + final ArgumentCaptor consumerCaptor = ArgumentCaptor.forClass(Consumer.class); + + Preview.SurfaceProvider previewSurfaceProvider = + previewHostApi.createSurfaceProvider(mockSurfaceTexture); + previewSurfaceProvider.onSurfaceRequested(mockSurfaceRequest); + + verify(mockSurfaceTexture).setDefaultBufferSize(resolutionWidth, resolutionHeight); + verify(mockSurfaceRequest) + .provideSurface(surfaceCaptor.capture(), any(Executor.class), consumerCaptor.capture()); + + // Test that the surface derived from the surface texture entry will be provided to the surface request. + assertEquals(surfaceCaptor.getValue(), mockSurface); + + // Test that the Consumer used to handle surface request result releases Flutter surface texture appropriately + // and sends camera errors appropriately. + Consumer capturedConsumer = consumerCaptor.getValue(); + + // Case where Surface should be released. + when(mockSurfaceRequestResult.getResultCode()) + .thenReturn(SurfaceRequest.Result.RESULT_REQUEST_CANCELLED); + capturedConsumer.accept(mockSurfaceRequestResult); + verify(mockSurface).release(); + reset(mockSurface); + + when(mockSurfaceRequestResult.getResultCode()) + .thenReturn(SurfaceRequest.Result.RESULT_REQUEST_CANCELLED); + capturedConsumer.accept(mockSurfaceRequestResult); + verify(mockSurface).release(); + reset(mockSurface); + + when(mockSurfaceRequestResult.getResultCode()) + .thenReturn(SurfaceRequest.Result.RESULT_WILL_NOT_PROVIDE_SURFACE); + capturedConsumer.accept(mockSurfaceRequestResult); + verify(mockSurface).release(); + reset(mockSurface); + + when(mockSurfaceRequestResult.getResultCode()) + .thenReturn(SurfaceRequest.Result.RESULT_SURFACE_USED_SUCCESSFULLY); + capturedConsumer.accept(mockSurfaceRequestResult); + verify(mockSurface).release(); + reset(mockSurface); + + // Case where error must be sent. + when(mockSurfaceRequestResult.getResultCode()) + .thenReturn(SurfaceRequest.Result.RESULT_INVALID_SURFACE); + capturedConsumer.accept(mockSurfaceRequestResult); + verify(mockSurface).release(); + verify(mockSystemServicesFlutterApi).sendCameraError(anyString(), any(Reply.class)); + } + + @Test + public void releaseFlutterSurfaceTexture_makesCallToReleaseFlutterSurfaceTexture() { + final PreviewHostApiImpl previewHostApi = + new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry); + final TextureRegistry.SurfaceTextureEntry mockSurfaceTextureEntry = + mock(TextureRegistry.SurfaceTextureEntry.class); + + previewHostApi.flutterSurfaceTexture = mockSurfaceTextureEntry; + + previewHostApi.releaseFlutterSurfaceTexture(); + verify(mockSurfaceTextureEntry).release(); + } + + @Test + public void getResolutionInfo_makesCallToRetrievePreviewResolutionInfo() { + final PreviewHostApiImpl previewHostApi = + new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry); + final androidx.camera.core.ResolutionInfo mockResolutionInfo = + mock(androidx.camera.core.ResolutionInfo.class); + final Long previewIdentifier = 23L; + final int resolutionWidth = 500; + final int resolutionHeight = 200; + + testInstanceManager.addDartCreatedInstance(mockPreview, previewIdentifier); + when(mockPreview.getResolutionInfo()).thenReturn(mockResolutionInfo); + when(mockResolutionInfo.getResolution()) + .thenReturn(new Size(resolutionWidth, resolutionHeight)); + + ResolutionInfo resolutionInfo = previewHostApi.getResolutionInfo(previewIdentifier); + assertEquals(resolutionInfo.getWidth(), Long.valueOf(resolutionWidth)); + assertEquals(resolutionInfo.getHeight(), Long.valueOf(resolutionHeight)); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ProcessCameraProviderTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ProcessCameraProviderTest.java index 5008e4ef34b0..47b4ed6ad26d 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ProcessCameraProviderTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ProcessCameraProviderTest.java @@ -13,8 +13,12 @@ import static org.mockito.Mockito.when; import android.content.Context; +import androidx.camera.core.Camera; import androidx.camera.core.CameraInfo; +import androidx.camera.core.CameraSelector; +import androidx.camera.core.UseCase; import androidx.camera.lifecycle.ProcessCameraProvider; +import androidx.lifecycle.LifecycleOwner; import androidx.test.core.app.ApplicationProvider; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -99,6 +103,58 @@ public void getAvailableCameraInfosTest() { verify(processCameraProvider).getAvailableCameraInfos(); } + @Test + public void bindToLifecycleTest() { + final ProcessCameraProviderHostApiImpl processCameraProviderHostApi = + new ProcessCameraProviderHostApiImpl(mockBinaryMessenger, testInstanceManager, context); + final Camera mockCamera = mock(Camera.class); + final CameraSelector mockCameraSelector = mock(CameraSelector.class); + final UseCase mockUseCase = mock(UseCase.class); + UseCase[] mockUseCases = new UseCase[] {mockUseCase}; + + LifecycleOwner mockLifecycleOwner = mock(LifecycleOwner.class); + processCameraProviderHostApi.setLifecycleOwner(mockLifecycleOwner); + + testInstanceManager.addDartCreatedInstance(processCameraProvider, 0); + testInstanceManager.addDartCreatedInstance(mockCameraSelector, 1); + testInstanceManager.addDartCreatedInstance(mockUseCase, 2); + testInstanceManager.addDartCreatedInstance(mockCamera, 3); + + when(processCameraProvider.bindToLifecycle( + mockLifecycleOwner, mockCameraSelector, mockUseCases)) + .thenReturn(mockCamera); + + assertEquals( + processCameraProviderHostApi.bindToLifecycle(0L, 1L, Arrays.asList(2L)), Long.valueOf(3)); + verify(processCameraProvider) + .bindToLifecycle(mockLifecycleOwner, mockCameraSelector, mockUseCases); + } + + @Test + public void unbindTest() { + final ProcessCameraProviderHostApiImpl processCameraProviderHostApi = + new ProcessCameraProviderHostApiImpl(mockBinaryMessenger, testInstanceManager, context); + final UseCase mockUseCase = mock(UseCase.class); + UseCase[] mockUseCases = new UseCase[] {mockUseCase}; + + testInstanceManager.addDartCreatedInstance(processCameraProvider, 0); + testInstanceManager.addDartCreatedInstance(mockUseCase, 1); + + processCameraProviderHostApi.unbind(0L, Arrays.asList(1L)); + verify(processCameraProvider).unbind(mockUseCases); + } + + @Test + public void unbindAllTest() { + final ProcessCameraProviderHostApiImpl processCameraProviderHostApi = + new ProcessCameraProviderHostApiImpl(mockBinaryMessenger, testInstanceManager, context); + + testInstanceManager.addDartCreatedInstance(processCameraProvider, 0); + + processCameraProviderHostApi.unbindAll(0L); + verify(processCameraProvider).unbindAll(); + } + @Test public void flutterApiCreateTest() { final ProcessCameraProviderFlutterApiImpl spyFlutterApi = diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java new file mode 100644 index 000000000000..eb36c452ec3b --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java @@ -0,0 +1,138 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.CameraPermissionsManager.PermissionsRegistry; +import io.flutter.plugins.camerax.CameraPermissionsManager.ResultCallback; +import io.flutter.plugins.camerax.DeviceOrientationManager.DeviceOrientationChangeCallback; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraPermissionsErrorData; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.Result; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesFlutterApi.Reply; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class SystemServicesTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public InstanceManager mockInstanceManager; + + @Test + public void requestCameraPermissionsTest() { + final SystemServicesHostApiImpl systemServicesHostApi = + new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager); + final CameraXProxy mockCameraXProxy = mock(CameraXProxy.class); + final CameraPermissionsManager mockCameraPermissionsManager = + mock(CameraPermissionsManager.class); + final Activity mockActivity = mock(Activity.class); + final PermissionsRegistry mockPermissionsRegistry = mock(PermissionsRegistry.class); + final Result mockResult = mock(Result.class); + final Boolean enableAudio = false; + + systemServicesHostApi.cameraXProxy = mockCameraXProxy; + systemServicesHostApi.setActivity(mockActivity); + systemServicesHostApi.setPermissionsRegistry(mockPermissionsRegistry); + when(mockCameraXProxy.createCameraPermissionsManager()) + .thenReturn(mockCameraPermissionsManager); + + final ArgumentCaptor resultCallbackCaptor = + ArgumentCaptor.forClass(ResultCallback.class); + + systemServicesHostApi.requestCameraPermissions(enableAudio, mockResult); + + // Test camera permissions are requested. + verify(mockCameraPermissionsManager) + .requestPermissions( + eq(mockActivity), + eq(mockPermissionsRegistry), + eq(enableAudio), + resultCallbackCaptor.capture()); + + ResultCallback resultCallback = (ResultCallback) resultCallbackCaptor.getValue(); + + // Test no error data is sent upon permissions request success. + resultCallback.onResult(null, null); + verify(mockResult).success(null); + + // Test expected error data is sent upon permissions request failure. + final String testErrorCode = "TestErrorCode"; + final String testErrorDescription = "Test error description."; + + final ArgumentCaptor cameraPermissionsErrorDataCaptor = + ArgumentCaptor.forClass(CameraPermissionsErrorData.class); + + resultCallback.onResult(testErrorCode, testErrorDescription); + verify(mockResult, times(2)).success(cameraPermissionsErrorDataCaptor.capture()); + + CameraPermissionsErrorData cameraPermissionsErrorData = + cameraPermissionsErrorDataCaptor.getValue(); + assertEquals(cameraPermissionsErrorData.getErrorCode(), testErrorCode); + assertEquals(cameraPermissionsErrorData.getDescription(), testErrorDescription); + } + + @Test + public void deviceOrientationChangeTest() { + final SystemServicesHostApiImpl systemServicesHostApi = + new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager); + final CameraXProxy mockCameraXProxy = mock(CameraXProxy.class); + final Activity mockActivity = mock(Activity.class); + final DeviceOrientationManager mockDeviceOrientationManager = + mock(DeviceOrientationManager.class); + final Boolean isFrontFacing = true; + final int sensorOrientation = 90; + + SystemServicesFlutterApiImpl systemServicesFlutterApi = + mock(SystemServicesFlutterApiImpl.class); + systemServicesHostApi.systemServicesFlutterApi = systemServicesFlutterApi; + + systemServicesHostApi.cameraXProxy = mockCameraXProxy; + systemServicesHostApi.setActivity(mockActivity); + when(mockCameraXProxy.createDeviceOrientationManager( + eq(mockActivity), + eq(isFrontFacing), + eq(sensorOrientation), + any(DeviceOrientationChangeCallback.class))) + .thenReturn(mockDeviceOrientationManager); + + final ArgumentCaptor deviceOrientationChangeCallbackCaptor = + ArgumentCaptor.forClass(DeviceOrientationChangeCallback.class); + + systemServicesHostApi.startListeningForDeviceOrientationChange( + isFrontFacing, Long.valueOf(sensorOrientation)); + + // Test callback method defined in Flutter API is called when device orientation changes. + verify(mockCameraXProxy) + .createDeviceOrientationManager( + eq(mockActivity), + eq(isFrontFacing), + eq(sensorOrientation), + deviceOrientationChangeCallbackCaptor.capture()); + DeviceOrientationChangeCallback deviceOrientationChangeCallback = + deviceOrientationChangeCallbackCaptor.getValue(); + + deviceOrientationChangeCallback.onChange(DeviceOrientation.PORTRAIT_DOWN); + verify(systemServicesFlutterApi) + .sendDeviceOrientationChangedEvent( + eq(DeviceOrientation.PORTRAIT_DOWN.toString()), any(Reply.class)); + + // Test that the DeviceOrientationManager starts listening for device orientation changes. + verify(mockDeviceOrientationManager).start(); + } +} diff --git a/packages/camera/camera_android_camerax/example/android/build.gradle b/packages/camera/camera_android_camerax/example/android/build.gradle index 20411f5f31a9..8640e4de86a1 100644 --- a/packages/camera/camera_android_camerax/example/android/build.gradle +++ b/packages/camera/camera_android_camerax/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.7.10' + ext.kotlin_version = '1.8.0' repositories { google() mavenCentral() diff --git a/packages/camera/camera_android_camerax/example/android/gradle.properties b/packages/camera/camera_android_camerax/example/android/gradle.properties index 94adc3a3f97a..598d13fee446 100644 --- a/packages/camera/camera_android_camerax/example/android/gradle.properties +++ b/packages/camera/camera_android_camerax/example/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/packages/camera/camera_android_camerax/example/integration_test/integration_test.dart b/packages/camera/camera_android_camerax/example/integration_test/integration_test.dart index 2b82b4bda5e4..b05d14a9cc79 100644 --- a/packages/camera/camera_android_camerax/example/integration_test/integration_test.dart +++ b/packages/camera/camera_android_camerax/example/integration_test/integration_test.dart @@ -2,11 +2,27 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:camera_android_camerax/camera_android_camerax.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('placeholder test', (WidgetTester tester) async {}); + setUpAll(() async { + CameraPlatform.instance = AndroidCameraCameraX(); + }); + + testWidgets('availableCameras only supports valid back or front cameras', + (WidgetTester tester) async { + final List availableCameras = + await CameraPlatform.instance.availableCameras(); + + for (final CameraDescription cameraDescription in availableCameras) { + expect( + cameraDescription.lensDirection, isNot(CameraLensDirection.external)); + expect(cameraDescription.sensorOrientation, anyOf(0, 90, 180, 270)); + } + }); } diff --git a/packages/camera/camera_android_camerax/example/lib/camera_controller.dart b/packages/camera/camera_android_camerax/example/lib/camera_controller.dart new file mode 100644 index 000000000000..b1b5e9d4ceb9 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/lib/camera_controller.dart @@ -0,0 +1,957 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:collection'; +import 'dart:math'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'camera_image.dart'; + +/// Signature for a callback receiving the a camera image. +/// +/// This is used by [CameraController.startImageStream]. +// TODO(stuartmorgan): Fix this naming the next time there's a breaking change +// to this package. +// ignore: camel_case_types +typedef onLatestImageAvailable = Function(CameraImage image); + +/// Completes with a list of available cameras. +/// +/// May throw a [CameraException]. +Future> availableCameras() async { + return CameraPlatform.instance.availableCameras(); +} + +// TODO(stuartmorgan): Remove this once the package requires 2.10, where the +// dart:async `unawaited` accepts a nullable future. +void _unawaited(Future? future) {} + +/// The state of a [CameraController]. +class CameraValue { + /// Creates a new camera controller state. + const CameraValue({ + required this.isInitialized, + this.errorDescription, + this.previewSize, + required this.isRecordingVideo, + required this.isTakingPicture, + required this.isStreamingImages, + required bool isRecordingPaused, + required this.flashMode, + required this.exposureMode, + required this.focusMode, + required this.exposurePointSupported, + required this.focusPointSupported, + required this.deviceOrientation, + this.lockedCaptureOrientation, + this.recordingOrientation, + this.isPreviewPaused = false, + this.previewPauseOrientation, + }) : _isRecordingPaused = isRecordingPaused; + + /// Creates a new camera controller state for an uninitialized controller. + const CameraValue.uninitialized() + : this( + isInitialized: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + isRecordingPaused: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + exposurePointSupported: false, + focusMode: FocusMode.auto, + focusPointSupported: false, + deviceOrientation: DeviceOrientation.portraitUp, + isPreviewPaused: false, + ); + + /// True after [CameraController.initialize] has completed successfully. + final bool isInitialized; + + /// True when a picture capture request has been sent but as not yet returned. + final bool isTakingPicture; + + /// True when the camera is recording (not the same as previewing). + final bool isRecordingVideo; + + /// True when images from the camera are being streamed. + final bool isStreamingImages; + + final bool _isRecordingPaused; + + /// True when the preview widget has been paused manually. + final bool isPreviewPaused; + + /// Set to the orientation the preview was paused in, if it is currently paused. + final DeviceOrientation? previewPauseOrientation; + + /// True when camera [isRecordingVideo] and recording is paused. + bool get isRecordingPaused => isRecordingVideo && _isRecordingPaused; + + /// Description of an error state. + /// + /// This is null while the controller is not in an error state. + /// When [hasError] is true this contains the error description. + final String? errorDescription; + + /// The size of the preview in pixels. + /// + /// Is `null` until [isInitialized] is `true`. + final Size? previewSize; + + /// Convenience getter for `previewSize.width / previewSize.height`. + /// + /// Can only be called when [initialize] is done. + double get aspectRatio => previewSize!.width / previewSize!.height; + + /// Whether the controller is in an error state. + /// + /// When true [errorDescription] describes the error. + bool get hasError => errorDescription != null; + + /// The flash mode the camera is currently set to. + final FlashMode flashMode; + + /// The exposure mode the camera is currently set to. + final ExposureMode exposureMode; + + /// The focus mode the camera is currently set to. + final FocusMode focusMode; + + /// Whether setting the exposure point is supported. + final bool exposurePointSupported; + + /// Whether setting the focus point is supported. + final bool focusPointSupported; + + /// The current device UI orientation. + final DeviceOrientation deviceOrientation; + + /// The currently locked capture orientation. + final DeviceOrientation? lockedCaptureOrientation; + + /// Whether the capture orientation is currently locked. + bool get isCaptureOrientationLocked => lockedCaptureOrientation != null; + + /// The orientation of the currently running video recording. + final DeviceOrientation? recordingOrientation; + + /// Creates a modified copy of the object. + /// + /// Explicitly specified fields get the specified value, all other fields get + /// the same value of the current object. + CameraValue copyWith({ + bool? isInitialized, + bool? isRecordingVideo, + bool? isTakingPicture, + bool? isStreamingImages, + String? errorDescription, + Size? previewSize, + bool? isRecordingPaused, + FlashMode? flashMode, + ExposureMode? exposureMode, + FocusMode? focusMode, + bool? exposurePointSupported, + bool? focusPointSupported, + DeviceOrientation? deviceOrientation, + Optional? lockedCaptureOrientation, + Optional? recordingOrientation, + bool? isPreviewPaused, + Optional? previewPauseOrientation, + }) { + return CameraValue( + isInitialized: isInitialized ?? this.isInitialized, + errorDescription: errorDescription, + previewSize: previewSize ?? this.previewSize, + isRecordingVideo: isRecordingVideo ?? this.isRecordingVideo, + isTakingPicture: isTakingPicture ?? this.isTakingPicture, + isStreamingImages: isStreamingImages ?? this.isStreamingImages, + isRecordingPaused: isRecordingPaused ?? _isRecordingPaused, + flashMode: flashMode ?? this.flashMode, + exposureMode: exposureMode ?? this.exposureMode, + focusMode: focusMode ?? this.focusMode, + exposurePointSupported: + exposurePointSupported ?? this.exposurePointSupported, + focusPointSupported: focusPointSupported ?? this.focusPointSupported, + deviceOrientation: deviceOrientation ?? this.deviceOrientation, + lockedCaptureOrientation: lockedCaptureOrientation == null + ? this.lockedCaptureOrientation + : lockedCaptureOrientation.orNull, + recordingOrientation: recordingOrientation == null + ? this.recordingOrientation + : recordingOrientation.orNull, + isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused, + previewPauseOrientation: previewPauseOrientation == null + ? this.previewPauseOrientation + : previewPauseOrientation.orNull, + ); + } + + @override + String toString() { + return '${objectRuntimeType(this, 'CameraValue')}(' + 'isRecordingVideo: $isRecordingVideo, ' + 'isInitialized: $isInitialized, ' + 'errorDescription: $errorDescription, ' + 'previewSize: $previewSize, ' + 'isStreamingImages: $isStreamingImages, ' + 'flashMode: $flashMode, ' + 'exposureMode: $exposureMode, ' + 'focusMode: $focusMode, ' + 'exposurePointSupported: $exposurePointSupported, ' + 'focusPointSupported: $focusPointSupported, ' + 'deviceOrientation: $deviceOrientation, ' + 'lockedCaptureOrientation: $lockedCaptureOrientation, ' + 'recordingOrientation: $recordingOrientation, ' + 'isPreviewPaused: $isPreviewPaused, ' + 'previewPausedOrientation: $previewPauseOrientation)'; + } +} + +/// Controls a device camera. +/// +/// Use [availableCameras] to get a list of available cameras. +/// +/// Before using a [CameraController] a call to [initialize] must complete. +/// +/// To show the camera preview on the screen use a [CameraPreview] widget. +class CameraController extends ValueNotifier { + /// Creates a new camera controller in an uninitialized state. + CameraController( + this.description, + this.resolutionPreset, { + this.enableAudio = true, + this.imageFormatGroup, + }) : super(const CameraValue.uninitialized()); + + /// The properties of the camera device controlled by this controller. + final CameraDescription description; + + /// The resolution this controller is targeting. + /// + /// This resolution preset is not guaranteed to be available on the device, + /// if unavailable a lower resolution will be used. + /// + /// See also: [ResolutionPreset]. + final ResolutionPreset resolutionPreset; + + /// Whether to include audio when recording a video. + final bool enableAudio; + + /// The [ImageFormatGroup] describes the output of the raw image format. + /// + /// When null the imageFormat will fallback to the platforms default. + final ImageFormatGroup? imageFormatGroup; + + /// The id of a camera that hasn't been initialized. + @visibleForTesting + static const int kUninitializedCameraId = -1; + int _cameraId = kUninitializedCameraId; + + bool _isDisposed = false; + StreamSubscription? _imageStreamSubscription; + FutureOr? _initCalled; + StreamSubscription? + _deviceOrientationSubscription; + + /// Checks whether [CameraController.dispose] has completed successfully. + /// + /// This is a no-op when asserts are disabled. + void debugCheckIsDisposed() { + assert(_isDisposed); + } + + /// The camera identifier with which the controller is associated. + int get cameraId => _cameraId; + + /// Initializes the camera on the device. + /// + /// Throws a [CameraException] if the initialization fails. + Future initialize() async { + if (_isDisposed) { + throw CameraException( + 'Disposed CameraController', + 'initialize was called on a disposed CameraController', + ); + } + try { + final Completer initializeCompleter = + Completer(); + + _deviceOrientationSubscription = CameraPlatform.instance + .onDeviceOrientationChanged() + .listen((DeviceOrientationChangedEvent event) { + value = value.copyWith( + deviceOrientation: event.orientation, + ); + }); + + _cameraId = await CameraPlatform.instance.createCamera( + description, + resolutionPreset, + enableAudio: enableAudio, + ); + + _unawaited(CameraPlatform.instance + .onCameraInitialized(_cameraId) + .first + .then((CameraInitializedEvent event) { + initializeCompleter.complete(event); + })); + + await CameraPlatform.instance.initializeCamera( + _cameraId, + imageFormatGroup: imageFormatGroup ?? ImageFormatGroup.unknown, + ); + + value = value.copyWith( + isInitialized: true, + previewSize: await initializeCompleter.future + .then((CameraInitializedEvent event) => Size( + event.previewWidth, + event.previewHeight, + )), + exposureMode: await initializeCompleter.future + .then((CameraInitializedEvent event) => event.exposureMode), + focusMode: await initializeCompleter.future + .then((CameraInitializedEvent event) => event.focusMode), + exposurePointSupported: await initializeCompleter.future.then( + (CameraInitializedEvent event) => event.exposurePointSupported), + focusPointSupported: await initializeCompleter.future + .then((CameraInitializedEvent event) => event.focusPointSupported), + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + + _initCalled = true; + } + + /// Prepare the capture session for video recording. + /// + /// Use of this method is optional, but it may be called for performance + /// reasons on iOS. + /// + /// Preparing audio can cause a minor delay in the CameraPreview view on iOS. + /// If video recording is intended, calling this early eliminates this delay + /// that would otherwise be experienced when video recording is started. + /// This operation is a no-op on Android and Web. + /// + /// Throws a [CameraException] if the prepare fails. + Future prepareForVideoRecording() async { + await CameraPlatform.instance.prepareForVideoRecording(); + } + + /// Pauses the current camera preview + Future pausePreview() async { + if (value.isPreviewPaused) { + return; + } + try { + await CameraPlatform.instance.pausePreview(_cameraId); + value = value.copyWith( + isPreviewPaused: true, + previewPauseOrientation: Optional.of( + value.lockedCaptureOrientation ?? value.deviceOrientation)); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Resumes the current camera preview + Future resumePreview() async { + if (!value.isPreviewPaused) { + return; + } + try { + await CameraPlatform.instance.resumePreview(_cameraId); + value = value.copyWith( + isPreviewPaused: false, + previewPauseOrientation: const Optional.absent()); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Captures an image and returns the file where it was saved. + /// + /// Throws a [CameraException] if the capture fails. + Future takePicture() async { + _throwIfNotInitialized('takePicture'); + if (value.isTakingPicture) { + throw CameraException( + 'Previous capture has not returned yet.', + 'takePicture was called before the previous capture returned.', + ); + } + try { + value = value.copyWith(isTakingPicture: true); + final XFile file = await CameraPlatform.instance.takePicture(_cameraId); + value = value.copyWith(isTakingPicture: false); + return file; + } on PlatformException catch (e) { + value = value.copyWith(isTakingPicture: false); + throw CameraException(e.code, e.message); + } + } + + /// Start streaming images from platform camera. + /// + /// Settings for capturing images on iOS and Android is set to always use the + /// latest image available from the camera and will drop all other images. + /// + /// When running continuously with [CameraPreview] widget, this function runs + /// best with [ResolutionPreset.low]. Running on [ResolutionPreset.high] can + /// have significant frame rate drops for [CameraPreview] on lower end + /// devices. + /// + /// Throws a [CameraException] if image streaming or video recording has + /// already started. + /// + /// The `startImageStream` method is only available on Android and iOS (other + /// platforms won't be supported in current setup). + /// + // TODO(bmparr): Add settings for resolution and fps. + Future startImageStream(onLatestImageAvailable onAvailable) async { + assert(defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS); + _throwIfNotInitialized('startImageStream'); + if (value.isRecordingVideo) { + throw CameraException( + 'A video recording is already started.', + 'startImageStream was called while a video is being recorded.', + ); + } + if (value.isStreamingImages) { + throw CameraException( + 'A camera has started streaming images.', + 'startImageStream was called while a camera was streaming images.', + ); + } + + try { + _imageStreamSubscription = CameraPlatform.instance + .onStreamedFrameAvailable(_cameraId) + .listen((CameraImageData imageData) { + onAvailable(CameraImage.fromPlatformInterface(imageData)); + }); + value = value.copyWith(isStreamingImages: true); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Stop streaming images from platform camera. + /// + /// Throws a [CameraException] if image streaming was not started or video + /// recording was started. + /// + /// The `stopImageStream` method is only available on Android and iOS (other + /// platforms won't be supported in current setup). + Future stopImageStream() async { + assert(defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS); + _throwIfNotInitialized('stopImageStream'); + if (!value.isStreamingImages) { + throw CameraException( + 'No camera is streaming images', + 'stopImageStream was called when no camera is streaming images.', + ); + } + + try { + value = value.copyWith(isStreamingImages: false); + await _imageStreamSubscription?.cancel(); + _imageStreamSubscription = null; + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Start a video recording. + /// + /// You may optionally pass an [onAvailable] callback to also have the + /// video frames streamed to this callback. + /// + /// The video is returned as a [XFile] after calling [stopVideoRecording]. + /// Throws a [CameraException] if the capture fails. + Future startVideoRecording( + {onLatestImageAvailable? onAvailable}) async { + _throwIfNotInitialized('startVideoRecording'); + if (value.isRecordingVideo) { + throw CameraException( + 'A video recording is already started.', + 'startVideoRecording was called when a recording is already started.', + ); + } + + Function(CameraImageData image)? streamCallback; + if (onAvailable != null) { + streamCallback = (CameraImageData imageData) { + onAvailable(CameraImage.fromPlatformInterface(imageData)); + }; + } + + try { + await CameraPlatform.instance.startVideoCapturing( + VideoCaptureOptions(_cameraId, streamCallback: streamCallback)); + value = value.copyWith( + isRecordingVideo: true, + isRecordingPaused: false, + recordingOrientation: Optional.of( + value.lockedCaptureOrientation ?? value.deviceOrientation), + isStreamingImages: onAvailable != null); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Stops the video recording and returns the file where it was saved. + /// + /// Throws a [CameraException] if the capture failed. + Future stopVideoRecording() async { + _throwIfNotInitialized('stopVideoRecording'); + if (!value.isRecordingVideo) { + throw CameraException( + 'No video is recording', + 'stopVideoRecording was called when no video is recording.', + ); + } + + if (value.isStreamingImages) { + stopImageStream(); + } + + try { + final XFile file = + await CameraPlatform.instance.stopVideoRecording(_cameraId); + value = value.copyWith( + isRecordingVideo: false, + recordingOrientation: const Optional.absent(), + ); + return file; + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Pause video recording. + /// + /// This feature is only available on iOS and Android sdk 24+. + Future pauseVideoRecording() async { + _throwIfNotInitialized('pauseVideoRecording'); + if (!value.isRecordingVideo) { + throw CameraException( + 'No video is recording', + 'pauseVideoRecording was called when no video is recording.', + ); + } + try { + await CameraPlatform.instance.pauseVideoRecording(_cameraId); + value = value.copyWith(isRecordingPaused: true); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Resume video recording after pausing. + /// + /// This feature is only available on iOS and Android sdk 24+. + Future resumeVideoRecording() async { + _throwIfNotInitialized('resumeVideoRecording'); + if (!value.isRecordingVideo) { + throw CameraException( + 'No video is recording', + 'resumeVideoRecording was called when no video is recording.', + ); + } + try { + await CameraPlatform.instance.resumeVideoRecording(_cameraId); + value = value.copyWith(isRecordingPaused: false); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Returns a widget showing a live camera preview. + Widget buildPreview() { + _throwIfNotInitialized('buildPreview'); + try { + return CameraPlatform.instance.buildPreview(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the maximum supported zoom level for the selected camera. + Future getMaxZoomLevel() { + _throwIfNotInitialized('getMaxZoomLevel'); + try { + return CameraPlatform.instance.getMaxZoomLevel(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the minimum supported zoom level for the selected camera. + Future getMinZoomLevel() { + _throwIfNotInitialized('getMinZoomLevel'); + try { + return CameraPlatform.instance.getMinZoomLevel(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Set the zoom level for the selected camera. + /// + /// The supplied [zoom] value should be between 1.0 and the maximum supported + /// zoom level returned by the `getMaxZoomLevel`. Throws an `CameraException` + /// when an illegal zoom level is suplied. + Future setZoomLevel(double zoom) { + _throwIfNotInitialized('setZoomLevel'); + try { + return CameraPlatform.instance.setZoomLevel(_cameraId, zoom); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the flash mode for taking pictures. + Future setFlashMode(FlashMode mode) async { + try { + await CameraPlatform.instance.setFlashMode(_cameraId, mode); + value = value.copyWith(flashMode: mode); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the exposure mode for taking pictures. + Future setExposureMode(ExposureMode mode) async { + try { + await CameraPlatform.instance.setExposureMode(_cameraId, mode); + value = value.copyWith(exposureMode: mode); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the exposure point for automatically determining the exposure value. + /// + /// Supplying a `null` value will reset the exposure point to it's default + /// value. + Future setExposurePoint(Offset? point) async { + if (point != null && + (point.dx < 0 || point.dx > 1 || point.dy < 0 || point.dy > 1)) { + throw ArgumentError( + 'The values of point should be anywhere between (0,0) and (1,1).'); + } + + try { + await CameraPlatform.instance.setExposurePoint( + _cameraId, + point == null + ? null + : Point( + point.dx, + point.dy, + ), + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the minimum supported exposure offset for the selected camera in EV units. + Future getMinExposureOffset() async { + _throwIfNotInitialized('getMinExposureOffset'); + try { + return CameraPlatform.instance.getMinExposureOffset(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the maximum supported exposure offset for the selected camera in EV units. + Future getMaxExposureOffset() async { + _throwIfNotInitialized('getMaxExposureOffset'); + try { + return CameraPlatform.instance.getMaxExposureOffset(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the supported step size for exposure offset for the selected camera in EV units. + /// + /// Returns 0 when the camera supports using a free value without stepping. + Future getExposureOffsetStepSize() async { + _throwIfNotInitialized('getExposureOffsetStepSize'); + try { + return CameraPlatform.instance.getExposureOffsetStepSize(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the exposure offset for the selected camera. + /// + /// The supplied [offset] value should be in EV units. 1 EV unit represents a + /// doubling in brightness. It should be between the minimum and maximum offsets + /// obtained through `getMinExposureOffset` and `getMaxExposureOffset` respectively. + /// Throws a `CameraException` when an illegal offset is supplied. + /// + /// When the supplied [offset] value does not align with the step size obtained + /// through `getExposureStepSize`, it will automatically be rounded to the nearest step. + /// + /// Returns the (rounded) offset value that was set. + Future setExposureOffset(double offset) async { + _throwIfNotInitialized('setExposureOffset'); + // Check if offset is in range + final List range = await Future.wait( + >[getMinExposureOffset(), getMaxExposureOffset()]); + if (offset < range[0] || offset > range[1]) { + throw CameraException( + 'exposureOffsetOutOfBounds', + 'The provided exposure offset was outside the supported range for this device.', + ); + } + + // Round to the closest step if needed + final double stepSize = await getExposureOffsetStepSize(); + if (stepSize > 0) { + final double inv = 1.0 / stepSize; + double roundedOffset = (offset * inv).roundToDouble() / inv; + if (roundedOffset > range[1]) { + roundedOffset = (offset * inv).floorToDouble() / inv; + } else if (roundedOffset < range[0]) { + roundedOffset = (offset * inv).ceilToDouble() / inv; + } + offset = roundedOffset; + } + + try { + return CameraPlatform.instance.setExposureOffset(_cameraId, offset); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Locks the capture orientation. + /// + /// If [orientation] is omitted, the current device orientation is used. + Future lockCaptureOrientation([DeviceOrientation? orientation]) async { + try { + await CameraPlatform.instance.lockCaptureOrientation( + _cameraId, orientation ?? value.deviceOrientation); + value = value.copyWith( + lockedCaptureOrientation: Optional.of( + orientation ?? value.deviceOrientation)); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the focus mode for taking pictures. + Future setFocusMode(FocusMode mode) async { + try { + await CameraPlatform.instance.setFocusMode(_cameraId, mode); + value = value.copyWith(focusMode: mode); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Unlocks the capture orientation. + Future unlockCaptureOrientation() async { + try { + await CameraPlatform.instance.unlockCaptureOrientation(_cameraId); + value = value.copyWith( + lockedCaptureOrientation: const Optional.absent()); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the focus point for automatically determining the focus value. + /// + /// Supplying a `null` value will reset the focus point to it's default + /// value. + Future setFocusPoint(Offset? point) async { + if (point != null && + (point.dx < 0 || point.dx > 1 || point.dy < 0 || point.dy > 1)) { + throw ArgumentError( + 'The values of point should be anywhere between (0,0) and (1,1).'); + } + try { + await CameraPlatform.instance.setFocusPoint( + _cameraId, + point == null + ? null + : Point( + point.dx, + point.dy, + ), + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Releases the resources of this camera. + @override + Future dispose() async { + if (_isDisposed) { + return; + } + _unawaited(_deviceOrientationSubscription?.cancel()); + _isDisposed = true; + super.dispose(); + if (_initCalled != null) { + await _initCalled; + await CameraPlatform.instance.dispose(_cameraId); + } + } + + void _throwIfNotInitialized(String functionName) { + if (!value.isInitialized) { + throw CameraException( + 'Uninitialized CameraController', + '$functionName() was called on an uninitialized CameraController.', + ); + } + if (_isDisposed) { + throw CameraException( + 'Disposed CameraController', + '$functionName() was called on a disposed CameraController.', + ); + } + } + + @override + void removeListener(VoidCallback listener) { + // Prevent ValueListenableBuilder in CameraPreview widget from causing an + // exception to be thrown by attempting to remove its own listener after + // the controller has already been disposed. + if (!_isDisposed) { + super.removeListener(listener); + } + } +} + +/// A value that might be absent. +/// +/// Used to represent [DeviceOrientation]s that are optional but also able +/// to be cleared. +@immutable +class Optional extends IterableBase { + /// Constructs an empty Optional. + const Optional.absent() : _value = null; + + /// Constructs an Optional of the given [value]. + /// + /// Throws [ArgumentError] if [value] is null. + Optional.of(T value) : _value = value { + // TODO(cbracken): Delete and make this ctor const once mixed-mode + // execution is no longer around. + ArgumentError.checkNotNull(value); + } + + /// Constructs an Optional of the given [value]. + /// + /// If [value] is null, returns [absent()]. + const Optional.fromNullable(T? value) : _value = value; + + final T? _value; + + /// True when this optional contains a value. + bool get isPresent => _value != null; + + /// True when this optional contains no value. + bool get isNotPresent => _value == null; + + /// Gets the Optional value. + /// + /// Throws [StateError] if [value] is null. + T get value { + if (_value == null) { + throw StateError('value called on absent Optional.'); + } + return _value!; + } + + /// Executes a function if the Optional value is present. + void ifPresent(void Function(T value) ifPresent) { + if (isPresent) { + ifPresent(_value as T); + } + } + + /// Execution a function if the Optional value is absent. + void ifAbsent(void Function() ifAbsent) { + if (!isPresent) { + ifAbsent(); + } + } + + /// Gets the Optional value with a default. + /// + /// The default is returned if the Optional is [absent()]. + /// + /// Throws [ArgumentError] if [defaultValue] is null. + T or(T defaultValue) { + return _value ?? defaultValue; + } + + /// Gets the Optional value, or `null` if there is none. + T? get orNull => _value; + + /// Transforms the Optional value. + /// + /// If the Optional is [absent()], returns [absent()] without applying the transformer. + /// + /// The transformer must not return `null`. If it does, an [ArgumentError] is thrown. + Optional transform(S Function(T value) transformer) { + return _value == null + ? Optional.absent() + : Optional.of(transformer(_value as T)); + } + + /// Transforms the Optional value. + /// + /// If the Optional is [absent()], returns [absent()] without applying the transformer. + /// + /// Returns [absent()] if the transformer returns `null`. + Optional transformNullable(S? Function(T value) transformer) { + return _value == null + ? Optional.absent() + : Optional.fromNullable(transformer(_value as T)); + } + + @override + Iterator get iterator => + isPresent ? [_value as T].iterator : Iterable.empty().iterator; + + /// Delegates to the underlying [value] hashCode. + @override + int get hashCode => _value.hashCode; + + /// Delegates to the underlying [value] operator==. + @override + bool operator ==(Object o) => o is Optional && o._value == _value; + + @override + String toString() { + return _value == null + ? 'Optional { absent }' + : 'Optional { value: $_value }'; + } +} diff --git a/packages/camera/camera_android_camerax/example/lib/camera_image.dart b/packages/camera/camera_android_camerax/example/lib/camera_image.dart new file mode 100644 index 000000000000..bfcad6626dd6 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/lib/camera_image.dart @@ -0,0 +1,177 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231) +// ignore: unnecessary_import +import 'dart:typed_data'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; + +// TODO(stuartmorgan): Remove all of these classes in a breaking change, and +// vend the platform interface versions directly. See +// https://github.com/flutter/flutter/issues/104188 + +/// A single color plane of image data. +/// +/// The number and meaning of the planes in an image are determined by the +/// format of the Image. +class Plane { + Plane._fromPlatformInterface(CameraImagePlane plane) + : bytes = plane.bytes, + bytesPerPixel = plane.bytesPerPixel, + bytesPerRow = plane.bytesPerRow, + height = plane.height, + width = plane.width; + + // Only used by the deprecated codepath that's kept to avoid breaking changes. + // Never called by the plugin itself. + Plane._fromPlatformData(Map data) + : bytes = data['bytes'] as Uint8List, + bytesPerPixel = data['bytesPerPixel'] as int?, + bytesPerRow = data['bytesPerRow'] as int, + height = data['height'] as int?, + width = data['width'] as int?; + + /// Bytes representing this plane. + final Uint8List bytes; + + /// The distance between adjacent pixel samples on Android, in bytes. + /// + /// Will be `null` on iOS. + final int? bytesPerPixel; + + /// The row stride for this color plane, in bytes. + final int bytesPerRow; + + /// Height of the pixel buffer on iOS. + /// + /// Will be `null` on Android + final int? height; + + /// Width of the pixel buffer on iOS. + /// + /// Will be `null` on Android. + final int? width; +} + +/// Describes how pixels are represented in an image. +class ImageFormat { + ImageFormat._fromPlatformInterface(CameraImageFormat format) + : group = format.group, + raw = format.raw; + + // Only used by the deprecated codepath that's kept to avoid breaking changes. + // Never called by the plugin itself. + ImageFormat._fromPlatformData(this.raw) : group = _asImageFormatGroup(raw); + + /// Describes the format group the raw image format falls into. + final ImageFormatGroup group; + + /// Raw version of the format from the Android or iOS platform. + /// + /// On Android, this is an `int` from class `android.graphics.ImageFormat`. See + /// https://developer.android.com/reference/android/graphics/ImageFormat + /// + /// On iOS, this is a `FourCharCode` constant from Pixel Format Identifiers. + /// See https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers?language=objc + final dynamic raw; +} + +// Only used by the deprecated codepath that's kept to avoid breaking changes. +// Never called by the plugin itself. +ImageFormatGroup _asImageFormatGroup(dynamic rawFormat) { + if (defaultTargetPlatform == TargetPlatform.android) { + switch (rawFormat) { + // android.graphics.ImageFormat.YUV_420_888 + case 35: + return ImageFormatGroup.yuv420; + // android.graphics.ImageFormat.JPEG + case 256: + return ImageFormatGroup.jpeg; + } + } + + if (defaultTargetPlatform == TargetPlatform.iOS) { + switch (rawFormat) { + // kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange + case 875704438: + return ImageFormatGroup.yuv420; + // kCVPixelFormatType_32BGRA + case 1111970369: + return ImageFormatGroup.bgra8888; + } + } + + return ImageFormatGroup.unknown; +} + +/// A single complete image buffer from the platform camera. +/// +/// This class allows for direct application access to the pixel data of an +/// Image through one or more [Uint8List]. Each buffer is encapsulated in a +/// [Plane] that describes the layout of the pixel data in that plane. The +/// [CameraImage] is not directly usable as a UI resource. +/// +/// Although not all image formats are planar on iOS, we treat 1-dimensional +/// images as single planar images. +class CameraImage { + /// Creates a [CameraImage] from the platform interface version. + CameraImage.fromPlatformInterface(CameraImageData data) + : format = ImageFormat._fromPlatformInterface(data.format), + height = data.height, + width = data.width, + planes = List.unmodifiable(data.planes.map( + (CameraImagePlane plane) => Plane._fromPlatformInterface(plane))), + lensAperture = data.lensAperture, + sensorExposureTime = data.sensorExposureTime, + sensorSensitivity = data.sensorSensitivity; + + /// Creates a [CameraImage] from method channel data. + @Deprecated('Use fromPlatformInterface instead') + CameraImage.fromPlatformData(Map data) + : format = ImageFormat._fromPlatformData(data['format']), + height = data['height'] as int, + width = data['width'] as int, + lensAperture = data['lensAperture'] as double?, + sensorExposureTime = data['sensorExposureTime'] as int?, + sensorSensitivity = data['sensorSensitivity'] as double?, + planes = List.unmodifiable((data['planes'] as List) + .map((dynamic planeData) => + Plane._fromPlatformData(planeData as Map))); + + /// Format of the image provided. + /// + /// Determines the number of planes needed to represent the image, and + /// the general layout of the pixel data in each [Uint8List]. + final ImageFormat format; + + /// Height of the image in pixels. + /// + /// For formats where some color channels are subsampled, this is the height + /// of the largest-resolution plane. + final int height; + + /// Width of the image in pixels. + /// + /// For formats where some color channels are subsampled, this is the width + /// of the largest-resolution plane. + final int width; + + /// The pixels planes for this image. + /// + /// The number of planes is determined by the format of the image. + final List planes; + + /// The aperture settings for this image. + /// + /// Represented as an f-stop value. + final double? lensAperture; + + /// The sensor exposure time for this image in nanoseconds. + final int? sensorExposureTime; + + /// The sensor sensitivity in standard ISO arithmetic units. + final double? sensorSensitivity; +} diff --git a/packages/camera/camera_android_camerax/example/lib/camera_preview.dart b/packages/camera/camera_android_camerax/example/lib/camera_preview.dart new file mode 100644 index 000000000000..3baaaf8b1fa1 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/lib/camera_preview.dart @@ -0,0 +1,81 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'camera_controller.dart'; + +/// A widget showing a live camera preview. +class CameraPreview extends StatelessWidget { + /// Creates a preview widget for the given camera controller. + const CameraPreview(this.controller, {super.key, this.child}); + + /// The controller for the camera that the preview is shown for. + final CameraController controller; + + /// A widget to overlay on top of the camera preview + final Widget? child; + + @override + Widget build(BuildContext context) { + return controller.value.isInitialized + ? ValueListenableBuilder( + valueListenable: controller, + builder: (BuildContext context, Object? value, Widget? child) { + return AspectRatio( + aspectRatio: _isLandscape() + ? controller.value.aspectRatio + : (1 / controller.value.aspectRatio), + child: Stack( + fit: StackFit.expand, + children: [ + _wrapInRotatedBox(child: controller.buildPreview()), + child ?? Container(), + ], + ), + ); + }, + child: child, + ) + : Container(); + } + + Widget _wrapInRotatedBox({required Widget child}) { + if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) { + return child; + } + + return RotatedBox( + quarterTurns: _getQuarterTurns(), + child: child, + ); + } + + bool _isLandscape() { + return [ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight + ].contains(_getApplicableOrientation()); + } + + int _getQuarterTurns() { + final Map turns = { + DeviceOrientation.portraitUp: 0, + DeviceOrientation.landscapeRight: 1, + DeviceOrientation.portraitDown: 2, + DeviceOrientation.landscapeLeft: 3, + }; + return turns[_getApplicableOrientation()]!; + } + + DeviceOrientation _getApplicableOrientation() { + return controller.value.isRecordingVideo + ? controller.value.recordingOrientation! + : (controller.value.previewPauseOrientation ?? + controller.value.lockedCaptureOrientation ?? + controller.value.deviceOrientation); + } +} diff --git a/packages/camera/camera_android_camerax/example/lib/main.dart b/packages/camera/camera_android_camerax/example/lib/main.dart index 244a15281e3f..4fd965271baa 100644 --- a/packages/camera/camera_android_camerax/example/lib/main.dart +++ b/packages/camera/camera_android_camerax/example/lib/main.dart @@ -2,43 +2,1046 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; +import 'dart:io'; + import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:video_player/video_player.dart'; -late List _cameras; +import 'camera_controller.dart'; +import 'camera_preview.dart'; -Future main() async { - WidgetsFlutterBinding.ensureInitialized(); - _cameras = await CameraPlatform.instance.availableCameras(); +/// Camera example home widget. +class CameraExampleHome extends StatefulWidget { + /// Default Constructor + const CameraExampleHome({super.key}); - runApp(const MyApp()); + @override + State createState() { + return _CameraExampleHomeState(); + } } -/// Example app -class MyApp extends StatefulWidget { - /// App instantiation - const MyApp({super.key}); - @override - State createState() => _MyAppState(); +/// Returns a suitable camera icon for [direction]. +IconData getCameraLensIcon(CameraLensDirection direction) { + switch (direction) { + case CameraLensDirection.back: + return Icons.camera_rear; + case CameraLensDirection.front: + return Icons.camera_front; + case CameraLensDirection.external: + return Icons.camera; + } + // This enum is from a different package, so a new value could be added at + // any time. The example should keep working if that happens. + // ignore: dead_code + return Icons.camera; +} + +void _logError(String code, String? message) { + // ignore: avoid_print + print('Error: $code${message == null ? '' : '\nError Message: $message'}'); } -class _MyAppState extends State { +class _CameraExampleHomeState extends State + with WidgetsBindingObserver, TickerProviderStateMixin { + CameraController? controller; + XFile? imageFile; + XFile? videoFile; + VideoPlayerController? videoController; + VoidCallback? videoPlayerListener; + bool enableAudio = true; + double _minAvailableExposureOffset = 0.0; + double _maxAvailableExposureOffset = 0.0; + double _currentExposureOffset = 0.0; + late AnimationController _flashModeControlRowAnimationController; + late Animation _flashModeControlRowAnimation; + late AnimationController _exposureModeControlRowAnimationController; + late Animation _exposureModeControlRowAnimation; + late AnimationController _focusModeControlRowAnimationController; + late Animation _focusModeControlRowAnimation; + double _minAvailableZoom = 1.0; + double _maxAvailableZoom = 1.0; + double _currentScale = 1.0; + double _baseScale = 1.0; + + // Counting pointers (number of user fingers on screen) + int _pointers = 0; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + + _flashModeControlRowAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _flashModeControlRowAnimation = CurvedAnimation( + parent: _flashModeControlRowAnimationController, + curve: Curves.easeInCubic, + ); + _exposureModeControlRowAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _exposureModeControlRowAnimation = CurvedAnimation( + parent: _exposureModeControlRowAnimationController, + curve: Curves.easeInCubic, + ); + _focusModeControlRowAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _focusModeControlRowAnimation = CurvedAnimation( + parent: _focusModeControlRowAnimationController, + curve: Curves.easeInCubic, + ); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _flashModeControlRowAnimationController.dispose(); + _exposureModeControlRowAnimationController.dispose(); + super.dispose(); + } + + // #docregion AppLifecycle + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + final CameraController? cameraController = controller; + + // App state changed before we got the chance to initialize. + if (cameraController == null || !cameraController.value.isInitialized) { + return; + } + + if (state == AppLifecycleState.inactive) { + cameraController.dispose(); + } else if (state == AppLifecycleState.resumed) { + onNewCameraSelected(cameraController.description); + } + } + // #enddocregion AppLifecycle + @override Widget build(BuildContext context) { - String availableCameraNames = 'Available cameras:'; - for (final CameraDescription cameraDescription in _cameras) { - availableCameraNames = '$availableCameraNames ${cameraDescription.name},'; + return Scaffold( + appBar: AppBar( + title: const Text('Camera example'), + ), + body: Column( + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: Colors.black, + border: Border.all( + color: + controller != null && controller!.value.isRecordingVideo + ? Colors.redAccent + : Colors.grey, + width: 3.0, + ), + ), + child: Padding( + padding: const EdgeInsets.all(1.0), + child: Center( + child: _cameraPreviewWidget(), + ), + ), + ), + ), + _captureControlRowWidget(), + _modeControlRowWidget(), + Padding( + padding: const EdgeInsets.all(5.0), + child: Row( + children: [ + _cameraTogglesRowWidget(), + _thumbnailWidget(), + ], + ), + ), + ], + ), + ); + } + + /// Display the preview from the camera (or a message if the preview is not available). + Widget _cameraPreviewWidget() { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isInitialized) { + return const Text( + 'Tap a camera', + style: TextStyle( + color: Colors.white, + fontSize: 24.0, + fontWeight: FontWeight.w900, + ), + ); + } else { + return Listener( + onPointerDown: (_) => _pointers++, + onPointerUp: (_) => _pointers--, + child: CameraPreview( + controller!, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onScaleStart: _handleScaleStart, + onScaleUpdate: _handleScaleUpdate, + onTapDown: (TapDownDetails details) => + onViewFinderTap(details, constraints), + ); + }), + ), + ); + } + } + + void _handleScaleStart(ScaleStartDetails details) { + _baseScale = _currentScale; + } + + Future _handleScaleUpdate(ScaleUpdateDetails details) async { + // When there are not exactly two fingers on screen don't scale + if (controller == null || _pointers != 2) { + return; } - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Camera Example'), + + _currentScale = (_baseScale * details.scale) + .clamp(_minAvailableZoom, _maxAvailableZoom); + + await controller!.setZoomLevel(_currentScale); + } + + /// Display the thumbnail of the captured image or video. + Widget _thumbnailWidget() { + final VideoPlayerController? localVideoController = videoController; + + return Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (localVideoController == null && imageFile == null) + Container() + else + SizedBox( + width: 64.0, + height: 64.0, + child: (localVideoController == null) + ? ( + // The captured image on the web contains a network-accessible URL + // pointing to a location within the browser. It may be displayed + // either with Image.network or Image.memory after loading the image + // bytes to memory. + kIsWeb + ? Image.network(imageFile!.path) + : Image.file(File(imageFile!.path))) + : Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.pink)), + child: Center( + child: AspectRatio( + aspectRatio: + localVideoController.value.size != null + ? localVideoController.value.aspectRatio + : 1.0, + child: VideoPlayer(localVideoController)), + ), + ), + ), + ], + ), + ), + ); + } + + /// Display a bar with buttons to change the flash and exposure modes + Widget _modeControlRowWidget() { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + icon: const Icon(Icons.flash_on), + color: Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + // The exposure and focus mode are currently not supported on the web. + ...!kIsWeb + ? [ + IconButton( + icon: const Icon(Icons.exposure), + color: Colors.blue, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.filter_center_focus), + color: Colors.blue, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + ) + ] + : [], + IconButton( + icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute), + color: Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: Icon(controller?.value.isCaptureOrientationLocked ?? false + ? Icons.screen_lock_rotation + : Icons.screen_rotation), + color: Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + ], ), - body: Center( - child: Text(availableCameraNames.substring( - 0, availableCameraNames.length - 1)), + _flashModeControlRowWidget(), + _exposureModeControlRowWidget(), + _focusModeControlRowWidget(), + ], + ); + } + + Widget _flashModeControlRowWidget() { + return SizeTransition( + sizeFactor: _flashModeControlRowAnimation, + child: ClipRect( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + icon: const Icon(Icons.flash_off), + color: controller?.value.flashMode == FlashMode.off + ? Colors.orange + : Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.flash_auto), + color: controller?.value.flashMode == FlashMode.auto + ? Colors.orange + : Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.flash_on), + color: controller?.value.flashMode == FlashMode.always + ? Colors.orange + : Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.highlight), + color: controller?.value.flashMode == FlashMode.torch + ? Colors.orange + : Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + ], ), ), ); } + + Widget _exposureModeControlRowWidget() { + final ButtonStyle styleAuto = TextButton.styleFrom( + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // ignore: deprecated_member_use + primary: controller?.value.exposureMode == ExposureMode.auto + ? Colors.orange + : Colors.blue, + ); + final ButtonStyle styleLocked = TextButton.styleFrom( + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // ignore: deprecated_member_use + primary: controller?.value.exposureMode == ExposureMode.locked + ? Colors.orange + : Colors.blue, + ); + + return SizeTransition( + sizeFactor: _exposureModeControlRowAnimation, + child: ClipRect( + child: Container( + color: Colors.grey.shade50, + child: Column( + children: [ + const Center( + child: Text('Exposure Mode'), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + style: styleAuto, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + onLongPress: () { + if (controller != null) { + controller!.setExposurePoint(null); + showInSnackBar('Resetting exposure point'); + } + }, + child: const Text('AUTO'), + ), + TextButton( + style: styleLocked, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + child: const Text('LOCKED'), + ), + TextButton( + style: styleLocked, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + child: const Text('RESET OFFSET'), + ), + ], + ), + const Center( + child: Text('Exposure Offset'), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text(_minAvailableExposureOffset.toString()), + Slider( + value: _currentExposureOffset, + min: _minAvailableExposureOffset, + max: _maxAvailableExposureOffset, + label: _currentExposureOffset.toString(), + onChanged: _minAvailableExposureOffset == + _maxAvailableExposureOffset + ? null + : setExposureOffset, + ), + Text(_maxAvailableExposureOffset.toString()), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _focusModeControlRowWidget() { + final ButtonStyle styleAuto = TextButton.styleFrom( + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // ignore: deprecated_member_use + primary: controller?.value.focusMode == FocusMode.auto + ? Colors.orange + : Colors.blue, + ); + final ButtonStyle styleLocked = TextButton.styleFrom( + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // ignore: deprecated_member_use + primary: controller?.value.focusMode == FocusMode.locked + ? Colors.orange + : Colors.blue, + ); + + return SizeTransition( + sizeFactor: _focusModeControlRowAnimation, + child: ClipRect( + child: Container( + color: Colors.grey.shade50, + child: Column( + children: [ + const Center( + child: Text('Focus Mode'), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + style: styleAuto, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + onLongPress: () { + if (controller != null) { + controller!.setFocusPoint(null); + } + showInSnackBar('Resetting focus point'); + }, + child: const Text('AUTO'), + ), + TextButton( + style: styleLocked, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + child: const Text('LOCKED'), + ), + ], + ), + ], + ), + ), + ), + ); + } + + /// Display the control bar with buttons to take pictures and record videos. + Widget _captureControlRowWidget() { + final CameraController? cameraController = controller; + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + icon: const Icon(Icons.camera_alt), + color: Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.videocam), + color: Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: cameraController != null && + cameraController.value.isRecordingPaused + ? const Icon(Icons.play_arrow) + : const Icon(Icons.pause), + color: Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.stop), + color: Colors.red, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.pause_presentation), + color: + cameraController != null && cameraController.value.isPreviewPaused + ? Colors.red + : Colors.blue, + onPressed: + cameraController == null ? null : onPausePreviewButtonPressed, + ), + ], + ); + } + + /// Display a row of toggle to select the camera (or a message if no camera is available). + Widget _cameraTogglesRowWidget() { + final List toggles = []; + + void onChanged(CameraDescription? description) { + if (description == null) { + return; + } + + onNewCameraSelected(description); + } + + if (_cameras.isEmpty) { + SchedulerBinding.instance.addPostFrameCallback((_) async { + showInSnackBar('No camera found.'); + }); + return const Text('None'); + } else { + for (final CameraDescription cameraDescription in _cameras) { + toggles.add( + SizedBox( + width: 90.0, + child: RadioListTile( + title: Icon(getCameraLensIcon(cameraDescription.lensDirection)), + groupValue: controller?.description, + value: cameraDescription, + onChanged: + controller != null && controller!.value.isRecordingVideo + ? null + : onChanged, + ), + ), + ); + } + } + + return Row(children: toggles); + } + + String timestamp() => DateTime.now().millisecondsSinceEpoch.toString(); + + void showInSnackBar(String message) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(message))); + } + + void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) { + if (controller == null) { + return; + } + + final CameraController cameraController = controller!; + + final Offset offset = Offset( + details.localPosition.dx / constraints.maxWidth, + details.localPosition.dy / constraints.maxHeight, + ); + cameraController.setExposurePoint(offset); + cameraController.setFocusPoint(offset); + } + + Future onNewCameraSelected(CameraDescription cameraDescription) async { + final CameraController? oldController = controller; + if (oldController != null) { + // `controller` needs to be set to null before getting disposed, + // to avoid a race condition when we use the controller that is being + // disposed. This happens when camera permission dialog shows up, + // which triggers `didChangeAppLifecycleState`, which disposes and + // re-creates the controller. + controller = null; + await oldController.dispose(); + } + + final CameraController cameraController = CameraController( + cameraDescription, + kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium, + enableAudio: enableAudio, + imageFormatGroup: ImageFormatGroup.jpeg, + ); + + controller = cameraController; + + // If the controller is updated then update the UI. + cameraController.addListener(() { + if (mounted) { + setState(() {}); + } + if (cameraController.value.hasError) { + showInSnackBar( + 'Camera error ${cameraController.value.errorDescription}'); + } + }); + + try { + await cameraController.initialize(); + await Future.wait(>[ + // The exposure mode is currently not supported on the web. + ...!kIsWeb + ? >[ + cameraController.getMinExposureOffset().then( + (double value) => _minAvailableExposureOffset = value), + cameraController + .getMaxExposureOffset() + .then((double value) => _maxAvailableExposureOffset = value) + ] + : >[], + cameraController + .getMaxZoomLevel() + .then((double value) => _maxAvailableZoom = value), + cameraController + .getMinZoomLevel() + .then((double value) => _minAvailableZoom = value), + ]); + } on CameraException catch (e) { + switch (e.code) { + case 'CameraAccessDenied': + showInSnackBar('You have denied camera access.'); + break; + case 'CameraAccessDeniedWithoutPrompt': + // iOS only + showInSnackBar('Please go to Settings app to enable camera access.'); + break; + case 'CameraAccessRestricted': + // iOS only + showInSnackBar('Camera access is restricted.'); + break; + case 'AudioAccessDenied': + showInSnackBar('You have denied audio access.'); + break; + case 'AudioAccessDeniedWithoutPrompt': + // iOS only + showInSnackBar('Please go to Settings app to enable audio access.'); + break; + case 'AudioAccessRestricted': + // iOS only + showInSnackBar('Audio access is restricted.'); + break; + default: + _showCameraException(e); + break; + } + } + + if (mounted) { + setState(() {}); + } + } + + void onTakePictureButtonPressed() { + takePicture().then((XFile? file) { + if (mounted) { + setState(() { + imageFile = file; + videoController?.dispose(); + videoController = null; + }); + if (file != null) { + showInSnackBar('Picture saved to ${file.path}'); + } + } + }); + } + + void onFlashModeButtonPressed() { + if (_flashModeControlRowAnimationController.value == 1) { + _flashModeControlRowAnimationController.reverse(); + } else { + _flashModeControlRowAnimationController.forward(); + _exposureModeControlRowAnimationController.reverse(); + _focusModeControlRowAnimationController.reverse(); + } + } + + void onExposureModeButtonPressed() { + if (_exposureModeControlRowAnimationController.value == 1) { + _exposureModeControlRowAnimationController.reverse(); + } else { + _exposureModeControlRowAnimationController.forward(); + _flashModeControlRowAnimationController.reverse(); + _focusModeControlRowAnimationController.reverse(); + } + } + + void onFocusModeButtonPressed() { + if (_focusModeControlRowAnimationController.value == 1) { + _focusModeControlRowAnimationController.reverse(); + } else { + _focusModeControlRowAnimationController.forward(); + _flashModeControlRowAnimationController.reverse(); + _exposureModeControlRowAnimationController.reverse(); + } + } + + void onAudioModeButtonPressed() { + enableAudio = !enableAudio; + if (controller != null) { + onNewCameraSelected(controller!.description); + } + } + + Future onCaptureOrientationLockButtonPressed() async { + try { + if (controller != null) { + final CameraController cameraController = controller!; + if (cameraController.value.isCaptureOrientationLocked) { + await cameraController.unlockCaptureOrientation(); + showInSnackBar('Capture orientation unlocked'); + } else { + await cameraController.lockCaptureOrientation(); + showInSnackBar( + 'Capture orientation locked to ${cameraController.value.lockedCaptureOrientation.toString().split('.').last}'); + } + } + } on CameraException catch (e) { + _showCameraException(e); + } + } + + void onSetFlashModeButtonPressed(FlashMode mode) { + setFlashMode(mode).then((_) { + if (mounted) { + setState(() {}); + } + showInSnackBar('Flash mode set to ${mode.toString().split('.').last}'); + }); + } + + void onSetExposureModeButtonPressed(ExposureMode mode) { + setExposureMode(mode).then((_) { + if (mounted) { + setState(() {}); + } + showInSnackBar('Exposure mode set to ${mode.toString().split('.').last}'); + }); + } + + void onSetFocusModeButtonPressed(FocusMode mode) { + setFocusMode(mode).then((_) { + if (mounted) { + setState(() {}); + } + showInSnackBar('Focus mode set to ${mode.toString().split('.').last}'); + }); + } + + void onVideoRecordButtonPressed() { + startVideoRecording().then((_) { + if (mounted) { + setState(() {}); + } + }); + } + + void onStopButtonPressed() { + stopVideoRecording().then((XFile? file) { + if (mounted) { + setState(() {}); + } + if (file != null) { + showInSnackBar('Video recorded to ${file.path}'); + videoFile = file; + _startVideoPlayer(); + } + }); + } + + Future onPausePreviewButtonPressed() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isInitialized) { + showInSnackBar('Error: select a camera first.'); + return; + } + + if (cameraController.value.isPreviewPaused) { + await cameraController.resumePreview(); + } else { + await cameraController.pausePreview(); + } + + if (mounted) { + setState(() {}); + } + } + + void onPauseButtonPressed() { + pauseVideoRecording().then((_) { + if (mounted) { + setState(() {}); + } + showInSnackBar('Video recording paused'); + }); + } + + void onResumeButtonPressed() { + resumeVideoRecording().then((_) { + if (mounted) { + setState(() {}); + } + showInSnackBar('Video recording resumed'); + }); + } + + Future startVideoRecording() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isInitialized) { + showInSnackBar('Error: select a camera first.'); + return; + } + + if (cameraController.value.isRecordingVideo) { + // A recording is already started, do nothing. + return; + } + + try { + await cameraController.startVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + return; + } + } + + Future stopVideoRecording() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isRecordingVideo) { + return null; + } + + try { + return cameraController.stopVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + return null; + } + } + + Future pauseVideoRecording() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isRecordingVideo) { + return; + } + + try { + await cameraController.pauseVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future resumeVideoRecording() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isRecordingVideo) { + return; + } + + try { + await cameraController.resumeVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future setFlashMode(FlashMode mode) async { + if (controller == null) { + return; + } + + try { + await controller!.setFlashMode(mode); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future setExposureMode(ExposureMode mode) async { + if (controller == null) { + return; + } + + try { + await controller!.setExposureMode(mode); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future setExposureOffset(double offset) async { + if (controller == null) { + return; + } + + setState(() { + _currentExposureOffset = offset; + }); + try { + offset = await controller!.setExposureOffset(offset); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future setFocusMode(FocusMode mode) async { + if (controller == null) { + return; + } + + try { + await controller!.setFocusMode(mode); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future _startVideoPlayer() async { + if (videoFile == null) { + return; + } + + final VideoPlayerController vController = kIsWeb + ? VideoPlayerController.network(videoFile!.path) + : VideoPlayerController.file(File(videoFile!.path)); + + videoPlayerListener = () { + if (videoController != null && videoController!.value.size != null) { + // Refreshing the state to update video player with the correct ratio. + if (mounted) { + setState(() {}); + } + videoController!.removeListener(videoPlayerListener!); + } + }; + vController.addListener(videoPlayerListener!); + await vController.setLooping(true); + await vController.initialize(); + await videoController?.dispose(); + if (mounted) { + setState(() { + imageFile = null; + videoController = vController; + }); + } + await vController.play(); + } + + Future takePicture() async { + final CameraController? cameraController = controller; + if (cameraController == null || !cameraController.value.isInitialized) { + showInSnackBar('Error: select a camera first.'); + return null; + } + + if (cameraController.value.isTakingPicture) { + // A capture is already pending, do nothing. + return null; + } + + try { + final XFile file = await cameraController.takePicture(); + return file; + } on CameraException catch (e) { + _showCameraException(e); + return null; + } + } + + void _showCameraException(CameraException e) { + _logError(e.code, e.description); + showInSnackBar('Error: ${e.code}\n${e.description}'); + } +} + +/// CameraApp is the Main Application. +class CameraApp extends StatelessWidget { + /// Default Constructor + const CameraApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: CameraExampleHome(), + ); + } +} + +List _cameras = []; + +Future main() async { + // Fetch the available cameras before initializing the app. + try { + WidgetsFlutterBinding.ensureInitialized(); + _cameras = await availableCameras(); + } on CameraException catch (e) { + _logError(e.code, e.description); + } + runApp(const CameraApp()); } diff --git a/packages/camera/camera_android_camerax/example/pubspec.yaml b/packages/camera/camera_android_camerax/example/pubspec.yaml index d9756f7ebd9b..49a29b8517d9 100644 --- a/packages/camera/camera_android_camerax/example/pubspec.yaml +++ b/packages/camera/camera_android_camerax/example/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: camera_platform_interface: ^2.2.0 flutter: sdk: flutter + video_player: ^2.4.10 dev_dependencies: flutter_test: diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart index f03273861793..18debf688547 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart @@ -5,6 +5,18 @@ import 'dart:async'; import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_transform/stream_transform.dart'; + +import 'camera.dart'; +import 'camera_info.dart'; +import 'camera_selector.dart'; +import 'camerax_library.g.dart'; +import 'preview.dart'; +import 'process_camera_provider.dart'; +import 'surface.dart'; +import 'system_services.dart'; +import 'use_case.dart'; /// The Android implementation of [CameraPlatform] that uses the CameraX library. class AndroidCameraCameraX extends CameraPlatform { @@ -13,9 +25,358 @@ class AndroidCameraCameraX extends CameraPlatform { CameraPlatform.instance = AndroidCameraCameraX(); } + /// The [ProcessCameraProvider] instance used to access camera functionality. + @visibleForTesting + ProcessCameraProvider? processCameraProvider; + + /// The [Camera] instance returned by the [processCameraProvider] when a [UseCase] is + /// bound to the lifecycle of the camera it manages. + @visibleForTesting + Camera? camera; + + /// The [Preview] instance that can be configured to present a live camera preview. + @visibleForTesting + Preview? preview; + + /// Whether or not the [preview] is currently bound to the lifecycle that the + /// [processCameraProvider] tracks. + @visibleForTesting + bool previewIsBound = false; + + bool _previewIsPaused = false; + + /// The [CameraSelector] used to configure the [processCameraProvider] to use + /// the desired camera. + @visibleForTesting + CameraSelector? cameraSelector; + + /// The controller we need to broadcast the different camera events. + /// + /// It is a `broadcast` because multiple controllers will connect to + /// different stream views of this Controller. + /// This is only exposed for test purposes. It shouldn't be used by clients of + /// the plugin as it may break or change at any time. + @visibleForTesting + final StreamController cameraEventStreamController = + StreamController.broadcast(); + + /// The stream of camera events. + Stream _cameraEvents(int cameraId) => + cameraEventStreamController.stream + .where((CameraEvent event) => event.cameraId == cameraId); + /// Returns list of all available cameras and their descriptions. @override Future> availableCameras() async { - throw UnimplementedError('availableCameras() is not implemented.'); + final List cameraDescriptions = []; + + processCameraProvider ??= await ProcessCameraProvider.getInstance(); + final List cameraInfos = + await processCameraProvider!.getAvailableCameraInfos(); + + CameraLensDirection? cameraLensDirection; + int cameraCount = 0; + int? cameraSensorOrientation; + String? cameraName; + + for (final CameraInfo cameraInfo in cameraInfos) { + // Determine the lens direction by filtering the CameraInfo + // TODO(gmackall): replace this with call to CameraInfo.getLensFacing when changes containing that method are available + if ((await createCameraSelector(CameraSelector.lensFacingBack) + .filter([cameraInfo])) + .isNotEmpty) { + cameraLensDirection = CameraLensDirection.back; + } else if ((await createCameraSelector(CameraSelector.lensFacingFront) + .filter([cameraInfo])) + .isNotEmpty) { + cameraLensDirection = CameraLensDirection.front; + } else { + //Skip this CameraInfo as its lens direction is unknown + continue; + } + + cameraSensorOrientation = await cameraInfo.getSensorRotationDegrees(); + cameraName = 'Camera $cameraCount'; + cameraCount++; + + cameraDescriptions.add(CameraDescription( + name: cameraName, + lensDirection: cameraLensDirection, + sensorOrientation: cameraSensorOrientation)); + } + + return cameraDescriptions; + } + + /// Creates an uninitialized camera instance and returns the camera ID. + /// + /// In the CameraX library, cameras are accessed by combining [UseCase]s + /// to an instance of a [ProcessCameraProvider]. Thus, to create an + /// unitialized camera instance, this method retrieves a + /// [ProcessCameraProvider] instance. + /// + /// To return the camera ID, which is equivalent to the ID of the surface texture + /// that a camera preview can be drawn to, a [Preview] instance is configured + /// and bound to the [ProcessCameraProvider] instance. + @override + Future createCamera( + CameraDescription cameraDescription, + ResolutionPreset? resolutionPreset, { + bool enableAudio = false, + }) async { + // Must obtain proper permissions before attempting to access a camera. + await requestCameraPermissions(enableAudio); + + // Save CameraSelector that matches cameraDescription. + final int cameraSelectorLensDirection = + _getCameraSelectorLensDirection(cameraDescription.lensDirection); + final bool cameraIsFrontFacing = + cameraSelectorLensDirection == CameraSelector.lensFacingFront; + cameraSelector = createCameraSelector(cameraSelectorLensDirection); + // Start listening for device orientation changes preceding camera creation. + startListeningForDeviceOrientationChange( + cameraIsFrontFacing, cameraDescription.sensorOrientation); + + // Retrieve a ProcessCameraProvider instance. + processCameraProvider ??= await ProcessCameraProvider.getInstance(); + + // Configure Preview instance and bind to ProcessCameraProvider. + final int targetRotation = + _getTargetRotation(cameraDescription.sensorOrientation); + final ResolutionInfo? targetResolution = + _getTargetResolutionForPreview(resolutionPreset); + preview = createPreview(targetRotation, targetResolution); + previewIsBound = false; + _previewIsPaused = false; + final int flutterSurfaceTextureId = await preview!.setSurfaceProvider(); + + return flutterSurfaceTextureId; + } + + /// Initializes the camera on the device. + /// + /// Since initialization of a camera does not directly map as an operation to + /// the CameraX library, this method just retrieves information about the + /// camera and sends a [CameraInitializedEvent]. + /// + /// [imageFormatGroup] is used to specify the image formatting used. + /// On Android this defaults to ImageFormat.YUV_420_888 and applies only to + /// the image stream. + @override + Future initializeCamera( + int cameraId, { + ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, + }) async { + // TODO(camsim99): Use imageFormatGroup to configure ImageAnalysis use case + // for image streaming. + // https://github.com/flutter/flutter/issues/120463 + + // Configure CameraInitializedEvent to send as representation of a + // configured camera: + // Retrieve preview resolution. + assert( + preview != null, + 'Preview instance not found. Please call the "createCamera" method before calling "initializeCamera"', + ); + await _bindPreviewToLifecycle(); + final ResolutionInfo previewResolutionInfo = + await preview!.getResolutionInfo(); + _unbindPreviewFromLifecycle(); + + // Retrieve exposure and focus mode configurations: + // TODO(camsim99): Implement support for retrieving exposure mode configuration. + // https://github.com/flutter/flutter/issues/120468 + const ExposureMode exposureMode = ExposureMode.auto; + const bool exposurePointSupported = false; + + // TODO(camsim99): Implement support for retrieving focus mode configuration. + // https://github.com/flutter/flutter/issues/120467 + const FocusMode focusMode = FocusMode.auto; + const bool focusPointSupported = false; + + cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + previewResolutionInfo.width.toDouble(), + previewResolutionInfo.height.toDouble(), + exposureMode, + exposurePointSupported, + focusMode, + focusPointSupported)); + } + + /// Releases the resources of the accessed camera. + /// + /// [cameraId] not used. + @override + Future dispose(int cameraId) async { + preview?.releaseFlutterSurfaceTexture(); + processCameraProvider?.unbindAll(); + } + + /// The camera has been initialized. + @override + Stream onCameraInitialized(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + /// The camera experienced an error. + @override + Stream onCameraError(int cameraId) { + return SystemServices.cameraErrorStreamController.stream + .map((String errorDescription) { + return CameraErrorEvent(cameraId, errorDescription); + }); + } + + /// The ui orientation changed. + @override + Stream onDeviceOrientationChanged() { + return SystemServices.deviceOrientationChangedStreamController.stream; + } + + /// Pause the active preview on the current frame for the selected camera. + /// + /// [cameraId] not used. + @override + Future pausePreview(int cameraId) async { + _unbindPreviewFromLifecycle(); + _previewIsPaused = true; + } + + /// Resume the paused preview for the selected camera. + /// + /// [cameraId] not used. + @override + Future resumePreview(int cameraId) async { + await _bindPreviewToLifecycle(); + _previewIsPaused = false; + } + + /// Returns a widget showing a live camera preview. + @override + Widget buildPreview(int cameraId) { + return FutureBuilder( + future: _bindPreviewToLifecycle(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + // Do nothing while waiting for preview to be bound to lifecyle. + return const SizedBox.shrink(); + case ConnectionState.done: + return Texture(textureId: cameraId); + } + }); + } + + // Methods for binding UseCases to the lifecycle of the camera controlled + // by a ProcessCameraProvider instance: + + /// Binds [preview] instance to the camera lifecycle controlled by the + /// [processCameraProvider]. + Future _bindPreviewToLifecycle() async { + assert(processCameraProvider != null); + assert(cameraSelector != null); + + if (previewIsBound || _previewIsPaused) { + // Only bind if preview is not already bound or intentionally paused. + return; + } + + camera = await processCameraProvider! + .bindToLifecycle(cameraSelector!, [preview!]); + previewIsBound = true; + } + + /// Unbinds [preview] instance to camera lifecycle controlled by the + /// [processCameraProvider]. + void _unbindPreviewFromLifecycle() { + if (preview == null || !previewIsBound) { + return; + } + + assert(processCameraProvider != null); + + processCameraProvider!.unbind([preview!]); + previewIsBound = false; + } + + // Methods for mapping Flutter camera constants to CameraX constants: + + /// Returns [CameraSelector] lens direction that maps to specified + /// [CameraLensDirection]. + int _getCameraSelectorLensDirection(CameraLensDirection lensDirection) { + switch (lensDirection) { + case CameraLensDirection.front: + return CameraSelector.lensFacingFront; + case CameraLensDirection.back: + return CameraSelector.lensFacingBack; + case CameraLensDirection.external: + return CameraSelector.lensFacingExternal; + } + } + + /// Returns [Surface] target rotation constant that maps to specified sensor + /// orientation. + int _getTargetRotation(int sensorOrientation) { + switch (sensorOrientation) { + case 90: + return Surface.ROTATION_90; + case 180: + return Surface.ROTATION_180; + case 270: + return Surface.ROTATION_270; + case 0: + return Surface.ROTATION_0; + default: + throw ArgumentError( + '"$sensorOrientation" is not a valid sensor orientation value'); + } + } + + /// Returns [ResolutionInfo] that maps to the specified resolution preset for + /// a camera preview. + ResolutionInfo? _getTargetResolutionForPreview(ResolutionPreset? resolution) { + // TODO(camsim99): Implement resolution configuration. + // https://github.com/flutter/flutter/issues/120462 + return null; + } + + // Methods for calls that need to be tested: + + /// Requests camera permissions. + @visibleForTesting + Future requestCameraPermissions(bool enableAudio) async { + await SystemServices.requestCameraPermissions(enableAudio); + } + + /// Subscribes the plugin as a listener to changes in device orientation. + @visibleForTesting + void startListeningForDeviceOrientationChange( + bool cameraIsFrontFacing, int sensorOrientation) { + SystemServices.startListeningForDeviceOrientationChange( + cameraIsFrontFacing, sensorOrientation); + } + + /// Returns a [CameraSelector] based on the specified camera lens direction. + @visibleForTesting + CameraSelector createCameraSelector(int cameraSelectorLensDirection) { + switch (cameraSelectorLensDirection) { + case CameraSelector.lensFacingFront: + return CameraSelector.getDefaultFrontCamera(); + case CameraSelector.lensFacingBack: + return CameraSelector.getDefaultBackCamera(); + default: + return CameraSelector(lensFacing: cameraSelectorLensDirection); + } + } + + /// Returns a [Preview] configured with the specified target rotation and + /// resolution. + @visibleForTesting + Preview createPreview(int targetRotation, ResolutionInfo? targetResolution) { + return Preview( + targetRotation: targetRotation, targetResolution: targetResolution); } } diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart index 9c6564a06c08..0a1b3ce3b285 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart @@ -2,20 +2,24 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'camera.dart'; import 'camera_info.dart'; import 'camera_selector.dart'; -import 'camerax_library.pigeon.dart'; +import 'camerax_library.g.dart'; import 'java_object.dart'; import 'process_camera_provider.dart'; +import 'system_services.dart'; /// Handles initialization of Flutter APIs for the Android CameraX library. class AndroidCameraXCameraFlutterApis { /// Creates a [AndroidCameraXCameraFlutterApis]. AndroidCameraXCameraFlutterApis({ JavaObjectFlutterApiImpl? javaObjectFlutterApi, + CameraFlutterApiImpl? cameraFlutterApi, CameraInfoFlutterApiImpl? cameraInfoFlutterApi, CameraSelectorFlutterApiImpl? cameraSelectorFlutterApi, ProcessCameraProviderFlutterApiImpl? processCameraProviderFlutterApi, + SystemServicesFlutterApiImpl? systemServicesFlutterApi, }) { this.javaObjectFlutterApi = javaObjectFlutterApi ?? JavaObjectFlutterApiImpl(); @@ -25,6 +29,9 @@ class AndroidCameraXCameraFlutterApis { cameraSelectorFlutterApi ?? CameraSelectorFlutterApiImpl(); this.processCameraProviderFlutterApi = processCameraProviderFlutterApi ?? ProcessCameraProviderFlutterApiImpl(); + this.cameraFlutterApi = cameraFlutterApi ?? CameraFlutterApiImpl(); + this.systemServicesFlutterApi = + systemServicesFlutterApi ?? SystemServicesFlutterApiImpl(); } static bool _haveBeenSetUp = false; @@ -48,6 +55,12 @@ class AndroidCameraXCameraFlutterApis { late final ProcessCameraProviderFlutterApiImpl processCameraProviderFlutterApi; + /// Flutter Api for [Camera]. + late final CameraFlutterApiImpl cameraFlutterApi; + + /// Flutter Api for [SystemServices]. + late final SystemServicesFlutterApiImpl systemServicesFlutterApi; + /// Ensures all the Flutter APIs have been setup to receive calls from native code. void ensureSetUp() { if (!_haveBeenSetUp) { @@ -55,6 +68,8 @@ class AndroidCameraXCameraFlutterApis { CameraInfoFlutterApi.setup(cameraInfoFlutterApi); CameraSelectorFlutterApi.setup(cameraSelectorFlutterApi); ProcessCameraProviderFlutterApi.setup(processCameraProviderFlutterApi); + CameraFlutterApi.setup(cameraFlutterApi); + SystemServicesFlutterApi.setup(systemServicesFlutterApi); _haveBeenSetUp = true; } } diff --git a/packages/camera/camera_android_camerax/lib/src/camera.dart b/packages/camera/camera_android_camerax/lib/src/camera.dart new file mode 100644 index 000000000000..24ff30540b28 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/camera.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:flutter/services.dart' show BinaryMessenger; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; + +/// The interface used to control the flow of data of use cases, control the +/// camera, and publich the state of the camera. +/// +/// See https://developer.android.com/reference/androidx/camera/core/Camera. +class Camera extends JavaObject { + /// Constructs a [Camera] that is not automatically attached to a native object. + Camera.detached({super.binaryMessenger, super.instanceManager}) + : super.detached() { + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } +} + +/// Flutter API implementation of [Camera]. +class CameraFlutterApiImpl implements CameraFlutterApi { + /// Constructs a [CameraSelectorFlutterApiImpl]. + CameraFlutterApiImpl({ + 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 identifier) { + instanceManager.addHostCreatedInstance( + Camera.detached( + binaryMessenger: binaryMessenger, instanceManager: instanceManager), + identifier, + onCopy: (Camera original) { + return Camera.detached( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + }, + ); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/camera_info.dart b/packages/camera/camera_android_camerax/lib/src/camera_info.dart index d03426f40027..8c2c7bcf0aec 100644 --- a/packages/camera/camera_android_camerax/lib/src/camera_info.dart +++ b/packages/camera/camera_android_camerax/lib/src/camera_info.dart @@ -5,7 +5,7 @@ import 'package:flutter/services.dart' show BinaryMessenger; import 'android_camera_camerax_flutter_api_impls.dart'; -import 'camerax_library.pigeon.dart'; +import 'camerax_library.g.dart'; import 'instance_manager.dart'; import 'java_object.dart'; diff --git a/packages/camera/camera_android_camerax/lib/src/camera_selector.dart b/packages/camera/camera_android_camerax/lib/src/camera_selector.dart index dd08b9bc571d..f1d3c5fdb663 100644 --- a/packages/camera/camera_android_camerax/lib/src/camera_selector.dart +++ b/packages/camera/camera_android_camerax/lib/src/camera_selector.dart @@ -6,7 +6,7 @@ import 'package:flutter/services.dart'; import 'android_camera_camerax_flutter_api_impls.dart'; import 'camera_info.dart'; -import 'camerax_library.pigeon.dart'; +import 'camerax_library.g.dart'; import 'instance_manager.dart'; import 'java_object.dart'; @@ -44,10 +44,24 @@ class CameraSelector extends JavaObject { late final CameraSelectorHostApiImpl _api; /// ID for front facing lens. - static const int LENS_FACING_FRONT = 0; + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraSelector#LENS_FACING_FRONT(). + static const int lensFacingFront = 0; /// ID for back facing lens. - static const int LENS_FACING_BACK = 1; + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraSelector#LENS_FACING_BACK(). + static const int lensFacingBack = 1; + + /// ID for external lens. + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraSelector#LENS_FACING_EXTERNAL(). + static const int lensFacingExternal = 2; + + /// ID for unknown lens. + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraSelector#LENS_FACING_UNKNOWN(). + static const int lensFacingUnknown = -1; /// Selector for default front facing camera. static CameraSelector getDefaultFrontCamera({ @@ -57,7 +71,7 @@ class CameraSelector extends JavaObject { return CameraSelector( binaryMessenger: binaryMessenger, instanceManager: instanceManager, - lensFacing: LENS_FACING_FRONT, + lensFacing: lensFacingFront, ); } @@ -69,7 +83,7 @@ class CameraSelector extends JavaObject { return CameraSelector( binaryMessenger: binaryMessenger, instanceManager: instanceManager, - lensFacing: LENS_FACING_BACK, + lensFacing: lensFacingBack, ); } @@ -128,15 +142,17 @@ class CameraSelectorHostApiImpl extends CameraSelectorHostApi { lensFacing: original.lensFacing); }); - final List cameraInfoIds = (cameraInfos.map( - (CameraInfo info) => instanceManager.getIdentifier(info)!)).toList(); + final List cameraInfoIds = cameraInfos + .map((CameraInfo info) => instanceManager.getIdentifier(info)!) + .toList(); final List filteredCameraInfoIds = await filter(identifier, cameraInfoIds); if (filteredCameraInfoIds.isEmpty) { return []; } - return (filteredCameraInfoIds.map((int? id) => - instanceManager.getInstanceWithWeakReference(id!)! as CameraInfo)) + return filteredCameraInfoIds + .map((int? id) => + instanceManager.getInstanceWithWeakReference(id!)! as CameraInfo) .toList(); } } diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart new file mode 100644 index 000000000000..1d315e5a1600 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart @@ -0,0 +1,855 @@ +// 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 (v3.2.9), 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 +import 'dart:async'; +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; + +import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/services.dart'; + +class ResolutionInfo { + ResolutionInfo({ + required this.width, + required this.height, + }); + + int width; + int height; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['width'] = width; + pigeonMap['height'] = height; + return pigeonMap; + } + + static ResolutionInfo decode(Object message) { + final Map pigeonMap = message as Map; + return ResolutionInfo( + width: pigeonMap['width']! as int, + height: pigeonMap['height']! as int, + ); + } +} + +class CameraPermissionsErrorData { + CameraPermissionsErrorData({ + required this.errorCode, + required this.description, + }); + + String errorCode; + String description; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['errorCode'] = errorCode; + pigeonMap['description'] = description; + return pigeonMap; + } + + static CameraPermissionsErrorData decode(Object message) { + final Map pigeonMap = message as Map; + return CameraPermissionsErrorData( + errorCode: pigeonMap['errorCode']! as String, + description: pigeonMap['description']! as String, + ); + } +} + +class _JavaObjectHostApiCodec extends StandardMessageCodec { + const _JavaObjectHostApiCodec(); +} + +class JavaObjectHostApi { + /// Constructor for [JavaObjectHostApi]. 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. + JavaObjectHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _JavaObjectHostApiCodec(); + + Future dispose(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.JavaObjectHostApi.dispose', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _JavaObjectFlutterApiCodec extends StandardMessageCodec { + const _JavaObjectFlutterApiCodec(); +} + +abstract class JavaObjectFlutterApi { + static const MessageCodec codec = _JavaObjectFlutterApiCodec(); + + void dispose(int identifier); + static void setup(JavaObjectFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.JavaObjectFlutterApi.dispose', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.JavaObjectFlutterApi.dispose 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.JavaObjectFlutterApi.dispose was null, expected non-null int.'); + api.dispose(arg_identifier!); + return; + }); + } + } + } +} + +class _CameraInfoHostApiCodec extends StandardMessageCodec { + const _CameraInfoHostApiCodec(); +} + +class CameraInfoHostApi { + /// Constructor for [CameraInfoHostApi]. 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. + CameraInfoHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _CameraInfoHostApiCodec(); + + Future getSensorRotationDegrees(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraInfoHostApi.getSensorRotationDegrees', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as int?)!; + } + } +} + +class _CameraInfoFlutterApiCodec extends StandardMessageCodec { + const _CameraInfoFlutterApiCodec(); +} + +abstract class CameraInfoFlutterApi { + static const MessageCodec codec = _CameraInfoFlutterApiCodec(); + + void create(int identifier); + static void setup(CameraInfoFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraInfoFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.CameraInfoFlutterApi.create 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.CameraInfoFlutterApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return; + }); + } + } + } +} + +class _CameraSelectorHostApiCodec extends StandardMessageCodec { + const _CameraSelectorHostApiCodec(); +} + +class CameraSelectorHostApi { + /// Constructor for [CameraSelectorHostApi]. 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. + CameraSelectorHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _CameraSelectorHostApiCodec(); + + Future create(int arg_identifier, int? arg_lensFacing) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraSelectorHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier, arg_lensFacing]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future> filter( + int arg_identifier, List arg_cameraInfoIds) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraSelectorHostApi.filter', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier, arg_cameraInfoIds]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as List?)!.cast(); + } + } +} + +class _CameraSelectorFlutterApiCodec extends StandardMessageCodec { + const _CameraSelectorFlutterApiCodec(); +} + +abstract class CameraSelectorFlutterApi { + static const MessageCodec codec = _CameraSelectorFlutterApiCodec(); + + void create(int identifier, int? lensFacing); + static void setup(CameraSelectorFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraSelectorFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.CameraSelectorFlutterApi.create 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.CameraSelectorFlutterApi.create was null, expected non-null int.'); + final int? arg_lensFacing = (args[1] as int?); + api.create(arg_identifier!, arg_lensFacing); + return; + }); + } + } + } +} + +class _ProcessCameraProviderHostApiCodec extends StandardMessageCodec { + const _ProcessCameraProviderHostApiCodec(); +} + +class ProcessCameraProviderHostApi { + /// Constructor for [ProcessCameraProviderHostApi]. 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. + ProcessCameraProviderHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = + _ProcessCameraProviderHostApiCodec(); + + Future getInstance() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getInstance', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as int?)!; + } + } + + Future> getAvailableCameraInfos(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getAvailableCameraInfos', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as List?)!.cast(); + } + } + + Future bindToLifecycle(int arg_identifier, + int arg_cameraSelectorIdentifier, List arg_useCaseIds) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.bindToLifecycle', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel.send([ + arg_identifier, + arg_cameraSelectorIdentifier, + arg_useCaseIds + ]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as int?)!; + } + } + + Future unbind(int arg_identifier, List arg_useCaseIds) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.unbind', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier, arg_useCaseIds]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future unbindAll(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.unbindAll', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _ProcessCameraProviderFlutterApiCodec extends StandardMessageCodec { + const _ProcessCameraProviderFlutterApiCodec(); +} + +abstract class ProcessCameraProviderFlutterApi { + static const MessageCodec codec = + _ProcessCameraProviderFlutterApiCodec(); + + void create(int identifier); + static void setup(ProcessCameraProviderFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderFlutterApi.create 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.ProcessCameraProviderFlutterApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return; + }); + } + } + } +} + +class _CameraFlutterApiCodec extends StandardMessageCodec { + const _CameraFlutterApiCodec(); +} + +abstract class CameraFlutterApi { + static const MessageCodec codec = _CameraFlutterApiCodec(); + + void create(int identifier); + static void setup(CameraFlutterApi? api, {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.CameraFlutterApi.create 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.CameraFlutterApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return; + }); + } + } + } +} + +class _SystemServicesHostApiCodec extends StandardMessageCodec { + const _SystemServicesHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is CameraPermissionsErrorData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return CameraPermissionsErrorData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class SystemServicesHostApi { + /// Constructor for [SystemServicesHostApi]. 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. + SystemServicesHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _SystemServicesHostApiCodec(); + + Future requestCameraPermissions( + bool arg_enableAudio) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesHostApi.requestCameraPermissions', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_enableAudio]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as CameraPermissionsErrorData?); + } + } + + Future startListeningForDeviceOrientationChange( + bool arg_isFrontFacing, int arg_sensorOrientation) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesHostApi.startListeningForDeviceOrientationChange', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_isFrontFacing, arg_sensorOrientation]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future stopListeningForDeviceOrientationChange() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesHostApi.stopListeningForDeviceOrientationChange', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _SystemServicesFlutterApiCodec extends StandardMessageCodec { + const _SystemServicesFlutterApiCodec(); +} + +abstract class SystemServicesFlutterApi { + static const MessageCodec codec = _SystemServicesFlutterApiCodec(); + + void onDeviceOrientationChanged(String orientation); + void onCameraError(String errorDescription); + static void setup(SystemServicesFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesFlutterApi.onDeviceOrientationChanged', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.SystemServicesFlutterApi.onDeviceOrientationChanged was null.'); + final List args = (message as List?)!; + final String? arg_orientation = (args[0] as String?); + assert(arg_orientation != null, + 'Argument for dev.flutter.pigeon.SystemServicesFlutterApi.onDeviceOrientationChanged was null, expected non-null String.'); + api.onDeviceOrientationChanged(arg_orientation!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError was null.'); + final List args = (message as List?)!; + final String? arg_errorDescription = (args[0] as String?); + assert(arg_errorDescription != null, + 'Argument for dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError was null, expected non-null String.'); + api.onCameraError(arg_errorDescription!); + return; + }); + } + } + } +} + +class _PreviewHostApiCodec extends StandardMessageCodec { + const _PreviewHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is ResolutionInfo) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is ResolutionInfo) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return ResolutionInfo.decode(readValue(buffer)!); + + case 129: + return ResolutionInfo.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class PreviewHostApi { + /// Constructor for [PreviewHostApi]. 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. + PreviewHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _PreviewHostApiCodec(); + + Future create(int arg_identifier, int? arg_rotation, + ResolutionInfo? arg_targetResolution) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PreviewHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_identifier, arg_rotation, arg_targetResolution]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setSurfaceProvider(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as int?)!; + } + } + + Future releaseFlutterSurfaceTexture() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PreviewHostApi.releaseFlutterSurfaceTexture', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future getResolutionInfo(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PreviewHostApi.getResolutionInfo', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as ResolutionInfo?)!; + } + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_library.pigeon.dart b/packages/camera/camera_android_camerax/lib/src/camerax_library.pigeon.dart deleted file mode 100644 index c0b052378def..000000000000 --- a/packages/camera/camera_android_camerax/lib/src/camerax_library.pigeon.dart +++ /dev/null @@ -1,374 +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. -// Autogenerated from Pigeon (v3.2.9), 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 -import 'dart:async'; -import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; - -import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; -import 'package:flutter/services.dart'; - -class _JavaObjectHostApiCodec extends StandardMessageCodec { - const _JavaObjectHostApiCodec(); -} - -class JavaObjectHostApi { - /// Constructor for [JavaObjectHostApi]. 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. - JavaObjectHostApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - - final BinaryMessenger? _binaryMessenger; - - static const MessageCodec codec = _JavaObjectHostApiCodec(); - - Future dispose(int arg_identifier) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.JavaObjectHostApi.dispose', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } -} - -class _JavaObjectFlutterApiCodec extends StandardMessageCodec { - const _JavaObjectFlutterApiCodec(); -} - -abstract class JavaObjectFlutterApi { - static const MessageCodec codec = _JavaObjectFlutterApiCodec(); - - void dispose(int identifier); - static void setup(JavaObjectFlutterApi? api, - {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.JavaObjectFlutterApi.dispose', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.JavaObjectFlutterApi.dispose 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.JavaObjectFlutterApi.dispose was null, expected non-null int.'); - api.dispose(arg_identifier!); - return; - }); - } - } - } -} - -class _CameraInfoHostApiCodec extends StandardMessageCodec { - const _CameraInfoHostApiCodec(); -} - -class CameraInfoHostApi { - /// Constructor for [CameraInfoHostApi]. 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. - CameraInfoHostApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - - final BinaryMessenger? _binaryMessenger; - - static const MessageCodec codec = _CameraInfoHostApiCodec(); - - Future getSensorRotationDegrees(int arg_identifier) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CameraInfoHostApi.getSensorRotationDegrees', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else if (replyMap['result'] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (replyMap['result'] as int?)!; - } - } -} - -class _CameraInfoFlutterApiCodec extends StandardMessageCodec { - const _CameraInfoFlutterApiCodec(); -} - -abstract class CameraInfoFlutterApi { - static const MessageCodec codec = _CameraInfoFlutterApiCodec(); - - void create(int identifier); - static void setup(CameraInfoFlutterApi? api, - {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CameraInfoFlutterApi.create', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.CameraInfoFlutterApi.create 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.CameraInfoFlutterApi.create was null, expected non-null int.'); - api.create(arg_identifier!); - return; - }); - } - } - } -} - -class _CameraSelectorHostApiCodec extends StandardMessageCodec { - const _CameraSelectorHostApiCodec(); -} - -class CameraSelectorHostApi { - /// Constructor for [CameraSelectorHostApi]. 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. - CameraSelectorHostApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - - final BinaryMessenger? _binaryMessenger; - - static const MessageCodec codec = _CameraSelectorHostApiCodec(); - - Future create(int arg_identifier, int? arg_lensFacing) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CameraSelectorHostApi.create', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier, arg_lensFacing]) - as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } - - Future> filter( - int arg_identifier, List arg_cameraInfoIds) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CameraSelectorHostApi.filter', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier, arg_cameraInfoIds]) - as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else if (replyMap['result'] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (replyMap['result'] as List?)!.cast(); - } - } -} - -class _CameraSelectorFlutterApiCodec extends StandardMessageCodec { - const _CameraSelectorFlutterApiCodec(); -} - -abstract class CameraSelectorFlutterApi { - static const MessageCodec codec = _CameraSelectorFlutterApiCodec(); - - void create(int identifier, int? lensFacing); - static void setup(CameraSelectorFlutterApi? api, - {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CameraSelectorFlutterApi.create', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.CameraSelectorFlutterApi.create 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.CameraSelectorFlutterApi.create was null, expected non-null int.'); - final int? arg_lensFacing = (args[1] as int?); - api.create(arg_identifier!, arg_lensFacing); - return; - }); - } - } - } -} - -class _ProcessCameraProviderHostApiCodec extends StandardMessageCodec { - const _ProcessCameraProviderHostApiCodec(); -} - -class ProcessCameraProviderHostApi { - /// Constructor for [ProcessCameraProviderHostApi]. 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. - ProcessCameraProviderHostApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - - final BinaryMessenger? _binaryMessenger; - - static const MessageCodec codec = - _ProcessCameraProviderHostApiCodec(); - - Future getInstance() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getInstance', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else if (replyMap['result'] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (replyMap['result'] as int?)!; - } - } - - Future> getAvailableCameraInfos(int arg_identifier) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getAvailableCameraInfos', - codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else if (replyMap['result'] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (replyMap['result'] as List?)!.cast(); - } - } -} - -class _ProcessCameraProviderFlutterApiCodec extends StandardMessageCodec { - const _ProcessCameraProviderFlutterApiCodec(); -} - -abstract class ProcessCameraProviderFlutterApi { - static const MessageCodec codec = - _ProcessCameraProviderFlutterApiCodec(); - - void create(int identifier); - static void setup(ProcessCameraProviderFlutterApi? api, - {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ProcessCameraProviderFlutterApi.create', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.ProcessCameraProviderFlutterApi.create 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.ProcessCameraProviderFlutterApi.create was null, expected non-null int.'); - api.create(arg_identifier!); - return; - }); - } - } - } -} diff --git a/packages/camera/camera_android_camerax/lib/src/instance_manager.dart b/packages/camera/camera_android_camerax/lib/src/instance_manager.dart index dd48610c8b56..8c6081c855ba 100644 --- a/packages/camera/camera_android_camerax/lib/src/instance_manager.dart +++ b/packages/camera/camera_android_camerax/lib/src/instance_manager.dart @@ -124,22 +124,26 @@ class InstanceManager { /// This method also expects the host `InstanceManager` to have a strong /// reference to the instance the identifier is associated with. T? getInstanceWithWeakReference(int identifier) { - final Object? weakInstance = _weakInstances[identifier]?.target; + final T? weakInstance = _weakInstances[identifier]?.target as T?; if (weakInstance == null) { - final Object? strongInstance = _strongInstances[identifier]; + final T? strongInstance = _strongInstances[identifier] as T?; if (strongInstance != null) { - final Object copy = - _copyCallbacks[identifier]!(strongInstance)! as Object; + // This cast is safe since it matches the argument type for + // _addInstanceWithIdentifier, which is the only place _copyCallbacks + // is populated. + final T Function(T) copyCallback = + _copyCallbacks[identifier]! as T Function(T); + final T copy = copyCallback(strongInstance); _identifiers[copy] = identifier; - _weakInstances[identifier] = WeakReference(copy); + _weakInstances[identifier] = WeakReference(copy); _finalizer.attach(copy, identifier, detach: copy); - return copy as T; + return copy; } - return strongInstance as T?; + return strongInstance; } - return weakInstance as T; + return weakInstance; } /// Retrieves the identifier associated with instance. diff --git a/packages/camera/camera_android_camerax/lib/src/java_object.dart b/packages/camera/camera_android_camerax/lib/src/java_object.dart index 36a29ed0517b..f6127d4a8106 100644 --- a/packages/camera/camera_android_camerax/lib/src/java_object.dart +++ b/packages/camera/camera_android_camerax/lib/src/java_object.dart @@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart' show immutable; import 'package:flutter/services.dart'; -import 'camerax_library.pigeon.dart'; +import 'camerax_library.g.dart'; import 'instance_manager.dart'; /// Root of the Java class hierarchy. diff --git a/packages/camera/camera_android_camerax/lib/src/preview.dart b/packages/camera/camera_android_camerax/lib/src/preview.dart new file mode 100644 index 000000000000..602bcb3da76a --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/preview.dart @@ -0,0 +1,126 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart' show BinaryMessenger; + +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; +import 'use_case.dart'; + +/// Use case that provides a camera preview stream for display. +/// +/// See https://developer.android.com/reference/androidx/camera/core/Preview. +class Preview extends UseCase { + /// Creates a [Preview]. + Preview( + {BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + this.targetRotation, + this.targetResolution}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = PreviewHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + _api.createFromInstance(this, targetRotation, targetResolution); + } + + /// Constructs a [Preview] that is not automatically attached to a native object. + Preview.detached( + {BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + this.targetRotation, + this.targetResolution}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = PreviewHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + } + + late final PreviewHostApiImpl _api; + + /// Target rotation of the camera used for the preview stream. + final int? targetRotation; + + /// Target resolution of the camera preview stream. + final ResolutionInfo? targetResolution; + + /// Sets the surface provider for the preview stream. + /// + /// Returns the ID of the FlutterSurfaceTextureEntry used on the native end + /// used to display the preview stream on a [Texture] of the same ID. + Future setSurfaceProvider() { + return _api.setSurfaceProviderFromInstance(this); + } + + /// Releases Flutter surface texture used to provide a surface for the preview + /// stream. + void releaseFlutterSurfaceTexture() { + _api.releaseFlutterSurfaceTextureFromInstance(); + } + + /// Retrieves the selected resolution information of this [Preview]. + Future getResolutionInfo() { + return _api.getResolutionInfoFromInstance(this); + } +} + +/// Host API implementation of [Preview]. +class PreviewHostApiImpl extends PreviewHostApi { + /// Constructs a [PreviewHostApiImpl]. + PreviewHostApiImpl({this.binaryMessenger, InstanceManager? instanceManager}) { + this.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. + late final InstanceManager instanceManager; + + /// Creates a [Preview] with the target rotation provided if specified. + void createFromInstance( + Preview instance, int? targetRotation, ResolutionInfo? targetResolution) { + final int identifier = instanceManager.addDartCreatedInstance(instance, + onCopy: (Preview original) { + return Preview.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + targetRotation: original.targetRotation); + }); + create(identifier, targetRotation, targetResolution); + } + + /// Sets the surface provider of the specified [Preview] instance and returns + /// the ID corresponding to the surface it will provide. + Future setSurfaceProviderFromInstance(Preview instance) async { + final int? identifier = instanceManager.getIdentifier(instance); + assert(identifier != null, + 'No Preview has the identifer of that requested to set the surface provider on.'); + + final int surfaceTextureEntryId = await setSurfaceProvider(identifier!); + return surfaceTextureEntryId; + } + + /// Releases Flutter surface texture used to provide a surface for the preview + /// stream if a surface provider was set for a [Preview] instance. + void releaseFlutterSurfaceTextureFromInstance() { + releaseFlutterSurfaceTexture(); + } + + /// Gets the resolution information of the specified [Preview] instance. + Future getResolutionInfoFromInstance(Preview instance) async { + final int? identifier = instanceManager.getIdentifier(instance); + assert(identifier != null, + 'No Preview has the identifer of that requested to get the resolution information for.'); + + final ResolutionInfo resolutionInfo = await getResolutionInfo(identifier!); + return resolutionInfo; + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/process_camera_provider.dart b/packages/camera/camera_android_camerax/lib/src/process_camera_provider.dart index e2b588d15faa..ed9e820a1fa0 100644 --- a/packages/camera/camera_android_camerax/lib/src/process_camera_provider.dart +++ b/packages/camera/camera_android_camerax/lib/src/process_camera_provider.dart @@ -5,10 +5,13 @@ import 'package:flutter/services.dart'; import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camera.dart'; import 'camera_info.dart'; -import 'camerax_library.pigeon.dart'; +import 'camera_selector.dart'; +import 'camerax_library.g.dart'; import 'instance_manager.dart'; import 'java_object.dart'; +import 'use_case.dart'; /// Provides an object to manage the camera. /// @@ -42,6 +45,25 @@ class ProcessCameraProvider extends JavaObject { Future> getAvailableCameraInfos() { return _api.getAvailableCameraInfosFromInstances(this); } + + /// Binds the specified [UseCase]s to the lifecycle of the camera that it + /// returns. + Future bindToLifecycle( + CameraSelector cameraSelector, List useCases) { + return _api.bindToLifecycleFromInstances(this, cameraSelector, useCases); + } + + /// Unbinds specified [UseCase]s from the lifecycle of the camera that this + /// instance tracks. + void unbind(List useCases) { + _api.unbindFromInstances(this, useCases); + } + + /// Unbinds all previously bound [UseCase]s from the lifecycle of the camera + /// that this tracks. + void unbindAll() { + _api.unbindAllFromInstances(this); + } } /// Host API implementation of [ProcessCameraProvider]. @@ -69,21 +91,71 @@ class ProcessCameraProviderHostApiImpl extends ProcessCameraProviderHostApi { as ProcessCameraProvider; } + /// Gets identifier that the [instanceManager] has set for + /// the [ProcessCameraProvider] instance. + int getProcessCameraProviderIdentifier(ProcessCameraProvider instance) { + final int? identifier = instanceManager.getIdentifier(instance); + + assert(identifier != null, + 'No ProcessCameraProvider has the identifer of that which was requested.'); + return identifier!; + } + /// Retrives the list of CameraInfos corresponding to the available cameras. Future> getAvailableCameraInfosFromInstances( ProcessCameraProvider instance) async { - int? identifier = instanceManager.getIdentifier(instance); - identifier ??= instanceManager.addDartCreatedInstance(instance, - onCopy: (ProcessCameraProvider original) { - return ProcessCameraProvider.detached( - binaryMessenger: binaryMessenger, instanceManager: instanceManager); - }); - + final int identifier = getProcessCameraProviderIdentifier(instance); final List cameraInfos = await getAvailableCameraInfos(identifier); - return (cameraInfos.map((int? id) => - instanceManager.getInstanceWithWeakReference(id!)! as CameraInfo)) + return cameraInfos + .map((int? id) => + instanceManager.getInstanceWithWeakReference(id!)! as CameraInfo) .toList(); } + + /// Binds the specified [UseCase]s to the lifecycle of the camera which + /// the provided [ProcessCameraProvider] instance tracks. + /// + /// The instance of the camera whose lifecycle the [UseCase]s are bound to + /// is returned. + Future bindToLifecycleFromInstances( + ProcessCameraProvider instance, + CameraSelector cameraSelector, + List useCases, + ) async { + final int identifier = getProcessCameraProviderIdentifier(instance); + final List useCaseIds = useCases + .map((UseCase useCase) => instanceManager.getIdentifier(useCase)!) + .toList(); + + final int cameraIdentifier = await bindToLifecycle( + identifier, + instanceManager.getIdentifier(cameraSelector)!, + useCaseIds, + ); + return instanceManager.getInstanceWithWeakReference(cameraIdentifier)! + as Camera; + } + + /// Unbinds specified [UseCase]s from the lifecycle of the camera which the + /// provided [ProcessCameraProvider] instance tracks. + void unbindFromInstances( + ProcessCameraProvider instance, + List useCases, + ) { + final int identifier = getProcessCameraProviderIdentifier(instance); + final List useCaseIds = useCases + .map((UseCase useCase) => instanceManager.getIdentifier(useCase)!) + .toList(); + + unbind(identifier, useCaseIds); + } + + /// Unbinds all previously bound [UseCase]s from the lifecycle of the camera + /// which the provided [ProcessCameraProvider] instance tracks. + void unbindAllFromInstances(ProcessCameraProvider instance) { + final int identifier = getProcessCameraProviderIdentifier(instance); + unbindAll(identifier); + } } /// Flutter API Implementation of [ProcessCameraProvider]. diff --git a/packages/camera/camera_android_camerax/lib/src/surface.dart b/packages/camera/camera_android_camerax/lib/src/surface.dart new file mode 100644 index 000000000000..ea8cf8cb751e --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/surface.dart @@ -0,0 +1,34 @@ +// 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 'java_object.dart'; + +/// Handle onto the raw buffer managed by screen compositor. +/// +/// See https://developer.android.com/reference/android/view/Surface.html. +class Surface extends JavaObject { + /// Creates a detached [UseCase]. + Surface.detached({super.binaryMessenger, super.instanceManager}) + : super.detached(); + + /// Rotation constant to signify the natural orientation. + /// + /// See https://developer.android.com/reference/android/view/Surface.html#ROTATION_0. + static const int ROTATION_0 = 0; + + /// Rotation constant to signify a 90 degrees rotation. + /// + /// See https://developer.android.com/reference/android/view/Surface.html#ROTATION_90. + static const int ROTATION_90 = 1; + + /// Rotation constant to signify a 180 degrees rotation. + /// + /// See https://developer.android.com/reference/android/view/Surface.html#ROTATION_180. + static const int ROTATION_180 = 2; + + /// Rotation constant to signify a 270 degrees rotation. + /// + /// See https://developer.android.com/reference/android/view/Surface.html#ROTATION_270. + static const int ROTATION_270 = 3; +} diff --git a/packages/camera/camera_android_camerax/lib/src/system_services.dart b/packages/camera/camera_android_camerax/lib/src/system_services.dart new file mode 100644 index 000000000000..e108b6140bed --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/system_services.dart @@ -0,0 +1,147 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:camera_platform_interface/camera_platform_interface.dart' + show CameraException, DeviceOrientationChangedEvent; +import 'package:flutter/services.dart'; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camerax_library.g.dart'; + +// Ignoring lint indicating this class only contains static members +// as this class is a wrapper for various Android system services. +// ignore_for_file: avoid_classes_with_only_static_members + +/// Utility class that offers access to Android system services needed for +/// camera usage and other informational streams. +class SystemServices { + /// Stream that emits the device orientation whenever it is changed. + /// + /// Values may start being added to the stream once + /// `startListeningForDeviceOrientationChange(...)` is called. + static final StreamController + deviceOrientationChangedStreamController = + StreamController.broadcast(); + + /// Stream that emits the errors caused by camera usage on the native side. + static final StreamController cameraErrorStreamController = + StreamController.broadcast(); + + /// Requests permission to access the camera and audio if specified. + static Future requestCameraPermissions(bool enableAudio, + {BinaryMessenger? binaryMessenger}) { + final SystemServicesHostApiImpl api = + SystemServicesHostApiImpl(binaryMessenger: binaryMessenger); + + return api.sendCameraPermissionsRequest(enableAudio); + } + + /// Requests that [deviceOrientationChangedStreamController] start + /// emitting values for any change in device orientation. + static void startListeningForDeviceOrientationChange( + bool isFrontFacing, int sensorOrientation, + {BinaryMessenger? binaryMessenger}) { + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + final SystemServicesHostApi api = + SystemServicesHostApi(binaryMessenger: binaryMessenger); + + api.startListeningForDeviceOrientationChange( + isFrontFacing, sensorOrientation); + } + + /// Stops the [deviceOrientationChangedStreamController] from emitting values + /// for changes in device orientation. + static void stopListeningForDeviceOrientationChange( + {BinaryMessenger? binaryMessenger}) { + final SystemServicesHostApi api = + SystemServicesHostApi(binaryMessenger: binaryMessenger); + + api.stopListeningForDeviceOrientationChange(); + } +} + +/// Host API implementation of [SystemServices]. +class SystemServicesHostApiImpl extends SystemServicesHostApi { + /// Creates a [SystemServicesHostApiImpl]. + SystemServicesHostApiImpl({this.binaryMessenger}) + : super(binaryMessenger: binaryMessenger); + + /// 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; + + /// Requests permission to access the camera and audio if specified. + /// + /// Will complete normally if permissions are successfully granted; otherwise, + /// will throw a [CameraException]. + Future sendCameraPermissionsRequest(bool enableAudio) async { + final CameraPermissionsErrorData? error = + await requestCameraPermissions(enableAudio); + + if (error != null) { + throw CameraException( + error.errorCode, + error.description, + ); + } + } +} + +/// Flutter API implementation of [SystemServices]. +class SystemServicesFlutterApiImpl implements SystemServicesFlutterApi { + /// Constructs a [SystemServicesFlutterApiImpl]. + SystemServicesFlutterApiImpl({ + this.binaryMessenger, + }); + + /// 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; + + /// Callback method for any changes in device orientation. + /// + /// Will only be called if + /// `SystemServices.startListeningForDeviceOrientationChange(...)` was called + /// to start listening for device orientation updates. + @override + void onDeviceOrientationChanged(String orientation) { + final DeviceOrientation deviceOrientation = + deserializeDeviceOrientation(orientation); + if (deviceOrientation == null) { + return; + } + SystemServices.deviceOrientationChangedStreamController + .add(DeviceOrientationChangedEvent(deviceOrientation)); + } + + /// Deserializes device orientation in [String] format into a + /// [DeviceOrientation]. + DeviceOrientation deserializeDeviceOrientation(String orientation) { + switch (orientation) { + case 'LANDSCAPE_LEFT': + return DeviceOrientation.landscapeLeft; + case 'LANDSCAPE_RIGHT': + return DeviceOrientation.landscapeRight; + case 'PORTRAIT_DOWN': + return DeviceOrientation.portraitDown; + case 'PORTRAIT_UP': + return DeviceOrientation.portraitUp; + default: + throw ArgumentError( + '"$orientation" is not a valid DeviceOrientation value'); + } + } + + /// Callback method for any errors caused by camera usage on the Java side. + @override + void onCameraError(String errorDescription) { + SystemServices.cameraErrorStreamController.add(errorDescription); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/use_case.dart b/packages/camera/camera_android_camerax/lib/src/use_case.dart new file mode 100644 index 000000000000..f8910d9c5347 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/use_case.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 'java_object.dart'; + +/// An object representing the different functionalitites of the camera. +/// +/// See https://developer.android.com/reference/androidx/camera/core/UseCase. +class UseCase extends JavaObject { + /// Creates a detached [UseCase]. + UseCase.detached({super.binaryMessenger, super.instanceManager}) + : super.detached(); +} diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart index 4d7d96910246..4172cd7db073 100644 --- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart +++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart @@ -6,8 +6,8 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon( PigeonOptions( - dartOut: 'lib/src/camerax_library.pigeon.dart', - dartTestOut: 'test/test_camerax_library.pigeon.dart', + dartOut: 'lib/src/camerax_library.g.dart', + dartTestOut: 'test/test_camerax_library.g.dart', dartOptions: DartOptions(copyrightHeader: [ 'Copyright 2013 The Flutter Authors. All rights reserved.', 'Use of this source code is governed by a BSD-style license that can be', @@ -26,6 +26,26 @@ import 'package:pigeon/pigeon.dart'; ), ), ) +class ResolutionInfo { + ResolutionInfo({ + required this.width, + required this.height, + }); + + int width; + int height; +} + +class CameraPermissionsErrorData { + CameraPermissionsErrorData({ + required this.errorCode, + required this.description, + }); + + String errorCode; + String description; +} + @HostApi(dartHostTestHandler: 'TestJavaObjectHostApi') abstract class JavaObjectHostApi { void dispose(int identifier); @@ -64,9 +84,50 @@ abstract class ProcessCameraProviderHostApi { int getInstance(); List getAvailableCameraInfos(int identifier); + + int bindToLifecycle( + int identifier, int cameraSelectorIdentifier, List useCaseIds); + + void unbind(int identifier, List useCaseIds); + + void unbindAll(int identifier); } @FlutterApi() abstract class ProcessCameraProviderFlutterApi { void create(int identifier); } + +@FlutterApi() +abstract class CameraFlutterApi { + void create(int identifier); +} + +@HostApi(dartHostTestHandler: 'TestSystemServicesHostApi') +abstract class SystemServicesHostApi { + @async + CameraPermissionsErrorData? requestCameraPermissions(bool enableAudio); + + void startListeningForDeviceOrientationChange( + bool isFrontFacing, int sensorOrientation); + + void stopListeningForDeviceOrientationChange(); +} + +@FlutterApi() +abstract class SystemServicesFlutterApi { + void onDeviceOrientationChanged(String orientation); + + void onCameraError(String errorDescription); +} + +@HostApi(dartHostTestHandler: 'TestPreviewHostApi') +abstract class PreviewHostApi { + void create(int identifier, int? rotation, ResolutionInfo? targetResolution); + + int setSurfaceProvider(int identifier); + + void releaseFlutterSurfaceTexture(); + + ResolutionInfo getResolutionInfo(int identifier); +} diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 9873db1a0121..f1496c640497 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -21,10 +21,14 @@ dependencies: camera_platform_interface: ^2.2.0 flutter: sdk: flutter + integration_test: + sdk: flutter + stream_transform: ^2.1.0 dev_dependencies: + async: ^2.5.0 build_runner: ^2.1.4 flutter_test: sdk: flutter - mockito: ^5.1.0 + mockito: ^5.3.2 pigeon: ^3.2.6 diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart new file mode 100644 index 000000000000..acfaf16b9ac4 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart @@ -0,0 +1,405 @@ +// 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:async/async.dart'; +import 'package:camera_android_camerax/camera_android_camerax.dart'; +import 'package:camera_android_camerax/src/camera.dart'; +import 'package:camera_android_camerax/src/camera_info.dart'; +import 'package:camera_android_camerax/src/camera_selector.dart'; +import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/preview.dart'; +import 'package:camera_android_camerax/src/process_camera_provider.dart'; +import 'package:camera_android_camerax/src/system_services.dart'; +import 'package:camera_android_camerax/src/use_case.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/services.dart' show DeviceOrientation; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'android_camera_camerax_test.mocks.dart'; + +@GenerateNiceMocks(>[ + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), +]) +@GenerateMocks([BuildContext]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('Should fetch CameraDescription instances for available cameras', + () async { + // Arrange + final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax(); + camera.processCameraProvider = MockProcessCameraProvider(); + final List returnData = [ + { + 'name': 'Camera 0', + 'lensFacing': 'back', + 'sensorOrientation': 0 + }, + { + 'name': 'Camera 1', + 'lensFacing': 'front', + 'sensorOrientation': 90 + } + ]; + + // Create mocks to use + final MockCameraInfo mockFrontCameraInfo = MockCameraInfo(); + final MockCameraInfo mockBackCameraInfo = MockCameraInfo(); + + // Mock calls to native platform + when(camera.processCameraProvider!.getAvailableCameraInfos()).thenAnswer( + (_) async => [mockBackCameraInfo, mockFrontCameraInfo]); + when(camera.mockBackCameraSelector + .filter([mockFrontCameraInfo])) + .thenAnswer((_) async => []); + when(camera.mockBackCameraSelector + .filter([mockBackCameraInfo])) + .thenAnswer((_) async => [mockBackCameraInfo]); + when(camera.mockFrontCameraSelector + .filter([mockBackCameraInfo])) + .thenAnswer((_) async => []); + when(camera.mockFrontCameraSelector + .filter([mockFrontCameraInfo])) + .thenAnswer((_) async => [mockFrontCameraInfo]); + when(mockBackCameraInfo.getSensorRotationDegrees()) + .thenAnswer((_) async => 0); + when(mockFrontCameraInfo.getSensorRotationDegrees()) + .thenAnswer((_) async => 90); + + final List cameraDescriptions = + await camera.availableCameras(); + + expect(cameraDescriptions.length, returnData.length); + for (int i = 0; i < returnData.length; i++) { + final Map typedData = + (returnData[i] as Map).cast(); + final CameraDescription cameraDescription = CameraDescription( + name: typedData['name']! as String, + lensDirection: (typedData['lensFacing']! as String) == 'front' + ? CameraLensDirection.front + : CameraLensDirection.back, + sensorOrientation: typedData['sensorOrientation']! as int, + ); + expect(cameraDescriptions[i], cameraDescription); + } + }); + + test( + 'createCamera requests permissions, starts listening for device orientation changes, and returns flutter surface texture ID', + () async { + final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax(); + camera.processCameraProvider = MockProcessCameraProvider(); + const CameraLensDirection testLensDirection = CameraLensDirection.back; + const int testSensorOrientation = 90; + const CameraDescription testCameraDescription = CameraDescription( + name: 'cameraName', + lensDirection: testLensDirection, + sensorOrientation: testSensorOrientation); + const ResolutionPreset testResolutionPreset = ResolutionPreset.veryHigh; + const bool enableAudio = true; + const int testSurfaceTextureId = 6; + + when(camera.testPreview.setSurfaceProvider()) + .thenAnswer((_) async => testSurfaceTextureId); + + expect( + await camera.createCamera(testCameraDescription, testResolutionPreset, + enableAudio: enableAudio), + equals(testSurfaceTextureId)); + + // Verify permissions are requested and the camera starts listening for device orientation changes. + expect(camera.cameraPermissionsRequested, isTrue); + expect(camera.startedListeningForDeviceOrientationChanges, isTrue); + + // Verify CameraSelector is set with appropriate lens direction. + expect(camera.cameraSelector, equals(camera.mockBackCameraSelector)); + + // Verify the camera's Preview instance is instantiated properly. + expect(camera.preview, equals(camera.testPreview)); + + // Verify the camera's Preview instance has its surface provider set. + verify(camera.preview!.setSurfaceProvider()); + }); + + test( + 'initializeCamera throws AssertionError when createCamera has not been called before initializedCamera', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + expect(() => camera.initializeCamera(3), throwsAssertionError); + }); + + test('initializeCamera sends expected CameraInitializedEvent', () async { + final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax(); + camera.processCameraProvider = MockProcessCameraProvider(); + const int cameraId = 10; + const CameraLensDirection testLensDirection = CameraLensDirection.back; + const int testSensorOrientation = 90; + const CameraDescription testCameraDescription = CameraDescription( + name: 'cameraName', + lensDirection: testLensDirection, + sensorOrientation: testSensorOrientation); + const ResolutionPreset testResolutionPreset = ResolutionPreset.veryHigh; + const bool enableAudio = true; + const int resolutionWidth = 350; + const int resolutionHeight = 750; + final Camera mockCamera = MockCamera(); + final ResolutionInfo testResolutionInfo = + ResolutionInfo(width: resolutionWidth, height: resolutionHeight); + + // TODO(camsim99): Modify this when camera configuration is supported and + // defualt values no longer being used. + // https://github.com/flutter/flutter/issues/120468 + // https://github.com/flutter/flutter/issues/120467 + final CameraInitializedEvent testCameraInitializedEvent = + CameraInitializedEvent( + cameraId, + resolutionWidth.toDouble(), + resolutionHeight.toDouble(), + ExposureMode.auto, + false, + FocusMode.auto, + false); + + // Call createCamera. + when(camera.testPreview.setSurfaceProvider()) + .thenAnswer((_) async => cameraId); + await camera.createCamera(testCameraDescription, testResolutionPreset, + enableAudio: enableAudio); + + when(camera.processCameraProvider!.bindToLifecycle( + camera.cameraSelector!, [camera.testPreview])) + .thenAnswer((_) async => mockCamera); + when(camera.testPreview.getResolutionInfo()) + .thenAnswer((_) async => testResolutionInfo); + + // Start listening to camera events stream to verify the proper CameraInitializedEvent is sent. + camera.cameraEventStreamController.stream.listen((CameraEvent event) { + expect(event, const TypeMatcher()); + expect(event, equals(testCameraInitializedEvent)); + }); + + await camera.initializeCamera(cameraId); + + // Verify preview was bound and unbound to get preview resolution information. + verify(camera.processCameraProvider!.bindToLifecycle( + camera.cameraSelector!, [camera.testPreview])); + verify(camera.processCameraProvider!.unbind([camera.testPreview])); + + // Check camera instance was received, but preview is no longer bound. + expect(camera.camera, equals(mockCamera)); + expect(camera.previewIsBound, isFalse); + }); + + test('dispose releases Flutter surface texture and unbinds all use cases', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + + camera.preview = MockPreview(); + camera.processCameraProvider = MockProcessCameraProvider(); + + camera.dispose(3); + + verify(camera.preview!.releaseFlutterSurfaceTexture()); + verify(camera.processCameraProvider!.unbindAll()); + }); + + test('onCameraInitialized stream emits CameraInitializedEvents', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 16; + final Stream eventStream = + camera.onCameraInitialized(cameraId); + final StreamQueue streamQueue = + StreamQueue(eventStream); + const CameraInitializedEvent testEvent = CameraInitializedEvent( + cameraId, 320, 80, ExposureMode.auto, false, FocusMode.auto, false); + + camera.cameraEventStreamController.add(testEvent); + + expect(await streamQueue.next, testEvent); + await streamQueue.cancel(); + }); + + test('onCameraError stream emits errors caught by system services', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 27; + const String testErrorDescription = 'Test error description!'; + final Stream eventStream = camera.onCameraError(cameraId); + final StreamQueue streamQueue = + StreamQueue(eventStream); + + SystemServices.cameraErrorStreamController.add(testErrorDescription); + + expect(await streamQueue.next, + equals(const CameraErrorEvent(cameraId, testErrorDescription))); + await streamQueue.cancel(); + }); + + test( + 'onDeviceOrientationChanged stream emits changes in device oreintation detected by system services', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final Stream eventStream = + camera.onDeviceOrientationChanged(); + final StreamQueue streamQueue = + StreamQueue(eventStream); + const DeviceOrientationChangedEvent testEvent = + DeviceOrientationChangedEvent(DeviceOrientation.portraitDown); + + SystemServices.deviceOrientationChangedStreamController.add(testEvent); + + expect(await streamQueue.next, testEvent); + await streamQueue.cancel(); + }); + + test( + 'pausePreview unbinds preview from lifecycle when preview is nonnull and has been bound to lifecycle', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + + camera.processCameraProvider = MockProcessCameraProvider(); + camera.preview = MockPreview(); + camera.previewIsBound = true; + + await camera.pausePreview(579); + + verify(camera.processCameraProvider!.unbind([camera.preview!])); + expect(camera.previewIsBound, isFalse); + }); + + test( + 'pausePreview does not unbind preview from lifecycle when preview has not been bound to lifecycle', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + + camera.processCameraProvider = MockProcessCameraProvider(); + camera.preview = MockPreview(); + + await camera.pausePreview(632); + + verifyNever( + camera.processCameraProvider!.unbind([camera.preview!])); + }); + + test('resumePreview does not bind preview to lifecycle if already bound', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + + camera.processCameraProvider = MockProcessCameraProvider(); + camera.cameraSelector = MockCameraSelector(); + camera.preview = MockPreview(); + camera.previewIsBound = true; + + await camera.resumePreview(78); + + verifyNever(camera.processCameraProvider! + .bindToLifecycle(camera.cameraSelector!, [camera.preview!])); + }); + + test('resumePreview binds preview to lifecycle if not already bound', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + + camera.processCameraProvider = MockProcessCameraProvider(); + camera.cameraSelector = MockCameraSelector(); + camera.preview = MockPreview(); + + await camera.resumePreview(78); + + verify(camera.processCameraProvider! + .bindToLifecycle(camera.cameraSelector!, [camera.preview!])); + }); + + test( + 'buildPreview returns a FutureBuilder that does not return a Texture until the preview is bound to the lifecycle', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int textureId = 75; + + camera.processCameraProvider = MockProcessCameraProvider(); + camera.cameraSelector = MockCameraSelector(); + camera.preview = MockPreview(); + + final FutureBuilder previewWidget = + camera.buildPreview(textureId) as FutureBuilder; + + expect( + previewWidget.builder( + MockBuildContext(), const AsyncSnapshot.nothing()), + isA()); + expect( + previewWidget.builder( + MockBuildContext(), const AsyncSnapshot.waiting()), + isA()); + expect( + previewWidget.builder(MockBuildContext(), + const AsyncSnapshot.withData(ConnectionState.active, null)), + isA()); + }); + + test( + 'buildPreview returns a FutureBuilder that returns a Texture once the preview is bound to the lifecycle', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int textureId = 75; + + camera.processCameraProvider = MockProcessCameraProvider(); + camera.cameraSelector = MockCameraSelector(); + camera.preview = MockPreview(); + + final FutureBuilder previewWidget = + camera.buildPreview(textureId) as FutureBuilder; + + final Texture previewTexture = previewWidget.builder(MockBuildContext(), + const AsyncSnapshot.withData(ConnectionState.done, null)) + as Texture; + expect(previewTexture.textureId, equals(textureId)); + }); +} + +/// Mock of [AndroidCameraCameraX] that stubs behavior of some methods for +/// testing. +class MockAndroidCameraCamerax extends AndroidCameraCameraX { + bool cameraPermissionsRequested = false; + bool startedListeningForDeviceOrientationChanges = false; + final MockPreview testPreview = MockPreview(); + final MockCameraSelector mockBackCameraSelector = MockCameraSelector(); + final MockCameraSelector mockFrontCameraSelector = MockCameraSelector(); + + @override + Future requestCameraPermissions(bool enableAudio) async { + cameraPermissionsRequested = true; + } + + @override + void startListeningForDeviceOrientationChange( + bool cameraIsFrontFacing, int sensorOrientation) { + startedListeningForDeviceOrientationChanges = true; + return; + } + + @override + CameraSelector createCameraSelector(int cameraSelectorLensDirection) { + switch (cameraSelectorLensDirection) { + case CameraSelector.lensFacingFront: + return mockFrontCameraSelector; + case CameraSelector.lensFacingBack: + default: + return mockBackCameraSelector; + } + } + + @override + Preview createPreview(int targetRotation, ResolutionInfo? targetResolution) { + return testPreview; + } +} diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart new file mode 100644 index 000000000000..af225a10c64a --- /dev/null +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart @@ -0,0 +1,389 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in camera_android_camerax/test/android_camera_camerax_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i8; + +import 'package:camera_android_camerax/src/camera.dart' as _i3; +import 'package:camera_android_camerax/src/camera_info.dart' as _i7; +import 'package:camera_android_camerax/src/camera_selector.dart' as _i9; +import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i2; +import 'package:camera_android_camerax/src/preview.dart' as _i10; +import 'package:camera_android_camerax/src/process_camera_provider.dart' + as _i11; +import 'package:camera_android_camerax/src/use_case.dart' as _i12; +import 'package:flutter/foundation.dart' as _i6; +import 'package:flutter/services.dart' as _i5; +import 'package:flutter/src/widgets/framework.dart' as _i4; +import 'package:flutter/src/widgets/notification_listener.dart' as _i13; +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 + +class _FakeResolutionInfo_0 extends _i1.SmartFake + implements _i2.ResolutionInfo { + _FakeResolutionInfo_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeCamera_1 extends _i1.SmartFake implements _i3.Camera { + _FakeCamera_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWidget_2 extends _i1.SmartFake implements _i4.Widget { + _FakeWidget_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_3 extends _i1.SmartFake + implements _i4.InheritedWidget { + _FakeInheritedWidget_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_4 extends _i1.SmartFake + implements _i6.DiagnosticsNode { + _FakeDiagnosticsNode_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({ + _i6.TextTreeConfiguration? parentConfiguration, + _i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info, + }) => + super.toString(); +} + +/// A class which mocks [Camera]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCamera extends _i1.Mock implements _i3.Camera {} + +/// A class which mocks [CameraInfo]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCameraInfo extends _i1.Mock implements _i7.CameraInfo { + @override + _i8.Future getSensorRotationDegrees() => (super.noSuchMethod( + Invocation.method( + #getSensorRotationDegrees, + [], + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); +} + +/// A class which mocks [CameraSelector]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCameraSelector extends _i1.Mock implements _i9.CameraSelector { + @override + _i8.Future> filter(List<_i7.CameraInfo>? cameraInfos) => + (super.noSuchMethod( + Invocation.method( + #filter, + [cameraInfos], + ), + returnValue: _i8.Future>.value(<_i7.CameraInfo>[]), + returnValueForMissingStub: + _i8.Future>.value(<_i7.CameraInfo>[]), + ) as _i8.Future>); +} + +/// A class which mocks [Preview]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPreview extends _i1.Mock implements _i10.Preview { + @override + _i8.Future setSurfaceProvider() => (super.noSuchMethod( + Invocation.method( + #setSurfaceProvider, + [], + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); + @override + void releaseFlutterSurfaceTexture() => super.noSuchMethod( + Invocation.method( + #releaseFlutterSurfaceTexture, + [], + ), + returnValueForMissingStub: null, + ); + @override + _i8.Future<_i2.ResolutionInfo> getResolutionInfo() => (super.noSuchMethod( + Invocation.method( + #getResolutionInfo, + [], + ), + returnValue: _i8.Future<_i2.ResolutionInfo>.value(_FakeResolutionInfo_0( + this, + Invocation.method( + #getResolutionInfo, + [], + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.ResolutionInfo>.value(_FakeResolutionInfo_0( + this, + Invocation.method( + #getResolutionInfo, + [], + ), + )), + ) as _i8.Future<_i2.ResolutionInfo>); +} + +/// A class which mocks [ProcessCameraProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockProcessCameraProvider extends _i1.Mock + implements _i11.ProcessCameraProvider { + @override + _i8.Future> getAvailableCameraInfos() => + (super.noSuchMethod( + Invocation.method( + #getAvailableCameraInfos, + [], + ), + returnValue: _i8.Future>.value(<_i7.CameraInfo>[]), + returnValueForMissingStub: + _i8.Future>.value(<_i7.CameraInfo>[]), + ) as _i8.Future>); + @override + _i8.Future<_i3.Camera> bindToLifecycle( + _i9.CameraSelector? cameraSelector, + List<_i12.UseCase>? useCases, + ) => + (super.noSuchMethod( + Invocation.method( + #bindToLifecycle, + [ + cameraSelector, + useCases, + ], + ), + returnValue: _i8.Future<_i3.Camera>.value(_FakeCamera_1( + this, + Invocation.method( + #bindToLifecycle, + [ + cameraSelector, + useCases, + ], + ), + )), + returnValueForMissingStub: _i8.Future<_i3.Camera>.value(_FakeCamera_1( + this, + Invocation.method( + #bindToLifecycle, + [ + cameraSelector, + useCases, + ], + ), + )), + ) as _i8.Future<_i3.Camera>); + @override + void unbind(List<_i12.UseCase>? useCases) => super.noSuchMethod( + Invocation.method( + #unbind, + [useCases], + ), + returnValueForMissingStub: null, + ); + @override + void unbindAll() => super.noSuchMethod( + Invocation.method( + #unbindAll, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i4.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.Widget get widget => (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_2( + this, + Invocation.getter(#widget), + ), + ) as _i4.Widget); + @override + bool get mounted => (super.noSuchMethod( + Invocation.getter(#mounted), + returnValue: false, + ) as bool); + @override + bool get debugDoingBuild => (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + ) as bool); + @override + _i4.InheritedWidget dependOnInheritedElement( + _i4.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_3( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) as _i4.InheritedWidget); + @override + void visitAncestorElements(bool Function(_i4.Element)? visitor) => + super.noSuchMethod( + Invocation.method( + #visitAncestorElements, + [visitor], + ), + returnValueForMissingStub: null, + ); + @override + void visitChildElements(_i4.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method( + #visitChildElements, + [visitor], + ), + returnValueForMissingStub: null, + ); + @override + void dispatchNotification(_i13.Notification? notification) => + super.noSuchMethod( + Invocation.method( + #dispatchNotification, + [notification], + ), + returnValueForMissingStub: null, + ); + @override + _i6.DiagnosticsNode describeElement( + String? name, { + _i6.DiagnosticsTreeStyle? style = _i6.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + returnValue: _FakeDiagnosticsNode_4( + this, + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + ), + ) as _i6.DiagnosticsNode); + @override + _i6.DiagnosticsNode describeWidget( + String? name, { + _i6.DiagnosticsTreeStyle? style = _i6.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + returnValue: _FakeDiagnosticsNode_4( + this, + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + ), + ) as _i6.DiagnosticsNode); + @override + List<_i6.DiagnosticsNode> describeMissingAncestor( + {required Type? expectedAncestorType}) => + (super.noSuchMethod( + Invocation.method( + #describeMissingAncestor, + [], + {#expectedAncestorType: expectedAncestorType}, + ), + returnValue: <_i6.DiagnosticsNode>[], + ) as List<_i6.DiagnosticsNode>); + @override + _i6.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method( + #describeOwnershipChain, + [name], + ), + returnValue: _FakeDiagnosticsNode_4( + this, + Invocation.method( + #describeOwnershipChain, + [name], + ), + ), + ) as _i6.DiagnosticsNode); +} diff --git a/packages/camera/camera_android_camerax/test/camera_info_test.dart b/packages/camera/camera_android_camerax/test/camera_info_test.dart index eda822b33f73..852c799ebfbe 100644 --- a/packages/camera/camera_android_camerax/test/camera_info_test.dart +++ b/packages/camera/camera_android_camerax/test/camera_info_test.dart @@ -3,14 +3,14 @@ // found in the LICENSE file. import 'package:camera_android_camerax/src/camera_info.dart'; -import 'package:camera_android_camerax/src/camerax_library.pigeon.dart'; +import 'package:camera_android_camerax/src/camerax_library.g.dart'; import 'package:camera_android_camerax/src/instance_manager.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'camera_info_test.mocks.dart'; -import 'test_camerax_library.pigeon.dart'; +import 'test_camerax_library.g.dart'; @GenerateMocks([TestCameraInfoHostApi]) void main() { diff --git a/packages/camera/camera_android_camerax/test/camera_info_test.mocks.dart b/packages/camera/camera_android_camerax/test/camera_info_test.mocks.dart index e1f1e3ca9e9b..5e558a8226b6 100644 --- a/packages/camera/camera_android_camerax/test/camera_info_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/camera_info_test.mocks.dart @@ -1,11 +1,11 @@ -// Mocks generated by Mockito 5.3.0 from annotations +// Mocks generated by Mockito 5.3.2 from annotations // in camera_android_camerax/test/camera_info_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:mockito/mockito.dart' as _i1; -import 'test_camerax_library.pigeon.dart' as _i2; +import 'test_camerax_library.g.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -29,6 +29,10 @@ class MockTestCameraInfoHostApi extends _i1.Mock @override int getSensorRotationDegrees(int? identifier) => (super.noSuchMethod( - Invocation.method(#getSensorRotationDegrees, [identifier]), - returnValue: 0) as int); + Invocation.method( + #getSensorRotationDegrees, + [identifier], + ), + returnValue: 0, + ) as int); } diff --git a/packages/camera/camera_android_camerax/test/camera_selector_test.dart b/packages/camera/camera_android_camerax/test/camera_selector_test.dart index c4ccd6262376..52f9a18d956e 100644 --- a/packages/camera/camera_android_camerax/test/camera_selector_test.dart +++ b/packages/camera/camera_android_camerax/test/camera_selector_test.dart @@ -4,14 +4,14 @@ import 'package:camera_android_camerax/src/camera_info.dart'; import 'package:camera_android_camerax/src/camera_selector.dart'; -import 'package:camera_android_camerax/src/camerax_library.pigeon.dart'; +import 'package:camera_android_camerax/src/camerax_library.g.dart'; import 'package:camera_android_camerax/src/instance_manager.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'camera_selector_test.mocks.dart'; -import 'test_camerax_library.pigeon.dart'; +import 'test_camerax_library.g.dart'; @GenerateMocks([TestCameraSelectorHostApi]) void main() { @@ -60,10 +60,10 @@ void main() { ); CameraSelector( instanceManager: instanceManager, - lensFacing: CameraSelector.LENS_FACING_BACK); + lensFacing: CameraSelector.lensFacingBack); verify( - mockApi.create(argThat(isA()), CameraSelector.LENS_FACING_BACK)); + mockApi.create(argThat(isA()), CameraSelector.lensFacingBack)); }); test('filterTest', () async { @@ -108,14 +108,14 @@ void main() { instanceManager: instanceManager, ); - flutterApi.create(0, CameraSelector.LENS_FACING_BACK); + flutterApi.create(0, CameraSelector.lensFacingBack); expect(instanceManager.getInstanceWithWeakReference(0), isA()); expect( (instanceManager.getInstanceWithWeakReference(0)! as CameraSelector) .lensFacing, - equals(CameraSelector.LENS_FACING_BACK)); + equals(CameraSelector.lensFacingBack)); }); }); } diff --git a/packages/camera/camera_android_camerax/test/camera_selector_test.mocks.dart b/packages/camera/camera_android_camerax/test/camera_selector_test.mocks.dart index 456db1eaf822..31dce5177e2d 100644 --- a/packages/camera/camera_android_camerax/test/camera_selector_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/camera_selector_test.mocks.dart @@ -1,11 +1,11 @@ -// Mocks generated by Mockito 5.3.0 from annotations +// Mocks generated by Mockito 5.3.2 from annotations // in camera_android_camerax/test/camera_selector_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:mockito/mockito.dart' as _i1; -import 'test_camerax_library.pigeon.dart' as _i2; +import 'test_camerax_library.g.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -28,11 +28,33 @@ class MockTestCameraSelectorHostApi extends _i1.Mock } @override - void create(int? identifier, int? lensFacing) => - super.noSuchMethod(Invocation.method(#create, [identifier, lensFacing]), - returnValueForMissingStub: null); + void create( + int? identifier, + int? lensFacing, + ) => + super.noSuchMethod( + Invocation.method( + #create, + [ + identifier, + lensFacing, + ], + ), + returnValueForMissingStub: null, + ); @override - List filter(int? identifier, List? cameraInfoIds) => (super - .noSuchMethod(Invocation.method(#filter, [identifier, cameraInfoIds]), - returnValue: []) as List); + List filter( + int? identifier, + List? cameraInfoIds, + ) => + (super.noSuchMethod( + Invocation.method( + #filter, + [ + identifier, + cameraInfoIds, + ], + ), + returnValue: [], + ) as List); } diff --git a/packages/camera/camera_android_camerax/test/camera_test.dart b/packages/camera/camera_android_camerax/test/camera_test.dart new file mode 100644 index 000000000000..c2948282dcf1 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/camera_test.dart @@ -0,0 +1,26 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_android_camerax/src/camera.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Camera', () { + test('flutterApiCreateTest', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final CameraFlutterApiImpl flutterApi = CameraFlutterApiImpl( + instanceManager: instanceManager, + ); + + flutterApi.create(0); + + expect(instanceManager.getInstanceWithWeakReference(0), isA()); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/preview_test.dart b/packages/camera/camera_android_camerax/test/preview_test.dart new file mode 100644 index 000000000000..36b56f0046e1 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/preview_test.dart @@ -0,0 +1,138 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:camera_android_camerax/src/preview.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'preview_test.mocks.dart'; +import 'test_camerax_library.g.dart'; + +@GenerateMocks([TestPreviewHostApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Preview', () { + tearDown(() => TestPreviewHostApi.setup(null)); + + test('detached create does not call create on the Java side', () async { + final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi(); + TestPreviewHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + Preview.detached( + instanceManager: instanceManager, + targetRotation: 90, + targetResolution: ResolutionInfo(width: 50, height: 10), + ); + + verifyNever(mockApi.create(argThat(isA()), argThat(isA()), + argThat(isA()))); + }); + + test('create calls create on the Java side', () async { + final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi(); + TestPreviewHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + const int targetRotation = 90; + const int targetResolutionWidth = 10; + const int targetResolutionHeight = 50; + Preview( + instanceManager: instanceManager, + targetRotation: targetRotation, + targetResolution: ResolutionInfo( + width: targetResolutionWidth, height: targetResolutionHeight), + ); + + final VerificationResult createVerification = verify(mockApi.create( + argThat(isA()), argThat(equals(targetRotation)), captureAny)); + final ResolutionInfo capturedResolutionInfo = + createVerification.captured.single as ResolutionInfo; + expect(capturedResolutionInfo.width, equals(targetResolutionWidth)); + expect(capturedResolutionInfo.height, equals(targetResolutionHeight)); + }); + + test( + 'setSurfaceProvider makes call to set surface provider for preview instance', + () async { + final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi(); + TestPreviewHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + const int textureId = 8; + final Preview preview = Preview.detached( + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance( + preview, + 0, + onCopy: (_) => Preview.detached(), + ); + + when(mockApi.setSurfaceProvider(instanceManager.getIdentifier(preview))) + .thenReturn(textureId); + expect(await preview.setSurfaceProvider(), equals(textureId)); + + verify( + mockApi.setSurfaceProvider(instanceManager.getIdentifier(preview))); + }); + + test( + 'releaseFlutterSurfaceTexture makes call to relase flutter surface texture entry', + () async { + final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi(); + TestPreviewHostApi.setup(mockApi); + + final Preview preview = Preview.detached(); + + preview.releaseFlutterSurfaceTexture(); + + verify(mockApi.releaseFlutterSurfaceTexture()); + }); + + test( + 'getResolutionInfo makes call to get resolution information for preview instance', + () async { + final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi(); + TestPreviewHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final Preview preview = Preview.detached( + instanceManager: instanceManager, + ); + const int resolutionWidth = 10; + const int resolutionHeight = 60; + final ResolutionInfo testResolutionInfo = + ResolutionInfo(width: resolutionWidth, height: resolutionHeight); + + instanceManager.addHostCreatedInstance( + preview, + 0, + onCopy: (_) => Preview.detached(), + ); + + when(mockApi.getResolutionInfo(instanceManager.getIdentifier(preview))) + .thenReturn(testResolutionInfo); + + final ResolutionInfo previewResolutionInfo = + await preview.getResolutionInfo(); + expect(previewResolutionInfo.width, equals(resolutionWidth)); + expect(previewResolutionInfo.height, equals(resolutionHeight)); + + verify(mockApi.getResolutionInfo(instanceManager.getIdentifier(preview))); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/preview_test.mocks.dart b/packages/camera/camera_android_camerax/test/preview_test.mocks.dart new file mode 100644 index 000000000000..60fa1527487b --- /dev/null +++ b/packages/camera/camera_android_camerax/test/preview_test.mocks.dart @@ -0,0 +1,89 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in camera_android_camerax/test/preview_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +import 'test_camerax_library.g.dart' as _i3; + +// 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 + +class _FakeResolutionInfo_0 extends _i1.SmartFake + implements _i2.ResolutionInfo { + _FakeResolutionInfo_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [TestPreviewHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestPreviewHostApi extends _i1.Mock + implements _i3.TestPreviewHostApi { + MockTestPreviewHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create( + int? identifier, + int? rotation, + _i2.ResolutionInfo? targetResolution, + ) => + super.noSuchMethod( + Invocation.method( + #create, + [ + identifier, + rotation, + targetResolution, + ], + ), + returnValueForMissingStub: null, + ); + @override + int setSurfaceProvider(int? identifier) => (super.noSuchMethod( + Invocation.method( + #setSurfaceProvider, + [identifier], + ), + returnValue: 0, + ) as int); + @override + void releaseFlutterSurfaceTexture() => super.noSuchMethod( + Invocation.method( + #releaseFlutterSurfaceTexture, + [], + ), + returnValueForMissingStub: null, + ); + @override + _i2.ResolutionInfo getResolutionInfo(int? identifier) => (super.noSuchMethod( + Invocation.method( + #getResolutionInfo, + [identifier], + ), + returnValue: _FakeResolutionInfo_0( + this, + Invocation.method( + #getResolutionInfo, + [identifier], + ), + ), + ) as _i2.ResolutionInfo); +} diff --git a/packages/camera/camera_android_camerax/test/process_camera_provider_test.dart b/packages/camera/camera_android_camerax/test/process_camera_provider_test.dart index 65e7d00ddaea..548ac3e00d65 100644 --- a/packages/camera/camera_android_camerax/test/process_camera_provider_test.dart +++ b/packages/camera/camera_android_camerax/test/process_camera_provider_test.dart @@ -2,15 +2,18 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:camera_android_camerax/src/camera.dart'; import 'package:camera_android_camerax/src/camera_info.dart'; +import 'package:camera_android_camerax/src/camera_selector.dart'; import 'package:camera_android_camerax/src/instance_manager.dart'; import 'package:camera_android_camerax/src/process_camera_provider.dart'; +import 'package:camera_android_camerax/src/use_case.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'process_camera_provider_test.mocks.dart'; -import 'test_camerax_library.pigeon.dart'; +import 'test_camerax_library.g.dart'; @GenerateMocks([TestProcessCameraProviderHostApi]) void main() { @@ -78,6 +81,114 @@ void main() { verify(mockApi.getAvailableCameraInfos(0)); }); + test('bindToLifecycleTest', () async { + final MockTestProcessCameraProviderHostApi mockApi = + MockTestProcessCameraProviderHostApi(); + TestProcessCameraProviderHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final ProcessCameraProvider processCameraProvider = + ProcessCameraProvider.detached( + instanceManager: instanceManager, + ); + final CameraSelector fakeCameraSelector = + CameraSelector.detached(instanceManager: instanceManager); + final UseCase fakeUseCase = + UseCase.detached(instanceManager: instanceManager); + final Camera fakeCamera = + Camera.detached(instanceManager: instanceManager); + + instanceManager.addHostCreatedInstance( + processCameraProvider, + 0, + onCopy: (_) => ProcessCameraProvider.detached(), + ); + instanceManager.addHostCreatedInstance( + fakeCameraSelector, + 1, + onCopy: (_) => CameraSelector.detached(), + ); + instanceManager.addHostCreatedInstance( + fakeUseCase, + 2, + onCopy: (_) => UseCase.detached(), + ); + instanceManager.addHostCreatedInstance( + fakeCamera, + 3, + onCopy: (_) => Camera.detached(), + ); + + when(mockApi.bindToLifecycle(0, 1, [2])).thenReturn(3); + expect( + await processCameraProvider + .bindToLifecycle(fakeCameraSelector, [fakeUseCase]), + equals(fakeCamera)); + verify(mockApi.bindToLifecycle(0, 1, [2])); + }); + + test('unbindTest', () async { + final MockTestProcessCameraProviderHostApi mockApi = + MockTestProcessCameraProviderHostApi(); + TestProcessCameraProviderHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final ProcessCameraProvider processCameraProvider = + ProcessCameraProvider.detached( + instanceManager: instanceManager, + ); + final UseCase fakeUseCase = + UseCase.detached(instanceManager: instanceManager); + + instanceManager.addHostCreatedInstance( + processCameraProvider, + 0, + onCopy: (_) => ProcessCameraProvider.detached(), + ); + instanceManager.addHostCreatedInstance( + fakeUseCase, + 1, + onCopy: (_) => UseCase.detached(), + ); + + processCameraProvider.unbind([fakeUseCase]); + verify(mockApi.unbind(0, [1])); + }); + + test('unbindAllTest', () async { + final MockTestProcessCameraProviderHostApi mockApi = + MockTestProcessCameraProviderHostApi(); + TestProcessCameraProviderHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final ProcessCameraProvider processCameraProvider = + ProcessCameraProvider.detached( + instanceManager: instanceManager, + ); + final UseCase fakeUseCase = + UseCase.detached(instanceManager: instanceManager); + + instanceManager.addHostCreatedInstance( + processCameraProvider, + 0, + onCopy: (_) => ProcessCameraProvider.detached(), + ); + instanceManager.addHostCreatedInstance( + fakeUseCase, + 1, + onCopy: (_) => UseCase.detached(), + ); + + processCameraProvider.unbind([fakeUseCase]); + verify(mockApi.unbind(0, [1])); + }); + test('flutterApiCreateTest', () { final InstanceManager instanceManager = InstanceManager( onWeakReferenceRemoved: (_) {}, diff --git a/packages/camera/camera_android_camerax/test/process_camera_provider_test.mocks.dart b/packages/camera/camera_android_camerax/test/process_camera_provider_test.mocks.dart index 9fcfe690c062..2ce4ab72fa57 100644 --- a/packages/camera/camera_android_camerax/test/process_camera_provider_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/process_camera_provider_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.3.0 from annotations +// Mocks generated by Mockito 5.3.2 from annotations // in camera_android_camerax/test/process_camera_provider_test.dart. // Do not manually edit this file. @@ -7,7 +7,7 @@ import 'dart:async' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'test_camerax_library.pigeon.dart' as _i2; +import 'test_camerax_library.g.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -30,11 +30,59 @@ class MockTestProcessCameraProviderHostApi extends _i1.Mock } @override - _i3.Future getInstance() => - (super.noSuchMethod(Invocation.method(#getInstance, []), - returnValue: _i3.Future.value(0)) as _i3.Future); + _i3.Future getInstance() => (super.noSuchMethod( + Invocation.method( + #getInstance, + [], + ), + returnValue: _i3.Future.value(0), + ) as _i3.Future); @override List getAvailableCameraInfos(int? identifier) => (super.noSuchMethod( - Invocation.method(#getAvailableCameraInfos, [identifier]), - returnValue: []) as List); + Invocation.method( + #getAvailableCameraInfos, + [identifier], + ), + returnValue: [], + ) as List); + @override + int bindToLifecycle( + int? identifier, + int? cameraSelectorIdentifier, + List? useCaseIds, + ) => + (super.noSuchMethod( + Invocation.method( + #bindToLifecycle, + [ + identifier, + cameraSelectorIdentifier, + useCaseIds, + ], + ), + returnValue: 0, + ) as int); + @override + void unbind( + int? identifier, + List? useCaseIds, + ) => + super.noSuchMethod( + Invocation.method( + #unbind, + [ + identifier, + useCaseIds, + ], + ), + returnValueForMissingStub: null, + ); + @override + void unbindAll(int? identifier) => super.noSuchMethod( + Invocation.method( + #unbindAll, + [identifier], + ), + returnValueForMissingStub: null, + ); } diff --git a/packages/camera/camera_android_camerax/test/system_services_test.dart b/packages/camera/camera_android_camerax/test/system_services_test.dart new file mode 100644 index 000000000000..38037eaa135c --- /dev/null +++ b/packages/camera/camera_android_camerax/test/system_services_test.dart @@ -0,0 +1,110 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_android_camerax/src/camerax_library.g.dart' + show CameraPermissionsErrorData; +import 'package:camera_android_camerax/src/system_services.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart' + show CameraException, DeviceOrientationChangedEvent; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'system_services_test.mocks.dart'; +import 'test_camerax_library.g.dart'; + +@GenerateMocks([TestSystemServicesHostApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('SystemServices', () { + tearDown(() => TestProcessCameraProviderHostApi.setup(null)); + + test( + 'requestCameraPermissionsFromInstance completes normally without errors test', + () async { + final MockTestSystemServicesHostApi mockApi = + MockTestSystemServicesHostApi(); + TestSystemServicesHostApi.setup(mockApi); + + when(mockApi.requestCameraPermissions(true)) + .thenAnswer((_) async => null); + + await SystemServices.requestCameraPermissions(true); + verify(mockApi.requestCameraPermissions(true)); + }); + + test( + 'requestCameraPermissionsFromInstance throws CameraException if there was a request error', + () { + final MockTestSystemServicesHostApi mockApi = + MockTestSystemServicesHostApi(); + TestSystemServicesHostApi.setup(mockApi); + final CameraPermissionsErrorData error = CameraPermissionsErrorData( + errorCode: 'Test error code', + description: 'Test error description', + ); + + when(mockApi.requestCameraPermissions(true)) + .thenAnswer((_) async => error); + + expect( + () async => SystemServices.requestCameraPermissions(true), + throwsA(isA() + .having((CameraException e) => e.code, 'code', 'Test error code') + .having((CameraException e) => e.description, 'description', + 'Test error description'))); + verify(mockApi.requestCameraPermissions(true)); + }); + + test('startListeningForDeviceOrientationChangeTest', () async { + final MockTestSystemServicesHostApi mockApi = + MockTestSystemServicesHostApi(); + TestSystemServicesHostApi.setup(mockApi); + + SystemServices.startListeningForDeviceOrientationChange(true, 90); + verify(mockApi.startListeningForDeviceOrientationChange(true, 90)); + }); + + test('stopListeningForDeviceOrientationChangeTest', () async { + final MockTestSystemServicesHostApi mockApi = + MockTestSystemServicesHostApi(); + TestSystemServicesHostApi.setup(mockApi); + + SystemServices.stopListeningForDeviceOrientationChange(); + verify(mockApi.stopListeningForDeviceOrientationChange()); + }); + + test('onDeviceOrientationChanged adds new orientation to stream', () { + SystemServices.deviceOrientationChangedStreamController.stream + .listen((DeviceOrientationChangedEvent event) { + expect(event.orientation, equals(DeviceOrientation.landscapeLeft)); + }); + SystemServicesFlutterApiImpl() + .onDeviceOrientationChanged('LANDSCAPE_LEFT'); + }); + + test( + 'onDeviceOrientationChanged throws error if new orientation is invalid', + () { + expect( + () => SystemServicesFlutterApiImpl() + .onDeviceOrientationChanged('FAKE_ORIENTATION'), + throwsA(isA().having( + (ArgumentError e) => e.message, + 'message', + '"FAKE_ORIENTATION" is not a valid DeviceOrientation value'))); + }); + + test('onCameraError adds new error to stream', () { + const String testErrorDescription = 'Test error description!'; + SystemServices.cameraErrorStreamController.stream + .listen((String errorDescription) { + expect(errorDescription, equals(testErrorDescription)); + }); + SystemServicesFlutterApiImpl().onCameraError(testErrorDescription); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart b/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart new file mode 100644 index 000000000000..0963ffb26a2a --- /dev/null +++ b/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart @@ -0,0 +1,66 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in camera_android_camerax/test/system_services_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; + +import 'test_camerax_library.g.dart' as _i2; + +// 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 [TestSystemServicesHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestSystemServicesHostApi extends _i1.Mock + implements _i2.TestSystemServicesHostApi { + MockTestSystemServicesHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future<_i4.CameraPermissionsErrorData?> requestCameraPermissions( + bool? enableAudio) => + (super.noSuchMethod( + Invocation.method( + #requestCameraPermissions, + [enableAudio], + ), + returnValue: _i3.Future<_i4.CameraPermissionsErrorData?>.value(), + ) as _i3.Future<_i4.CameraPermissionsErrorData?>); + @override + void startListeningForDeviceOrientationChange( + bool? isFrontFacing, + int? sensorOrientation, + ) => + super.noSuchMethod( + Invocation.method( + #startListeningForDeviceOrientationChange, + [ + isFrontFacing, + sensorOrientation, + ], + ), + returnValueForMissingStub: null, + ); + @override + void stopListeningForDeviceOrientationChange() => super.noSuchMethod( + Invocation.method( + #stopListeningForDeviceOrientationChange, + [], + ), + returnValueForMissingStub: null, + ); +} diff --git a/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart new file mode 100644 index 000000000000..3f0e9c2d38a5 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart @@ -0,0 +1,475 @@ +// 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 (v3.2.9), 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 +import 'dart:async'; +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; +import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:camera_android_camerax/src/camerax_library.g.dart'; + +class _TestJavaObjectHostApiCodec extends StandardMessageCodec { + const _TestJavaObjectHostApiCodec(); +} + +abstract class TestJavaObjectHostApi { + static const MessageCodec codec = _TestJavaObjectHostApiCodec(); + + void dispose(int identifier); + static void setup(TestJavaObjectHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.JavaObjectHostApi.dispose', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.JavaObjectHostApi.dispose 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.JavaObjectHostApi.dispose was null, expected non-null int.'); + api.dispose(arg_identifier!); + return {}; + }); + } + } + } +} + +class _TestCameraInfoHostApiCodec extends StandardMessageCodec { + const _TestCameraInfoHostApiCodec(); +} + +abstract class TestCameraInfoHostApi { + static const MessageCodec codec = _TestCameraInfoHostApiCodec(); + + int getSensorRotationDegrees(int identifier); + static void setup(TestCameraInfoHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraInfoHostApi.getSensorRotationDegrees', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.CameraInfoHostApi.getSensorRotationDegrees 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.CameraInfoHostApi.getSensorRotationDegrees was null, expected non-null int.'); + final int output = api.getSensorRotationDegrees(arg_identifier!); + return {'result': output}; + }); + } + } + } +} + +class _TestCameraSelectorHostApiCodec extends StandardMessageCodec { + const _TestCameraSelectorHostApiCodec(); +} + +abstract class TestCameraSelectorHostApi { + static const MessageCodec codec = _TestCameraSelectorHostApiCodec(); + + void create(int identifier, int? lensFacing); + List filter(int identifier, List cameraInfoIds); + static void setup(TestCameraSelectorHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraSelectorHostApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.CameraSelectorHostApi.create 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.CameraSelectorHostApi.create was null, expected non-null int.'); + final int? arg_lensFacing = (args[1] as int?); + api.create(arg_identifier!, arg_lensFacing); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraSelectorHostApi.filter', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.CameraSelectorHostApi.filter 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.CameraSelectorHostApi.filter was null, expected non-null int.'); + final List? arg_cameraInfoIds = + (args[1] as List?)?.cast(); + assert(arg_cameraInfoIds != null, + 'Argument for dev.flutter.pigeon.CameraSelectorHostApi.filter was null, expected non-null List.'); + final List output = + api.filter(arg_identifier!, arg_cameraInfoIds!); + return {'result': output}; + }); + } + } + } +} + +class _TestProcessCameraProviderHostApiCodec extends StandardMessageCodec { + const _TestProcessCameraProviderHostApiCodec(); +} + +abstract class TestProcessCameraProviderHostApi { + static const MessageCodec codec = + _TestProcessCameraProviderHostApiCodec(); + + Future getInstance(); + List getAvailableCameraInfos(int identifier); + int bindToLifecycle( + int identifier, int cameraSelectorIdentifier, List useCaseIds); + void unbind(int identifier, List useCaseIds); + void unbindAll(int identifier); + static void setup(TestProcessCameraProviderHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getInstance', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + final int output = await api.getInstance(); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getAvailableCameraInfos', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.getAvailableCameraInfos 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.ProcessCameraProviderHostApi.getAvailableCameraInfos was null, expected non-null int.'); + final List output = + api.getAvailableCameraInfos(arg_identifier!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.bindToLifecycle', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.bindToLifecycle 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.ProcessCameraProviderHostApi.bindToLifecycle was null, expected non-null int.'); + final int? arg_cameraSelectorIdentifier = (args[1] as int?); + assert(arg_cameraSelectorIdentifier != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.bindToLifecycle was null, expected non-null int.'); + final List? arg_useCaseIds = + (args[2] as List?)?.cast(); + assert(arg_useCaseIds != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.bindToLifecycle was null, expected non-null List.'); + final int output = api.bindToLifecycle( + arg_identifier!, arg_cameraSelectorIdentifier!, arg_useCaseIds!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.unbind', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.unbind 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.ProcessCameraProviderHostApi.unbind was null, expected non-null int.'); + final List? arg_useCaseIds = + (args[1] as List?)?.cast(); + assert(arg_useCaseIds != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.unbind was null, expected non-null List.'); + api.unbind(arg_identifier!, arg_useCaseIds!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.unbindAll', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.unbindAll 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.ProcessCameraProviderHostApi.unbindAll was null, expected non-null int.'); + api.unbindAll(arg_identifier!); + return {}; + }); + } + } + } +} + +class _TestSystemServicesHostApiCodec extends StandardMessageCodec { + const _TestSystemServicesHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is CameraPermissionsErrorData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return CameraPermissionsErrorData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestSystemServicesHostApi { + static const MessageCodec codec = _TestSystemServicesHostApiCodec(); + + Future requestCameraPermissions( + bool enableAudio); + void startListeningForDeviceOrientationChange( + bool isFrontFacing, int sensorOrientation); + void stopListeningForDeviceOrientationChange(); + static void setup(TestSystemServicesHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesHostApi.requestCameraPermissions', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.SystemServicesHostApi.requestCameraPermissions was null.'); + final List args = (message as List?)!; + final bool? arg_enableAudio = (args[0] as bool?); + assert(arg_enableAudio != null, + 'Argument for dev.flutter.pigeon.SystemServicesHostApi.requestCameraPermissions was null, expected non-null bool.'); + final CameraPermissionsErrorData? output = + await api.requestCameraPermissions(arg_enableAudio!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesHostApi.startListeningForDeviceOrientationChange', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.SystemServicesHostApi.startListeningForDeviceOrientationChange was null.'); + final List args = (message as List?)!; + final bool? arg_isFrontFacing = (args[0] as bool?); + assert(arg_isFrontFacing != null, + 'Argument for dev.flutter.pigeon.SystemServicesHostApi.startListeningForDeviceOrientationChange was null, expected non-null bool.'); + final int? arg_sensorOrientation = (args[1] as int?); + assert(arg_sensorOrientation != null, + 'Argument for dev.flutter.pigeon.SystemServicesHostApi.startListeningForDeviceOrientationChange was null, expected non-null int.'); + api.startListeningForDeviceOrientationChange( + arg_isFrontFacing!, arg_sensorOrientation!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesHostApi.stopListeningForDeviceOrientationChange', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + api.stopListeningForDeviceOrientationChange(); + return {}; + }); + } + } + } +} + +class _TestPreviewHostApiCodec extends StandardMessageCodec { + const _TestPreviewHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is ResolutionInfo) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is ResolutionInfo) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return ResolutionInfo.decode(readValue(buffer)!); + + case 129: + return ResolutionInfo.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestPreviewHostApi { + static const MessageCodec codec = _TestPreviewHostApiCodec(); + + void create(int identifier, int? rotation, ResolutionInfo? targetResolution); + int setSurfaceProvider(int identifier); + void releaseFlutterSurfaceTexture(); + ResolutionInfo getResolutionInfo(int identifier); + static void setup(TestPreviewHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PreviewHostApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PreviewHostApi.create 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.PreviewHostApi.create was null, expected non-null int.'); + final int? arg_rotation = (args[1] as int?); + final ResolutionInfo? arg_targetResolution = + (args[2] as ResolutionInfo?); + api.create(arg_identifier!, arg_rotation, arg_targetResolution); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider 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.PreviewHostApi.setSurfaceProvider was null, expected non-null int.'); + final int output = api.setSurfaceProvider(arg_identifier!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PreviewHostApi.releaseFlutterSurfaceTexture', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + api.releaseFlutterSurfaceTexture(); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PreviewHostApi.getResolutionInfo', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PreviewHostApi.getResolutionInfo 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.PreviewHostApi.getResolutionInfo was null, expected non-null int.'); + final ResolutionInfo output = api.getResolutionInfo(arg_identifier!); + return {'result': output}; + }); + } + } + } +} diff --git a/packages/camera/camera_android_camerax/test/test_camerax_library.pigeon.dart b/packages/camera/camera_android_camerax/test/test_camerax_library.pigeon.dart deleted file mode 100644 index 2196b73d7fdb..000000000000 --- a/packages/camera/camera_android_camerax/test/test_camerax_library.pigeon.dart +++ /dev/null @@ -1,187 +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. -// Autogenerated from Pigeon (v3.2.9), 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 -import 'dart:async'; -import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; -import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:camera_android_camerax/src/camerax_library.pigeon.dart'; - -class _TestJavaObjectHostApiCodec extends StandardMessageCodec { - const _TestJavaObjectHostApiCodec(); -} - -abstract class TestJavaObjectHostApi { - static const MessageCodec codec = _TestJavaObjectHostApiCodec(); - - void dispose(int identifier); - static void setup(TestJavaObjectHostApi? api, - {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.JavaObjectHostApi.dispose', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.JavaObjectHostApi.dispose 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.JavaObjectHostApi.dispose was null, expected non-null int.'); - api.dispose(arg_identifier!); - return {}; - }); - } - } - } -} - -class _TestCameraInfoHostApiCodec extends StandardMessageCodec { - const _TestCameraInfoHostApiCodec(); -} - -abstract class TestCameraInfoHostApi { - static const MessageCodec codec = _TestCameraInfoHostApiCodec(); - - int getSensorRotationDegrees(int identifier); - static void setup(TestCameraInfoHostApi? api, - {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CameraInfoHostApi.getSensorRotationDegrees', - codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.CameraInfoHostApi.getSensorRotationDegrees 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.CameraInfoHostApi.getSensorRotationDegrees was null, expected non-null int.'); - final int output = api.getSensorRotationDegrees(arg_identifier!); - return {'result': output}; - }); - } - } - } -} - -class _TestCameraSelectorHostApiCodec extends StandardMessageCodec { - const _TestCameraSelectorHostApiCodec(); -} - -abstract class TestCameraSelectorHostApi { - static const MessageCodec codec = _TestCameraSelectorHostApiCodec(); - - void create(int identifier, int? lensFacing); - List filter(int identifier, List cameraInfoIds); - static void setup(TestCameraSelectorHostApi? api, - {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CameraSelectorHostApi.create', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.CameraSelectorHostApi.create 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.CameraSelectorHostApi.create was null, expected non-null int.'); - final int? arg_lensFacing = (args[1] as int?); - api.create(arg_identifier!, arg_lensFacing); - return {}; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CameraSelectorHostApi.filter', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.CameraSelectorHostApi.filter 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.CameraSelectorHostApi.filter was null, expected non-null int.'); - final List? arg_cameraInfoIds = - (args[1] as List?)?.cast(); - assert(arg_cameraInfoIds != null, - 'Argument for dev.flutter.pigeon.CameraSelectorHostApi.filter was null, expected non-null List.'); - final List output = - api.filter(arg_identifier!, arg_cameraInfoIds!); - return {'result': output}; - }); - } - } - } -} - -class _TestProcessCameraProviderHostApiCodec extends StandardMessageCodec { - const _TestProcessCameraProviderHostApiCodec(); -} - -abstract class TestProcessCameraProviderHostApi { - static const MessageCodec codec = - _TestProcessCameraProviderHostApiCodec(); - - Future getInstance(); - List getAvailableCameraInfos(int identifier); - static void setup(TestProcessCameraProviderHostApi? api, - {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getInstance', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - // ignore message - final int output = await api.getInstance(); - return {'result': output}; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getAvailableCameraInfos', - codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.getAvailableCameraInfos 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.ProcessCameraProviderHostApi.getAvailableCameraInfos was null, expected non-null int.'); - final List output = - api.getAvailableCameraInfos(arg_identifier!); - return {'result': output}; - }); - } - } - } -} diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index 12d9a53ea248..f0605b7914cc 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,24 @@ +## 0.9.11 + +* Adds back use of Optional type. +* Updates minimum Flutter version to 3.0. + +## 0.9.10+2 + +* Updates code for stricter lint checks. + +## 0.9.10+1 + +* Updates code for stricter lint checks. + +## 0.9.10 + +* Remove usage of deprecated quiver Optional type. + +## 0.9.9 + +* Implements option to also stream when recording a video. + ## 0.9.8+6 * Updates code for `no_leading_underscores_for_local_identifiers` lint. diff --git a/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart b/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart index 3e62edc2c495..34d460d44ec7 100644 --- a/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart +++ b/packages/camera/camera_avfoundation/example/integration_test/camera_test.dart @@ -56,8 +56,6 @@ void main() { Future testCaptureImageResolution( CameraController controller, ResolutionPreset preset) async { final Size expectedSize = presetExpectedSizes[preset]!; - print( - 'Capturing photo at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}'); // Take Picture final XFile file = await controller.takePicture(); @@ -102,8 +100,6 @@ void main() { Future testCaptureVideoResolution( CameraController controller, ResolutionPreset preset) async { final Size expectedSize = presetExpectedSizes[preset]!; - print( - 'Capturing video at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}'); // Take Video await controller.startVideoRecording(); @@ -253,4 +249,33 @@ void main() { expect(image.planes.length, 1); }, ); + + testWidgets('Recording with video streaming', (WidgetTester tester) async { + final List cameras = + await CameraPlatform.instance.availableCameras(); + if (cameras.isEmpty) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + await controller.prepareForVideoRecording(); + final Completer completer = Completer(); + await controller.startVideoRecording( + streamCallback: (CameraImageData image) { + if (!completer.isCompleted) { + completer.complete(image); + } + }); + sleep(const Duration(milliseconds: 500)); + await controller.stopVideoRecording(); + await controller.dispose(); + + expect(await completer.future, isNotNull); + }); } diff --git a/packages/camera/camera_avfoundation/example/lib/camera_controller.dart b/packages/camera/camera_avfoundation/example/lib/camera_controller.dart index 09441cc5449c..524186816aab 100644 --- a/packages/camera/camera_avfoundation/example/lib/camera_controller.dart +++ b/packages/camera/camera_avfoundation/example/lib/camera_controller.dart @@ -3,12 +3,12 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:collection'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:quiver/core.dart'; /// The state of a [CameraController]. class CameraValue { @@ -306,11 +306,14 @@ class CameraController extends ValueNotifier { /// /// The video is returned as a [XFile] after calling [stopVideoRecording]. /// Throws a [CameraException] if the capture fails. - Future startVideoRecording() async { - await CameraPlatform.instance.startVideoRecording(_cameraId); + Future startVideoRecording( + {Function(CameraImageData image)? streamCallback}) async { + await CameraPlatform.instance.startVideoCapturing( + VideoCaptureOptions(_cameraId, streamCallback: streamCallback)); value = value.copyWith( isRecordingVideo: true, isRecordingPaused: false, + isStreamingImages: streamCallback != null, recordingOrientation: Optional.of( value.lockedCaptureOrientation ?? value.deviceOrientation)); } @@ -319,6 +322,10 @@ class CameraController extends ValueNotifier { /// /// Throws a [CameraException] if the capture failed. Future stopVideoRecording() async { + if (value.isStreamingImages) { + await stopImageStream(); + } + final XFile file = await CameraPlatform.instance.stopVideoRecording(_cameraId); value = value.copyWith( @@ -435,3 +442,112 @@ class CameraController extends ValueNotifier { } } } + +/// A value that might be absent. +/// +/// Used to represent [DeviceOrientation]s that are optional but also able +/// to be cleared. +@immutable +class Optional extends IterableBase { + /// Constructs an empty Optional. + const Optional.absent() : _value = null; + + /// Constructs an Optional of the given [value]. + /// + /// Throws [ArgumentError] if [value] is null. + Optional.of(T value) : _value = value { + // TODO(cbracken): Delete and make this ctor const once mixed-mode + // execution is no longer around. + ArgumentError.checkNotNull(value); + } + + /// Constructs an Optional of the given [value]. + /// + /// If [value] is null, returns [absent()]. + const Optional.fromNullable(T? value) : _value = value; + + final T? _value; + + /// True when this optional contains a value. + bool get isPresent => _value != null; + + /// True when this optional contains no value. + bool get isNotPresent => _value == null; + + /// Gets the Optional value. + /// + /// Throws [StateError] if [value] is null. + T get value { + if (_value == null) { + throw StateError('value called on absent Optional.'); + } + return _value!; + } + + /// Executes a function if the Optional value is present. + void ifPresent(void Function(T value) ifPresent) { + if (isPresent) { + ifPresent(_value as T); + } + } + + /// Execution a function if the Optional value is absent. + void ifAbsent(void Function() ifAbsent) { + if (!isPresent) { + ifAbsent(); + } + } + + /// Gets the Optional value with a default. + /// + /// The default is returned if the Optional is [absent()]. + /// + /// Throws [ArgumentError] if [defaultValue] is null. + T or(T defaultValue) { + return _value ?? defaultValue; + } + + /// Gets the Optional value, or `null` if there is none. + T? get orNull => _value; + + /// Transforms the Optional value. + /// + /// If the Optional is [absent()], returns [absent()] without applying the transformer. + /// + /// The transformer must not return `null`. If it does, an [ArgumentError] is thrown. + Optional transform(S Function(T value) transformer) { + return _value == null + ? Optional.absent() + : Optional.of(transformer(_value as T)); + } + + /// Transforms the Optional value. + /// + /// If the Optional is [absent()], returns [absent()] without applying the transformer. + /// + /// Returns [absent()] if the transformer returns `null`. + Optional transformNullable(S? Function(T value) transformer) { + return _value == null + ? Optional.absent() + : Optional.fromNullable(transformer(_value as T)); + } + + @override + Iterator get iterator => + isPresent ? [_value as T].iterator : Iterable.empty().iterator; + + /// Delegates to the underlying [value] hashCode. + @override + int get hashCode => _value.hashCode; + + /// Delegates to the underlying [value] operator==. + @override + bool operator ==(Object o) => o is Optional && o._value == _value; + + @override + String toString() { + return _value == null + ? 'Optional { absent }' + : 'Optional { value: $_value }'; + } +} diff --git a/packages/camera/camera_avfoundation/example/lib/main.dart b/packages/camera/camera_avfoundation/example/lib/main.dart index 9ebc27e4be5b..4d98aed9a4c2 100644 --- a/packages/camera/camera_avfoundation/example/lib/main.dart +++ b/packages/camera/camera_avfoundation/example/lib/main.dart @@ -35,17 +35,16 @@ IconData getCameraLensIcon(CameraLensDirection direction) { return Icons.camera_front; case CameraLensDirection.external: return Icons.camera; - default: - throw ArgumentError('Unknown lens direction'); } + // This enum is from a different package, so a new value could be added at + // any time. The example should keep working if that happens. + // ignore: dead_code + return Icons.camera; } void _logError(String code, String? message) { - if (message != null) { - print('Error: $code\nError Message: $message'); - } else { - print('Error: $code'); - } + // ignore: avoid_print + print('Error: $code${message == null ? '' : '\nError Message: $message'}'); } class _CameraExampleHomeState extends State @@ -1092,5 +1091,4 @@ Future main() async { /// /// We use this so that APIs that have become non-nullable can still be used /// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. T? _ambiguate(T? value) => value; diff --git a/packages/camera/camera_avfoundation/example/pubspec.yaml b/packages/camera/camera_avfoundation/example/pubspec.yaml index a9252cbd6d61..7c85ba807193 100644 --- a/packages/camera/camera_avfoundation/example/pubspec.yaml +++ b/packages/camera/camera_avfoundation/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: camera_avfoundation: diff --git a/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m index 628211ac7f7a..b85f68d1f957 100644 --- a/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera_avfoundation/ios/Classes/CameraPlugin.m @@ -201,7 +201,12 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call [self.camera setUpCaptureSessionForAudio]; [result sendSuccess]; } else if ([@"startVideoRecording" isEqualToString:call.method]) { - [_camera startVideoRecordingWithResult:result]; + BOOL enableStream = [call.arguments[@"enableStream"] boolValue]; + if (enableStream) { + [_camera startVideoRecordingWithResult:result messengerForStreaming:_messenger]; + } else { + [_camera startVideoRecordingWithResult:result]; + } } else if ([@"stopVideoRecording" isEqualToString:call.method]) { [_camera stopVideoRecordingWithResult:result]; } else if ([@"pauseVideoRecording" isEqualToString:call.method]) { diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h index 8a5dafaf8354..85b8e2ae06f2 100644 --- a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.h @@ -50,6 +50,15 @@ NS_ASSUME_NONNULL_BEGIN - (void)captureToFile:(FLTThreadSafeFlutterResult *)result API_AVAILABLE(ios(10)); - (void)close; - (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result; +/** + * Starts recording a video with an optional streaming messenger. + * If the messenger is non-null then it will be called for each + * captured frame, allowing streaming concurrently with recording. + * + * @param messenger Nullable messenger for capturing each frame. + */ +- (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result + messengerForStreaming:(nullable NSObject *)messenger; - (void)stopVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result; - (void)pauseVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result; - (void)resumeVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result; diff --git a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m index 90b81adbd84c..a7d6cd24be3c 100644 --- a/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m +++ b/packages/camera/camera_avfoundation/ios/Classes/FLTCam.m @@ -623,7 +623,16 @@ - (CVPixelBufferRef)copyPixelBuffer { } - (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result { + [self startVideoRecordingWithResult:result messengerForStreaming:nil]; +} + +- (void)startVideoRecordingWithResult:(FLTThreadSafeFlutterResult *)result + messengerForStreaming:(nullable NSObject *)messenger { if (!_isRecording) { + if (messenger != nil) { + [self startImageStreamWithMessenger:messenger]; + } + NSError *error; _videoRecordingPath = [self getTemporaryFilePathWithExtension:@"mp4" subfolder:@"videos" diff --git a/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart b/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart index 9bdadfb4536f..5080c57a736f 100644 --- a/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart +++ b/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart @@ -145,6 +145,7 @@ class AVFoundationCamera extends CameraPlatform { // ignore: body_might_complete_normally_catch_error (Object error, StackTrace stackTrace) { if (error is! PlatformException) { + // ignore: only_throw_errors throw error; } completer.completeError( @@ -248,13 +249,26 @@ class AVFoundationCamera extends CameraPlatform { @override Future startVideoRecording(int cameraId, {Duration? maxVideoDuration}) async { + return startVideoCapturing( + VideoCaptureOptions(cameraId, maxDuration: maxVideoDuration)); + } + + @override + Future startVideoCapturing(VideoCaptureOptions options) async { await _channel.invokeMethod( 'startVideoRecording', { - 'cameraId': cameraId, - 'maxVideoDuration': maxVideoDuration?.inMilliseconds, + 'cameraId': options.cameraId, + 'maxVideoDuration': options.maxDuration?.inMilliseconds, + 'enableStream': options.streamCallback != null, }, ); + + if (options.streamCallback != null) { + _frameStreamController = _createStreamController(); + _frameStreamController!.stream.listen(options.streamCallback); + _startStreamListener(); + } } @override @@ -290,13 +304,19 @@ class AVFoundationCamera extends CameraPlatform { @override Stream onStreamedFrameAvailable(int cameraId, {CameraImageStreamOptions? options}) { - _frameStreamController = StreamController( - onListen: _onFrameStreamListen, + _frameStreamController = + _createStreamController(onListen: _onFrameStreamListen); + return _frameStreamController!.stream; + } + + StreamController _createStreamController( + {Function()? onListen}) { + return StreamController( + onListen: onListen ?? () {}, onPause: _onFrameStreamPauseResume, onResume: _onFrameStreamPauseResume, onCancel: _onFrameStreamCancel, ); - return _frameStreamController!.stream; } void _onFrameStreamListen() { @@ -305,6 +325,10 @@ class AVFoundationCamera extends CameraPlatform { Future _startPlatformStream() async { await _channel.invokeMethod('startImageStream'); + _startStreamListener(); + } + + void _startStreamListener() { const EventChannel cameraEventChannel = EventChannel('plugins.flutter.io/camera_avfoundation/imageStream'); _platformImageStreamSubscription = @@ -503,9 +527,14 @@ class AVFoundationCamera extends CameraPlatform { return 'always'; case FlashMode.torch: return 'torch'; - default: - throw ArgumentError('Unknown FlashMode value'); } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return 'off'; } /// Returns the resolution preset as a String. @@ -523,18 +552,23 @@ class AVFoundationCamera extends CameraPlatform { return 'medium'; case ResolutionPreset.low: return 'low'; - default: - throw ArgumentError('Unknown ResolutionPreset value'); } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return 'max'; } /// Converts messages received from the native platform into device events. Future _handleDeviceMethodCall(MethodCall call) async { switch (call.method) { case 'orientation_changed': + final Map arguments = _getArgumentDictionary(call); _deviceEventStreamController.add(DeviceOrientationChangedEvent( - deserializeDeviceOrientation( - call.arguments['orientation']! as String))); + deserializeDeviceOrientation(arguments['orientation']! as String))); break; default: throw MissingPluginException(); @@ -549,21 +583,23 @@ class AVFoundationCamera extends CameraPlatform { Future handleCameraMethodCall(MethodCall call, int cameraId) async { switch (call.method) { case 'initialized': + final Map arguments = _getArgumentDictionary(call); cameraEventStreamController.add(CameraInitializedEvent( cameraId, - call.arguments['previewWidth']! as double, - call.arguments['previewHeight']! as double, - deserializeExposureMode(call.arguments['exposureMode']! as String), - call.arguments['exposurePointSupported']! as bool, - deserializeFocusMode(call.arguments['focusMode']! as String), - call.arguments['focusPointSupported']! as bool, + arguments['previewWidth']! as double, + arguments['previewHeight']! as double, + deserializeExposureMode(arguments['exposureMode']! as String), + arguments['exposurePointSupported']! as bool, + deserializeFocusMode(arguments['focusMode']! as String), + arguments['focusPointSupported']! as bool, )); break; case 'resolution_changed': + final Map arguments = _getArgumentDictionary(call); cameraEventStreamController.add(CameraResolutionChangedEvent( cameraId, - call.arguments['captureWidth']! as double, - call.arguments['captureHeight']! as double, + arguments['captureWidth']! as double, + arguments['captureHeight']! as double, )); break; case 'camera_closing': @@ -572,23 +608,32 @@ class AVFoundationCamera extends CameraPlatform { )); break; case 'video_recorded': + final Map arguments = _getArgumentDictionary(call); cameraEventStreamController.add(VideoRecordedEvent( cameraId, - XFile(call.arguments['path']! as String), - call.arguments['maxVideoDuration'] != null - ? Duration( - milliseconds: call.arguments['maxVideoDuration']! as int) + XFile(arguments['path']! as String), + arguments['maxVideoDuration'] != null + ? Duration(milliseconds: arguments['maxVideoDuration']! as int) : null, )); break; case 'error': + final Map arguments = _getArgumentDictionary(call); cameraEventStreamController.add(CameraErrorEvent( cameraId, - call.arguments['description']! as String, + arguments['description']! as String, )); break; default: throw MissingPluginException(); } } + + /// Returns the arguments of [call] as typed string-keyed Map. + /// + /// This does not do any type validation, so is only safe to call if the + /// arguments are known to be a map. + Map _getArgumentDictionary(MethodCall call) { + return (call.arguments as Map).cast(); + } } diff --git a/packages/camera/camera_avfoundation/lib/src/utils.dart b/packages/camera/camera_avfoundation/lib/src/utils.dart index 663ec6da7a97..8d58f7fe1297 100644 --- a/packages/camera/camera_avfoundation/lib/src/utils.dart +++ b/packages/camera/camera_avfoundation/lib/src/utils.dart @@ -29,9 +29,14 @@ String serializeDeviceOrientation(DeviceOrientation orientation) { return 'landscapeRight'; case DeviceOrientation.landscapeLeft: return 'landscapeLeft'; - default: - throw ArgumentError('Unknown DeviceOrientation value'); } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return 'portraitUp'; } /// Returns the device orientation for a given String. diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index f394d59e81d5..b272a4c5c68d 100644 --- a/packages/camera/camera_avfoundation/pubspec.yaml +++ b/packages/camera/camera_avfoundation/pubspec.yaml @@ -2,11 +2,11 @@ name: camera_avfoundation description: iOS implementation of the camera plugin. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.8+6 +version: 0.9.11 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -17,7 +17,7 @@ flutter: dartPluginClass: AVFoundationCamera dependencies: - camera_platform_interface: ^2.2.0 + camera_platform_interface: ^2.3.1 flutter: sdk: flutter stream_transform: ^2.0.0 diff --git a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart index 60109a4172b7..5d0b74cf0c0c 100644 --- a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart +++ b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart @@ -32,14 +32,15 @@ void main() { // registerWith is called very early in initialization the bindings won't // have been initialized. While registerWith could intialize them, that // could slow down startup, so instead the handler should be set up lazily. - final ByteData? response = await TestDefaultBinaryMessengerBinding - .instance!.defaultBinaryMessenger - .handlePlatformMessage( - AVFoundationCamera.deviceEventChannelName, - const StandardMethodCodec().encodeMethodCall(const MethodCall( - 'orientation_changed', - {'orientation': 'portraitDown'})), - (ByteData? data) {}); + final ByteData? response = + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .handlePlatformMessage( + AVFoundationCamera.deviceEventChannelName, + const StandardMethodCodec().encodeMethodCall(const MethodCall( + 'orientation_changed', + {'orientation': 'portraitDown'})), + (ByteData? data) {}); expect(response, null); }); @@ -421,7 +422,8 @@ void main() { const DeviceOrientationChangedEvent event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); for (int i = 0; i < 3; i++) { - await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( AVFoundationCamera.deviceEventChannelName, const StandardMethodCodec().encodeMethodCall( @@ -478,6 +480,9 @@ void main() { test('Should fetch CameraDescription instances for available cameras', () async { // Arrange + // This deliberately uses 'dynamic' since that's what actual platform + // channel results will be, so using typed mock data could mask type + // handling bugs in the code under test. final List returnData = [ { 'name': 'Test 1', @@ -504,11 +509,13 @@ void main() { ]); expect(cameras.length, returnData.length); for (int i = 0; i < returnData.length; i++) { + final Map typedData = + (returnData[i] as Map).cast(); final CameraDescription cameraDescription = CameraDescription( - name: returnData[i]['name']! as String, + name: typedData['name']! as String, lensDirection: - parseCameraLensDirection(returnData[i]['lensFacing']! as String), - sensorOrientation: returnData[i]['sensorOrientation']! as int, + parseCameraLensDirection(typedData['lensFacing']! as String), + sensorOrientation: typedData['sensorOrientation']! as int, ); expect(cameras[i], cameraDescription); } @@ -587,6 +594,7 @@ void main() { isMethodCall('startVideoRecording', arguments: { 'cameraId': cameraId, 'maxVideoDuration': null, + 'enableStream': false, }), ]); }); @@ -609,7 +617,31 @@ void main() { expect(channel.log, [ isMethodCall('startVideoRecording', arguments: { 'cameraId': cameraId, - 'maxVideoDuration': 10000 + 'maxVideoDuration': 10000, + 'enableStream': false, + }), + ]); + }); + + test( + 'Should pass enableStream if callback is passed when starting recording a video', + () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: _channelName, + methods: {'startVideoRecording': null}, + ); + + // Act + await camera.startVideoCapturing(VideoCaptureOptions(cameraId, + streamCallback: (CameraImageData imageData) {})); + + // Assert + expect(channel.log, [ + isMethodCall('startVideoRecording', arguments: { + 'cameraId': cameraId, + 'maxVideoDuration': null, + 'enableStream': true, }), ]); }); @@ -1092,3 +1124,9 @@ void main() { }); }); } + +/// 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/camera/camera_avfoundation/test/method_channel_mock.dart b/packages/camera/camera_avfoundation/test/method_channel_mock.dart index 413c10633cc1..f26d12a3688a 100644 --- a/packages/camera/camera_avfoundation/test/method_channel_mock.dart +++ b/packages/camera/camera_avfoundation/test/method_channel_mock.dart @@ -11,7 +11,9 @@ class MethodChannelMock { this.delay, required this.methods, }) : methodChannel = MethodChannel(channelName) { - methodChannel.setMockMethodCallHandler(_handler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, _handler); } final Duration? delay; @@ -37,3 +39,9 @@ class MethodChannelMock { }); } } + +/// 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/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index 3bfc56f3f6e2..b51eb9c78a43 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,3 +1,24 @@ +## 2.4.0 + +* Allows camera to be switched while video recording. +* Updates minimum Flutter version to 3.0. + +## 2.3.4 + +* Updates code for stricter lint checks. + +## 2.3.3 + +* Updates code for stricter lint checks. + +## 2.3.2 + +* Updates MethodChannelCamera to have startVideoRecording call the newer startVideoCapturing. + +## 2.3.1 + +* Exports VideoCaptureOptions to allow dependencies to implement concurrent stream and record. + ## 2.3.0 * Adds new capture method for a camera to allow concurrent streaming and recording. diff --git a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart index 37c00d64ede2..14d20fc817b2 100644 --- a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart @@ -137,6 +137,7 @@ class MethodChannelCamera extends CameraPlatform { // ignore: body_might_complete_normally_catch_error (Object error, StackTrace stackTrace) { if (error is! PlatformException) { + // ignore: only_throw_errors throw error; } completer.completeError( @@ -240,13 +241,25 @@ class MethodChannelCamera extends CameraPlatform { @override Future startVideoRecording(int cameraId, {Duration? maxVideoDuration}) async { + return startVideoCapturing( + VideoCaptureOptions(cameraId, maxDuration: maxVideoDuration)); + } + + @override + Future startVideoCapturing(VideoCaptureOptions options) async { await _channel.invokeMethod( 'startVideoRecording', { - 'cameraId': cameraId, - 'maxVideoDuration': maxVideoDuration?.inMilliseconds, + 'cameraId': options.cameraId, + 'maxVideoDuration': options.maxDuration?.inMilliseconds, + 'enableStream': options.streamCallback != null, }, ); + + if (options.streamCallback != null) { + _installStreamController().stream.listen(options.streamCallback); + _startStreamListener(); + } } @override @@ -282,13 +295,19 @@ class MethodChannelCamera extends CameraPlatform { @override Stream onStreamedFrameAvailable(int cameraId, {CameraImageStreamOptions? options}) { + _installStreamController(onListen: _onFrameStreamListen); + return _frameStreamController!.stream; + } + + StreamController _installStreamController( + {Function()? onListen}) { _frameStreamController = StreamController( - onListen: _onFrameStreamListen, + onListen: onListen ?? () {}, onPause: _onFrameStreamPauseResume, onResume: _onFrameStreamPauseResume, onCancel: _onFrameStreamCancel, ); - return _frameStreamController!.stream; + return _frameStreamController!; } void _onFrameStreamListen() { @@ -297,6 +316,10 @@ class MethodChannelCamera extends CameraPlatform { Future _startPlatformStream() async { await _channel.invokeMethod('startImageStream'); + _startStreamListener(); + } + + void _startStreamListener() { const EventChannel cameraEventChannel = EventChannel('plugins.flutter.io/camera/imageStream'); _platformImageStreamSubscription = @@ -481,6 +504,17 @@ class MethodChannelCamera extends CameraPlatform { ); } + @override + Future setDescriptionWhileRecording( + CameraDescription description) async { + await _channel.invokeMethod( + 'setDescriptionWhileRecording', + { + 'cameraName': description.name, + }, + ); + } + @override Widget buildPreview(int cameraId) { return Texture(textureId: cameraId); @@ -497,8 +531,6 @@ class MethodChannelCamera extends CameraPlatform { return 'always'; case FlashMode.torch: return 'torch'; - default: - throw ArgumentError('Unknown FlashMode value'); } } @@ -517,8 +549,6 @@ class MethodChannelCamera extends CameraPlatform { return 'medium'; case ResolutionPreset.low: return 'low'; - default: - throw ArgumentError('Unknown ResolutionPreset value'); } } @@ -529,9 +559,9 @@ class MethodChannelCamera extends CameraPlatform { Future handleDeviceMethodCall(MethodCall call) async { switch (call.method) { case 'orientation_changed': + final Map arguments = _getArgumentDictionary(call); deviceEventStreamController.add(DeviceOrientationChangedEvent( - deserializeDeviceOrientation( - call.arguments['orientation']! as String))); + deserializeDeviceOrientation(arguments['orientation']! as String))); break; default: throw MissingPluginException(); @@ -546,21 +576,23 @@ class MethodChannelCamera extends CameraPlatform { Future handleCameraMethodCall(MethodCall call, int cameraId) async { switch (call.method) { case 'initialized': + final Map arguments = _getArgumentDictionary(call); cameraEventStreamController.add(CameraInitializedEvent( cameraId, - call.arguments['previewWidth']! as double, - call.arguments['previewHeight']! as double, - deserializeExposureMode(call.arguments['exposureMode']! as String), - call.arguments['exposurePointSupported']! as bool, - deserializeFocusMode(call.arguments['focusMode']! as String), - call.arguments['focusPointSupported']! as bool, + arguments['previewWidth']! as double, + arguments['previewHeight']! as double, + deserializeExposureMode(arguments['exposureMode']! as String), + arguments['exposurePointSupported']! as bool, + deserializeFocusMode(arguments['focusMode']! as String), + arguments['focusPointSupported']! as bool, )); break; case 'resolution_changed': + final Map arguments = _getArgumentDictionary(call); cameraEventStreamController.add(CameraResolutionChangedEvent( cameraId, - call.arguments['captureWidth']! as double, - call.arguments['captureHeight']! as double, + arguments['captureWidth']! as double, + arguments['captureHeight']! as double, )); break; case 'camera_closing': @@ -569,23 +601,32 @@ class MethodChannelCamera extends CameraPlatform { )); break; case 'video_recorded': + final Map arguments = _getArgumentDictionary(call); cameraEventStreamController.add(VideoRecordedEvent( cameraId, - XFile(call.arguments['path']! as String), - call.arguments['maxVideoDuration'] != null - ? Duration( - milliseconds: call.arguments['maxVideoDuration']! as int) + XFile(arguments['path']! as String), + arguments['maxVideoDuration'] != null + ? Duration(milliseconds: arguments['maxVideoDuration']! as int) : null, )); break; case 'error': + final Map arguments = _getArgumentDictionary(call); cameraEventStreamController.add(CameraErrorEvent( cameraId, - call.arguments['description']! as String, + arguments['description']! as String, )); break; default: throw MissingPluginException(); } } + + /// Returns the arguments of [call] as typed string-keyed Map. + /// + /// This does not do any type validation, so is only safe to call if the + /// arguments are known to be a map. + Map _getArgumentDictionary(MethodCall call) { + return (call.arguments as Map).cast(); + } } diff --git a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart index d8f8f9ca4cc9..b43629d4e0c3 100644 --- a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -11,7 +11,6 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import '../../camera_platform_interface.dart'; import '../method_channel/method_channel_camera.dart'; -import '../types/video_capture_options.dart'; /// The interface that implementations of camera must implement. /// @@ -270,6 +269,12 @@ abstract class CameraPlatform extends PlatformInterface { throw UnimplementedError('pausePreview() is not implemented.'); } + /// Sets the active camera while recording. + Future setDescriptionWhileRecording(CameraDescription description) { + throw UnimplementedError( + 'setDescriptionWhileRecording() is not implemented.'); + } + /// Returns a widget showing a live camera preview. Widget buildPreview(int cameraId) { throw UnimplementedError('buildView() has not been implemented.'); diff --git a/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart b/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart index 56a05cd2d0f1..6da44c98ddc8 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart @@ -18,8 +18,6 @@ String serializeExposureMode(ExposureMode exposureMode) { return 'locked'; case ExposureMode.auto: return 'auto'; - default: - throw ArgumentError('Unknown ExposureMode value'); } } diff --git a/packages/camera/camera_platform_interface/lib/src/types/focus_mode.dart b/packages/camera/camera_platform_interface/lib/src/types/focus_mode.dart index 6baae0c1f63e..1f9cbef1bab9 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/focus_mode.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/focus_mode.dart @@ -18,8 +18,6 @@ String serializeFocusMode(FocusMode focusMode) { return 'locked'; case FocusMode.auto: return 'auto'; - default: - throw ArgumentError('Unknown FocusMode value'); } } diff --git a/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart b/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart index edbf7d24098c..8dc69e09f58a 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart @@ -47,7 +47,6 @@ extension ImageFormatGroupName on ImageFormatGroup { case ImageFormatGroup.jpeg: return 'jpeg'; case ImageFormatGroup.unknown: - default: return 'unknown'; } } diff --git a/packages/camera/camera_platform_interface/lib/src/types/types.dart b/packages/camera/camera_platform_interface/lib/src/types/types.dart index 3eb09fcb833c..a8a4f8ca5dc4 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/types.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/types.dart @@ -10,3 +10,4 @@ export 'flash_mode.dart'; export 'focus_mode.dart'; export 'image_format_group.dart'; export 'resolution_preset.dart'; +export 'video_capture_options.dart'; diff --git a/packages/camera/camera_platform_interface/lib/src/utils/utils.dart b/packages/camera/camera_platform_interface/lib/src/utils/utils.dart index d86880afd216..771a94be416e 100644 --- a/packages/camera/camera_platform_interface/lib/src/utils/utils.dart +++ b/packages/camera/camera_platform_interface/lib/src/utils/utils.dart @@ -30,8 +30,6 @@ String serializeDeviceOrientation(DeviceOrientation orientation) { return 'landscapeRight'; case DeviceOrientation.landscapeLeft: return 'landscapeLeft'; - default: - throw ArgumentError('Unknown DeviceOrientation value'); } } diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index 7ddc6d561aa4..4cdb2855a156 100644 --- a/packages/camera/camera_platform_interface/pubspec.yaml +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -4,11 +4,11 @@ repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 # 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.3.0 +version: 2.4.0 environment: sdk: '>=2.12.0 <3.0.0' - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: cross_file: ^0.3.1 diff --git a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart index eab518ce3b23..e3b6858e6d25 100644 --- a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart +++ b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart @@ -442,6 +442,52 @@ void main() { ); }); }); + + group('exports', () { + test('CameraDescription is exported', () { + const CameraDescription( + name: 'abc-123', + sensorOrientation: 1, + lensDirection: CameraLensDirection.external); + }); + + test('CameraException is exported', () { + CameraException('1', 'error'); + }); + + test('CameraImageData is exported', () { + const CameraImageData( + width: 1, + height: 1, + format: CameraImageFormat(ImageFormatGroup.bgra8888, raw: 1), + planes: [], + ); + }); + + test('ExposureMode is exported', () { + // ignore: unnecessary_statements + ExposureMode.auto; + }); + + test('FlashMode is exported', () { + // ignore: unnecessary_statements + FlashMode.auto; + }); + + test('FocusMode is exported', () { + // ignore: unnecessary_statements + FocusMode.auto; + }); + + test('ResolutionPreset is exported', () { + // ignore: unnecessary_statements + ResolutionPreset.high; + }); + + test('VideoCaptureOptions is exported', () { + const VideoCaptureOptions(123); + }); + }); } class ImplementsCameraPlatform implements CameraPlatform { diff --git a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart index 60f42fd4af4a..b01123d7cb29 100644 --- a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart +++ b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart @@ -484,11 +484,13 @@ void main() { ]); expect(cameras.length, returnData.length); for (int i = 0; i < returnData.length; i++) { + final Map typedData = + (returnData[i] as Map).cast(); final CameraDescription cameraDescription = CameraDescription( - name: returnData[i]['name']! as String, - lensDirection: parseCameraLensDirection( - returnData[i]['lensFacing']! as String), - sensorOrientation: returnData[i]['sensorOrientation']! as int, + name: typedData['name']! as String, + lensDirection: + parseCameraLensDirection(typedData['lensFacing']! as String), + sensorOrientation: typedData['sensorOrientation']! as int, ); expect(cameras[i], cameraDescription); } @@ -569,10 +571,34 @@ void main() { isMethodCall('startVideoRecording', arguments: { 'cameraId': cameraId, 'maxVideoDuration': null, + 'enableStream': false, }), ]); }); + test('Should set description while recording', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setDescriptionWhileRecording': null}, + ); + + // Act + const CameraDescription cameraDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0); + await camera.setDescriptionWhileRecording(cameraDescription); + + // Assert + expect(channel.log, [ + isMethodCall('setDescriptionWhileRecording', + arguments: { + 'cameraName': cameraDescription.name + }), + ]); + }); + test('Should pass maxVideoDuration when starting recording a video', () async { // Arrange @@ -591,7 +617,8 @@ void main() { expect(channel.log, [ isMethodCall('startVideoRecording', arguments: { 'cameraId': cameraId, - 'maxVideoDuration': 10000 + 'maxVideoDuration': 10000, + 'enableStream': false, }), ]); }); diff --git a/packages/camera/camera_platform_interface/test/utils/method_channel_mock.dart b/packages/camera/camera_platform_interface/test/utils/method_channel_mock.dart index 413c10633cc1..f26d12a3688a 100644 --- a/packages/camera/camera_platform_interface/test/utils/method_channel_mock.dart +++ b/packages/camera/camera_platform_interface/test/utils/method_channel_mock.dart @@ -11,7 +11,9 @@ class MethodChannelMock { this.delay, required this.methods, }) : methodChannel = MethodChannel(channelName) { - methodChannel.setMockMethodCallHandler(_handler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, _handler); } final Duration? delay; @@ -37,3 +39,9 @@ class MethodChannelMock { }); } } + +/// 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/camera/camera_web/CHANGELOG.md b/packages/camera/camera_web/CHANGELOG.md index f4989cfd5bff..2a8d43b95e18 100644 --- a/packages/camera/camera_web/CHANGELOG.md +++ b/packages/camera/camera_web/CHANGELOG.md @@ -1,3 +1,15 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 0.3.1+1 + +* Updates code for stricter lint checks. + +## 0.3.1 + +* Updates to latest camera platform interface, and fails if user attempts to use streaming with recording (since streaming is currently unsupported on web). + ## 0.3.0+1 * Updates imports for `prefer_relative_imports`. diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart index 50451b9778af..705d7750e1a4 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -1286,11 +1286,10 @@ void main() { capturedVideoPartTwo, ]; - videoDataAvailableListener - ..call(FakeBlobEvent(capturedVideoPartOne)) - ..call(FakeBlobEvent(capturedVideoPartTwo)); + videoDataAvailableListener(FakeBlobEvent(capturedVideoPartOne)); + videoDataAvailableListener(FakeBlobEvent(capturedVideoPartTwo)); - videoRecordingStoppedListener.call(Event('stop')); + videoRecordingStoppedListener(Event('stop')); final XFile videoFile = await videoFileFuture; @@ -1378,7 +1377,7 @@ void main() { when(() => mediaRecorder.state).thenReturn('recording'); - videoDataAvailableListener.call(FakeBlobEvent(Blob([]))); + videoDataAvailableListener(FakeBlobEvent(Blob([]))); await Future.microtask(() {}); @@ -1412,7 +1411,7 @@ void main() { await camera.startVideoRecording(); - videoRecordingStoppedListener.call(Event('stop')); + videoRecordingStoppedListener(Event('stop')); await Future.microtask(() {}); @@ -1435,7 +1434,7 @@ void main() { await camera.startVideoRecording(); - videoRecordingStoppedListener.call(Event('stop')); + videoRecordingStoppedListener(Event('stop')); await Future.microtask(() {}); @@ -1464,7 +1463,7 @@ void main() { await camera.startVideoRecording(); - videoRecordingStoppedListener.call(Event('stop')); + videoRecordingStoppedListener(Event('stop')); await Future.microtask(() {}); @@ -1588,8 +1587,8 @@ void main() { return finalVideo!; }; - videoDataAvailableListener.call(FakeBlobEvent(Blob([]))); - videoRecordingStoppedListener.call(Event('stop')); + videoDataAvailableListener(FakeBlobEvent(Blob([]))); + videoRecordingStoppedListener(Event('stop')); expect( await streamQueue.next, diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index e3f11383469c..820a84be7207 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -1191,6 +1191,33 @@ void main() { }); }); + group('startVideoCapturing', () { + late Camera camera; + + setUp(() { + camera = MockCamera(); + + when(camera.startVideoRecording).thenAnswer((Invocation _) async {}); + + when(() => camera.onVideoRecordingError) + .thenAnswer((Invocation _) => const Stream.empty()); + }); + + testWidgets('fails if trying to stream', (WidgetTester tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.startVideoCapturing(VideoCaptureOptions( + cameraId, + streamCallback: (CameraImageData imageData) {})), + throwsA( + isA(), + ), + ); + }); + }); + group('stopVideoRecording', () { testWidgets('stops a video recording', (WidgetTester tester) async { final MockCamera camera = MockCamera(); @@ -1664,7 +1691,7 @@ void main() { 'with notFound error ' 'if the camera does not exist', (WidgetTester tester) async { expect( - () async => await CameraPlatform.instance.getMaxZoomLevel( + () async => CameraPlatform.instance.getMaxZoomLevel( cameraId, ), throwsA( @@ -1689,7 +1716,7 @@ void main() { (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; expect( - () async => await CameraPlatform.instance.getMaxZoomLevel( + () async => CameraPlatform.instance.getMaxZoomLevel( cameraId, ), throwsA( @@ -1717,7 +1744,7 @@ void main() { (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; expect( - () async => await CameraPlatform.instance.getMaxZoomLevel( + () async => CameraPlatform.instance.getMaxZoomLevel( cameraId, ), throwsA( @@ -1758,7 +1785,7 @@ void main() { 'with notFound error ' 'if the camera does not exist', (WidgetTester tester) async { expect( - () async => await CameraPlatform.instance.getMinZoomLevel( + () async => CameraPlatform.instance.getMinZoomLevel( cameraId, ), throwsA( @@ -1783,7 +1810,7 @@ void main() { (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; expect( - () async => await CameraPlatform.instance.getMinZoomLevel( + () async => CameraPlatform.instance.getMinZoomLevel( cameraId, ), throwsA( @@ -1811,7 +1838,7 @@ void main() { (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; expect( - () async => await CameraPlatform.instance.getMinZoomLevel( + () async => CameraPlatform.instance.getMinZoomLevel( cameraId, ), throwsA( @@ -1846,7 +1873,7 @@ void main() { 'with notFound error ' 'if the camera does not exist', (WidgetTester tester) async { expect( - () async => await CameraPlatform.instance.setZoomLevel( + () async => CameraPlatform.instance.setZoomLevel( cameraId, 100.0, ), @@ -1872,7 +1899,7 @@ void main() { (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; expect( - () async => await CameraPlatform.instance.setZoomLevel( + () async => CameraPlatform.instance.setZoomLevel( cameraId, 100.0, ), @@ -1900,7 +1927,7 @@ void main() { (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; expect( - () async => await CameraPlatform.instance.setZoomLevel( + () async => CameraPlatform.instance.setZoomLevel( cameraId, 100.0, ), @@ -1929,7 +1956,7 @@ void main() { (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; expect( - () async => await CameraPlatform.instance.setZoomLevel( + () async => CameraPlatform.instance.setZoomLevel( cameraId, 100.0, ), @@ -1962,7 +1989,7 @@ void main() { 'with notFound error ' 'if the camera does not exist', (WidgetTester tester) async { expect( - () async => await CameraPlatform.instance.pausePreview(cameraId), + () async => CameraPlatform.instance.pausePreview(cameraId), throwsA( isA().having( (PlatformException e) => e.code, @@ -1985,7 +2012,7 @@ void main() { (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; expect( - () async => await CameraPlatform.instance.pausePreview(cameraId), + () async => CameraPlatform.instance.pausePreview(cameraId), throwsA( isA().having( (PlatformException e) => e.code, @@ -2017,7 +2044,7 @@ void main() { 'with notFound error ' 'if the camera does not exist', (WidgetTester tester) async { expect( - () async => await CameraPlatform.instance.resumePreview(cameraId), + () async => CameraPlatform.instance.resumePreview(cameraId), throwsA( isA().having( (PlatformException e) => e.code, @@ -2040,7 +2067,7 @@ void main() { (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; expect( - () async => await CameraPlatform.instance.resumePreview(cameraId), + () async => CameraPlatform.instance.resumePreview(cameraId), throwsA( isA().having( (PlatformException e) => e.code, @@ -2066,7 +2093,7 @@ void main() { (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; expect( - () async => await CameraPlatform.instance.resumePreview(cameraId), + () async => CameraPlatform.instance.resumePreview(cameraId), throwsA( isA().having( (PlatformException e) => e.code, @@ -2523,7 +2550,7 @@ void main() { StreamQueue(eventStream); expect( - () async => await CameraPlatform.instance.takePicture(cameraId), + () async => CameraPlatform.instance.takePicture(cameraId), throwsA( isA(), ), @@ -2560,7 +2587,7 @@ void main() { StreamQueue(eventStream); expect( - () async => await CameraPlatform.instance.setFlashMode( + () async => CameraPlatform.instance.setFlashMode( cameraId, FlashMode.always, ), @@ -2600,7 +2627,7 @@ void main() { StreamQueue(eventStream); expect( - () async => await CameraPlatform.instance.getMaxZoomLevel( + () async => CameraPlatform.instance.getMaxZoomLevel( cameraId, ), throwsA( @@ -2639,7 +2666,7 @@ void main() { StreamQueue(eventStream); expect( - () async => await CameraPlatform.instance.getMinZoomLevel( + () async => CameraPlatform.instance.getMinZoomLevel( cameraId, ), throwsA( @@ -2678,7 +2705,7 @@ void main() { StreamQueue(eventStream); expect( - () async => await CameraPlatform.instance.setZoomLevel( + () async => CameraPlatform.instance.setZoomLevel( cameraId, 100.0, ), @@ -2718,7 +2745,7 @@ void main() { StreamQueue(eventStream); expect( - () async => await CameraPlatform.instance.resumePreview(cameraId), + () async => CameraPlatform.instance.resumePreview(cameraId), throwsA( isA(), ), @@ -2762,8 +2789,7 @@ void main() { StreamQueue(eventStream); expect( - () async => - await CameraPlatform.instance.startVideoRecording(cameraId), + () async => CameraPlatform.instance.startVideoRecording(cameraId), throwsA( isA(), ), @@ -2830,8 +2856,7 @@ void main() { StreamQueue(eventStream); expect( - () async => - await CameraPlatform.instance.stopVideoRecording(cameraId), + () async => CameraPlatform.instance.stopVideoRecording(cameraId), throwsA( isA(), ), @@ -2868,8 +2893,7 @@ void main() { StreamQueue(eventStream); expect( - () async => - await CameraPlatform.instance.pauseVideoRecording(cameraId), + () async => CameraPlatform.instance.pauseVideoRecording(cameraId), throwsA( isA(), ), @@ -2906,8 +2930,7 @@ void main() { StreamQueue(eventStream); expect( - () async => - await CameraPlatform.instance.resumeVideoRecording(cameraId), + () async => CameraPlatform.instance.resumeVideoRecording(cameraId), throwsA( isA(), ), diff --git a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart index 521c4bf5a18d..855ef2b9c58e 100644 --- a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart +++ b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: avoid_implementing_value_types + import 'dart:async'; import 'dart:html'; import 'dart:ui'; diff --git a/packages/camera/camera_web/example/pubspec.yaml b/packages/camera/camera_web/example/pubspec.yaml index e82bbe392ceb..ee66870c051d 100644 --- a/packages/camera/camera_web/example/pubspec.yaml +++ b/packages/camera/camera_web/example/pubspec.yaml @@ -3,7 +3,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/camera/camera_web/lib/src/camera_service.dart b/packages/camera/camera_web/lib/src/camera_service.dart index 6e20c7d74f78..451278c23fc3 100644 --- a/packages/camera/camera_web/lib/src/camera_service.dart +++ b/packages/camera/camera_web/lib/src/camera_service.dart @@ -299,9 +299,15 @@ class CameraService { case ResolutionPreset.medium: return const Size(720, 480); case ResolutionPreset.low: - default: return const Size(320, 240); } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return const Size(320, 240); } /// Maps the given [deviceOrientation] to [OrientationType]. diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index d440653cd424..52fdc1c3f8d6 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -451,23 +451,33 @@ class CameraPlugin extends CameraPlatform { @override Future startVideoRecording(int cameraId, {Duration? maxVideoDuration}) { + return startVideoCapturing( + VideoCaptureOptions(cameraId, maxDuration: maxVideoDuration)); + } + + @override + Future startVideoCapturing(VideoCaptureOptions options) { + if (options.streamCallback != null || options.streamOptions != null) { + throw UnimplementedError('Streaming is not currently supported on web'); + } + try { - final Camera camera = getCamera(cameraId); + final Camera camera = getCamera(options.cameraId); // Add camera's video recording errors to the camera events stream. // The error event fires when the video recording is not allowed or an unsupported // codec is used. - _cameraVideoRecordingErrorSubscriptions[cameraId] = + _cameraVideoRecordingErrorSubscriptions[options.cameraId] = camera.onVideoRecordingError.listen((html.ErrorEvent errorEvent) { cameraEventStreamController.add( CameraErrorEvent( - cameraId, + options.cameraId, 'Error code: ${errorEvent.type}, error message: ${errorEvent.message}.', ), ); }); - return camera.startVideoRecording(maxVideoDuration: maxVideoDuration); + return camera.startVideoRecording(maxVideoDuration: options.maxDuration); } on html.DomException catch (e) { throw PlatformException(code: e.name, message: e.message); } on CameraWebException catch (e) { diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index ef9c45c71796..101444b98fe4 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -2,11 +2,11 @@ name: camera_web description: A Flutter plugin for getting information about and controlling the camera on Web. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.3.0+1 +version: 0.3.1+1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -17,7 +17,7 @@ flutter: fileName: camera_web.dart dependencies: - camera_platform_interface: ^2.1.0 + camera_platform_interface: ^2.3.1 flutter: sdk: flutter flutter_web_plugins: diff --git a/packages/camera/camera_web/test/more_tests_exist_elsewhere_test.dart b/packages/camera/camera_web/test/more_tests_exist_elsewhere_test.dart index dc2b64c111d7..32f037effdf1 100644 --- a/packages/camera/camera_web/test/more_tests_exist_elsewhere_test.dart +++ b/packages/camera/camera_web/test/more_tests_exist_elsewhere_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: avoid_print + import 'package:flutter_test/flutter_test.dart'; void main() { diff --git a/packages/camera/camera_windows/CHANGELOG.md b/packages/camera/camera_windows/CHANGELOG.md index 71c5d56524a6..34ee66815aa6 100644 --- a/packages/camera/camera_windows/CHANGELOG.md +++ b/packages/camera/camera_windows/CHANGELOG.md @@ -1,3 +1,15 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 0.2.1+4 + +* Updates code for stricter lint checks. + +## 0.2.1+3 + +* Updates to latest camera platform interface but fails if user attempts to use streaming with recording (since streaming is currently unsupported on Windows). + ## 0.2.1+2 * Updates code for `no_leading_underscores_for_local_identifiers` lint. diff --git a/packages/camera/camera_windows/example/integration_test/camera_test.dart b/packages/camera/camera_windows/example/integration_test/camera_test.dart index cda0f402de6c..01db9e2aee46 100644 --- a/packages/camera/camera_windows/example/integration_test/camera_test.dart +++ b/packages/camera/camera_windows/example/integration_test/camera_test.dart @@ -23,7 +23,7 @@ void main() { (WidgetTester _) async { final CameraPlatform camera = CameraPlatform.instance; - expect(() async => await camera.initializeCamera(1234), + expect(() async => camera.initializeCamera(1234), throwsA(isA())); }); }); @@ -33,7 +33,7 @@ void main() { (WidgetTester _) async { final CameraPlatform camera = CameraPlatform.instance; - expect(() async => await camera.takePicture(1234), + expect(() async => camera.takePicture(1234), throwsA(isA())); }); }); @@ -43,7 +43,7 @@ void main() { (WidgetTester _) async { final CameraPlatform camera = CameraPlatform.instance; - expect(() async => await camera.startVideoRecording(1234), + expect(() async => camera.startVideoRecording(1234), throwsA(isA())); }); }); @@ -53,7 +53,7 @@ void main() { (WidgetTester _) async { final CameraPlatform camera = CameraPlatform.instance; - expect(() async => await camera.stopVideoRecording(1234), + expect(() async => camera.stopVideoRecording(1234), throwsA(isA())); }); }); @@ -63,7 +63,7 @@ void main() { (WidgetTester _) async { final CameraPlatform camera = CameraPlatform.instance; - expect(() async => await camera.pausePreview(1234), + expect(() async => camera.pausePreview(1234), throwsA(isA())); }); }); @@ -73,7 +73,7 @@ void main() { (WidgetTester _) async { final CameraPlatform camera = CameraPlatform.instance; - expect(() async => await camera.resumePreview(1234), + expect(() async => camera.resumePreview(1234), throwsA(isA())); }); }); diff --git a/packages/camera/camera_windows/example/pubspec.yaml b/packages/camera/camera_windows/example/pubspec.yaml index 80ce958a0e84..69ce1c330156 100644 --- a/packages/camera/camera_windows/example/pubspec.yaml +++ b/packages/camera/camera_windows/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: 'none' environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: camera_platform_interface: ^2.1.2 diff --git a/packages/camera/camera_windows/lib/camera_windows.dart b/packages/camera/camera_windows/lib/camera_windows.dart index 14134479994b..4b0c1586f433 100644 --- a/packages/camera/camera_windows/lib/camera_windows.dart +++ b/packages/camera/camera_windows/lib/camera_windows.dart @@ -214,15 +214,24 @@ class CameraWindows extends CameraPlatform { pluginChannel.invokeMethod('prepareForVideoRecording'); @override - Future startVideoRecording( - int cameraId, { - Duration? maxVideoDuration, - }) async { + Future startVideoRecording(int cameraId, + {Duration? maxVideoDuration}) async { + return startVideoCapturing( + VideoCaptureOptions(cameraId, maxDuration: maxVideoDuration)); + } + + @override + Future startVideoCapturing(VideoCaptureOptions options) async { + if (options.streamCallback != null || options.streamOptions != null) { + throw UnimplementedError( + 'Streaming is not currently supported on Windows'); + } + await pluginChannel.invokeMethod( 'startVideoRecording', { - 'cameraId': cameraId, - 'maxVideoDuration': maxVideoDuration?.inMilliseconds, + 'cameraId': options.cameraId, + 'maxVideoDuration': options.maxDuration?.inMilliseconds, }, ); } @@ -390,24 +399,25 @@ class CameraWindows extends CameraPlatform { ); break; case 'video_recorded': + final Map arguments = + (call.arguments as Map).cast(); + final int? maxDuration = arguments['maxVideoDuration'] as int?; // This is called if maxVideoDuration was given on record start. cameraEventStreamController.add( VideoRecordedEvent( cameraId, - XFile(call.arguments['path'] as String), - call.arguments['maxVideoDuration'] != null - ? Duration( - milliseconds: call.arguments['maxVideoDuration'] as int, - ) - : null, + XFile(arguments['path']! as String), + maxDuration != null ? Duration(milliseconds: maxDuration) : null, ), ); break; case 'error': + final Map arguments = + (call.arguments as Map).cast(); cameraEventStreamController.add( CameraErrorEvent( cameraId, - call.arguments['description'] as String, + arguments['description']! as String, ), ); break; diff --git a/packages/camera/camera_windows/pubspec.yaml b/packages/camera/camera_windows/pubspec.yaml index 1eab9fa108ef..e028559c28ab 100644 --- a/packages/camera/camera_windows/pubspec.yaml +++ b/packages/camera/camera_windows/pubspec.yaml @@ -2,11 +2,11 @@ name: camera_windows description: A Flutter plugin for getting information about and controlling the camera on Windows. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.2.1+2 +version: 0.2.1+4 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -17,7 +17,7 @@ flutter: dartPluginClass: CameraWindows dependencies: - camera_platform_interface: ^2.1.2 + camera_platform_interface: ^2.3.1 cross_file: ^0.3.1 flutter: sdk: flutter diff --git a/packages/camera/camera_windows/test/camera_windows_test.dart b/packages/camera/camera_windows/test/camera_windows_test.dart index c1a0fe40325f..8d7b5d3d7185 100644 --- a/packages/camera/camera_windows/test/camera_windows_test.dart +++ b/packages/camera/camera_windows/test/camera_windows_test.dart @@ -335,11 +335,13 @@ void main() { ]); expect(cameras.length, returnData.length); for (int i = 0; i < returnData.length; i++) { + final Map typedData = + (returnData[i] as Map).cast(); final CameraDescription cameraDescription = CameraDescription( - name: returnData[i]['name']! as String, - lensDirection: plugin.parseCameraLensDirection( - returnData[i]['lensFacing']! as String), - sensorOrientation: returnData[i]['sensorOrientation']! as int, + name: typedData['name']! as String, + lensDirection: plugin + .parseCameraLensDirection(typedData['lensFacing']! as String), + sensorOrientation: typedData['sensorOrientation']! as int, ); expect(cameras[i], cameraDescription); } @@ -447,6 +449,15 @@ void main() { ]); }); + test('capturing fails if trying to stream', () async { + // Act and Assert + expect( + () => plugin.startVideoCapturing(VideoCaptureOptions(cameraId, + streamCallback: (CameraImageData imageData) {})), + throwsA(isA()), + ); + }); + test('Should stop a video recording and return the file', () async { // Arrange final MethodChannelMock channel = MethodChannelMock( diff --git a/packages/camera/camera_windows/test/utils/method_channel_mock.dart b/packages/camera/camera_windows/test/utils/method_channel_mock.dart index 22f7ecead589..559f60662844 100644 --- a/packages/camera/camera_windows/test/utils/method_channel_mock.dart +++ b/packages/camera/camera_windows/test/utils/method_channel_mock.dart @@ -17,7 +17,9 @@ class MethodChannelMock { this.delay, required this.methods, }) : methodChannel = MethodChannel(channelName) { - methodChannel.setMockMethodCallHandler(_handler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, _handler); } final Duration? delay; @@ -43,3 +45,9 @@ class MethodChannelMock { }); } } + +/// 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/espresso/CHANGELOG.md b/packages/espresso/CHANGELOG.md index 38726df01fa6..96ccb32f0325 100644 --- a/packages/espresso/CHANGELOG.md +++ b/packages/espresso/CHANGELOG.md @@ -1,3 +1,17 @@ +## 0.2.0+8 + +* Updates espresso and junit dependencies. + +## 0.2.0+7 + +* Updates espresso gradle and gson dependencies. +* Updates minimum Flutter version to 3.0. + +## 0.2.0+6 + +* Updates espresso-accessibility to 3.5.1. +* Updates espresso-idling-resource to 3.5.1. + ## 0.2.0+5 * Updates android gradle plugin to 7.3.1. diff --git a/packages/espresso/android/build.gradle b/packages/espresso/android/build.gradle index ce461b1ebe55..bda13fc52780 100644 --- a/packages/espresso/android/build.gradle +++ b/packages/espresso/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.3.1' + classpath 'com.android.tools.build:gradle:7.4.1' } } @@ -29,10 +29,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - // TODO(stuartmorgan): Enable when gradle is updated. - // disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' baseline file("lint-baseline.xml") } @@ -53,7 +50,7 @@ android { dependencies { implementation 'com.google.guava:guava:31.1-android' implementation 'com.squareup.okhttp3:okhttp:4.10.0' - implementation 'com.google.code.gson:gson:2.9.1' + implementation 'com.google.code.gson:gson:2.10.1' androidTestImplementation 'org.hamcrest:hamcrest:2.2' testImplementation 'junit:junit:4.13.2' @@ -69,23 +66,23 @@ dependencies { api 'androidx.test:rules:1.1.0' // Assertions - api 'androidx.test.ext:junit:1.1.3' - api 'androidx.test.ext:truth:1.4.0' + api 'androidx.test.ext:junit:1.1.5' + api 'androidx.test.ext:truth:1.5.0' api 'com.google.truth:truth:0.42' // Espresso dependencies - api 'androidx.test.espresso:espresso-core:3.1.0' - api 'androidx.test.espresso:espresso-contrib:3.1.0' - api 'androidx.test.espresso:espresso-intents:3.1.0' - api 'androidx.test.espresso:espresso-accessibility:3.1.0' - api 'androidx.test.espresso:espresso-web:3.1.0' - api 'androidx.test.espresso.idling:idling-concurrent:3.1.0' + api 'androidx.test.espresso:espresso-core:3.5.1' + api 'androidx.test.espresso:espresso-contrib:3.5.1' + api 'androidx.test.espresso:espresso-intents:3.5.1' + api 'androidx.test.espresso:espresso-accessibility:3.5.1' + api 'androidx.test.espresso:espresso-web:3.5.1' + api 'androidx.test.espresso.idling:idling-concurrent:3.5.1' // The following Espresso dependency can be either "implementation" // or "androidTestImplementation", depending on whether you want the // dependency to appear on your APK's compile classpath or the test APK // classpath. - api 'androidx.test.espresso:espresso-idling-resource:3.1.0' + api 'androidx.test.espresso:espresso-idling-resource:3.5.1' } diff --git a/packages/espresso/example/android/gradle.properties b/packages/espresso/example/android/gradle.properties index 38c8d4544ff1..2f3603c9ff62 100644 --- a/packages/espresso/example/android/gradle.properties +++ b/packages/espresso/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/packages/espresso/example/pubspec.yaml b/packages/espresso/example/pubspec.yaml index 67f9edcd4644..0adf623b728a 100644 --- a/packages/espresso/example/pubspec.yaml +++ b/packages/espresso/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/espresso/pubspec.yaml b/packages/espresso/pubspec.yaml index 16fcbfb3aa2c..21aa5dfb27d9 100644 --- a/packages/espresso/pubspec.yaml +++ b/packages/espresso/pubspec.yaml @@ -3,11 +3,11 @@ description: Java classes for testing Flutter apps using Espresso. Allows driving Flutter widgets from a native Espresso test. repository: https://github.com/flutter/plugins/tree/main/packages/espresso issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+espresso%22 -version: 0.2.0+5 +version: 0.2.0+8 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/file_selector/file_selector/CHANGELOG.md b/packages/file_selector/file_selector/CHANGELOG.md index 7983aa57561f..9fd2341501b3 100644 --- a/packages/file_selector/file_selector/CHANGELOG.md +++ b/packages/file_selector/file_selector/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Updates example code for `use_build_context_synchronously` lint. +* Updates minimum Flutter version to 3.0. + ## 0.9.2+2 * Improves API docs and examples. diff --git a/packages/file_selector/file_selector/example/lib/get_directory_page.dart b/packages/file_selector/file_selector/example/lib/get_directory_page.dart index de80aa56be56..dfe166db96c4 100644 --- a/packages/file_selector/file_selector/example/lib/get_directory_page.dart +++ b/packages/file_selector/file_selector/example/lib/get_directory_page.dart @@ -22,10 +22,12 @@ class GetDirectoryPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( - context: context, - builder: (BuildContext context) => TextDisplay(directoryPath), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(directoryPath), + ); + } } @override diff --git a/packages/file_selector/file_selector/example/lib/open_image_page.dart b/packages/file_selector/file_selector/example/lib/open_image_page.dart index ba18e6e78594..7717f28c39fe 100644 --- a/packages/file_selector/file_selector/example/lib/open_image_page.dart +++ b/packages/file_selector/file_selector/example/lib/open_image_page.dart @@ -29,10 +29,12 @@ class OpenImagePage extends StatelessWidget { final String fileName = file.name; final String filePath = file.path; - await showDialog( - context: context, - builder: (BuildContext context) => ImageDisplay(fileName, filePath), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => ImageDisplay(fileName, filePath), + ); + } } @override diff --git a/packages/file_selector/file_selector/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector/example/lib/open_multiple_images_page.dart index 8ae83c2a85dc..a09a6db9d7a7 100644 --- a/packages/file_selector/file_selector/example/lib/open_multiple_images_page.dart +++ b/packages/file_selector/file_selector/example/lib/open_multiple_images_page.dart @@ -32,10 +32,12 @@ class OpenMultipleImagesPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( - context: context, - builder: (BuildContext context) => MultipleImagesDisplay(files), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => MultipleImagesDisplay(files), + ); + } } @override diff --git a/packages/file_selector/file_selector/example/lib/open_text_page.dart b/packages/file_selector/file_selector/example/lib/open_text_page.dart index f052db1eefc1..e28a67a02ddf 100644 --- a/packages/file_selector/file_selector/example/lib/open_text_page.dart +++ b/packages/file_selector/file_selector/example/lib/open_text_page.dart @@ -32,10 +32,12 @@ class OpenTextPage extends StatelessWidget { final String fileName = file.name; final String fileContent = await file.readAsString(); - await showDialog( - context: context, - builder: (BuildContext context) => TextDisplay(fileName, fileContent), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(fileName, fileContent), + ); + } } @override diff --git a/packages/file_selector/file_selector/example/lib/save_text_page.dart b/packages/file_selector/file_selector/example/lib/save_text_page.dart index 6dc765f7accf..0a49e6f0382c 100644 --- a/packages/file_selector/file_selector/example/lib/save_text_page.dart +++ b/packages/file_selector/file_selector/example/lib/save_text_page.dart @@ -56,7 +56,7 @@ class SaveTextPage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( + SizedBox( width: 300, child: TextField( minLines: 1, @@ -67,7 +67,7 @@ class SaveTextPage extends StatelessWidget { ), ), ), - Container( + SizedBox( width: 300, child: TextField( minLines: 1, diff --git a/packages/file_selector/file_selector/example/pubspec.yaml b/packages/file_selector/file_selector/example/pubspec.yaml index 011d95874ae4..ff9d6d0d2e17 100644 --- a/packages/file_selector/file_selector/example/pubspec.yaml +++ b/packages/file_selector/file_selector/example/pubspec.yaml @@ -6,6 +6,7 @@ version: 1.0.0+1 environment: sdk: ">=2.12.0 <3.0.0" + flutter: ">=3.0.0" dependencies: file_selector: diff --git a/packages/file_selector/file_selector/pubspec.yaml b/packages/file_selector/file_selector/pubspec.yaml index ad187d6f446a..17e41cd656dd 100644 --- a/packages/file_selector/file_selector/pubspec.yaml +++ b/packages/file_selector/file_selector/pubspec.yaml @@ -7,7 +7,7 @@ version: 0.9.2+2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/file_selector/file_selector_ios/CHANGELOG.md b/packages/file_selector/file_selector_ios/CHANGELOG.md index 439e1d4fd4c1..40d232ed25d0 100644 --- a/packages/file_selector/file_selector_ios/CHANGELOG.md +++ b/packages/file_selector/file_selector_ios/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Updates example code for `use_build_context_synchronously` lint. +* Updates minimum Flutter version to 3.0. + ## 0.5.0+2 * Changes XTypeGroup initialization from final to const. diff --git a/packages/file_selector/file_selector_ios/example/lib/open_image_page.dart b/packages/file_selector/file_selector_ios/example/lib/open_image_page.dart index 606a64870566..6fcbcbfbafd6 100644 --- a/packages/file_selector/file_selector_ios/example/lib/open_image_page.dart +++ b/packages/file_selector/file_selector_ios/example/lib/open_image_page.dart @@ -29,10 +29,12 @@ class OpenImagePage extends StatelessWidget { final String fileName = file.name; final String filePath = file.path; - await showDialog( - context: context, - builder: (BuildContext context) => ImageDisplay(fileName, filePath), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => ImageDisplay(fileName, filePath), + ); + } } @override diff --git a/packages/file_selector/file_selector_ios/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector_ios/example/lib/open_multiple_images_page.dart index adc4a65f12b5..30cc5159b060 100644 --- a/packages/file_selector/file_selector_ios/example/lib/open_multiple_images_page.dart +++ b/packages/file_selector/file_selector_ios/example/lib/open_multiple_images_page.dart @@ -34,10 +34,12 @@ class OpenMultipleImagesPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( - context: context, - builder: (BuildContext context) => MultipleImagesDisplay(files), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => MultipleImagesDisplay(files), + ); + } } @override diff --git a/packages/file_selector/file_selector_ios/example/lib/open_text_page.dart b/packages/file_selector/file_selector_ios/example/lib/open_text_page.dart index e7bbf8bc937f..f21daf9a96bf 100644 --- a/packages/file_selector/file_selector_ios/example/lib/open_text_page.dart +++ b/packages/file_selector/file_selector_ios/example/lib/open_text_page.dart @@ -26,10 +26,12 @@ class OpenTextPage extends StatelessWidget { final String fileName = file.name; final String fileContent = await file.readAsString(); - await showDialog( - context: context, - builder: (BuildContext context) => TextDisplay(fileName, fileContent), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(fileName, fileContent), + ); + } } @override diff --git a/packages/file_selector/file_selector_ios/example/pubspec.yaml b/packages/file_selector/file_selector_ios/example/pubspec.yaml index 5a2eaa6f7dcd..175ec6c6e7d0 100644 --- a/packages/file_selector/file_selector_ios/example/pubspec.yaml +++ b/packages/file_selector/file_selector_ios/example/pubspec.yaml @@ -5,6 +5,7 @@ version: 1.0.0 environment: sdk: ">=2.14.4 <3.0.0" + flutter: ">=3.0.0" dependencies: # The following adds the Cupertino Icons font to your application. @@ -28,4 +29,4 @@ dev_dependencies: sdk: flutter flutter: - uses-material-design: true \ No newline at end of file + uses-material-design: true diff --git a/packages/file_selector/file_selector_ios/pigeons/messages.dart b/packages/file_selector/file_selector_ios/pigeons/messages.dart index d0ea73cde111..66706cc2406e 100644 --- a/packages/file_selector/file_selector_ios/pigeons/messages.dart +++ b/packages/file_selector/file_selector_ios/pigeons/messages.dart @@ -6,7 +6,7 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions( dartOut: 'lib/src/messages.g.dart', - dartTestOut: 'test/test_api.dart', + dartTestOut: 'test/test_api.g.dart', objcHeaderOut: 'ios/Classes/messages.g.h', objcSourceOut: 'ios/Classes/messages.g.m', objcOptions: ObjcOptions( diff --git a/packages/file_selector/file_selector_ios/pubspec.yaml b/packages/file_selector/file_selector_ios/pubspec.yaml index 3f8ecfac04ce..e772cb7d8632 100644 --- a/packages/file_selector/file_selector_ios/pubspec.yaml +++ b/packages/file_selector/file_selector_ios/pubspec.yaml @@ -6,7 +6,7 @@ version: 0.5.0+2 environment: sdk: ">=2.14.4 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/file_selector/file_selector_ios/test/file_selector_ios_test.dart b/packages/file_selector/file_selector_ios/test/file_selector_ios_test.dart index f66bd7dc7ced..e10ad17a2fb4 100644 --- a/packages/file_selector/file_selector_ios/test/file_selector_ios_test.dart +++ b/packages/file_selector/file_selector_ios/test/file_selector_ios_test.dart @@ -11,7 +11,7 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'file_selector_ios_test.mocks.dart'; -import 'test_api.dart'; +import 'test_api.g.dart'; @GenerateMocks([TestFileSelectorApi]) void main() { diff --git a/packages/file_selector/file_selector_ios/test/file_selector_ios_test.mocks.dart b/packages/file_selector/file_selector_ios/test/file_selector_ios_test.mocks.dart index 38c91b46f65e..1d22ba75a10a 100644 --- a/packages/file_selector/file_selector_ios/test/file_selector_ios_test.mocks.dart +++ b/packages/file_selector/file_selector_ios/test/file_selector_ios_test.mocks.dart @@ -8,7 +8,7 @@ import 'dart:async' as _i3; import 'package:file_selector_ios/src/messages.g.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'test_api.dart' as _i2; +import 'test_api.g.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values diff --git a/packages/file_selector/file_selector_ios/test/test_api.dart b/packages/file_selector/file_selector_ios/test/test_api.g.dart similarity index 100% rename from packages/file_selector/file_selector_ios/test/test_api.dart rename to packages/file_selector/file_selector_ios/test/test_api.g.dart diff --git a/packages/file_selector/file_selector_linux/CHANGELOG.md b/packages/file_selector/file_selector_linux/CHANGELOG.md index a1f57b5cc857..6f7853cc5f13 100644 --- a/packages/file_selector/file_selector_linux/CHANGELOG.md +++ b/packages/file_selector/file_selector_linux/CHANGELOG.md @@ -1,3 +1,12 @@ +## NEXT + +* Updates example code for `use_build_context_synchronously` lint. +* Updates minimum Flutter version to 3.0. + +## 0.9.1 + +* Adds `getDirectoryPaths` implementation. + ## 0.9.0+1 * Changes XTypeGroup initialization from final to const. diff --git a/packages/file_selector/file_selector_linux/example/lib/get_directory_page.dart b/packages/file_selector/file_selector_linux/example/lib/get_directory_page.dart index 0699dd121541..f6390ccef20d 100644 --- a/packages/file_selector/file_selector_linux/example/lib/get_directory_page.dart +++ b/packages/file_selector/file_selector_linux/example/lib/get_directory_page.dart @@ -21,10 +21,12 @@ class GetDirectoryPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( - context: context, - builder: (BuildContext context) => TextDisplay(directoryPath), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(directoryPath), + ); + } } @override diff --git a/packages/file_selector/file_selector_linux/example/lib/get_multiple_directories_page.dart b/packages/file_selector/file_selector_linux/example/lib/get_multiple_directories_page.dart new file mode 100644 index 000000000000..087240be765e --- /dev/null +++ b/packages/file_selector/file_selector_linux/example/lib/get_multiple_directories_page.dart @@ -0,0 +1,87 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/material.dart'; + +/// Screen that allows the user to select one or more directories using `getDirectoryPaths`, +/// then displays the selected directories in a dialog. +class GetMultipleDirectoriesPage extends StatelessWidget { + /// Default Constructor + const GetMultipleDirectoriesPage({Key? key}) : super(key: key); + + Future _getDirectoryPaths(BuildContext context) async { + const String confirmButtonText = 'Choose'; + final List directoryPaths = + await FileSelectorPlatform.instance.getDirectoryPaths( + confirmButtonText: confirmButtonText, + ); + if (directoryPaths.isEmpty) { + // Operation was canceled by the user. + return; + } + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => + TextDisplay(directoryPaths.join('\n')), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Select multiple directories'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // ignore: deprecated_member_use + primary: Colors.blue, + // ignore: deprecated_member_use + onPrimary: Colors.white, + ), + child: const Text( + 'Press to ask user to choose multiple directories'), + onPressed: () => _getDirectoryPaths(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog. +class TextDisplay extends StatelessWidget { + /// Creates a `TextDisplay`. + const TextDisplay(this.directoriesPaths, {Key? key}) : super(key: key); + + /// The path selected in the dialog. + final String directoriesPaths; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Selected Directories'), + content: Scrollbar( + child: SingleChildScrollView( + child: Text(directoriesPaths), + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_linux/example/lib/home_page.dart b/packages/file_selector/file_selector_linux/example/lib/home_page.dart index a4b2ae1f63ea..80e16332a017 100644 --- a/packages/file_selector/file_selector_linux/example/lib/home_page.dart +++ b/packages/file_selector/file_selector_linux/example/lib/home_page.dart @@ -55,6 +55,13 @@ class HomePage extends StatelessWidget { child: const Text('Open a get directory dialog'), onPressed: () => Navigator.pushNamed(context, '/directory'), ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Open a get directories dialog'), + onPressed: () => + Navigator.pushNamed(context, '/multi-directories'), + ), ], ), ), diff --git a/packages/file_selector/file_selector_linux/example/lib/main.dart b/packages/file_selector/file_selector_linux/example/lib/main.dart index 3e447104ef9f..b8f047645a1d 100644 --- a/packages/file_selector/file_selector_linux/example/lib/main.dart +++ b/packages/file_selector/file_selector_linux/example/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'get_directory_page.dart'; +import 'get_multiple_directories_page.dart'; import 'home_page.dart'; import 'open_image_page.dart'; import 'open_multiple_images_page.dart'; @@ -36,6 +37,8 @@ class MyApp extends StatelessWidget { '/open/text': (BuildContext context) => const OpenTextPage(), '/save/text': (BuildContext context) => SaveTextPage(), '/directory': (BuildContext context) => const GetDirectoryPage(), + '/multi-directories': (BuildContext context) => + const GetMultipleDirectoriesPage() }, ); } diff --git a/packages/file_selector/file_selector_linux/example/lib/open_image_page.dart b/packages/file_selector/file_selector_linux/example/lib/open_image_page.dart index b6ada56ebb2b..9252d25f113c 100644 --- a/packages/file_selector/file_selector_linux/example/lib/open_image_page.dart +++ b/packages/file_selector/file_selector_linux/example/lib/open_image_page.dart @@ -28,10 +28,12 @@ class OpenImagePage extends StatelessWidget { final String fileName = file.name; final String filePath = file.path; - await showDialog( - context: context, - builder: (BuildContext context) => ImageDisplay(fileName, filePath), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => ImageDisplay(fileName, filePath), + ); + } } @override diff --git a/packages/file_selector/file_selector_linux/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector_linux/example/lib/open_multiple_images_page.dart index c8e352a5b8bd..787717cdea13 100644 --- a/packages/file_selector/file_selector_linux/example/lib/open_multiple_images_page.dart +++ b/packages/file_selector/file_selector_linux/example/lib/open_multiple_images_page.dart @@ -32,10 +32,12 @@ class OpenMultipleImagesPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( - context: context, - builder: (BuildContext context) => MultipleImagesDisplay(files), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => MultipleImagesDisplay(files), + ); + } } @override diff --git a/packages/file_selector/file_selector_linux/example/lib/open_text_page.dart b/packages/file_selector/file_selector_linux/example/lib/open_text_page.dart index 4c88d7475049..97812f2b3505 100644 --- a/packages/file_selector/file_selector_linux/example/lib/open_text_page.dart +++ b/packages/file_selector/file_selector_linux/example/lib/open_text_page.dart @@ -25,10 +25,12 @@ class OpenTextPage extends StatelessWidget { final String fileName = file.name; final String fileContent = await file.readAsString(); - await showDialog( - context: context, - builder: (BuildContext context) => TextDisplay(fileName, fileContent), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(fileName, fileContent), + ); + } } @override diff --git a/packages/file_selector/file_selector_linux/example/lib/save_text_page.dart b/packages/file_selector/file_selector_linux/example/lib/save_text_page.dart index 9803f285a536..aca041f474c7 100644 --- a/packages/file_selector/file_selector_linux/example/lib/save_text_page.dart +++ b/packages/file_selector/file_selector_linux/example/lib/save_text_page.dart @@ -42,7 +42,7 @@ class SaveTextPage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( + SizedBox( width: 300, child: TextField( minLines: 1, @@ -53,7 +53,7 @@ class SaveTextPage extends StatelessWidget { ), ), ), - Container( + SizedBox( width: 300, child: TextField( minLines: 1, diff --git a/packages/file_selector/file_selector_linux/example/pubspec.yaml b/packages/file_selector/file_selector_linux/example/pubspec.yaml index 51bdb28717aa..f90d1c88ef97 100644 --- a/packages/file_selector/file_selector_linux/example/pubspec.yaml +++ b/packages/file_selector/file_selector_linux/example/pubspec.yaml @@ -1,15 +1,16 @@ name: file_selector_linux_example description: Local testbed for Linux file_selector implementation. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: 'none' version: 1.0.0+1 environment: sdk: ">=2.12.0 <3.0.0" + flutter: ">=3.0.0" dependencies: file_selector_linux: path: ../ - file_selector_platform_interface: ^2.2.0 + file_selector_platform_interface: ^2.4.0 flutter: sdk: flutter diff --git a/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart b/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart index 430b41c398db..b8e3df6a11bd 100644 --- a/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart +++ b/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart @@ -102,13 +102,26 @@ class FileSelectorLinux extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - return _channel.invokeMethod( - _getDirectoryPathMethod, - { - _initialDirectoryKey: initialDirectory, - _confirmButtonTextKey: confirmButtonText, - }, - ); + final List? path = await _channel + .invokeListMethod(_getDirectoryPathMethod, { + _initialDirectoryKey: initialDirectory, + _confirmButtonTextKey: confirmButtonText, + }); + return path?.first; + } + + @override + Future> getDirectoryPaths({ + String? initialDirectory, + String? confirmButtonText, + }) async { + final List? pathList = await _channel + .invokeListMethod(_getDirectoryPathMethod, { + _initialDirectoryKey: initialDirectory, + _confirmButtonTextKey: confirmButtonText, + _multipleKey: true, + }); + return pathList ?? []; } } diff --git a/packages/file_selector/file_selector_linux/linux/.gitignore b/packages/file_selector/file_selector_linux/linux/.gitignore new file mode 100644 index 000000000000..83fee186aa98 --- /dev/null +++ b/packages/file_selector/file_selector_linux/linux/.gitignore @@ -0,0 +1,2 @@ +CMakeCache.txt +CMakeFiles/ \ No newline at end of file diff --git a/packages/file_selector/file_selector_linux/linux/file_selector_plugin.cc b/packages/file_selector/file_selector_linux/linux/file_selector_plugin.cc index 833771955120..5a8cc2132595 100644 --- a/packages/file_selector/file_selector_linux/linux/file_selector_plugin.cc +++ b/packages/file_selector/file_selector_linux/linux/file_selector_plugin.cc @@ -192,10 +192,10 @@ static void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call, FlValue* args = fl_method_call_get_args(method_call); g_autoptr(FlMethodResponse) response = nullptr; - if (strcmp(method, kOpenFileMethod) == 0) { + if (strcmp(method, kOpenFileMethod) == 0 || + strcmp(method, kGetDirectoryPathMethod) == 0) { response = show_dialog(self, method, args, true); - } else if (strcmp(method, kGetDirectoryPathMethod) == 0 || - strcmp(method, kGetSavePathMethod) == 0) { + } else if (strcmp(method, kGetSavePathMethod) == 0) { response = show_dialog(self, method, args, false); } else { response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); diff --git a/packages/file_selector/file_selector_linux/linux/test/file_selector_plugin_test.cc b/packages/file_selector/file_selector_linux/linux/test/file_selector_plugin_test.cc index 84c55ac91900..8762b4a5f9f6 100644 --- a/packages/file_selector/file_selector_linux/linux/test/file_selector_plugin_test.cc +++ b/packages/file_selector/file_selector_linux/linux/test/file_selector_plugin_test.cc @@ -169,3 +169,17 @@ TEST(FileSelectorPlugin, TestGetDirectory) { EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)), false); } + +TEST(FileSelectorPlugin, TestGetMultipleDirectories) { + g_autoptr(FlValue) args = fl_value_new_map(); + fl_value_set_string_take(args, "multiple", fl_value_new_bool(true)); + + g_autoptr(GtkFileChooserNative) dialog = + create_dialog_for_method(nullptr, "getDirectoryPath", args); + + ASSERT_NE(dialog, nullptr); + EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)), + GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER); + EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)), + true); +} diff --git a/packages/file_selector/file_selector_linux/pubspec.yaml b/packages/file_selector/file_selector_linux/pubspec.yaml index a8aea37d72e2..af88485b0ef2 100644 --- a/packages/file_selector/file_selector_linux/pubspec.yaml +++ b/packages/file_selector/file_selector_linux/pubspec.yaml @@ -2,11 +2,11 @@ name: file_selector_linux description: Liunx implementation of the file_selector plugin. repository: https://github.com/flutter/plugins/tree/main/packages/file_selector/file_selector_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.9.0+1 +version: 0.9.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -18,7 +18,7 @@ flutter: dependencies: cross_file: ^0.3.1 - file_selector_platform_interface: ^2.2.0 + file_selector_platform_interface: ^2.4.0 flutter: sdk: flutter diff --git a/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart b/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart index 748f922ae6ef..53a549da3d4a 100644 --- a/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart +++ b/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart @@ -16,10 +16,15 @@ void main() { setUp(() { plugin = FileSelectorLinux(); log = []; - plugin.channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - return null; - }); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + plugin.channel, + (MethodCall methodCall) async { + log.add(methodCall); + return null; + }, + ); }); test('registers instance', () { @@ -46,57 +51,54 @@ void main() { await plugin.openFile(acceptedTypeGroups: [group, groupTwo]); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': >[ - { - 'label': 'text', - 'extensions': ['*.txt'], - 'mimeTypes': ['text/plain'], - }, - { - 'label': 'image', - 'extensions': ['*.jpg'], - 'mimeTypes': ['image/jpg'], - }, - ], - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': false, - }), - ], + 'openFile', + arguments: { + 'acceptedTypeGroups': >[ + { + 'label': 'text', + 'extensions': ['*.txt'], + 'mimeTypes': ['text/plain'], + }, + { + 'label': 'image', + 'extensions': ['*.jpg'], + 'mimeTypes': ['image/jpg'], + }, + ], + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': false, + }, ); }); test('passes initialDirectory correctly', () async { await plugin.openFile(initialDirectory: '/example/directory'); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - 'multiple': false, - }), - ], + 'openFile', + arguments: { + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + 'multiple': false, + }, ); }); test('passes confirmButtonText correctly', () async { await plugin.openFile(confirmButtonText: 'Open File'); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - 'multiple': false, - }), - ], + 'openFile', + arguments: { + 'initialDirectory': null, + 'confirmButtonText': 'Open File', + 'multiple': false, + }, ); }); @@ -118,21 +120,20 @@ void main() { await plugin.openFile(acceptedTypeGroups: [group]); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': >[ - { - 'label': 'any', - 'extensions': ['*'], - }, - ], - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': false, - }), - ], + 'openFile', + arguments: { + 'acceptedTypeGroups': >[ + { + 'label': 'any', + 'extensions': ['*'], + }, + ], + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': false, + }, ); }); }); @@ -156,57 +157,54 @@ void main() { await plugin.openFiles(acceptedTypeGroups: [group, groupTwo]); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': >[ - { - 'label': 'text', - 'extensions': ['*.txt'], - 'mimeTypes': ['text/plain'], - }, - { - 'label': 'image', - 'extensions': ['*.jpg'], - 'mimeTypes': ['image/jpg'], - }, - ], - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': true, - }), - ], + 'openFile', + arguments: { + 'acceptedTypeGroups': >[ + { + 'label': 'text', + 'extensions': ['*.txt'], + 'mimeTypes': ['text/plain'], + }, + { + 'label': 'image', + 'extensions': ['*.jpg'], + 'mimeTypes': ['image/jpg'], + }, + ], + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': true, + }, ); }); test('passes initialDirectory correctly', () async { await plugin.openFiles(initialDirectory: '/example/directory'); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - 'multiple': true, - }), - ], + 'openFile', + arguments: { + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + 'multiple': true, + }, ); }); test('passes confirmButtonText correctly', () async { await plugin.openFiles(confirmButtonText: 'Open File'); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - 'multiple': true, - }), - ], + 'openFile', + arguments: { + 'initialDirectory': null, + 'confirmButtonText': 'Open File', + 'multiple': true, + }, ); }); @@ -228,21 +226,20 @@ void main() { await plugin.openFile(acceptedTypeGroups: [group]); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': >[ - { - 'label': 'any', - 'extensions': ['*'], - }, - ], - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': false, - }), - ], + 'openFile', + arguments: { + 'acceptedTypeGroups': >[ + { + 'label': 'any', + 'extensions': ['*'], + }, + ], + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': false, + }, ); }); }); @@ -267,57 +264,54 @@ void main() { await plugin .getSavePath(acceptedTypeGroups: [group, groupTwo]); - expect( + expectMethodCall( log, - [ - isMethodCall('getSavePath', arguments: { - 'acceptedTypeGroups': >[ - { - 'label': 'text', - 'extensions': ['*.txt'], - 'mimeTypes': ['text/plain'], - }, - { - 'label': 'image', - 'extensions': ['*.jpg'], - 'mimeTypes': ['image/jpg'], - }, - ], - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': null, - }), - ], + 'getSavePath', + arguments: { + 'acceptedTypeGroups': >[ + { + 'label': 'text', + 'extensions': ['*.txt'], + 'mimeTypes': ['text/plain'], + }, + { + 'label': 'image', + 'extensions': ['*.jpg'], + 'mimeTypes': ['image/jpg'], + }, + ], + 'initialDirectory': null, + 'suggestedName': null, + 'confirmButtonText': null, + }, ); }); test('passes initialDirectory correctly', () async { await plugin.getSavePath(initialDirectory: '/example/directory'); - expect( + expectMethodCall( log, - [ - isMethodCall('getSavePath', arguments: { - 'initialDirectory': '/example/directory', - 'suggestedName': null, - 'confirmButtonText': null, - }), - ], + 'getSavePath', + arguments: { + 'initialDirectory': '/example/directory', + 'suggestedName': null, + 'confirmButtonText': null, + }, ); }); test('passes confirmButtonText correctly', () async { await plugin.getSavePath(confirmButtonText: 'Open File'); - expect( + expectMethodCall( log, - [ - isMethodCall('getSavePath', arguments: { - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': 'Open File', - }), - ], + 'getSavePath', + arguments: { + 'initialDirectory': null, + 'suggestedName': null, + 'confirmButtonText': 'Open File', + }, ); }); @@ -339,21 +333,20 @@ void main() { await plugin.openFile(acceptedTypeGroups: [group]); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': >[ - { - 'label': 'any', - 'extensions': ['*'], - }, - ], - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': false, - }), - ], + 'openFile', + arguments: { + 'acceptedTypeGroups': >[ + { + 'label': 'any', + 'extensions': ['*'], + }, + ], + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': false, + }, ); }); }); @@ -362,28 +355,83 @@ void main() { test('passes initialDirectory correctly', () async { await plugin.getDirectoryPath(initialDirectory: '/example/directory'); - expect( + expectMethodCall( + log, + 'getDirectoryPath', + arguments: { + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + }, + ); + }); + test('passes confirmButtonText correctly', () async { + await plugin.getDirectoryPath(confirmButtonText: 'Select Folder'); + + expectMethodCall( + log, + 'getDirectoryPath', + arguments: { + 'initialDirectory': null, + 'confirmButtonText': 'Select Folder', + }, + ); + }); + }); + + group('#getDirectoryPaths', () { + test('passes initialDirectory correctly', () async { + await plugin.getDirectoryPaths(initialDirectory: '/example/directory'); + + expectMethodCall( log, - [ - isMethodCall('getDirectoryPath', arguments: { - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - }), - ], + 'getDirectoryPath', + arguments: { + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + 'multiple': true, + }, ); }); test('passes confirmButtonText correctly', () async { - await plugin.getDirectoryPath(confirmButtonText: 'Open File'); + await plugin.getDirectoryPaths( + confirmButtonText: 'Select one or mode folders'); + + expectMethodCall( + log, + 'getDirectoryPath', + arguments: { + 'initialDirectory': null, + 'confirmButtonText': 'Select one or mode folders', + 'multiple': true, + }, + ); + }); + test('passes multiple flag correctly', () async { + await plugin.getDirectoryPaths(); - expect( + expectMethodCall( log, - [ - isMethodCall('getDirectoryPath', arguments: { - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - }), - ], + 'getDirectoryPath', + arguments: { + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': true, + }, ); }); }); } + +void expectMethodCall( + List log, + String methodName, { + Map? arguments, +}) { + expect(log, [isMethodCall(methodName, arguments: arguments)]); +} + +/// 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/file_selector/file_selector_macos/CHANGELOG.md b/packages/file_selector/file_selector_macos/CHANGELOG.md index af17db8ae3ef..4fdab0b73b5d 100644 --- a/packages/file_selector/file_selector_macos/CHANGELOG.md +++ b/packages/file_selector/file_selector_macos/CHANGELOG.md @@ -1,3 +1,12 @@ +## NEXT + +* Updates example code for `use_build_context_synchronously` lint. +* Updates minimum Flutter version to 3.0. + +## 0.9.0+4 + +* Converts platform channel to Pigeon. + ## 0.9.0+3 * Changes XTypeGroup initialization from final to const. diff --git a/packages/file_selector/file_selector_macos/example/lib/get_directory_page.dart b/packages/file_selector/file_selector_macos/example/lib/get_directory_page.dart index a2a209dc9529..a3f6f6ab8798 100644 --- a/packages/file_selector/file_selector_macos/example/lib/get_directory_page.dart +++ b/packages/file_selector/file_selector_macos/example/lib/get_directory_page.dart @@ -21,10 +21,12 @@ class GetDirectoryPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( - context: context, - builder: (BuildContext context) => TextDisplay(directoryPath), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(directoryPath), + ); + } } @override diff --git a/packages/file_selector/file_selector_macos/example/lib/open_image_page.dart b/packages/file_selector/file_selector_macos/example/lib/open_image_page.dart index b6ada56ebb2b..9252d25f113c 100644 --- a/packages/file_selector/file_selector_macos/example/lib/open_image_page.dart +++ b/packages/file_selector/file_selector_macos/example/lib/open_image_page.dart @@ -28,10 +28,12 @@ class OpenImagePage extends StatelessWidget { final String fileName = file.name; final String filePath = file.path; - await showDialog( - context: context, - builder: (BuildContext context) => ImageDisplay(fileName, filePath), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => ImageDisplay(fileName, filePath), + ); + } } @override diff --git a/packages/file_selector/file_selector_macos/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector_macos/example/lib/open_multiple_images_page.dart index c8e352a5b8bd..787717cdea13 100644 --- a/packages/file_selector/file_selector_macos/example/lib/open_multiple_images_page.dart +++ b/packages/file_selector/file_selector_macos/example/lib/open_multiple_images_page.dart @@ -32,10 +32,12 @@ class OpenMultipleImagesPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( - context: context, - builder: (BuildContext context) => MultipleImagesDisplay(files), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => MultipleImagesDisplay(files), + ); + } } @override diff --git a/packages/file_selector/file_selector_macos/example/lib/open_text_page.dart b/packages/file_selector/file_selector_macos/example/lib/open_text_page.dart index 4c88d7475049..97812f2b3505 100644 --- a/packages/file_selector/file_selector_macos/example/lib/open_text_page.dart +++ b/packages/file_selector/file_selector_macos/example/lib/open_text_page.dart @@ -25,10 +25,12 @@ class OpenTextPage extends StatelessWidget { final String fileName = file.name; final String fileContent = await file.readAsString(); - await showDialog( - context: context, - builder: (BuildContext context) => TextDisplay(fileName, fileContent), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(fileName, fileContent), + ); + } } @override diff --git a/packages/file_selector/file_selector_macos/example/lib/save_text_page.dart b/packages/file_selector/file_selector_macos/example/lib/save_text_page.dart index 3f215fea0a23..f80aeadbed09 100644 --- a/packages/file_selector/file_selector_macos/example/lib/save_text_page.dart +++ b/packages/file_selector/file_selector_macos/example/lib/save_text_page.dart @@ -42,7 +42,7 @@ class SaveTextPage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( + SizedBox( width: 300, child: TextField( minLines: 1, @@ -53,7 +53,7 @@ class SaveTextPage extends StatelessWidget { ), ), ), - Container( + SizedBox( width: 300, child: TextField( minLines: 1, diff --git a/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift b/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift index bffc3452c49d..2dbd016f66ef 100644 --- a/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift +++ b/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift @@ -47,9 +47,13 @@ class exampleTests: XCTestCase { panelController.openURLs = [URL(fileURLWithPath: returnPath)] let called = XCTestExpectation() - let call = FlutterMethodCall(methodName: "openFile", arguments: [:]) - plugin.handle(call) { result in - XCTAssertEqual((result as! [String]?)![0], returnPath) + let options = OpenPanelOptions( + allowsMultipleSelection: false, + canChooseDirectories: false, + canChooseFiles: true, + baseOptions: SavePanelOptions()) + plugin.displayOpenPanel(options: options) { paths in + XCTAssertEqual(paths[0], returnPath) called.fulfill() } @@ -72,16 +76,16 @@ class exampleTests: XCTestCase { panelController.openURLs = [URL(fileURLWithPath: returnPath)] let called = XCTestExpectation() - let call = FlutterMethodCall( - methodName: "openFile", - arguments: [ - "initialDirectory": "/some/dir", - "suggestedName": "a name", - "confirmButtonText": "Open it!", - ] - ) - plugin.handle(call) { result in - XCTAssertEqual((result as! [String]?)![0], returnPath) + let options = OpenPanelOptions( + allowsMultipleSelection: false, + canChooseDirectories: false, + canChooseFiles: true, + baseOptions: SavePanelOptions( + directoryPath: "/some/dir", + nameFieldStringValue: "a name", + prompt: "Open it!")) + plugin.displayOpenPanel(options: options) { paths in + XCTAssertEqual(paths[0], returnPath) called.fulfill() } @@ -104,12 +108,12 @@ class exampleTests: XCTestCase { panelController.openURLs = returnPaths.map({ path in URL(fileURLWithPath: path) }) let called = XCTestExpectation() - let call = FlutterMethodCall( - methodName: "openFile", - arguments: ["multiple": true] - ) - plugin.handle(call) { result in - let paths = (result as! [String]?)! + let options = OpenPanelOptions( + allowsMultipleSelection: true, + canChooseDirectories: false, + canChooseFiles: true, + baseOptions: SavePanelOptions()) + plugin.displayOpenPanel(options: options) { paths in XCTAssertEqual(paths.count, returnPaths.count) XCTAssertEqual(paths[0], returnPaths[0]) XCTAssertEqual(paths[1], returnPaths[1]) @@ -130,17 +134,17 @@ class exampleTests: XCTestCase { panelController.openURLs = [URL(fileURLWithPath: returnPath)] let called = XCTestExpectation() - let call = FlutterMethodCall( - methodName: "openFile", - arguments: [ - "acceptedTypes": [ - "extensions": ["txt", "json"], - "UTIs": ["public.text", "public.image"], - ] - ] - ) - plugin.handle(call) { result in - XCTAssertEqual((result as! [String]?)![0], returnPath) + let options = OpenPanelOptions( + allowsMultipleSelection: true, + canChooseDirectories: false, + canChooseFiles: true, + baseOptions: SavePanelOptions( + allowedFileTypes: AllowedTypes( + extensions: ["txt", "json"], + mimeTypes: [], + utis: ["public.text", "public.image"]))) + plugin.displayOpenPanel(options: options) { paths in + XCTAssertEqual(paths[0], returnPath) called.fulfill() } @@ -158,9 +162,13 @@ class exampleTests: XCTestCase { panelController: panelController) let called = XCTestExpectation() - let call = FlutterMethodCall(methodName: "openFile", arguments: [:]) - plugin.handle(call) { result in - XCTAssertNil(result) + let options = OpenPanelOptions( + allowsMultipleSelection: false, + canChooseDirectories: false, + canChooseFiles: true, + baseOptions: SavePanelOptions()) + plugin.displayOpenPanel(options: options) { paths in + XCTAssertEqual(paths.count, 0) called.fulfill() } @@ -178,9 +186,9 @@ class exampleTests: XCTestCase { panelController.saveURL = URL(fileURLWithPath: returnPath) let called = XCTestExpectation() - let call = FlutterMethodCall(methodName: "getSavePath", arguments: [:]) - plugin.handle(call) { result in - XCTAssertEqual(result as! String?, returnPath) + let options = SavePanelOptions() + plugin.displaySavePanel(options: options) { path in + XCTAssertEqual(path, returnPath) called.fulfill() } @@ -198,15 +206,11 @@ class exampleTests: XCTestCase { panelController.saveURL = URL(fileURLWithPath: returnPath) let called = XCTestExpectation() - let call = FlutterMethodCall( - methodName: "getSavePath", - arguments: [ - "initialDirectory": "/some/dir", - "confirmButtonText": "Save it!", - ] - ) - plugin.handle(call) { result in - XCTAssertEqual(result as! String?, returnPath) + let options = SavePanelOptions( + directoryPath: "/some/dir", + prompt: "Save it!") + plugin.displaySavePanel(options: options) { path in + XCTAssertEqual(path, returnPath) called.fulfill() } @@ -225,9 +229,9 @@ class exampleTests: XCTestCase { panelController: panelController) let called = XCTestExpectation() - let call = FlutterMethodCall(methodName: "getSavePath", arguments: [:]) - plugin.handle(call) { result in - XCTAssertNil(result) + let options = SavePanelOptions() + plugin.displaySavePanel(options: options) { path in + XCTAssertNil(path) called.fulfill() } @@ -245,9 +249,13 @@ class exampleTests: XCTestCase { panelController.openURLs = [URL(fileURLWithPath: returnPath)] let called = XCTestExpectation() - let call = FlutterMethodCall(methodName: "getDirectoryPath", arguments: [:]) - plugin.handle(call) { result in - XCTAssertEqual(result as! String?, returnPath) + let options = OpenPanelOptions( + allowsMultipleSelection: false, + canChooseDirectories: true, + canChooseFiles: false, + baseOptions: SavePanelOptions()) + plugin.displayOpenPanel(options: options) { paths in + XCTAssertEqual(paths[0], returnPath) called.fulfill() } @@ -270,9 +278,13 @@ class exampleTests: XCTestCase { panelController: panelController) let called = XCTestExpectation() - let call = FlutterMethodCall(methodName: "getDirectoryPath", arguments: [:]) - plugin.handle(call) { result in - XCTAssertNil(result) + let options = OpenPanelOptions( + allowsMultipleSelection: false, + canChooseDirectories: true, + canChooseFiles: false, + baseOptions: SavePanelOptions()) + plugin.displayOpenPanel(options: options) { paths in + XCTAssertEqual(paths.count, 0) called.fulfill() } diff --git a/packages/file_selector/file_selector_macos/example/pubspec.yaml b/packages/file_selector/file_selector_macos/example/pubspec.yaml index d3f3114bb481..a2122b2858b7 100644 --- a/packages/file_selector/file_selector_macos/example/pubspec.yaml +++ b/packages/file_selector/file_selector_macos/example/pubspec.yaml @@ -5,7 +5,7 @@ version: 1.0.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: file_selector_macos: diff --git a/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart b/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart index 74ce2835d18c..f8a087fa6877 100644 --- a/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart +++ b/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart @@ -3,17 +3,12 @@ // found in the LICENSE file. import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; -import 'package:flutter/foundation.dart' show visibleForTesting; -import 'package:flutter/services.dart'; -const MethodChannel _channel = - MethodChannel('plugins.flutter.io/file_selector_macos'); +import 'src/messages.g.dart'; /// An implementation of [FileSelectorPlatform] for macOS. class FileSelectorMacOS extends FileSelectorPlatform { - /// The MethodChannel that is being used by this implementation of the plugin. - @visibleForTesting - MethodChannel get channel => _channel; + final FileSelectorApi _hostApi = FileSelectorApi(); /// Registers the macOS implementation. static void registerWith() { @@ -26,16 +21,17 @@ class FileSelectorMacOS extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - final List? path = await _channel.invokeListMethod( - 'openFile', - { - 'acceptedTypes': _allowedTypeListFromTypeGroups(acceptedTypeGroups), - 'initialDirectory': initialDirectory, - 'confirmButtonText': confirmButtonText, - 'multiple': false, - }, - ); - return path == null ? null : XFile(path.first); + final List paths = + await _hostApi.displayOpenPanel(OpenPanelOptions( + allowsMultipleSelection: false, + canChooseDirectories: false, + canChooseFiles: true, + baseOptions: SavePanelOptions( + allowedFileTypes: _allowedTypesFromTypeGroups(acceptedTypeGroups), + directoryPath: initialDirectory, + prompt: confirmButtonText, + ))); + return paths.isEmpty ? null : XFile(paths.first!); } @override @@ -44,16 +40,17 @@ class FileSelectorMacOS extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - final List? pathList = await _channel.invokeListMethod( - 'openFile', - { - 'acceptedTypes': _allowedTypeListFromTypeGroups(acceptedTypeGroups), - 'initialDirectory': initialDirectory, - 'confirmButtonText': confirmButtonText, - 'multiple': true, - }, - ); - return pathList?.map((String path) => XFile(path)).toList() ?? []; + final List paths = + await _hostApi.displayOpenPanel(OpenPanelOptions( + allowsMultipleSelection: true, + canChooseDirectories: false, + canChooseFiles: true, + baseOptions: SavePanelOptions( + allowedFileTypes: _allowedTypesFromTypeGroups(acceptedTypeGroups), + directoryPath: initialDirectory, + prompt: confirmButtonText, + ))); + return paths.map((String? path) => XFile(path!)).toList(); } @override @@ -63,15 +60,12 @@ class FileSelectorMacOS extends FileSelectorPlatform { String? suggestedName, String? confirmButtonText, }) async { - return _channel.invokeMethod( - 'getSavePath', - { - 'acceptedTypes': _allowedTypeListFromTypeGroups(acceptedTypeGroups), - 'initialDirectory': initialDirectory, - 'suggestedName': suggestedName, - 'confirmButtonText': confirmButtonText, - }, - ); + return _hostApi.displaySavePanel(SavePanelOptions( + allowedFileTypes: _allowedTypesFromTypeGroups(acceptedTypeGroups), + directoryPath: initialDirectory, + nameFieldStringValue: suggestedName, + prompt: confirmButtonText, + )); } @override @@ -79,30 +73,29 @@ class FileSelectorMacOS extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - return _channel.invokeMethod( - 'getDirectoryPath', - { - 'initialDirectory': initialDirectory, - 'confirmButtonText': confirmButtonText, - }, - ); + final List paths = + await _hostApi.displayOpenPanel(OpenPanelOptions( + allowsMultipleSelection: false, + canChooseDirectories: true, + canChooseFiles: false, + baseOptions: SavePanelOptions( + directoryPath: initialDirectory, + prompt: confirmButtonText, + ))); + return paths.isEmpty ? null : paths.first; } // Converts the type group list into a flat list of all allowed types, since // macOS doesn't support filter groups. - Map>? _allowedTypeListFromTypeGroups( - List? typeGroups) { - const String extensionKey = 'extensions'; - const String mimeTypeKey = 'mimeTypes'; - const String utiKey = 'UTIs'; + AllowedTypes? _allowedTypesFromTypeGroups(List? typeGroups) { if (typeGroups == null || typeGroups.isEmpty) { return null; } - final Map> allowedTypes = >{ - extensionKey: [], - mimeTypeKey: [], - utiKey: [], - }; + final AllowedTypes allowedTypes = AllowedTypes( + extensions: [], + mimeTypes: [], + utis: [], + ); for (final XTypeGroup typeGroup in typeGroups) { // If any group allows everything, no filtering should be done. if (typeGroup.allowsAny) { @@ -119,9 +112,9 @@ class FileSelectorMacOS extends FileSelectorPlatform { '"mimeTypes" must be non-empty for macOS if anything is ' 'non-empty.'); } - allowedTypes[extensionKey]!.addAll(typeGroup.extensions ?? []); - allowedTypes[mimeTypeKey]!.addAll(typeGroup.mimeTypes ?? []); - allowedTypes[utiKey]!.addAll(typeGroup.macUTIs ?? []); + allowedTypes.extensions.addAll(typeGroup.extensions ?? []); + allowedTypes.mimeTypes.addAll(typeGroup.mimeTypes ?? []); + allowedTypes.utis.addAll(typeGroup.macUTIs ?? []); } return allowedTypes; diff --git a/packages/file_selector/file_selector_macos/lib/src/messages.g.dart b/packages/file_selector/file_selector_macos/lib/src/messages.g.dart new file mode 100644 index 000000000000..5f1daf94283e --- /dev/null +++ b/packages/file_selector/file_selector_macos/lib/src/messages.g.dart @@ -0,0 +1,227 @@ +// 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 (v4.2.14), 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 +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +/// A Pigeon representation of the macOS portion of an `XTypeGroup`. +class AllowedTypes { + AllowedTypes({ + required this.extensions, + required this.mimeTypes, + required this.utis, + }); + + List extensions; + + List mimeTypes; + + List utis; + + Object encode() { + return [ + extensions, + mimeTypes, + utis, + ]; + } + + static AllowedTypes decode(Object result) { + result as List; + return AllowedTypes( + extensions: (result[0] as List?)!.cast(), + mimeTypes: (result[1] as List?)!.cast(), + utis: (result[2] as List?)!.cast(), + ); + } +} + +/// Options for save panels. +/// +/// These correspond to NSSavePanel properties (which are, by extension +/// NSOpenPanel properties as well). +class SavePanelOptions { + SavePanelOptions({ + this.allowedFileTypes, + this.directoryPath, + this.nameFieldStringValue, + this.prompt, + }); + + AllowedTypes? allowedFileTypes; + + String? directoryPath; + + String? nameFieldStringValue; + + String? prompt; + + Object encode() { + return [ + allowedFileTypes?.encode(), + directoryPath, + nameFieldStringValue, + prompt, + ]; + } + + static SavePanelOptions decode(Object result) { + result as List; + return SavePanelOptions( + allowedFileTypes: result[0] != null + ? AllowedTypes.decode(result[0]! as List) + : null, + directoryPath: result[1] as String?, + nameFieldStringValue: result[2] as String?, + prompt: result[3] as String?, + ); + } +} + +/// Options for open panels. +/// +/// These correspond to NSOpenPanel properties. +class OpenPanelOptions { + OpenPanelOptions({ + required this.allowsMultipleSelection, + required this.canChooseDirectories, + required this.canChooseFiles, + required this.baseOptions, + }); + + bool allowsMultipleSelection; + + bool canChooseDirectories; + + bool canChooseFiles; + + SavePanelOptions baseOptions; + + Object encode() { + return [ + allowsMultipleSelection, + canChooseDirectories, + canChooseFiles, + baseOptions.encode(), + ]; + } + + static OpenPanelOptions decode(Object result) { + result as List; + return OpenPanelOptions( + allowsMultipleSelection: result[0]! as bool, + canChooseDirectories: result[1]! as bool, + canChooseFiles: result[2]! as bool, + baseOptions: SavePanelOptions.decode(result[3]! as List), + ); + } +} + +class _FileSelectorApiCodec extends StandardMessageCodec { + const _FileSelectorApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is AllowedTypes) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is OpenPanelOptions) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is SavePanelOptions) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return AllowedTypes.decode(readValue(buffer)!); + + case 129: + return OpenPanelOptions.decode(readValue(buffer)!); + + case 130: + return SavePanelOptions.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class FileSelectorApi { + /// Constructor for [FileSelectorApi]. 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. + FileSelectorApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _FileSelectorApiCodec(); + + /// Shows an open panel with the given [options], returning the list of + /// selected paths. + /// + /// An empty list corresponds to a cancelled selection. + Future> displayOpenPanel(OpenPanelOptions arg_options) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FileSelectorApi.displayOpenPanel', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_options]) 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(); + } + } + + /// Shows a save panel with the given [options], returning the selected path. + /// + /// A null return corresponds to a cancelled save. + Future displaySavePanel(SavePanelOptions arg_options) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FileSelectorApi.displaySavePanel', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_options]) 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 (replyList[0] as String?); + } + } +} diff --git a/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift b/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift index 9551671d1575..4e1c935dad73 100644 --- a/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift +++ b/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift @@ -40,7 +40,7 @@ protocol ViewProvider { var view: NSView? { get } } -public class FileSelectorPlugin: NSObject, FlutterPlugin { +public class FileSelectorPlugin: NSObject, FlutterPlugin, FileSelectorApi { private let viewProvider: ViewProvider private let panelController: PanelController @@ -49,13 +49,10 @@ public class FileSelectorPlugin: NSObject, FlutterPlugin { private let saveMethod = "getSavePath" public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel( - name: "plugins.flutter.io/file_selector_macos", - binaryMessenger: registrar.messenger) let instance = FileSelectorPlugin( viewProvider: DefaultViewProvider(registrar: registrar), panelController: DefaultPanelController()) - registrar.addMethodCallDelegate(instance, channel: channel) + FileSelectorApiSetup.setUp(binaryMessenger: registrar.messenger, api: instance) } init(viewProvider: ViewProvider, panelController: PanelController) { @@ -63,30 +60,20 @@ public class FileSelectorPlugin: NSObject, FlutterPlugin { self.panelController = panelController } - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - let arguments = (call.arguments ?? [:]) as! [String: Any] - switch call.method { - case openMethod, - openDirectoryMethod: - let choosingDirectory = call.method == openDirectoryMethod - let panel = NSOpenPanel() - configure(panel: panel, with: arguments) - configure(openPanel: panel, with: arguments, choosingDirectory: choosingDirectory) - panelController.display(panel, for: viewProvider.view?.window) { (selection: [URL]?) in - if (choosingDirectory) { - result(selection?.first?.path) - } else { - result(selection?.map({ item in item.path })) - } - } - case saveMethod: - let panel = NSSavePanel() - configure(panel: panel, with: arguments) - panelController.display(panel, for: viewProvider.view?.window) { (selection: URL?) in - result(selection?.path) - } - default: - result(FlutterMethodNotImplemented) + func displayOpenPanel(options: OpenPanelOptions, completion: @escaping ([String?]) -> Void) { + + let panel = NSOpenPanel() + configure(openPanel: panel, with: options) + panelController.display(panel, for: viewProvider.view?.window) { (selection: [URL]?) in + completion(selection?.map({ item in item.path }) ?? []) + } + } + + func displaySavePanel(options: SavePanelOptions, completion: @escaping (String?) -> Void) { + let panel = NSSavePanel() + configure(panel: panel, with: options) + panelController.display(panel, for: viewProvider.view?.window) { (selection: URL?) in + completion(selection?.path) } } @@ -94,28 +81,25 @@ public class FileSelectorPlugin: NSObject, FlutterPlugin { /// - Parameters: /// - panel: The panel to configure. /// - arguments: The arguments dictionary from a FlutterMethodCall to this plugin. - private func configure(panel: NSSavePanel, with arguments: [String: Any]) { - if let initialDirectory = getNonNullStringValue(for: "initialDirectory", from: arguments) { - panel.directoryURL = URL(fileURLWithPath: initialDirectory) + private func configure(panel: NSSavePanel, with options: SavePanelOptions) { + if let directoryPath = options.directoryPath { + panel.directoryURL = URL(fileURLWithPath: directoryPath) } - if let suggestedName = getNonNullStringValue(for: "suggestedName", from: arguments) { + if let suggestedName = options.nameFieldStringValue { panel.nameFieldStringValue = suggestedName } - if let confirmButtonText = getNonNullStringValue(for: "confirmButtonText", from: arguments) { - panel.prompt = confirmButtonText + if let prompt = options.prompt { + panel.prompt = prompt } - let acceptedTypes = getNonNullValue( - for: "acceptedTypes", - from: arguments - ) as! [String: Any]? - if let acceptedTypes = acceptedTypes { + if let acceptedTypes = options.allowedFileTypes { var allowedTypes: [String] = [] - let extensions = getNonNullStringArrayValue(for: "extensions", from: acceptedTypes) - let UTIs = getNonNullStringArrayValue(for: "UTIs", from: acceptedTypes) - allowedTypes.append(contentsOf: extensions) - allowedTypes.append(contentsOf: UTIs) - // TODO: Add support for mimeTypes in macOS 11+. + // The array values are non-null by convention even though Pigeon can't currently express + // that via the types; see messages.dart. + allowedTypes.append(contentsOf: acceptedTypes.extensions.map({ $0! })) + allowedTypes.append(contentsOf: acceptedTypes.utis.map({ $0! })) + // TODO: Add support for mimeTypes in macOS 11+. See + // https://github.com/flutter/flutter/issues/117843 if !allowedTypes.isEmpty { panel.allowedFileTypes = allowedTypes @@ -130,13 +114,12 @@ public class FileSelectorPlugin: NSObject, FlutterPlugin { /// - choosingDirectory: True if the panel should allow choosing directories rather than files. private func configure( openPanel panel: NSOpenPanel, - with arguments: [String: Any], - choosingDirectory: Bool + with options: OpenPanelOptions ) { - panel.allowsMultipleSelection = - getNonNullValue(for: "multiple", from: arguments) as! Bool? ?? false - panel.canChooseDirectories = choosingDirectory; - panel.canChooseFiles = !choosingDirectory; + configure(panel: panel, with: options.baseOptions) + panel.allowsMultipleSelection = options.allowsMultipleSelection + panel.canChooseDirectories = options.canChooseDirectories; + panel.canChooseFiles = options.canChooseFiles; } } @@ -188,31 +171,3 @@ private class DefaultViewProvider: ViewProvider { } } } - -/// Returns the value for the given key from the provided dictionary, unless the value is NSNull -/// in which case it returns nil. -/// - Parameters: -/// - key: The key to get a value for. -/// - dictionary: The dictionary to get the value from. -/// - Returns: The value, or nil for NSNull. -private func getNonNullValue(for key: String, from dictionary: [String: Any]) -> Any? { - let value = dictionary[key]; - return value is NSNull ? nil : value; -} - -/// A convenience wrapper for getNonNullValue for string values. -private func getNonNullStringValue(for key: String, from dictionary: [String: Any]) -> String? { - return getNonNullValue(for: key, from: dictionary) as! String? -} - -/// A convenience wrapper for getNonNullValue for array-of-string values. -/// - Parameters: -/// - key: The key to get a value for. -/// - dictionary: The dictionary to get the value from. -/// - Returns: The value, or an empty array for nil for NSNull. -private func getNonNullStringArrayValue( - for key: String, - from dictionary: [String: Any] -) -> [String] { - return getNonNullValue(for: key, from: dictionary) as! [String]? ?? [] -} diff --git a/packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift b/packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift new file mode 100644 index 000000000000..75753d962525 --- /dev/null +++ b/packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift @@ -0,0 +1,228 @@ +// 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 (v4.2.14), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation +#if os(iOS) +import Flutter +#elseif os(macOS) +import FlutterMacOS +#else +#error("Unsupported platform.") +#endif + + +/// Generated class from Pigeon. + +/// A Pigeon representation of the macOS portion of an `XTypeGroup`. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct AllowedTypes { + var extensions: [String?] + var mimeTypes: [String?] + var utis: [String?] + + static func fromList(_ list: [Any?]) -> AllowedTypes? { + let extensions = list[0] as! [String?] + let mimeTypes = list[1] as! [String?] + let utis = list[2] as! [String?] + + return AllowedTypes( + extensions: extensions, + mimeTypes: mimeTypes, + utis: utis + ) + } + func toList() -> [Any?] { + return [ + extensions, + mimeTypes, + utis, + ] + } +} + +/// Options for save panels. +/// +/// These correspond to NSSavePanel properties (which are, by extension +/// NSOpenPanel properties as well). +/// +/// Generated class from Pigeon that represents data sent in messages. +struct SavePanelOptions { + var allowedFileTypes: AllowedTypes? = nil + var directoryPath: String? = nil + var nameFieldStringValue: String? = nil + var prompt: String? = nil + + static func fromList(_ list: [Any?]) -> SavePanelOptions? { + var allowedFileTypes: AllowedTypes? = nil + if let allowedFileTypesList = list[0] as? [Any?] { + allowedFileTypes = AllowedTypes.fromList(allowedFileTypesList) + } + let directoryPath = list[1] as? String + let nameFieldStringValue = list[2] as? String + let prompt = list[3] as? String + + return SavePanelOptions( + allowedFileTypes: allowedFileTypes, + directoryPath: directoryPath, + nameFieldStringValue: nameFieldStringValue, + prompt: prompt + ) + } + func toList() -> [Any?] { + return [ + allowedFileTypes?.toList(), + directoryPath, + nameFieldStringValue, + prompt, + ] + } +} + +/// Options for open panels. +/// +/// These correspond to NSOpenPanel properties. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct OpenPanelOptions { + var allowsMultipleSelection: Bool + var canChooseDirectories: Bool + var canChooseFiles: Bool + var baseOptions: SavePanelOptions + + static func fromList(_ list: [Any?]) -> OpenPanelOptions? { + let allowsMultipleSelection = list[0] as! Bool + let canChooseDirectories = list[1] as! Bool + let canChooseFiles = list[2] as! Bool + let baseOptions = SavePanelOptions.fromList(list[3] as! [Any?])! + + return OpenPanelOptions( + allowsMultipleSelection: allowsMultipleSelection, + canChooseDirectories: canChooseDirectories, + canChooseFiles: canChooseFiles, + baseOptions: baseOptions + ) + } + func toList() -> [Any?] { + return [ + allowsMultipleSelection, + canChooseDirectories, + canChooseFiles, + baseOptions.toList(), + ] + } +} + +private class FileSelectorApiCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 128: + return AllowedTypes.fromList(self.readValue() as! [Any]) + case 129: + return OpenPanelOptions.fromList(self.readValue() as! [Any]) + case 130: + return SavePanelOptions.fromList(self.readValue() as! [Any]) + default: + return super.readValue(ofType: type) + + } + } +} +private class FileSelectorApiCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? AllowedTypes { + super.writeByte(128) + super.writeValue(value.toList()) + } else if let value = value as? OpenPanelOptions { + super.writeByte(129) + super.writeValue(value.toList()) + } else if let value = value as? SavePanelOptions { + super.writeByte(130) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class FileSelectorApiCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return FileSelectorApiCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return FileSelectorApiCodecWriter(data: data) + } +} + +class FileSelectorApiCodec: FlutterStandardMessageCodec { + static let shared = FileSelectorApiCodec(readerWriter: FileSelectorApiCodecReaderWriter()) +} + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol FileSelectorApi { + /// Shows an open panel with the given [options], returning the list of + /// selected paths. + /// + /// An empty list corresponds to a cancelled selection. + func displayOpenPanel(options: OpenPanelOptions, completion: @escaping ([String?]) -> Void) + /// Shows a save panel with the given [options], returning the selected path. + /// + /// A null return corresponds to a cancelled save. + func displaySavePanel(options: SavePanelOptions, completion: @escaping (String?) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class FileSelectorApiSetup { + /// The codec used by FileSelectorApi. + static var codec: FlutterStandardMessageCodec { FileSelectorApiCodec.shared } + /// Sets up an instance of `FileSelectorApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: FileSelectorApi?) { + /// Shows an open panel with the given [options], returning the list of + /// selected paths. + /// + /// An empty list corresponds to a cancelled selection. + let displayOpenPanelChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.FileSelectorApi.displayOpenPanel", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + displayOpenPanelChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let optionsArg = args[0] as! OpenPanelOptions + api.displayOpenPanel(options: optionsArg) { result in + reply(wrapResult(result)) + } + } + } else { + displayOpenPanelChannel.setMessageHandler(nil) + } + /// Shows a save panel with the given [options], returning the selected path. + /// + /// A null return corresponds to a cancelled save. + let displaySavePanelChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.FileSelectorApi.displaySavePanel", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + displaySavePanelChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let optionsArg = args[0] as! SavePanelOptions + api.displaySavePanel(options: optionsArg) { result in + reply(wrapResult(result)) + } + } + } else { + displaySavePanelChannel.setMessageHandler(nil) + } + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: FlutterError) -> [Any?] { + return [ + error.code, + error.message, + error.details + ] +} diff --git a/packages/path_provider/path_provider_ios/pigeons/copyright.txt b/packages/file_selector/file_selector_macos/pigeons/copyright.txt similarity index 100% rename from packages/path_provider/path_provider_ios/pigeons/copyright.txt rename to packages/file_selector/file_selector_macos/pigeons/copyright.txt diff --git a/packages/file_selector/file_selector_macos/pigeons/messages.dart b/packages/file_selector/file_selector_macos/pigeons/messages.dart new file mode 100644 index 000000000000..85b2996baf8a --- /dev/null +++ b/packages/file_selector/file_selector_macos/pigeons/messages.dart @@ -0,0 +1,84 @@ +// 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:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + input: 'pigeons/messages.dart', + swiftOut: 'macos/Classes/messages.g.swift', + dartOut: 'lib/src/messages.g.dart', + dartTestOut: 'test/messages_test.g.dart', + copyrightHeader: 'pigeons/copyright.txt', +)) + +/// A Pigeon representation of the macOS portion of an `XTypeGroup`. +class AllowedTypes { + const AllowedTypes({ + this.extensions = const [], + this.mimeTypes = const [], + this.utis = const [], + }); + + // TODO(stuartmorgan): Declare these as non-nullable generics once + // https://github.com/flutter/flutter/issues/97848 is fixed. In practice, + // the values will never be null, and the native implementation assumes that. + final List extensions; + final List mimeTypes; + final List utis; +} + +/// Options for save panels. +/// +/// These correspond to NSSavePanel properties (which are, by extension +/// NSOpenPanel properties as well). +class SavePanelOptions { + const SavePanelOptions({ + this.allowedFileTypes, + this.directoryPath, + this.nameFieldStringValue, + this.prompt, + }); + final AllowedTypes? allowedFileTypes; + final String? directoryPath; + final String? nameFieldStringValue; + final String? prompt; +} + +/// Options for open panels. +/// +/// These correspond to NSOpenPanel properties. +class OpenPanelOptions extends SavePanelOptions { + const OpenPanelOptions({ + this.allowsMultipleSelection = false, + this.canChooseDirectories = false, + this.canChooseFiles = true, + this.baseOptions = const SavePanelOptions(), + }); + final bool allowsMultipleSelection; + final bool canChooseDirectories; + final bool canChooseFiles; + // NSOpenPanel inherits from NSSavePanel, so shares all of its options. + // Ideally this would be done with inheritance rather than composition, but + // Pigeon doesn't currently support data class inheritance: + // https://github.com/flutter/flutter/issues/117819. + final SavePanelOptions baseOptions; +} + +@HostApi(dartHostTestHandler: 'TestFileSelectorApi') +abstract class FileSelectorApi { + /// Shows an open panel with the given [options], returning the list of + /// selected paths. + /// + /// An empty list corresponds to a cancelled selection. + // TODO(stuartmorgan): Declare this return as a non-nullable generic once + // https://github.com/flutter/flutter/issues/97848 is fixed. In practice, + // the values will never be null, and the calling code assumes that. + @async + List displayOpenPanel(OpenPanelOptions options); + + /// Shows a save panel with the given [options], returning the selected path. + /// + /// A null return corresponds to a cancelled save. + @async + String? displaySavePanel(SavePanelOptions options); +} diff --git a/packages/file_selector/file_selector_macos/pubspec.yaml b/packages/file_selector/file_selector_macos/pubspec.yaml index 3fc3832d7280..3654beaca4c0 100644 --- a/packages/file_selector/file_selector_macos/pubspec.yaml +++ b/packages/file_selector/file_selector_macos/pubspec.yaml @@ -2,11 +2,11 @@ name: file_selector_macos description: macOS implementation of the file_selector plugin. repository: https://github.com/flutter/plugins/tree/main/packages/file_selector/file_selector_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.9.0+3 +version: 0.9.0+4 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -23,5 +23,8 @@ dependencies: sdk: flutter dev_dependencies: + build_runner: ^2.3.2 flutter_test: sdk: flutter + mockito: ^5.3.2 + pigeon: ^4.2.14 diff --git a/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart b/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart index 789d70a51777..181409e6f1b4 100644 --- a/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart +++ b/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart @@ -3,24 +3,32 @@ // found in the LICENSE file. import 'package:file_selector_macos/file_selector_macos.dart'; +import 'package:file_selector_macos/src/messages.g.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'file_selector_macos_test.mocks.dart'; +import 'messages_test.g.dart'; + +@GenerateMocks([TestFileSelectorApi]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final FileSelectorMacOS plugin = FileSelectorMacOS(); - - final List log = []; + late FileSelectorMacOS plugin; + late MockTestFileSelectorApi mockApi; setUp(() { - plugin.channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - return null; - }); - - log.clear(); + plugin = FileSelectorMacOS(); + mockApi = MockTestFileSelectorApi(); + TestFileSelectorApi.setup(mockApi); + + // Set default stubs for tests that don't expect a specific return value, + // so calls don't throw. Tests that `expect` return values should override + // these locally. + when(mockApi.displayOpenPanel(any)).thenAnswer((_) async => []); + when(mockApi.displaySavePanel(any)).thenAnswer((_) async => null); }); test('registered instance', () { @@ -29,6 +37,33 @@ void main() { }); group('openFile', () { + test('works as expected with no arguments', () async { + when(mockApi.displayOpenPanel(any)) + .thenAnswer((_) async => ['foo']); + + final XFile? file = await plugin.openFile(); + + expect(file!.path, 'foo'); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.allowsMultipleSelection, false); + expect(options.canChooseFiles, true); + expect(options.canChooseDirectories, false); + expect(options.baseOptions.allowedFileTypes, null); + expect(options.baseOptions.directoryPath, null); + expect(options.baseOptions.nameFieldStringValue, null); + expect(options.baseOptions.prompt, null); + }); + + test('handles cancel', () async { + when(mockApi.displayOpenPanel(any)).thenAnswer((_) async => []); + + final XFile? file = await plugin.openFile(); + + expect(file, null); + }); + test('passes the accepted type groups correctly', () async { const XTypeGroup group = XTypeGroup( label: 'text', @@ -46,53 +81,33 @@ void main() { await plugin.openFile(acceptedTypeGroups: [group, groupTwo]); - expect( - log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypes': { - 'extensions': ['txt', 'jpg'], - 'mimeTypes': ['text/plain', 'image/jpg'], - 'UTIs': ['public.text', 'public.image'], - }, - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': false, - }), - ], - ); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.baseOptions.allowedFileTypes!.extensions, + ['txt', 'jpg']); + expect(options.baseOptions.allowedFileTypes!.mimeTypes, + ['text/plain', 'image/jpg']); + expect(options.baseOptions.allowedFileTypes!.utis, + ['public.text', 'public.image']); }); test('passes initialDirectory correctly', () async { await plugin.openFile(initialDirectory: '/example/directory'); - expect( - log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypes': null, - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - 'multiple': false, - }), - ], - ); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.baseOptions.directoryPath, '/example/directory'); }); test('passes confirmButtonText correctly', () async { await plugin.openFile(confirmButtonText: 'Open File'); - expect( - log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypes': null, - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - 'multiple': false, - }), - ], - ); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.baseOptions.prompt, 'Open File'); }); test('throws for a type group that does not support macOS', () async { @@ -117,6 +132,34 @@ void main() { }); group('openFiles', () { + test('works as expected with no arguments', () async { + when(mockApi.displayOpenPanel(any)) + .thenAnswer((_) async => ['foo', 'bar']); + + final List files = await plugin.openFiles(); + + expect(files[0].path, 'foo'); + expect(files[1].path, 'bar'); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.allowsMultipleSelection, true); + expect(options.canChooseFiles, true); + expect(options.canChooseDirectories, false); + expect(options.baseOptions.allowedFileTypes, null); + expect(options.baseOptions.directoryPath, null); + expect(options.baseOptions.nameFieldStringValue, null); + expect(options.baseOptions.prompt, null); + }); + + test('handles cancel', () async { + when(mockApi.displayOpenPanel(any)).thenAnswer((_) async => []); + + final List files = await plugin.openFiles(); + + expect(files, isEmpty); + }); + test('passes the accepted type groups correctly', () async { const XTypeGroup group = XTypeGroup( label: 'text', @@ -134,53 +177,33 @@ void main() { await plugin.openFiles(acceptedTypeGroups: [group, groupTwo]); - expect( - log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypes': >{ - 'extensions': ['txt', 'jpg'], - 'mimeTypes': ['text/plain', 'image/jpg'], - 'UTIs': ['public.text', 'public.image'], - }, - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': true, - }), - ], - ); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.baseOptions.allowedFileTypes!.extensions, + ['txt', 'jpg']); + expect(options.baseOptions.allowedFileTypes!.mimeTypes, + ['text/plain', 'image/jpg']); + expect(options.baseOptions.allowedFileTypes!.utis, + ['public.text', 'public.image']); }); test('passes initialDirectory correctly', () async { await plugin.openFiles(initialDirectory: '/example/directory'); - expect( - log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypes': null, - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - 'multiple': true, - }), - ], - ); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.baseOptions.directoryPath, '/example/directory'); }); test('passes confirmButtonText correctly', () async { await plugin.openFiles(confirmButtonText: 'Open File'); - expect( - log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypes': null, - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - 'multiple': true, - }), - ], - ); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.baseOptions.prompt, 'Open File'); }); test('throws for a type group that does not support macOS', () async { @@ -205,6 +228,29 @@ void main() { }); group('getSavePath', () { + test('works as expected with no arguments', () async { + when(mockApi.displaySavePanel(any)).thenAnswer((_) async => 'foo'); + + final String? path = await plugin.getSavePath(); + + expect(path, 'foo'); + final VerificationResult result = + verify(mockApi.displaySavePanel(captureAny)); + final SavePanelOptions options = result.captured[0] as SavePanelOptions; + expect(options.allowedFileTypes, null); + expect(options.directoryPath, null); + expect(options.nameFieldStringValue, null); + expect(options.prompt, null); + }); + + test('handles cancel', () async { + when(mockApi.displaySavePanel(any)).thenAnswer((_) async => null); + + final String? path = await plugin.getSavePath(); + + expect(path, null); + }); + test('passes the accepted type groups correctly', () async { const XTypeGroup group = XTypeGroup( label: 'text', @@ -223,53 +269,32 @@ void main() { await plugin .getSavePath(acceptedTypeGroups: [group, groupTwo]); - expect( - log, - [ - isMethodCall('getSavePath', arguments: { - 'acceptedTypes': >{ - 'extensions': ['txt', 'jpg'], - 'mimeTypes': ['text/plain', 'image/jpg'], - 'UTIs': ['public.text', 'public.image'], - }, - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': null, - }), - ], - ); + final VerificationResult result = + verify(mockApi.displaySavePanel(captureAny)); + final SavePanelOptions options = result.captured[0] as SavePanelOptions; + expect(options.allowedFileTypes!.extensions, ['txt', 'jpg']); + expect(options.allowedFileTypes!.mimeTypes, + ['text/plain', 'image/jpg']); + expect(options.allowedFileTypes!.utis, + ['public.text', 'public.image']); }); test('passes initialDirectory correctly', () async { await plugin.getSavePath(initialDirectory: '/example/directory'); - expect( - log, - [ - isMethodCall('getSavePath', arguments: { - 'acceptedTypes': null, - 'initialDirectory': '/example/directory', - 'suggestedName': null, - 'confirmButtonText': null, - }), - ], - ); + final VerificationResult result = + verify(mockApi.displaySavePanel(captureAny)); + final SavePanelOptions options = result.captured[0] as SavePanelOptions; + expect(options.directoryPath, '/example/directory'); }); test('passes confirmButtonText correctly', () async { await plugin.getSavePath(confirmButtonText: 'Open File'); - expect( - log, - [ - isMethodCall('getSavePath', arguments: { - 'acceptedTypes': null, - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': 'Open File', - }), - ], - ); + final VerificationResult result = + verify(mockApi.displaySavePanel(captureAny)); + final SavePanelOptions options = result.captured[0] as SavePanelOptions; + expect(options.prompt, 'Open File'); }); test('throws for a type group that does not support macOS', () async { @@ -295,32 +320,49 @@ void main() { }); group('getDirectoryPath', () { + test('works as expected with no arguments', () async { + when(mockApi.displayOpenPanel(any)) + .thenAnswer((_) async => ['foo']); + + final String? path = await plugin.getDirectoryPath(); + + expect(path, 'foo'); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.allowsMultipleSelection, false); + expect(options.canChooseFiles, false); + expect(options.canChooseDirectories, true); + expect(options.baseOptions.allowedFileTypes, null); + expect(options.baseOptions.directoryPath, null); + expect(options.baseOptions.nameFieldStringValue, null); + expect(options.baseOptions.prompt, null); + }); + + test('handles cancel', () async { + when(mockApi.displayOpenPanel(any)).thenAnswer((_) async => []); + + final String? path = await plugin.getDirectoryPath(); + + expect(path, null); + }); + test('passes initialDirectory correctly', () async { await plugin.getDirectoryPath(initialDirectory: '/example/directory'); - expect( - log, - [ - isMethodCall('getDirectoryPath', arguments: { - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - }), - ], - ); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.baseOptions.directoryPath, '/example/directory'); }); test('passes confirmButtonText correctly', () async { await plugin.getDirectoryPath(confirmButtonText: 'Open File'); - expect( - log, - [ - isMethodCall('getDirectoryPath', arguments: { - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - }), - ], - ); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.baseOptions.prompt, 'Open File'); }); }); @@ -343,16 +385,9 @@ void main() { ), ]); - expect( - log, - [ - isMethodCall('getSavePath', arguments: { - 'acceptedTypes': null, - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': null, - }), - ], - ); + final VerificationResult result = + verify(mockApi.displaySavePanel(captureAny)); + final SavePanelOptions options = result.captured[0] as SavePanelOptions; + expect(options.allowedFileTypes, null); }); } diff --git a/packages/file_selector/file_selector_macos/test/file_selector_macos_test.mocks.dart b/packages/file_selector/file_selector_macos/test/file_selector_macos_test.mocks.dart new file mode 100644 index 000000000000..ddd563b2869a --- /dev/null +++ b/packages/file_selector/file_selector_macos/test/file_selector_macos_test.mocks.dart @@ -0,0 +1,51 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in file_selector_macos/example/macos/Flutter/ephemeral/.symlinks/plugins/file_selector_macos/test/file_selector_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_macos/src/messages.g.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; + +import 'messages_test.g.dart' as _i2; + +// 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 [TestFileSelectorApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestFileSelectorApi extends _i1.Mock + implements _i2.TestFileSelectorApi { + MockTestFileSelectorApi() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future> displayOpenPanel(_i4.OpenPanelOptions? options) => + (super.noSuchMethod( + Invocation.method( + #displayOpenPanel, + [options], + ), + returnValue: _i3.Future>.value([]), + ) as _i3.Future>); + @override + _i3.Future displaySavePanel(_i4.SavePanelOptions? options) => + (super.noSuchMethod( + Invocation.method( + #displaySavePanel, + [options], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); +} diff --git a/packages/file_selector/file_selector_macos/test/messages_test.g.dart b/packages/file_selector/file_selector_macos/test/messages_test.g.dart new file mode 100644 index 000000000000..731f1fb1d51f --- /dev/null +++ b/packages/file_selector/file_selector_macos/test/messages_test.g.dart @@ -0,0 +1,107 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v4.2.14), 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 +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:file_selector_macos/src/messages.g.dart'; + +class _TestFileSelectorApiCodec extends StandardMessageCodec { + const _TestFileSelectorApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is AllowedTypes) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is OpenPanelOptions) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is SavePanelOptions) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return AllowedTypes.decode(readValue(buffer)!); + + case 129: + return OpenPanelOptions.decode(readValue(buffer)!); + + case 130: + return SavePanelOptions.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestFileSelectorApi { + static const MessageCodec codec = _TestFileSelectorApiCodec(); + + /// Shows an open panel with the given [options], returning the list of + /// selected paths. + /// + /// An empty list corresponds to a cancelled selection. + Future> displayOpenPanel(OpenPanelOptions options); + + /// Shows a save panel with the given [options], returning the selected path. + /// + /// A null return corresponds to a cancelled save. + Future displaySavePanel(SavePanelOptions options); + + static void setup(TestFileSelectorApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FileSelectorApi.displayOpenPanel', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.FileSelectorApi.displayOpenPanel was null.'); + final List args = (message as List?)!; + final OpenPanelOptions? arg_options = (args[0] as OpenPanelOptions?); + assert(arg_options != null, + 'Argument for dev.flutter.pigeon.FileSelectorApi.displayOpenPanel was null, expected non-null OpenPanelOptions.'); + final List output = await api.displayOpenPanel(arg_options!); + return [output]; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FileSelectorApi.displaySavePanel', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.FileSelectorApi.displaySavePanel was null.'); + final List args = (message as List?)!; + final SavePanelOptions? arg_options = (args[0] as SavePanelOptions?); + assert(arg_options != null, + 'Argument for dev.flutter.pigeon.FileSelectorApi.displaySavePanel was null, expected non-null SavePanelOptions.'); + final String? output = await api.displaySavePanel(arg_options!); + return [output]; + }); + } + } + } +} diff --git a/packages/file_selector/file_selector_platform_interface/CHANGELOG.md b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md index ad803fb12e66..ae415ef8600d 100644 --- a/packages/file_selector/file_selector_platform_interface/CHANGELOG.md +++ b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md @@ -1,3 +1,11 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 2.4.0 + +* Adds `getDirectoryPaths` method to the interface. + ## 2.3.0 * Replaces `macUTIs` with `uniformTypeIdentifiers`. `macUTIs` is available as an alias, but will be deprecated in a future release. diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart b/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart index d6aebd01730f..98184cab8768 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart +++ b/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart @@ -16,7 +16,6 @@ class MethodChannelFileSelector extends FileSelectorPlatform { @visibleForTesting MethodChannel get channel => _channel; - /// Load a file from user's computer and return it as an XFile @override Future openFile({ List? acceptedTypeGroups, @@ -37,7 +36,6 @@ class MethodChannelFileSelector extends FileSelectorPlatform { return path == null ? null : XFile(path.first); } - /// Load multiple files from user's computer and return it as an XFile @override Future> openFiles({ List? acceptedTypeGroups, @@ -58,7 +56,6 @@ class MethodChannelFileSelector extends FileSelectorPlatform { return pathList?.map((String path) => XFile(path)).toList() ?? []; } - /// Gets the path from a save dialog @override Future getSavePath({ List? acceptedTypeGroups, @@ -79,7 +76,6 @@ class MethodChannelFileSelector extends FileSelectorPlatform { ); } - /// Gets a directory path from a dialog @override Future getDirectoryPath({ String? initialDirectory, @@ -93,4 +89,17 @@ class MethodChannelFileSelector extends FileSelectorPlatform { }, ); } + + @override + Future> getDirectoryPaths( + {String? initialDirectory, String? confirmButtonText}) async { + final List? pathList = await _channel.invokeListMethod( + 'getDirectoryPaths', + { + 'initialDirectory': initialDirectory, + 'confirmButtonText': confirmButtonText, + }, + ); + return pathList ?? []; + } } diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart b/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart index eb4563c47917..ad4fe617e44e 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart +++ b/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart @@ -36,7 +36,7 @@ abstract class FileSelectorPlatform extends PlatformInterface { _instance = instance; } - /// Open file dialog for loading files and return a file path + /// Opens a file dialog for loading files and returns a file path. /// Returns `null` if user cancels the operation. Future openFile({ List? acceptedTypeGroups, @@ -46,7 +46,7 @@ abstract class FileSelectorPlatform extends PlatformInterface { throw UnimplementedError('openFile() has not been implemented.'); } - /// Open file dialog for loading files and return a list of file paths + /// Opens a file dialog for loading files and returns a list of file paths. Future> openFiles({ List? acceptedTypeGroups, String? initialDirectory, @@ -55,7 +55,7 @@ abstract class FileSelectorPlatform extends PlatformInterface { throw UnimplementedError('openFiles() has not been implemented.'); } - /// Open file dialog for saving files and return a file path at which to save + /// Opens a file dialog for saving files and returns a file path at which to save. /// Returns `null` if user cancels the operation. Future getSavePath({ List? acceptedTypeGroups, @@ -66,7 +66,7 @@ abstract class FileSelectorPlatform extends PlatformInterface { throw UnimplementedError('getSavePath() has not been implemented.'); } - /// Open file dialog for loading directories and return a directory path + /// Opens a file dialog for loading directories and returns a directory path. /// Returns `null` if user cancels the operation. Future getDirectoryPath({ String? initialDirectory, @@ -74,4 +74,12 @@ abstract class FileSelectorPlatform extends PlatformInterface { }) { throw UnimplementedError('getDirectoryPath() has not been implemented.'); } + + /// Opens a file dialog for loading directories and returns multiple directory paths. + Future> getDirectoryPaths({ + String? initialDirectory, + String? confirmButtonText, + }) { + throw UnimplementedError('getDirectoryPaths() has not been implemented.'); + } } diff --git a/packages/file_selector/file_selector_platform_interface/pubspec.yaml b/packages/file_selector/file_selector_platform_interface/pubspec.yaml index ac8727c09e36..b2461ee2a6d0 100644 --- a/packages/file_selector/file_selector_platform_interface/pubspec.yaml +++ b/packages/file_selector/file_selector_platform_interface/pubspec.yaml @@ -4,11 +4,11 @@ repository: https://github.com/flutter/plugins/tree/main/packages/file_selector/ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%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.3.0 +version: 2.4.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: cross_file: ^0.3.0 diff --git a/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart b/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart index 91e78b452961..18334e885fc7 100644 --- a/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart +++ b/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart @@ -19,6 +19,16 @@ void main() { FileSelectorPlatform.instance = ExtendsFileSelectorPlatform(); }); }); + + group('#GetDirectoryPaths', () { + test('Should throw unimplemented exception', () async { + final FileSelectorPlatform fileSelector = ExtendsFileSelectorPlatform(); + + await expectLater(() async { + return fileSelector.getDirectoryPaths(); + }, throwsA(isA())); + }); + }); } class ExtendsFileSelectorPlatform extends FileSelectorPlatform {} diff --git a/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart b/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart index 0f5f3a17ae0c..c5438f7ecbc2 100644 --- a/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart +++ b/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart @@ -16,10 +16,15 @@ void main() { final List log = []; setUp(() { - plugin.channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - return null; - }); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + plugin.channel, + (MethodCall methodCall) async { + log.add(methodCall); + return null; + }, + ); log.clear(); }); @@ -43,49 +48,46 @@ void main() { await plugin .openFile(acceptedTypeGroups: [group, groupTwo]); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': >[ - group.toJSON(), - groupTwo.toJSON() - ], - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': false, - }), - ], + 'openFile', + arguments: { + 'acceptedTypeGroups': >[ + group.toJSON(), + groupTwo.toJSON() + ], + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': false, + }, ); }); test('passes initialDirectory correctly', () async { await plugin.openFile(initialDirectory: '/example/directory'); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': null, - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - 'multiple': false, - }), - ], + 'openFile', + arguments: { + 'acceptedTypeGroups': null, + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + 'multiple': false, + }, ); }); test('passes confirmButtonText correctly', () async { await plugin.openFile(confirmButtonText: 'Open File'); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': null, - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - 'multiple': false, - }), - ], + 'openFile', + arguments: { + 'acceptedTypeGroups': null, + 'initialDirectory': null, + 'confirmButtonText': 'Open File', + 'multiple': false, + }, ); }); }); @@ -108,49 +110,46 @@ void main() { await plugin .openFiles(acceptedTypeGroups: [group, groupTwo]); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': >[ - group.toJSON(), - groupTwo.toJSON() - ], - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': true, - }), - ], + 'openFile', + arguments: { + 'acceptedTypeGroups': >[ + group.toJSON(), + groupTwo.toJSON() + ], + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': true, + }, ); }); test('passes initialDirectory correctly', () async { await plugin.openFiles(initialDirectory: '/example/directory'); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': null, - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - 'multiple': true, - }), - ], + 'openFile', + arguments: { + 'acceptedTypeGroups': null, + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + 'multiple': true, + }, ); }); test('passes confirmButtonText correctly', () async { await plugin.openFiles(confirmButtonText: 'Open File'); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': null, - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - 'multiple': true, - }), - ], + 'openFile', + arguments: { + 'acceptedTypeGroups': null, + 'initialDirectory': null, + 'confirmButtonText': 'Open File', + 'multiple': true, + }, ); }); }); @@ -174,79 +173,115 @@ void main() { await plugin .getSavePath(acceptedTypeGroups: [group, groupTwo]); - expect( + expectMethodCall( log, - [ - isMethodCall('getSavePath', arguments: { - 'acceptedTypeGroups': >[ - group.toJSON(), - groupTwo.toJSON() - ], - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': null, - }), - ], + 'getSavePath', + arguments: { + 'acceptedTypeGroups': >[ + group.toJSON(), + groupTwo.toJSON() + ], + 'initialDirectory': null, + 'suggestedName': null, + 'confirmButtonText': null, + }, ); }); test('passes initialDirectory correctly', () async { await plugin.getSavePath(initialDirectory: '/example/directory'); - expect( + expectMethodCall( log, - [ - isMethodCall('getSavePath', arguments: { - 'acceptedTypeGroups': null, - 'initialDirectory': '/example/directory', - 'suggestedName': null, - 'confirmButtonText': null, - }), - ], + 'getSavePath', + arguments: { + 'acceptedTypeGroups': null, + 'initialDirectory': '/example/directory', + 'suggestedName': null, + 'confirmButtonText': null, + }, ); }); test('passes confirmButtonText correctly', () async { await plugin.getSavePath(confirmButtonText: 'Open File'); - expect( + expectMethodCall( log, - [ - isMethodCall('getSavePath', arguments: { - 'acceptedTypeGroups': null, - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': 'Open File', - }), - ], + 'getSavePath', + arguments: { + 'acceptedTypeGroups': null, + 'initialDirectory': null, + 'suggestedName': null, + 'confirmButtonText': 'Open File', + }, ); }); - group('#getDirectoryPath', () { - test('passes initialDirectory correctly', () async { - await plugin.getDirectoryPath(initialDirectory: '/example/directory'); - - expect( - log, - [ - isMethodCall('getDirectoryPath', arguments: { - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - }), - ], - ); - }); - test('passes confirmButtonText correctly', () async { - await plugin.getDirectoryPath(confirmButtonText: 'Open File'); - - expect( - log, - [ - isMethodCall('getDirectoryPath', arguments: { - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - }), - ], - ); - }); + }); + group('#getDirectoryPath', () { + test('passes initialDirectory correctly', () async { + await plugin.getDirectoryPath(initialDirectory: '/example/directory'); + + expectMethodCall( + log, + 'getDirectoryPath', + arguments: { + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + }, + ); + }); + test('passes confirmButtonText correctly', () async { + await plugin.getDirectoryPath(confirmButtonText: 'Select Folder'); + + expectMethodCall( + log, + 'getDirectoryPath', + arguments: { + 'initialDirectory': null, + 'confirmButtonText': 'Select Folder', + }, + ); + }); + }); + group('#getDirectoryPaths', () { + test('passes initialDirectory correctly', () async { + await plugin.getDirectoryPaths(initialDirectory: '/example/directory'); + + expectMethodCall( + log, + 'getDirectoryPaths', + arguments: { + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + }, + ); + }); + test('passes confirmButtonText correctly', () async { + await plugin.getDirectoryPaths( + confirmButtonText: 'Select one or more Folders'); + + expectMethodCall( + log, + 'getDirectoryPaths', + arguments: { + 'initialDirectory': null, + 'confirmButtonText': 'Select one or more Folders', + }, + ); }); }); }); } + +void expectMethodCall( + List log, + String methodName, { + Map? arguments, +}) { + expect(log, [isMethodCall(methodName, arguments: arguments)]); +} + +/// 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/file_selector/file_selector_web/CHANGELOG.md b/packages/file_selector/file_selector_web/CHANGELOG.md index 5e531bb633d2..fbb58d61f999 100644 --- a/packages/file_selector/file_selector_web/CHANGELOG.md +++ b/packages/file_selector/file_selector_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 0.9.0+2 * Changes XTypeGroup initialization from final to const. diff --git a/packages/file_selector/file_selector_web/example/pubspec.yaml b/packages/file_selector/file_selector_web/example/pubspec.yaml index e14f5c2eedea..985ce35f69a8 100644 --- a/packages/file_selector/file_selector_web/example/pubspec.yaml +++ b/packages/file_selector/file_selector_web/example/pubspec.yaml @@ -3,7 +3,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: file_selector_platform_interface: ^2.2.0 diff --git a/packages/file_selector/file_selector_web/pubspec.yaml b/packages/file_selector/file_selector_web/pubspec.yaml index 848a41b754af..aceeb8b13693 100644 --- a/packages/file_selector/file_selector_web/pubspec.yaml +++ b/packages/file_selector/file_selector_web/pubspec.yaml @@ -6,7 +6,7 @@ version: 0.9.0+2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/file_selector/file_selector_web/test/more_tests_exist_elsewhere_test.dart b/packages/file_selector/file_selector_web/test/more_tests_exist_elsewhere_test.dart index 37c6eb644c9b..2fef89bb48df 100644 --- a/packages/file_selector/file_selector_web/test/more_tests_exist_elsewhere_test.dart +++ b/packages/file_selector/file_selector_web/test/more_tests_exist_elsewhere_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: avoid_print + import 'package:flutter_test/flutter_test.dart'; void main() { diff --git a/packages/file_selector/file_selector_windows/CHANGELOG.md b/packages/file_selector/file_selector_windows/CHANGELOG.md index 13e895ca46f1..1f9405d2c987 100644 --- a/packages/file_selector/file_selector_windows/CHANGELOG.md +++ b/packages/file_selector/file_selector_windows/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Updates example code for `use_build_context_synchronously` lint. +* Updates minimum Flutter version to 3.0. + ## 0.9.1+4 * Changes XTypeGroup initialization from final to const. diff --git a/packages/file_selector/file_selector_windows/example/lib/get_directory_page.dart b/packages/file_selector/file_selector_windows/example/lib/get_directory_page.dart index 0699dd121541..f6390ccef20d 100644 --- a/packages/file_selector/file_selector_windows/example/lib/get_directory_page.dart +++ b/packages/file_selector/file_selector_windows/example/lib/get_directory_page.dart @@ -21,10 +21,12 @@ class GetDirectoryPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( - context: context, - builder: (BuildContext context) => TextDisplay(directoryPath), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(directoryPath), + ); + } } @override diff --git a/packages/file_selector/file_selector_windows/example/lib/open_image_page.dart b/packages/file_selector/file_selector_windows/example/lib/open_image_page.dart index b6ada56ebb2b..9252d25f113c 100644 --- a/packages/file_selector/file_selector_windows/example/lib/open_image_page.dart +++ b/packages/file_selector/file_selector_windows/example/lib/open_image_page.dart @@ -28,10 +28,12 @@ class OpenImagePage extends StatelessWidget { final String fileName = file.name; final String filePath = file.path; - await showDialog( - context: context, - builder: (BuildContext context) => ImageDisplay(fileName, filePath), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => ImageDisplay(fileName, filePath), + ); + } } @override diff --git a/packages/file_selector/file_selector_windows/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector_windows/example/lib/open_multiple_images_page.dart index c8e352a5b8bd..787717cdea13 100644 --- a/packages/file_selector/file_selector_windows/example/lib/open_multiple_images_page.dart +++ b/packages/file_selector/file_selector_windows/example/lib/open_multiple_images_page.dart @@ -32,10 +32,12 @@ class OpenMultipleImagesPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( - context: context, - builder: (BuildContext context) => MultipleImagesDisplay(files), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => MultipleImagesDisplay(files), + ); + } } @override diff --git a/packages/file_selector/file_selector_windows/example/lib/open_text_page.dart b/packages/file_selector/file_selector_windows/example/lib/open_text_page.dart index 4c88d7475049..97812f2b3505 100644 --- a/packages/file_selector/file_selector_windows/example/lib/open_text_page.dart +++ b/packages/file_selector/file_selector_windows/example/lib/open_text_page.dart @@ -25,10 +25,12 @@ class OpenTextPage extends StatelessWidget { final String fileName = file.name; final String fileContent = await file.readAsString(); - await showDialog( - context: context, - builder: (BuildContext context) => TextDisplay(fileName, fileContent), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(fileName, fileContent), + ); + } } @override diff --git a/packages/file_selector/file_selector_windows/example/lib/save_text_page.dart b/packages/file_selector/file_selector_windows/example/lib/save_text_page.dart index 9803f285a536..aca041f474c7 100644 --- a/packages/file_selector/file_selector_windows/example/lib/save_text_page.dart +++ b/packages/file_selector/file_selector_windows/example/lib/save_text_page.dart @@ -42,7 +42,7 @@ class SaveTextPage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( + SizedBox( width: 300, child: TextField( minLines: 1, @@ -53,7 +53,7 @@ class SaveTextPage extends StatelessWidget { ), ), ), - Container( + SizedBox( width: 300, child: TextField( minLines: 1, diff --git a/packages/file_selector/file_selector_windows/example/pubspec.yaml b/packages/file_selector/file_selector_windows/example/pubspec.yaml index bc886d32c896..d270c3067325 100644 --- a/packages/file_selector/file_selector_windows/example/pubspec.yaml +++ b/packages/file_selector/file_selector_windows/example/pubspec.yaml @@ -5,7 +5,7 @@ version: 1.0.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: file_selector_platform_interface: ^2.2.0 diff --git a/packages/file_selector/file_selector_windows/pigeons/messages.dart b/packages/file_selector/file_selector_windows/pigeons/messages.dart index f2c9ab71bd82..c3b3aff192b8 100644 --- a/packages/file_selector/file_selector_windows/pigeons/messages.dart +++ b/packages/file_selector/file_selector_windows/pigeons/messages.dart @@ -6,7 +6,7 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions( dartOut: 'lib/src/messages.g.dart', - dartTestOut: 'test/test_api.dart', + dartTestOut: 'test/test_api.g.dart', cppOptions: CppOptions(namespace: 'file_selector_windows'), cppHeaderOut: 'windows/messages.g.h', cppSourceOut: 'windows/messages.g.cpp', diff --git a/packages/file_selector/file_selector_windows/pubspec.yaml b/packages/file_selector/file_selector_windows/pubspec.yaml index ee0701b3fd30..a0a0f39fbd1f 100644 --- a/packages/file_selector/file_selector_windows/pubspec.yaml +++ b/packages/file_selector/file_selector_windows/pubspec.yaml @@ -6,7 +6,7 @@ version: 0.9.1+4 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart index f07c9b67618d..62745f7df707 100644 --- a/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart +++ b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart @@ -11,7 +11,7 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'file_selector_windows_test.mocks.dart'; -import 'test_api.dart'; +import 'test_api.g.dart'; @GenerateMocks([TestFileSelectorApi]) void main() { diff --git a/packages/file_selector/file_selector_windows/test/file_selector_windows_test.mocks.dart b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.mocks.dart index 61e17fcdfeaa..f60c92e6b7ee 100644 --- a/packages/file_selector/file_selector_windows/test/file_selector_windows_test.mocks.dart +++ b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.mocks.dart @@ -5,7 +5,7 @@ import 'package:file_selector_windows/src/messages.g.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'test_api.dart' as _i2; +import 'test_api.g.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values diff --git a/packages/file_selector/file_selector_windows/test/test_api.dart b/packages/file_selector/file_selector_windows/test/test_api.g.dart similarity index 100% rename from packages/file_selector/file_selector_windows/test/test_api.dart rename to packages/file_selector/file_selector_windows/test/test_api.g.dart diff --git a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md index 81202f8159de..c169487f6a81 100644 --- a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md +++ b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md @@ -1,6 +1,6 @@ ## NEXT -* Updates minimum Flutter version to 2.10. +* Updates minimum Flutter version to 3.0. ## 2.0.7 diff --git a/packages/flutter_plugin_android_lifecycle/android/build.gradle b/packages/flutter_plugin_android_lifecycle/android/build.gradle index 5786a74e2e78..62c603262989 100644 --- a/packages/flutter_plugin_android_lifecycle/android/build.gradle +++ b/packages/flutter_plugin_android_lifecycle/android/build.gradle @@ -30,10 +30,7 @@ android { consumerProguardFiles 'proguard.txt' } lintOptions { - // TODO(stuartmorgan): Enable when gradle is updated. - // disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' } dependencies { @@ -56,6 +53,6 @@ android { dependencies { testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:4.7.0' + testImplementation 'org.mockito:mockito-core:5.1.1' } diff --git a/packages/flutter_plugin_android_lifecycle/example/android/gradle.properties b/packages/flutter_plugin_android_lifecycle/example/android/gradle.properties index 38c8d4544ff1..2f3603c9ff62 100644 --- a/packages/flutter_plugin_android_lifecycle/example/android/gradle.properties +++ b/packages/flutter_plugin_android_lifecycle/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml b/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml index e732497eee95..4c97e6c44cd1 100644 --- a/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml +++ b/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml @@ -4,6 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/flutter_plugin_android_lifecycle/pubspec.yaml b/packages/flutter_plugin_android_lifecycle/pubspec.yaml index 3a6a2e017b53..4711d1c3629a 100644 --- a/packages/flutter_plugin_android_lifecycle/pubspec.yaml +++ b/packages/flutter_plugin_android_lifecycle/pubspec.yaml @@ -6,7 +6,7 @@ version: 2.0.7 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index 3707aa86e95a..bab8412142d9 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,5 +1,15 @@ ## NEXT +* Updates minimum Flutter version to 3.0. + +## 2.2.3 + +* Fixes a minor syntax error in `README.md`. + +## 2.2.2 + +* Modified `README.md` to fix minor syntax issues and added Code Excerpt to `README.md`. +* Updates code for new analysis options. * Updates code for `no_leading_underscores_for_local_identifiers` lint. ## 2.2.1 diff --git a/packages/google_maps_flutter/google_maps_flutter/README.md b/packages/google_maps_flutter/google_maps_flutter/README.md index 58726a1faaa1..687672353a7e 100644 --- a/packages/google_maps_flutter/google_maps_flutter/README.md +++ b/packages/google_maps_flutter/google_maps_flutter/README.md @@ -1,5 +1,7 @@ # Google Maps for Flutter + + [![pub package](https://img.shields.io/pub/v/google_maps_flutter.svg)](https://pub.dev/packages/google_maps_flutter) A Flutter plugin that provides a [Google Maps](https://developers.google.com/maps/) widget. @@ -105,38 +107,25 @@ the `GoogleMap`'s `onMapCreated` callback. ### Sample Usage + ```dart -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; - -void main() => runApp(MyApp()); - -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Google Maps Demo', - home: MapSample(), - ); - } -} - class MapSample extends StatefulWidget { + const MapSample({Key? key}) : super(key: key); + @override State createState() => MapSampleState(); } class MapSampleState extends State { - Completer _controller = Completer(); + final Completer _controller = + Completer(); - static final CameraPosition _kGooglePlex = CameraPosition( + static const CameraPosition _kGooglePlex = CameraPosition( target: LatLng(37.42796133580664, -122.085749655962), zoom: 14.4746, ); - static final CameraPosition _kLake = CameraPosition( + static const CameraPosition _kLake = CameraPosition( bearing: 192.8334901395799, target: LatLng(37.43296265331129, -122.08832357078792), tilt: 59.440717697143555, @@ -144,7 +133,7 @@ class MapSampleState extends State { @override Widget build(BuildContext context) { - return new Scaffold( + return Scaffold( body: GoogleMap( mapType: MapType.hybrid, initialCameraPosition: _kGooglePlex, @@ -154,8 +143,8 @@ class MapSampleState extends State { ), floatingActionButton: FloatingActionButton.extended( onPressed: _goToTheLake, - label: Text('To the lake!'), - icon: Icon(Icons.directions_boat), + label: const Text('To the lake!'), + icon: const Icon(Icons.directions_boat), ), ); } diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/gradle.properties b/packages/google_maps_flutter/google_maps_flutter/example/android/gradle.properties index 207beb63fb48..c6c9db00b996 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/gradle.properties +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/packages/google_maps_flutter/google_maps_flutter/example/build.excerpt.yaml b/packages/google_maps_flutter/google_maps_flutter/example/build.excerpt.yaml new file mode 100644 index 000000000000..2102d25a193c --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/build.excerpt.yaml @@ -0,0 +1,15 @@ +targets: + $default: + sources: + include: + - lib/** + # Some default includes that aren't really used here but will prevent + # false-negative warnings: + - $package$ + - lib/$lib$ + exclude: + - '**/.*/**' + # - '**/build/**' + # builders: + # code_excerpter|code_excerpter: + # enabled: true \ No newline at end of file diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_coordinates.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_coordinates.dart index 12e31be8f7c7..efb4a105fd0f 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_coordinates.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_coordinates.dart @@ -72,7 +72,7 @@ class _MapCoordinatesBodyState extends State<_MapCoordinatesBody> { // Add a block at the bottom of this list to allow validation that the visible region of the map // does not change when scrolled under the safe view on iOS. // https://github.com/flutter/flutter/issues/107913 - Container( + const SizedBox( width: 300, height: 1000, ), diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_ui.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_ui.dart index 3f56f40799af..0a3146cfaf40 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_ui.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_ui.dart @@ -240,7 +240,7 @@ class MapUiBodyState extends State { } Future _getFileData(String path) async { - return await rootBundle.loadString(path); + return rootBundle.loadString(path); } void _setMapStyle(String mapStyle) { diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart index fa49917e715f..8fde95016f51 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart @@ -285,7 +285,7 @@ class PlaceMarkerBodyState extends State { bitmapIcon.complete(bitmap); })); - return await bitmapIcon.future; + return bitmapIcon.future; } @override diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/readme_sample.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/readme_sample.dart new file mode 100644 index 000000000000..7352945fb2d5 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/readme_sample.dart @@ -0,0 +1,72 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +void main() => runApp(const MyApp()); + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + title: 'Flutter Google Maps Demo', + home: MapSample(), + ); + } +} + +// #docregion MapSample +class MapSample extends StatefulWidget { + const MapSample({Key? key}) : super(key: key); + + @override + State createState() => MapSampleState(); +} + +class MapSampleState extends State { + final Completer _controller = + Completer(); + + static const CameraPosition _kGooglePlex = CameraPosition( + target: LatLng(37.42796133580664, -122.085749655962), + zoom: 14.4746, + ); + + static const CameraPosition _kLake = CameraPosition( + bearing: 192.8334901395799, + target: LatLng(37.43296265331129, -122.08832357078792), + tilt: 59.440717697143555, + zoom: 19.151926040649414); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: GoogleMap( + mapType: MapType.hybrid, + initialCameraPosition: _kGooglePlex, + onMapCreated: (GoogleMapController controller) { + _controller.complete(controller); + }, + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: _goToTheLake, + label: const Text('To the lake!'), + icon: const Icon(Icons.directions_boat), + ), + ); + } + + Future _goToTheLake() async { + final GoogleMapController controller = await _controller.future; + controller.animateCamera(CameraUpdate.newCameraPosition(_kLake)); + } +} +// #enddocregion MapSample 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 b86f05f3360a..5813d42e617e 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml @@ -4,10 +4,10 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: - cupertino_icons: ^0.1.0 + cupertino_icons: ^1.0.5 flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 @@ -22,6 +22,7 @@ dependencies: google_maps_flutter_platform_interface: ^2.2.1 dev_dependencies: + build_runner: ^2.1.10 espresso: ^0.2.0 flutter_driver: sdk: flutter diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 540f5d810966..0771314b9e44 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/plugins/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.2.1 +version: 2.2.3 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.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 6d650661c5e7..459e16b60c42 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 @@ -26,8 +26,12 @@ void main() { FakePlatformViewsController(); setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( - fakePlatformViewsController.fakePlatformViewsMethodHandler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform_views, + fakePlatformViewsController.fakePlatformViewsMethodHandler, + ); }); setUp(() { @@ -180,8 +184,7 @@ void main() { testWidgets('Update non platform related attr', (WidgetTester tester) async { Circle c1 = const Circle(circleId: CircleId('circle_1')); final Set prev = {c1}; - c1 = Circle( - circleId: const CircleId('circle_1'), onTap: () => print('hello')); + c1 = Circle(circleId: const CircleId('circle_1'), onTap: () {}); final Set cur = {c1}; await tester.pumpWidget(_mapWithCircles(prev)); @@ -195,3 +198,9 @@ void main() { expect(platformGoogleMap.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; 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 index 9fe6923204ab..2c6aba1bb0ba 100644 --- 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 @@ -13,7 +13,9 @@ class FakePlatformGoogleMap { : cameraPosition = CameraPosition.fromMap(params['initialCameraPosition']), channel = MethodChannel('plugins.flutter.io/google_maps_$id') { - channel.setMockMethodCallHandler(onMethodCall); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, onMethodCall); updateOptions(params['options'] as Map); updateMarkers(params); updatePolygons(params); @@ -93,7 +95,9 @@ class FakePlatformGoogleMap { Future onMethodCall(MethodCall call) { switch (call.method) { case 'map#update': - updateOptions(call.arguments['options'] as Map); + final Map arguments = + (call.arguments as Map).cast(); + updateOptions(arguments['options']! as Map); return Future.sync(() {}); case 'markers#update': updateMarkers(call.arguments as Map?); @@ -218,17 +222,18 @@ class FakePlatformGoogleMap { 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 list) { - return LatLng(list[0] as double, list[1] as double); + 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 hole.map((dynamic list) { - return LatLng(list[0] as double, list[1] as double); - }).toList() as List; + return _deserializePoints(hole as List); }).toList(); } @@ -475,3 +480,9 @@ Map? _decodeParams(Uint8List paramsMessage) { 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 0fc5e7723df5..99b12988f3b4 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 @@ -16,8 +16,12 @@ void main() { FakePlatformViewsController(); setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( - fakePlatformViewsController.fakePlatformViewsMethodHandler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform_views, + fakePlatformViewsController.fakePlatformViewsMethodHandler, + ); }); setUp(() { @@ -576,3 +580,9 @@ void main() { expect(platformGoogleMap.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 b34fccbfa422..49b64b1b4b2a 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 @@ -29,8 +29,12 @@ 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( 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 [ GoogleMap( 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 b5bba55671c8..75a153e0eaa2 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 @@ -26,8 +26,12 @@ void main() { FakePlatformViewsController(); setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( - fakePlatformViewsController.fakePlatformViewsMethodHandler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform_views, + fakePlatformViewsController.fakePlatformViewsMethodHandler, + ); }); setUp(() { @@ -185,8 +189,8 @@ void main() { final Set prev = {m1}; m1 = Marker( markerId: const MarkerId('marker_1'), - onTap: () => print('hello'), - onDragEnd: (LatLng latLng) => print(latLng)); + onTap: () {}, + onDragEnd: (LatLng latLng) {}); final Set cur = {m1}; await tester.pumpWidget(_mapWithMarkers(prev)); @@ -200,3 +204,9 @@ void main() { expect(platformGoogleMap.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; 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 34959832b36b..152cbddfc34a 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 @@ -49,8 +49,12 @@ void main() { FakePlatformViewsController(); setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( - fakePlatformViewsController.fakePlatformViewsMethodHandler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform_views, + fakePlatformViewsController.fakePlatformViewsMethodHandler, + ); }); setUp(() { @@ -208,8 +212,7 @@ void main() { testWidgets('Update non platform related attr', (WidgetTester tester) async { Polygon p1 = const Polygon(polygonId: PolygonId('polygon_1')); final Set prev = {p1}; - p1 = Polygon( - polygonId: const PolygonId('polygon_1'), onTap: () => print(2 + 2)); + p1 = Polygon(polygonId: const PolygonId('polygon_1'), onTap: () {}); final Set cur = {p1}; await tester.pumpWidget(_mapWithPolygons(prev)); @@ -404,3 +407,9 @@ void main() { expect(platformGoogleMap.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; 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 f9d091695383..03b6c620190a 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 @@ -26,8 +26,12 @@ void main() { FakePlatformViewsController(); setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( - fakePlatformViewsController.fakePlatformViewsMethodHandler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform_views, + fakePlatformViewsController.fakePlatformViewsMethodHandler, + ); }); setUp(() { @@ -202,8 +206,7 @@ void main() { testWidgets('Update non platform related attr', (WidgetTester tester) async { Polyline p1 = const Polyline(polylineId: PolylineId('polyline_1')); final Set prev = {p1}; - p1 = Polyline( - polylineId: const PolylineId('polyline_1'), onTap: () => print(2 + 2)); + p1 = Polyline(polylineId: const PolylineId('polyline_1'), onTap: () {}); final Set cur = {p1}; await tester.pumpWidget(_mapWithPolylines(prev)); @@ -217,3 +220,9 @@ void main() { expect(platformGoogleMap.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; 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 b4586f743296..e4e4514dd501 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 @@ -24,8 +24,12 @@ void main() { FakePlatformViewsController(); setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( - fakePlatformViewsController.fakePlatformViewsMethodHandler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform_views, + fakePlatformViewsController.fakePlatformViewsMethodHandler, + ); }); setUp(() { @@ -198,3 +202,9 @@ void main() { expect(platformGoogleMap.tileOverlaysToAdd.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; diff --git a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md index 9bc8b195f6e2..68b9f677e2db 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md @@ -1,3 +1,29 @@ +## 2.4.5 + +* Fixes Initial padding not working when map has not been created yet. + +## 2.4.4 + +* Fixes Points losing precision when converting to LatLng. +* Updates minimum Flutter version to 3.0. + +## 2.4.3 + +* Updates code for stricter lint checks. + +## 2.4.2 + +* Updates code for stricter lint checks. + +## 2.4.1 + +* Update `androidx.test.espresso:espresso-core` to 3.5.1. + +## 2.4.0 + +* Adds the ability to request a specific map renderer. +* Updates code for new analysis options. + ## 2.3.3 * Update android gradle plugin to 7.3.1. diff --git a/packages/google_maps_flutter/google_maps_flutter_android/README.md b/packages/google_maps_flutter/google_maps_flutter_android/README.md index 877b9bbe9102..e07b0bc8d406 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/README.md +++ b/packages/google_maps_flutter/google_maps_flutter_android/README.md @@ -48,7 +48,30 @@ Hybrid Composition, but currently [misses certain map updates][4]. This mode will likely become the default in future versions if/when the missed updates issue can be resolved. +## Map renderer + +This plugin supports the option to request a specific [map renderer][5]. + +The renderer must be requested before creating GoogleMap instances, as the renderer can be initialized only once per application context. + + +```dart +AndroidMapRenderer mapRenderer = AndroidMapRenderer.platformDefault; +// ··· + final GoogleMapsFlutterPlatform mapsImplementation = + GoogleMapsFlutterPlatform.instance; + if (mapsImplementation is GoogleMapsFlutterAndroid) { + WidgetsFlutterBinding.ensureInitialized(); + mapRenderer = await mapsImplementation + .initializeWithRenderer(AndroidMapRenderer.latest); + } +``` + +Available values are `AndroidMapRenderer.latest`, `AndroidMapRenderer.legacy`, `AndroidMapRenderer.platformDefault`. +Note that getting the requested renderer as a response is not guaranteed. + [1]: https://pub.dev/packages/google_maps_flutter [2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin [3]: https://docs.flutter.dev/development/platform-integration/android/platform-views [4]: https://github.com/flutter/flutter/issues/103686 +[5]: https://developers.google.com/maps/documentation/android-sdk/renderer diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle b/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle index 2d507c6cc9fa..6f8d3060a9cf 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle @@ -29,10 +29,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - // TODO(stuartmorgan): Enable when gradle is updated. - // disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' } dependencies { @@ -40,9 +37,9 @@ android { implementation 'com.google.android.gms:play-services-maps:18.1.0' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:rules:1.4.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:4.7.0' + testImplementation 'org.mockito:mockito-core:5.1.1' testImplementation 'androidx.test:core:1.2.0' testImplementation "org.robolectric:robolectric:4.3.1" } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java index 72c6959fe55e..22c8f4d24be6 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java @@ -7,6 +7,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Point; +import androidx.annotation.VisibleForTesting; import com.google.android.gms.maps.CameraUpdate; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.model.BitmapDescriptor; @@ -592,13 +593,14 @@ static String interpretCircleOptions(Object o, CircleOptionsSink sink) { } } - private static List toPoints(Object o) { + @VisibleForTesting + static List toPoints(Object o) { final List data = toList(o); final List points = new ArrayList<>(data.size()); for (Object rawPoint : data) { final List point = toList(rawPoint); - points.add(new LatLng(toFloat(point.get(0)), toFloat(point.get(1)))); + points.add(new LatLng(toDouble(point.get(0)), toDouble(point.get(1)))); } return points; } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index 66d3e283b8df..a57cd1a34c97 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java @@ -70,7 +70,7 @@ final class GoogleMapController private boolean trafficEnabled = false; private boolean buildingsEnabled = true; private boolean disposed = false; - private final float density; + @VisibleForTesting final float density; private MethodChannel.Result mapReadyResult; private final Context context; private final LifecycleProvider lifecycleProvider; @@ -84,6 +84,7 @@ final class GoogleMapController private List initialPolylines; private List initialCircles; private List> initialTileOverlays; + @VisibleForTesting List initialPadding; GoogleMapController( int id, @@ -209,6 +210,13 @@ public void onMapReady(GoogleMap googleMap) { updateInitialPolylines(); updateInitialCircles(); updateInitialTileOverlays(); + if (initialPadding != null && initialPadding.size() == 4) { + setPadding( + initialPadding.get(0), + initialPadding.get(1), + initialPadding.get(2), + initialPadding.get(3)); + } } @Override @@ -741,7 +749,22 @@ public void setPadding(float top, float left, float bottom, float right) { (int) (top * density), (int) (right * density), (int) (bottom * density)); + } else { + setInitialPadding(top, left, bottom, right); + } + } + + @VisibleForTesting + void setInitialPadding(float top, float left, float bottom, float right) { + if (initialPadding == null) { + initialPadding = new ArrayList<>(); + } else { + initialPadding.clear(); } + initialPadding.add(top); + initialPadding.add(left); + initialPadding.add(bottom); + initialPadding.add(right); } @Override diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java index ca9ac184a76e..ffa2412f9c42 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java @@ -17,11 +17,15 @@ public class GoogleMapFactory extends PlatformViewFactory { private final BinaryMessenger binaryMessenger; private final LifecycleProvider lifecycleProvider; + private final GoogleMapInitializer googleMapInitializer; - GoogleMapFactory(BinaryMessenger binaryMessenger, LifecycleProvider lifecycleProvider) { + GoogleMapFactory( + BinaryMessenger binaryMessenger, Context context, LifecycleProvider lifecycleProvider) { super(StandardMessageCodec.INSTANCE); + this.binaryMessenger = binaryMessenger; this.lifecycleProvider = lifecycleProvider; + this.googleMapInitializer = new GoogleMapInitializer(context, binaryMessenger); } @SuppressWarnings("unchecked") diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapInitializer.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapInitializer.java new file mode 100644 index 000000000000..a113c0a1c4c3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapInitializer.java @@ -0,0 +1,109 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemaps; + +import android.content.Context; +import androidx.annotation.VisibleForTesting; +import com.google.android.gms.maps.MapsInitializer; +import com.google.android.gms.maps.MapsInitializer.Renderer; +import com.google.android.gms.maps.OnMapsSdkInitializedCallback; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +/** GoogleMaps initializer used to initialize the Google Maps SDK with preferred settings. */ +final class GoogleMapInitializer + implements OnMapsSdkInitializedCallback, MethodChannel.MethodCallHandler { + private final MethodChannel methodChannel; + private final Context context; + private static MethodChannel.Result initializationResult; + private boolean rendererInitialized = false; + + GoogleMapInitializer(Context context, BinaryMessenger binaryMessenger) { + this.context = context; + + methodChannel = + new MethodChannel(binaryMessenger, "plugins.flutter.dev/google_maps_android_initializer"); + methodChannel.setMethodCallHandler(this); + } + + @Override + public void onMethodCall(MethodCall call, MethodChannel.Result result) { + switch (call.method) { + case "initializer#preferRenderer": + { + String preferredRenderer = (String) call.argument("value"); + initializeWithPreferredRenderer(preferredRenderer, result); + break; + } + default: + result.notImplemented(); + } + } + + /** + * Initializes map renderer to with preferred renderer type. Renderer can be initialized only once + * per application context. + * + *

Supported renderer types are "latest", "legacy" and "default". + */ + private void initializeWithPreferredRenderer( + String preferredRenderer, MethodChannel.Result result) { + if (rendererInitialized || initializationResult != null) { + result.error( + "Renderer already initialized", "Renderer initialization called multiple times", null); + } else { + initializationResult = result; + switch (preferredRenderer) { + case "latest": + initializeWithRendererRequest(Renderer.LATEST); + break; + case "legacy": + initializeWithRendererRequest(Renderer.LEGACY); + break; + case "default": + initializeWithRendererRequest(null); + break; + default: + initializationResult.error( + "Invalid renderer type", + "Renderer initialization called with invalid renderer type", + null); + initializationResult = null; + } + } + } + + /** + * Initializes map renderer to with preferred renderer type. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + */ + @VisibleForTesting + public void initializeWithRendererRequest(MapsInitializer.Renderer renderer) { + MapsInitializer.initialize(context, renderer, this); + } + + /** Is called by Google Maps SDK to determine which version of the renderer was initialized. */ + @Override + public void onMapsSdkInitialized(MapsInitializer.Renderer renderer) { + rendererInitialized = true; + if (initializationResult != null) { + switch (renderer) { + case LATEST: + initializationResult.success("latest"); + break; + case LEGACY: + initializationResult.success("legacy"); + break; + default: + initializationResult.error( + "Unknown renderer type", "Initialized with unknown renderer type", null); + } + initializationResult = null; + } + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java index 715b357566da..20fc15e72b6e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java @@ -46,6 +46,7 @@ public static void registerWith( VIEW_TYPE, new GoogleMapFactory( registrar.messenger(), + registrar.context(), new LifecycleProvider() { @Override public Lifecycle getLifecycle() { @@ -57,7 +58,10 @@ public Lifecycle getLifecycle() { .platformViewRegistry() .registerViewFactory( VIEW_TYPE, - new GoogleMapFactory(registrar.messenger(), new ProxyLifecycleProvider(activity))); + new GoogleMapFactory( + registrar.messenger(), + registrar.context(), + new ProxyLifecycleProvider(activity))); } } @@ -73,6 +77,7 @@ public void onAttachedToEngine(FlutterPluginBinding binding) { VIEW_TYPE, new GoogleMapFactory( binding.getBinaryMessenger(), + binding.getApplicationContext(), new LifecycleProvider() { @Nullable @Override diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java new file mode 100644 index 000000000000..0d635170c1f3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java @@ -0,0 +1,29 @@ +// 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.googlemaps; + +import com.google.android.gms.maps.model.LatLng; +import java.util.ArrayList; +import java.util.List; +import org.junit.Assert; +import org.junit.Test; + +public class ConvertTest { + + @Test + public void ConvertToPointsConvertsThePointsWithFullPrecision() { + double latitude = 43.03725568057; + double longitude = -87.90466904649; + ArrayList point = new ArrayList(); + point.add(latitude); + point.add(longitude); + ArrayList> pointsList = new ArrayList<>(); + pointsList.add(point); + List latLngs = Convert.toPoints(pointsList); + LatLng latLng = latLngs.get(0); + Assert.assertEquals(latitude, latLng.latitude, 1e-15); + Assert.assertEquals(longitude, latLng.longitude, 1e-15); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java index d8082b57e3db..52576962ba8d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java @@ -4,10 +4,12 @@ package io.flutter.plugins.googlemaps; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.content.Context; @@ -20,6 +22,7 @@ import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import java.util.HashMap; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -145,4 +148,22 @@ public void MethodCalledAfterControllerIsDestroyed() throws InterruptedException argument.getValue().onMapLoaded(); verify(mapView, never()).invalidate(); } + + @Test + public void OnMapReadySetsPaddingIfInitialPaddingIsThere() { + float padding = 10f; + int paddingWithDensity = (int) (padding * googleMapController.density); + googleMapController.setInitialPadding(padding, padding, padding, padding); + googleMapController.onMapReady(mockGoogleMap); + verify(mockGoogleMap, times(1)) + .setPadding(paddingWithDensity, paddingWithDensity, paddingWithDensity, paddingWithDensity); + } + + @Test + public void SetPaddingStoresThePaddingValuesInInInitialPaddingWhenGoogleMapIsNull() { + assertNull(googleMapController.initialPadding); + googleMapController.setPadding(0f, 0f, 0f, 0f); + assertNotNull(googleMapController.initialPadding); + Assert.assertEquals(4, googleMapController.initialPadding.size()); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapInitializerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapInitializerTest.java new file mode 100644 index 000000000000..2f9f5e5619fd --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapInitializerTest.java @@ -0,0 +1,98 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemaps; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.os.Build; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.gms.maps.MapsInitializer.Renderer; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Build.VERSION_CODES.P) +public class GoogleMapInitializerTest { + private GoogleMapInitializer googleMapInitializer; + + @Mock BinaryMessenger mockMessenger; + + @Before + public void before() { + MockitoAnnotations.openMocks(this); + Context context = ApplicationProvider.getApplicationContext(); + googleMapInitializer = spy(new GoogleMapInitializer(context, mockMessenger)); + } + + @Test + public void initializer_OnMapsSdkInitializedWithLatestRenderer() { + doNothing().when(googleMapInitializer).initializeWithRendererRequest(Renderer.LATEST); + MethodChannel.Result result = mock(MethodChannel.Result.class); + googleMapInitializer.onMethodCall( + new MethodCall( + "initializer#preferRenderer", + new HashMap() { + { + put("value", "latest"); + } + }), + result); + googleMapInitializer.onMapsSdkInitialized(Renderer.LATEST); + verify(result, times(1)).success("latest"); + verify(result, never()).error(any(), any(), any()); + } + + @Test + public void initializer_OnMapsSdkInitializedWithLegacyRenderer() { + doNothing().when(googleMapInitializer).initializeWithRendererRequest(Renderer.LEGACY); + MethodChannel.Result result = mock(MethodChannel.Result.class); + googleMapInitializer.onMethodCall( + new MethodCall( + "initializer#preferRenderer", + new HashMap() { + { + put("value", "legacy"); + } + }), + result); + googleMapInitializer.onMapsSdkInitialized(Renderer.LEGACY); + verify(result, times(1)).success("legacy"); + verify(result, never()).error(any(), any(), any()); + } + + @Test + public void initializer_onMethodCallWithUnknownRenderer() { + doNothing().when(googleMapInitializer).initializeWithRendererRequest(Renderer.LEGACY); + MethodChannel.Result result = mock(MethodChannel.Result.class); + googleMapInitializer.onMethodCall( + new MethodCall( + "initializer#preferRenderer", + new HashMap() { + { + put("value", "wrong_renderer"); + } + }), + result); + verify(result, never()).success(any()); + verify(result, times(1)).error(eq("Invalid renderer type"), any(), any()); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/gradle.properties b/packages/google_maps_flutter/google_maps_flutter_android/example/android/gradle.properties index 207beb63fb48..c6c9db00b996 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/android/gradle.properties +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_test.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_tests.dart similarity index 99% rename from packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_test.dart rename to packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_tests.dart index 0945740b1e45..bd72b7ba52d2 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/google_maps_tests.dart @@ -12,15 +12,13 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter_android/google_maps_flutter_android.dart'; import 'package:google_maps_flutter_example/example_google_map.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'package:integration_test/integration_test.dart'; const LatLng _kInitialMapCenter = LatLng(0, 0); const double _kInitialZoomLevel = 5; const CameraPosition _kInitialCameraPosition = CameraPosition(target: _kInitialMapCenter, zoom: _kInitialZoomLevel); -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); +void googleMapsTests() { GoogleMapsFlutterPlatform.instance.enableDebugInspection(); // Repeatedly checks an asynchronous value against a test condition, waiting @@ -511,7 +509,9 @@ void main() { await waitForValueMatchingPredicate( tester, () => mapController.getVisibleRegion(), - (LatLngBounds bounds) => bounds != zeroLatLngBounds) ?? + (LatLngBounds bounds) => + bounds != zeroLatLngBounds && + bounds.northeast != bounds.southwest) ?? zeroLatLngBounds; expect(firstVisibleRegion, isNot(zeroLatLngBounds)); expect(firstVisibleRegion.contains(_kInitialMapCenter), isTrue); diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/latest_renderer_test.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/latest_renderer_test.dart new file mode 100644 index 000000000000..64bff8f6c616 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/latest_renderer_test.dart @@ -0,0 +1,40 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_android/google_maps_flutter_android.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'google_maps_tests.dart' show googleMapsTests; + +void main() { + late AndroidMapRenderer initializedRenderer; + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(() async { + final GoogleMapsFlutterAndroid instance = + GoogleMapsFlutterPlatform.instance as GoogleMapsFlutterAndroid; + initializedRenderer = + await instance.initializeWithRenderer(AndroidMapRenderer.latest); + }); + + testWidgets('initialized with latest renderer', (WidgetTester _) async { + expect(initializedRenderer, AndroidMapRenderer.latest); + }); + + testWidgets('throws PlatformException on multiple renderer initializations', + (WidgetTester _) async { + final GoogleMapsFlutterAndroid instance = + GoogleMapsFlutterPlatform.instance as GoogleMapsFlutterAndroid; + expect( + () async => instance.initializeWithRenderer(AndroidMapRenderer.latest), + throwsA(isA().having((PlatformException e) => e.code, + 'code', 'Renderer already initialized'))); + }); + + // Run tests. + googleMapsTests(); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/legacy_renderer_test.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/legacy_renderer_test.dart new file mode 100644 index 000000000000..95b1134d566f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/integration_test/legacy_renderer_test.dart @@ -0,0 +1,40 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_android/google_maps_flutter_android.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'google_maps_tests.dart' show googleMapsTests; + +void main() { + late AndroidMapRenderer initializedRenderer; + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(() async { + final GoogleMapsFlutterAndroid instance = + GoogleMapsFlutterPlatform.instance as GoogleMapsFlutterAndroid; + initializedRenderer = + await instance.initializeWithRenderer(AndroidMapRenderer.legacy); + }); + + testWidgets('initialized with legacy renderer', (WidgetTester _) async { + expect(initializedRenderer, AndroidMapRenderer.legacy); + }); + + testWidgets('throws PlatformException on multiple renderer initializations', + (WidgetTester _) async { + final GoogleMapsFlutterAndroid instance = + GoogleMapsFlutterPlatform.instance as GoogleMapsFlutterAndroid; + expect( + () async => instance.initializeWithRenderer(AndroidMapRenderer.legacy), + throwsA(isA().having((PlatformException e) => e.code, + 'code', 'Renderer already initialized'))); + }); + + // Run tests. + googleMapsTests(); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_coordinates.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_coordinates.dart index 185a97e08f00..22f383bd1254 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_coordinates.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_coordinates.dart @@ -74,7 +74,7 @@ class _MapCoordinatesBodyState extends State<_MapCoordinatesBody> { // Add a block at the bottom of this list to allow validation that the visible region of the map // does not change when scrolled under the safe view on iOS. // https://github.com/flutter/flutter/issues/107913 - Container( + const SizedBox( width: 300, height: 1000, ), diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_ui.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_ui.dart index 009ee71d8400..546cf1d08ff8 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_ui.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/map_ui.dart @@ -241,7 +241,7 @@ class MapUiBodyState extends State { } Future _getFileData(String path) async { - return await rootBundle.loadString(path); + return rootBundle.loadString(path); } void _setMapStyle(String mapStyle) { diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_marker.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_marker.dart index 7d12f4c81684..2c6c725a4fa5 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/place_marker.dart @@ -286,7 +286,7 @@ class PlaceMarkerBodyState extends State { bitmapIcon.complete(bitmap); })); - return await bitmapIcon.future; + return bitmapIcon.future; } @override diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/readme_excerpts.dart b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/readme_excerpts.dart index 5911c062e444..0f6b26de00b6 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/lib/readme_excerpts.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/lib/readme_excerpts.dart @@ -2,6 +2,8 @@ // 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 'package:flutter/material.dart'; // #docregion DisplayMode import 'package:google_maps_flutter_android/google_maps_flutter_android.dart'; @@ -15,7 +17,44 @@ void main() { mapsImplementation.useAndroidViewSurface = true; } // #enddocregion DisplayMode - runApp(const MaterialApp()); + runApp(const MyApp()); // #docregion DisplayMode } // #enddocregion DisplayMode + +class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + // #docregion MapRenderer + AndroidMapRenderer mapRenderer = AndroidMapRenderer.platformDefault; + // #enddocregion MapRenderer + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('README snippet app'), + ), + body: const Text('See example in main.dart'), + ), + ); + } + + Future initializeLatestMapRenderer() async { + // #docregion MapRenderer + final GoogleMapsFlutterPlatform mapsImplementation = + GoogleMapsFlutterPlatform.instance; + if (mapsImplementation is GoogleMapsFlutterAndroid) { + WidgetsFlutterBinding.ensureInitialized(); + mapRenderer = await mapsImplementation + .initializeWithRenderer(AndroidMapRenderer.latest); + } + // #enddocregion MapRenderer + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml index 07decea39920..aa29fa99a97b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml @@ -4,10 +4,10 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: - cupertino_icons: ^0.1.0 + cupertino_icons: ^1.0.5 flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart index 06c5bdcd7e0f..0461b4cf71bc 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart @@ -41,6 +41,19 @@ class UnknownMapIDError extends Error { } } +/// The possible android map renderer types that can be +/// requested from the native Google Maps SDK. +enum AndroidMapRenderer { + /// Latest renderer type. + latest, + + /// Legacy renderer type. + legacy, + + /// Requests the default map renderer type. + platformDefault, +} + /// An implementation of [GoogleMapsFlutterPlatform] for Android. class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { /// Registers the Android implementation of GoogleMapsFlutterPlatform. @@ -48,6 +61,10 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { GoogleMapsFlutterPlatform.instance = GoogleMapsFlutterAndroid(); } + /// The method channel used to initialize the native Google Maps SDK. + final MethodChannel _initializerChannel = const MethodChannel( + 'plugins.flutter.dev/google_maps_android_initializer'); + // Keep a collection of id -> channel // Every method call passes the int mapId final Map _channels = {}; @@ -173,81 +190,93 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { _mapEventStreamController.add(CameraMoveStartedEvent(mapId)); break; case 'camera#onMove': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(CameraMoveEvent( mapId, - CameraPosition.fromMap(call.arguments['position'])!, + CameraPosition.fromMap(arguments['position'])!, )); break; case 'camera#onIdle': _mapEventStreamController.add(CameraIdleEvent(mapId)); break; case 'marker#onTap': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(MarkerTapEvent( mapId, - MarkerId(call.arguments['markerId'] as String), + MarkerId(arguments['markerId']! as String), )); break; case 'marker#onDragStart': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(MarkerDragStartEvent( mapId, - LatLng.fromJson(call.arguments['position'])!, - MarkerId(call.arguments['markerId'] as String), + LatLng.fromJson(arguments['position'])!, + MarkerId(arguments['markerId']! as String), )); break; case 'marker#onDrag': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(MarkerDragEvent( mapId, - LatLng.fromJson(call.arguments['position'])!, - MarkerId(call.arguments['markerId'] as String), + LatLng.fromJson(arguments['position'])!, + MarkerId(arguments['markerId']! as String), )); break; case 'marker#onDragEnd': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(MarkerDragEndEvent( mapId, - LatLng.fromJson(call.arguments['position'])!, - MarkerId(call.arguments['markerId'] as String), + LatLng.fromJson(arguments['position'])!, + MarkerId(arguments['markerId']! as String), )); break; case 'infoWindow#onTap': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(InfoWindowTapEvent( mapId, - MarkerId(call.arguments['markerId'] as String), + MarkerId(arguments['markerId']! as String), )); break; case 'polyline#onTap': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(PolylineTapEvent( mapId, - PolylineId(call.arguments['polylineId'] as String), + PolylineId(arguments['polylineId']! as String), )); break; case 'polygon#onTap': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(PolygonTapEvent( mapId, - PolygonId(call.arguments['polygonId'] as String), + PolygonId(arguments['polygonId']! as String), )); break; case 'circle#onTap': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(CircleTapEvent( mapId, - CircleId(call.arguments['circleId'] as String), + CircleId(arguments['circleId']! as String), )); break; case 'map#onTap': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(MapTapEvent( mapId, - LatLng.fromJson(call.arguments['position'])!, + LatLng.fromJson(arguments['position'])!, )); break; case 'map#onLongPress': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(MapLongPressEvent( mapId, - LatLng.fromJson(call.arguments['position'])!, + LatLng.fromJson(arguments['position'])!, )); break; case 'tileOverlay#getTile': + final Map arguments = _getArgumentDictionary(call); final Map? tileOverlaysForThisMap = _tileOverlays[mapId]; - final String tileOverlayId = call.arguments['tileOverlayId'] as String; + final String tileOverlayId = arguments['tileOverlayId']! as String; final TileOverlay? tileOverlay = tileOverlaysForThisMap?[TileOverlayId(tileOverlayId)]; final TileProvider? tileProvider = tileOverlay?.tileProvider; @@ -255,9 +284,9 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { return TileProvider.noTile.toJson(); } final Tile tile = await tileProvider.getTile( - call.arguments['x'] as int, - call.arguments['y'] as int, - call.arguments['zoom'] as int?, + arguments['x']! as int, + arguments['y']! as int, + arguments['zoom'] as int?, ); return tile.toJson(); default: @@ -265,6 +294,14 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { } } + /// Returns the arguments of [call] as typed string-keyed Map. + /// + /// This does not do any type validation, so is only safe to call if the + /// arguments are known to be a map. + Map _getArgumentDictionary(MethodCall call) { + return (call.arguments as Map).cast(); + } + @override Future updateMapOptions( Map optionsUpdate, { @@ -480,6 +517,52 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { /// Currently defaults to true, but the default is subject to change. bool useAndroidViewSurface = true; + /// Requests Google Map Renderer with [AndroidMapRenderer] type. + /// + /// See https://pub.dev/packages/google_maps_flutter_android#map-renderer + /// for more information. + /// + /// The renderer must be requested before creating GoogleMap instances as the + /// renderer can be initialized only once per application context. + /// Throws a [PlatformException] if method is called multiple times. + /// + /// The returned [Future] completes after renderer has been initialized. + /// Initialized [AndroidMapRenderer] type is returned. + Future initializeWithRenderer( + AndroidMapRenderer? rendererType) async { + String preferredRenderer; + switch (rendererType) { + case AndroidMapRenderer.latest: + preferredRenderer = 'latest'; + break; + case AndroidMapRenderer.legacy: + preferredRenderer = 'legacy'; + break; + case AndroidMapRenderer.platformDefault: + case null: + preferredRenderer = 'default'; + } + + final String? initializedRenderer = await _initializerChannel + .invokeMethod('initializer#preferRenderer', + {'value': preferredRenderer}); + + if (initializedRenderer == null) { + throw AndroidMapRendererException('Failed to initialize map renderer.'); + } + + // Returns mapped [AndroidMapRenderer] enum type. + switch (initializedRenderer) { + case 'latest': + return AndroidMapRenderer.latest; + case 'legacy': + return AndroidMapRenderer.legacy; + default: + throw AndroidMapRendererException( + 'Failed to initialize latest or legacy renderer, got $initializedRenderer.'); + } + } + Widget _buildView( int creationId, PlatformViewCreatedCallback onPlatformViewCreated, { @@ -689,3 +772,16 @@ class _TileOverlayUpdates extends MapsObjectUpdates { /// Set of TileOverlays to be changed in this update. Set get tileOverlaysToChange => objectsToChange; } + +/// Thrown to indicate that a platform interaction failed to initialize renderer. +class AndroidMapRendererException implements Exception { + /// Creates a [AndroidMapRendererException] with an optional human-readable + /// error message. + AndroidMapRendererException([this.message]); + + /// A human-readable error message, possibly null. + final String? message; + + @override + String toString() => 'AndroidMapRendererException($message)'; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml index d20322bc6ee1..cf8bc81e7e7c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_android description: Android implementation of the google_maps_flutter plugin. repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.3.3 +version: 2.4.5 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart index 431c2472945e..29c02c836a85 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart @@ -25,19 +25,24 @@ void main() { required int mapId, required Future? Function(MethodCall call) handler, }) { - maps - .ensureChannelInitialized(mapId) - .setMockMethodCallHandler((MethodCall methodCall) { - log.add(methodCall.method); - return handler(methodCall); - }); + final MethodChannel channel = maps.ensureChannelInitialized(mapId); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + channel, + (MethodCall methodCall) { + log.add(methodCall.method); + return handler(methodCall); + }, + ); } Future sendPlatformMessage( int mapId, String method, Map data) async { final ByteData byteData = const StandardMethodCodec().encodeMethodCall(MethodCall(method, data)); - await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage('plugins.flutter.dev/google_maps_android_$mapId', byteData, (ByteData? data) {}); } @@ -164,3 +169,9 @@ void main() { expect(widget, isA()); }); } + +/// 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/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md index 7788e9146809..a65523f426c1 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md @@ -1,3 +1,14 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 2.1.13 + +* Updates code for stricter lint checks. +* Updates code for new analysis options. +* Re-enable XCUITests: testUserInterface. +* Remove unnecessary `RunnerUITests` target from Podfile of the example app. + ## 2.1.12 * Updates imports for `prefer_relative_imports`. diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Podfile b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Podfile index 14b4bdc51c96..29bfe631a3e7 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Podfile +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/Podfile @@ -34,9 +34,6 @@ target 'Runner' do pod 'OCMock', '~> 3.9.1' end - target 'RunnerUITests' do - inherit! :search_paths - end end post_install do |installer| diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerUITests/GoogleMapsUITests.m b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerUITests/GoogleMapsUITests.m index f4cdb7c50ab2..c3af06691a3f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerUITests/GoogleMapsUITests.m +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios/RunnerUITests/GoogleMapsUITests.m @@ -2,9 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +@import CoreLocation; @import XCTest; @import os.log; -@import GoogleMaps; @interface GoogleMapsUITests : XCTestCase @property(nonatomic, strong) XCUIApplication *app; @@ -18,8 +18,6 @@ - (void)setUp { self.app = [[XCUIApplication alloc] init]; [self.app launch]; - // The location permission interception is currently not working. - // See: https://github.com/flutter/flutter/issues/93325. [self addUIInterruptionMonitorWithDescription:@"Permission popups" handler:^BOOL(XCUIElement *_Nonnull interruptingElement) { @@ -45,8 +43,7 @@ - (void)setUp { }]; } -// Temporarily disabled due to https://github.com/flutter/flutter/issues/93325 -- (void)skip_testUserInterface { +- (void)testUserInterface { XCUIApplication *app = self.app; XCUIElement *userInteface = app.staticTexts[@"User interface"]; if (![userInteface waitForExistenceWithTimeout:30.0]) { @@ -54,17 +51,27 @@ - (void)skip_testUserInterface { XCTFail(@"Failed due to not able to find User interface"); } [userInteface tap]; + XCUIElement *platformView = app.otherElements[@"platform_view[0]"]; if (![platformView waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); XCTFail(@"Failed due to not able to find platform view"); } + + // There is a known bug where the permission popups interruption won't get fired until a tap + // happened in the app. We expect a permission popup so we do a tap here. + // iOS 16 has a bug where if the app itself is directly tapped: [app tap], the first button + // (disable compass) in the app is also tapped, so instead we tap a arbitrary location in the app + // instead. + XCUICoordinate *coordinate = [app coordinateWithNormalizedOffset:CGVectorMake(0, 0)]; + [coordinate tap]; XCUIElement *compass = app.buttons[@"disable compass"]; if (![compass waitForExistenceWithTimeout:30.0]) { os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); - XCTFail(@"Failed due to not able to find compass button"); + XCTFail(@"Failed due to not able to find disable compass button"); } - [compass tap]; + + [self forceTap:compass]; } - (void)testMapCoordinatesPage { @@ -190,4 +197,16 @@ - (void)testMapClickPage { } } +- (void)forceTap:(XCUIElement *)button { + // iOS 16 introduced a bug where hittable is NO for buttons. We force hit the location of the + // button if that is the case. It is likely similar to + // https://github.com/flutter/flutter/issues/113377. + if (button.isHittable) { + [button tap]; + return; + } + XCUICoordinate *coordinate = [button coordinateWithNormalizedOffset:CGVectorMake(0, 0)]; + [coordinate tap]; +} + @end diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_coordinates.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_coordinates.dart index 185a97e08f00..25247bc7c7bd 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_coordinates.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_coordinates.dart @@ -53,17 +53,28 @@ class _MapCoordinatesBodyState extends State<_MapCoordinatesBody> { _updateVisibleRegion(); return true; }, - child: ListView( + child: Stack( children: [ - Padding( - padding: const EdgeInsets.all(10.0), - child: Center( - child: SizedBox( - width: 300.0, - height: 200.0, - child: googleMap, + ListView( + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Center( + child: SizedBox( + width: 300.0, + height: 200.0, + child: googleMap, + ), + ), ), - ), + // Add a block at the bottom of this list to allow validation that the visible region of the map + // does not change when scrolled under the safe view on iOS. + // https://github.com/flutter/flutter/issues/107913 + const SizedBox( + width: 300, + height: 1000, + ), + ], ), if (mapController != null) Center( @@ -71,13 +82,6 @@ class _MapCoordinatesBodyState extends State<_MapCoordinatesBody> { '\nnortheast: ${_visibleRegion.northeast},' '\nsouthwest: ${_visibleRegion.southwest}'), ), - // Add a block at the bottom of this list to allow validation that the visible region of the map - // does not change when scrolled under the safe view on iOS. - // https://github.com/flutter/flutter/issues/107913 - Container( - width: 300, - height: 1000, - ), ], ), ); diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_ui.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_ui.dart index 009ee71d8400..546cf1d08ff8 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_ui.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_ui.dart @@ -241,7 +241,7 @@ class MapUiBodyState extends State { } Future _getFileData(String path) async { - return await rootBundle.loadString(path); + return rootBundle.loadString(path); } void _setMapStyle(String mapStyle) { diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_marker.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_marker.dart index 7d12f4c81684..2c6c725a4fa5 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/place_marker.dart @@ -286,7 +286,7 @@ class PlaceMarkerBodyState extends State { bitmapIcon.complete(bitmap); })); - return await bitmapIcon.future; + return bitmapIcon.future; } @override diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml index ae61f5d92f3c..ac27996fbc25 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml @@ -4,10 +4,10 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: - cupertino_icons: ^0.1.0 + cupertino_icons: ^1.0.5 flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart index 5298377763aa..a0b46f0a96d1 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/lib/src/google_maps_flutter_ios.dart @@ -172,81 +172,93 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { _mapEventStreamController.add(CameraMoveStartedEvent(mapId)); break; case 'camera#onMove': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(CameraMoveEvent( mapId, - CameraPosition.fromMap(call.arguments['position'])!, + CameraPosition.fromMap(arguments['position'])!, )); break; case 'camera#onIdle': _mapEventStreamController.add(CameraIdleEvent(mapId)); break; case 'marker#onTap': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(MarkerTapEvent( mapId, - MarkerId(call.arguments['markerId'] as String), + MarkerId(arguments['markerId']! as String), )); break; case 'marker#onDragStart': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(MarkerDragStartEvent( mapId, - LatLng.fromJson(call.arguments['position'])!, - MarkerId(call.arguments['markerId'] as String), + LatLng.fromJson(arguments['position'])!, + MarkerId(arguments['markerId']! as String), )); break; case 'marker#onDrag': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(MarkerDragEvent( mapId, - LatLng.fromJson(call.arguments['position'])!, - MarkerId(call.arguments['markerId'] as String), + LatLng.fromJson(arguments['position'])!, + MarkerId(arguments['markerId']! as String), )); break; case 'marker#onDragEnd': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(MarkerDragEndEvent( mapId, - LatLng.fromJson(call.arguments['position'])!, - MarkerId(call.arguments['markerId'] as String), + LatLng.fromJson(arguments['position'])!, + MarkerId(arguments['markerId']! as String), )); break; case 'infoWindow#onTap': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(InfoWindowTapEvent( mapId, - MarkerId(call.arguments['markerId'] as String), + MarkerId(arguments['markerId']! as String), )); break; case 'polyline#onTap': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(PolylineTapEvent( mapId, - PolylineId(call.arguments['polylineId'] as String), + PolylineId(arguments['polylineId']! as String), )); break; case 'polygon#onTap': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(PolygonTapEvent( mapId, - PolygonId(call.arguments['polygonId'] as String), + PolygonId(arguments['polygonId']! as String), )); break; case 'circle#onTap': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(CircleTapEvent( mapId, - CircleId(call.arguments['circleId'] as String), + CircleId(arguments['circleId']! as String), )); break; case 'map#onTap': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(MapTapEvent( mapId, - LatLng.fromJson(call.arguments['position'])!, + LatLng.fromJson(arguments['position'])!, )); break; case 'map#onLongPress': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(MapLongPressEvent( mapId, - LatLng.fromJson(call.arguments['position'])!, + LatLng.fromJson(arguments['position'])!, )); break; case 'tileOverlay#getTile': + final Map arguments = _getArgumentDictionary(call); final Map? tileOverlaysForThisMap = _tileOverlays[mapId]; - final String tileOverlayId = call.arguments['tileOverlayId'] as String; + final String tileOverlayId = arguments['tileOverlayId']! as String; final TileOverlay? tileOverlay = tileOverlaysForThisMap?[TileOverlayId(tileOverlayId)]; final TileProvider? tileProvider = tileOverlay?.tileProvider; @@ -254,9 +266,9 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { return TileProvider.noTile.toJson(); } final Tile tile = await tileProvider.getTile( - call.arguments['x'] as int, - call.arguments['y'] as int, - call.arguments['zoom'] as int?, + arguments['x']! as int, + arguments['y']! as int, + arguments['zoom'] as int?, ); return tile.toJson(); default: @@ -264,6 +276,14 @@ class GoogleMapsFlutterIOS extends GoogleMapsFlutterPlatform { } } + /// Returns the arguments of [call] as typed string-keyed Map. + /// + /// This does not do any type validation, so is only safe to call if the + /// arguments are known to be a map. + Map _getArgumentDictionary(MethodCall call) { + return (call.arguments as Map).cast(); + } + @override Future updateMapOptions( Map optionsUpdate, { diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml index 7ca13a9273f2..c4f8d23cb382 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml @@ -2,11 +2,11 @@ name: google_maps_flutter_ios description: iOS implementation of the google_maps_flutter plugin. repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.1.12 +version: 2.1.13 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart b/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart index 136481cf3abb..a5d376da1684 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart @@ -24,19 +24,24 @@ void main() { required int mapId, required Future? Function(MethodCall call) handler, }) { - maps - .ensureChannelInitialized(mapId) - .setMockMethodCallHandler((MethodCall methodCall) { - log.add(methodCall.method); - return handler(methodCall); - }); + final MethodChannel channel = maps.ensureChannelInitialized(mapId); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + channel, + (MethodCall methodCall) { + log.add(methodCall.method); + return handler(methodCall); + }, + ); } Future sendPlatformMessage( int mapId, String method, Map data) async { final ByteData byteData = const StandardMethodCodec().encodeMethodCall(MethodCall(method, data)); - await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage('plugins.flutter.dev/google_maps_ios_$mapId', byteData, (ByteData? data) {}); } @@ -122,3 +127,9 @@ void main() { equals('drag-end-marker')); }); } + +/// 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_platform_interface/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md index a41d1fe487f3..b3d6c5540e7a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,11 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 2.2.5 + +* Updates code for stricter lint checks. + ## 2.2.4 * Updates code for `no_leading_underscores_for_local_identifiers` lint. diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart index e17510f90624..3fd860e126eb 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart @@ -175,81 +175,93 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { _mapEventStreamController.add(CameraMoveStartedEvent(mapId)); break; case 'camera#onMove': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(CameraMoveEvent( mapId, - CameraPosition.fromMap(call.arguments['position'])!, + CameraPosition.fromMap(arguments['position'])!, )); break; case 'camera#onIdle': _mapEventStreamController.add(CameraIdleEvent(mapId)); break; case 'marker#onTap': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(MarkerTapEvent( mapId, - MarkerId(call.arguments['markerId'] as String), + MarkerId(arguments['markerId']! as String), )); break; case 'marker#onDragStart': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(MarkerDragStartEvent( mapId, - LatLng.fromJson(call.arguments['position'])!, - MarkerId(call.arguments['markerId'] as String), + LatLng.fromJson(arguments['position'])!, + MarkerId(arguments['markerId']! as String), )); break; case 'marker#onDrag': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(MarkerDragEvent( mapId, - LatLng.fromJson(call.arguments['position'])!, - MarkerId(call.arguments['markerId'] as String), + LatLng.fromJson(arguments['position'])!, + MarkerId(arguments['markerId']! as String), )); break; case 'marker#onDragEnd': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(MarkerDragEndEvent( mapId, - LatLng.fromJson(call.arguments['position'])!, - MarkerId(call.arguments['markerId'] as String), + LatLng.fromJson(arguments['position'])!, + MarkerId(arguments['markerId']! as String), )); break; case 'infoWindow#onTap': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(InfoWindowTapEvent( mapId, - MarkerId(call.arguments['markerId'] as String), + MarkerId(arguments['markerId']! as String), )); break; case 'polyline#onTap': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(PolylineTapEvent( mapId, - PolylineId(call.arguments['polylineId'] as String), + PolylineId(arguments['polylineId']! as String), )); break; case 'polygon#onTap': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(PolygonTapEvent( mapId, - PolygonId(call.arguments['polygonId'] as String), + PolygonId(arguments['polygonId']! as String), )); break; case 'circle#onTap': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(CircleTapEvent( mapId, - CircleId(call.arguments['circleId'] as String), + CircleId(arguments['circleId']! as String), )); break; case 'map#onTap': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(MapTapEvent( mapId, - LatLng.fromJson(call.arguments['position'])!, + LatLng.fromJson(arguments['position'])!, )); break; case 'map#onLongPress': + final Map arguments = _getArgumentDictionary(call); _mapEventStreamController.add(MapLongPressEvent( mapId, - LatLng.fromJson(call.arguments['position'])!, + LatLng.fromJson(arguments['position'])!, )); break; case 'tileOverlay#getTile': + final Map arguments = _getArgumentDictionary(call); final Map? tileOverlaysForThisMap = _tileOverlays[mapId]; - final String tileOverlayId = call.arguments['tileOverlayId'] as String; + final String tileOverlayId = arguments['tileOverlayId']! as String; final TileOverlay? tileOverlay = tileOverlaysForThisMap?[TileOverlayId(tileOverlayId)]; final TileProvider? tileProvider = tileOverlay?.tileProvider; @@ -257,9 +269,9 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { return TileProvider.noTile.toJson(); } final Tile tile = await tileProvider.getTile( - call.arguments['x'] as int, - call.arguments['y'] as int, - call.arguments['zoom'] as int?, + arguments['x']! as int, + arguments['y']! as int, + arguments['zoom'] as int?, ); return tile.toJson(); default: @@ -267,6 +279,14 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { } } + /// Returns the arguments of [call] as typed string-keyed Map. + /// + /// This does not do any type validation, so is only safe to call if the + /// arguments are known to be a map. + Map _getArgumentDictionary(MethodCall call) { + return (call.arguments as Map).cast(); + } + @override Future updateMapOptions( Map optionsUpdate, { diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml index 5639ee8c6ad7..6dfff89f8c4b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml @@ -4,11 +4,11 @@ repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_fl issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%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.2.4 +version: 2.2.5 environment: sdk: '>=2.12.0 <3.0.0' - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: collection: ^1.15.0 diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/method_channel/method_channel_google_maps_flutter_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/method_channel/method_channel_google_maps_flutter_test.dart index e5052184915f..ef37c2f221fa 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/method_channel/method_channel_google_maps_flutter_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/method_channel/method_channel_google_maps_flutter_test.dart @@ -24,19 +24,24 @@ void main() { required int mapId, required Future? Function(MethodCall call) handler, }) { - maps - .ensureChannelInitialized(mapId) - .setMockMethodCallHandler((MethodCall methodCall) { - log.add(methodCall.method); - return handler(methodCall); - }); + final MethodChannel channel = maps.ensureChannelInitialized(mapId); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + channel, + (MethodCall methodCall) { + log.add(methodCall.method); + return handler(methodCall); + }, + ); } Future sendPlatformMessage( int mapId, String method, Map data) async { final ByteData byteData = const StandardMethodCodec() .encodeMethodCall(MethodCall(method, data)); - await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage('plugins.flutter.io/google_maps_$mapId', byteData, (ByteData? data) {}); } @@ -120,3 +125,9 @@ void main() { }); }); } + +/// 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_web/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md index 2333f7d16028..42930348965f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -1,5 +1,14 @@ ## NEXT +* Updates minimum Flutter version to 3.0. + +## 0.4.0+5 + +* Updates code for stricter lint checks. + +## 0.4.0+4 + +* Updates code for stricter lint checks. * Updates code for `no_leading_underscores_for_local_identifiers` lint. ## 0.4.0+3 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 35c9c903e982..43f67946464a 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 @@ -4,7 +4,7 @@ publish_to: none # Tests require flutter beta or greater to run. environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart index 2b09950cc00d..25cba849475b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart @@ -98,9 +98,15 @@ gmaps.MapTypeId _gmapTypeIDForPluginType(MapType type) { return gmaps.MapTypeId.HYBRID; case MapType.normal: case MapType.none: - default: return gmaps.MapTypeId.ROADMAP; } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return gmaps.MapTypeId.ROADMAP; } gmaps.MapOptions _applyInitialPosition( @@ -416,31 +422,46 @@ gmaps.PolylineOptions _polylineOptionsFromPolyline( // Translates a [CameraUpdate] into operations on a [gmaps.GMap]. void _applyCameraUpdate(gmaps.GMap map, CameraUpdate update) { + // Casts [value] to a JSON dictionary (string -> nullable object). [value] + // must be a non-null JSON dictionary. + Map asJsonObject(dynamic value) { + return (value as Map).cast(); + } + + // Casts [value] to a JSON list. [value] must be a non-null JSON list. + List asJsonList(dynamic value) { + return value as List; + } + final List json = update.toJson() as List; switch (json[0]) { case 'newCameraPosition': - map.heading = json[1]['bearing'] as num?; - map.zoom = json[1]['zoom'] as num?; + final Map position = asJsonObject(json[1]); + final List latLng = asJsonList(position['target']); + map.heading = position['bearing'] as num?; + map.zoom = position['zoom'] as num?; map.panTo( - gmaps.LatLng( - json[1]['target'][0] as num?, - json[1]['target'][1] as num?, - ), + gmaps.LatLng(latLng[0] as num?, latLng[1] as num?), ); - map.tilt = json[1]['tilt'] as num?; + map.tilt = position['tilt'] as num?; break; case 'newLatLng': - map.panTo(gmaps.LatLng(json[1][0] as num?, json[1][1] as num?)); + final List latLng = asJsonList(json[1]); + map.panTo(gmaps.LatLng(latLng[0] as num?, latLng[1] as num?)); break; case 'newLatLngZoom': + final List latLng = asJsonList(json[1]); map.zoom = json[2] as num?; - map.panTo(gmaps.LatLng(json[1][0] as num?, json[1][1] as num?)); + map.panTo(gmaps.LatLng(latLng[0] as num?, latLng[1] as num?)); break; case 'newLatLngBounds': + final List latLngPair = asJsonList(json[1]); + final List latLng1 = asJsonList(latLngPair[0]); + final List latLng2 = asJsonList(latLngPair[1]); map.fitBounds( gmaps.LatLngBounds( - gmaps.LatLng(json[1][0][0] as num?, json[1][0][1] as num?), - gmaps.LatLng(json[1][1][0] as num?, json[1][1][1] as num?), + gmaps.LatLng(latLng1[0] as num?, latLng1[1] as num?), + gmaps.LatLng(latLng2[0] as num?, latLng2[1] as num?), ), ); // padding = json[2]; @@ -456,10 +477,11 @@ void _applyCameraUpdate(gmaps.GMap map, CameraUpdate update) { final int newZoomDelta = zoomDelta < 0 ? zoomDelta.floor() : zoomDelta.ceil(); if (json.length == 3) { + final List latLng = asJsonList(json[2]); // With focus try { focusLatLng = - _pixelToLatLng(map, json[2][0] as int, json[2][1] as int); + _pixelToLatLng(map, latLng[0]! as int, latLng[1]! as int); } catch (e) { // https://github.com/a14n/dart-google-maps/issues/87 // print('Error computing new focus LatLng. JS Error: ' + e.toString()); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml index 572d9110be8e..072d584b133f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -2,11 +2,11 @@ name: google_maps_flutter_web description: Web platform implementation of google_maps_flutter repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 0.4.0+3 +version: 0.4.0+5 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/tests_exist_elsewhere_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/tests_exist_elsewhere_test.dart index 442c50144727..cc32e6c72f1e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/tests_exist_elsewhere_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/tests_exist_elsewhere_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: avoid_print + import 'package:flutter_test/flutter_test.dart'; void main() { diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index 93497841fbd5..f6b1e5790cc4 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,3 +1,24 @@ +## 6.0.0 + +* **Breaking change** for platform `web`: + * Endorses `google_sign_in_web: ^0.11.0` as the web implementation of the plugin. + * The web package is now backed by the **Google Identity Services (GIS) SDK**, + instead of the **Google Sign-In for Web JS SDK**, which is set to be deprecated + after March 31, 2023. + * Migration information can be found in the + [`google_sign_in_web` package README](https://pub.dev/packages/google_sign_in_web). + +For every platform other than `web`, this version should be identical to `5.4.4`. + +## 5.4.4 + +* Adds documentation for iOS auth with SERVER_CLIENT_ID +* Updates minimum Flutter version to 3.0. + +## 5.4.3 + +* Updates code for stricter lint checks. + ## 5.4.2 * Updates minimum Flutter version to 2.10. diff --git a/packages/google_sign_in/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md index e467ca8541b9..6961bc67b7df 100644 --- a/packages/google_sign_in/google_sign_in/README.md +++ b/packages/google_sign_in/google_sign_in/README.md @@ -43,7 +43,13 @@ This plugin requires iOS 9.0 or higher. 5. Select `GoogleService-Info.plist` from the file manager. 6. A dialog will show up and ask you to select the targets, select the `Runner` target. -7. Then add the `CFBundleURLTypes` attributes below into the +7. If you need to authenticate to a backend server you can add a + `SERVER_CLIENT_ID` key value pair in your `GoogleService-Info.plist`. + ```xml + SERVER_CLIENT_ID + [YOUR SERVER CLIENT ID] + ``` +8. Then add the `CFBundleURLTypes` attributes below into the `[my_project]/ios/Runner/Info.plist` file. ```xml @@ -65,9 +71,9 @@ This plugin requires iOS 9.0 or higher. ``` -As an alternative to adding `GoogleService-Info.plist` to your Xcode project, you can instead -configure your app in Dart code. In this case, skip steps 3-6 and pass `clientId` and -`serverClientId` to the `GoogleSignIn` constructor: +As an alternative to adding `GoogleService-Info.plist` to your Xcode project, +you can instead configure your app in Dart code. In this case, skip steps 3 to 7 + and pass `clientId` and `serverClientId` to the `GoogleSignIn` constructor: ```dart GoogleSignIn _googleSignIn = GoogleSignIn( @@ -79,7 +85,7 @@ GoogleSignIn _googleSignIn = GoogleSignIn( ); ``` -Note that step 7 is still required. +Note that step 8 is still required. #### iOS additional requirement diff --git a/packages/google_sign_in/google_sign_in/example/android/gradle.properties b/packages/google_sign_in/google_sign_in/example/android/gradle.properties index d12b9a8297e5..5c693e744274 100644 --- a/packages/google_sign_in/google_sign_in/example/android/gradle.properties +++ b/packages/google_sign_in/google_sign_in/example/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true diff --git a/packages/google_sign_in/google_sign_in/example/lib/main.dart b/packages/google_sign_in/google_sign_in/example/lib/main.dart index 4c27543f5b18..271069e6e96b 100644 --- a/packages/google_sign_in/google_sign_in/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in/example/lib/main.dart @@ -2,7 +2,7 @@ // 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 +// ignore_for_file: public_member_api_docs, avoid_print import 'dart:async'; import 'dart:convert' show json; @@ -86,12 +86,14 @@ class SignInDemoState extends State { String? _pickFirstNamedContact(Map data) { final List? connections = data['connections'] as List?; final Map? contact = connections?.firstWhere( - (dynamic contact) => contact['names'] != null, + (dynamic contact) => (contact as Map)['names'] != null, orElse: () => null, ) as Map?; if (contact != null) { - final Map? name = contact['names'].firstWhere( - (dynamic name) => name['displayName'] != null, + final List names = contact['names'] as List; + final Map? name = names.firstWhere( + (dynamic name) => + (name as Map)['displayName'] != null, orElse: () => null, ) as Map?; if (name != null) { diff --git a/packages/google_sign_in/google_sign_in/example/pubspec.yaml b/packages/google_sign_in/google_sign_in/example/pubspec.yaml index fbf8f7cf0591..f1cd3828bd87 100644 --- a/packages/google_sign_in/google_sign_in/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index 3ae022306fe6..8e908dc479ed 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -267,7 +267,8 @@ class GoogleSignIn { // Future that completes when we've finished calling `init` on the native side Future? _initialization; - Future _callMethod(Function method) async { + Future _callMethod( + Future Function() method) async { await _ensureInitialized(); final dynamic response = await method(); @@ -324,7 +325,7 @@ class GoogleSignIn { /// method call may be skipped, if there's already [_currentUser] information. /// This is used from the [signIn] and [signInSilently] methods. Future _addMethodCall( - Function method, { + Future Function() method, { bool canSkipCall = false, }) async { Future response; diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index c32dee78468b..ec61a31598d7 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -3,12 +3,11 @@ description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android and iOS. repository: https://github.com/flutter/plugins/tree/main/packages/google_sign_in/google_sign_in issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 5.4.2 - +version: 6.0.0 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -26,7 +25,7 @@ dependencies: google_sign_in_android: ^6.1.0 google_sign_in_ios: ^5.5.0 google_sign_in_platform_interface: ^2.2.0 - google_sign_in_web: ^0.10.0 + google_sign_in_web: ^0.11.0 dev_dependencies: build_runner: ^2.1.10 diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart index b8a596b02065..2296f2d79887 100644 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart @@ -13,7 +13,7 @@ import 'package:mockito/mockito.dart'; import 'google_sign_in_test.mocks.dart'; /// Verify that [GoogleSignInAccount] can be mocked even though it's unused -// ignore: must_be_immutable +// ignore: avoid_implementing_value_types, must_be_immutable class MockGoogleSignInAccount extends Mock implements GoogleSignInAccount {} @GenerateMocks([GoogleSignInPlatform]) 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 342166a8a6af..6ce3cb97e8db 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,24 @@ +## 6.1.6 + +* Minor implementation cleanup +* Updates minimum Flutter version to 3.0. + +## 6.1.5 + +* Updates play-services-auth version to 20.4.1. + +## 6.1.4 + +* Rolls Guava to version 31.1. + +## 6.1.3 + +* Updates play-services-auth version to 20.4.0. + +## 6.1.2 + +* Fixes passing `serverClientId` via the channelled `init` call + ## 6.1.1 * Corrects typos in plugin error logs and removes not actionable warnings. 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 f3a324fefbc9..21b7fa178c8f 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 @@ -29,10 +29,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - // TODO(stuartmorgan): Enable when gradle is updated. - // disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' } @@ -50,8 +47,8 @@ android { } dependencies { - implementation 'com.google.android.gms:play-services-auth:20.3.0' - implementation 'com.google.guava:guava:28.1-android' + implementation 'com.google.android.gms:play-services-auth:20.4.1' + implementation 'com.google.guava:guava:31.1-android' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-inline:4.7.0' + testImplementation 'org.mockito:mockito-inline:5.0.0' } diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java index d345d4976c63..8963a5169e89 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java @@ -404,9 +404,9 @@ public void init( public void signInSilently(Result result) { checkAndSetPendingOperation(METHOD_SIGN_IN_SILENTLY, result); Task task = signInClient.silentSignIn(); - if (task.isSuccessful()) { + if (task.isComplete()) { // There's immediate result available. - onSignInAccount(task.getResult()); + onSignInResult(task); } else { task.addOnCompleteListener( new OnCompleteListener() { @@ -516,7 +516,7 @@ private void onSignInResult(Task completedTask) { GoogleSignInAccount account = completedTask.getResult(ApiException.class); onSignInAccount(account); } catch (ApiException e) { - // Forward all errors and let Dart side decide how to handle. + // Forward all errors and let Dart decide how to handle. String errorCode = errorCodeForStatus(e.getStatusCode()); finishWithError(errorCode, e.toString()); } catch (RuntimeExecutionException e) { @@ -538,14 +538,20 @@ private void onSignInAccount(GoogleSignInAccount account) { } private String errorCodeForStatus(int statusCode) { - if (statusCode == GoogleSignInStatusCodes.SIGN_IN_CANCELLED) { - return ERROR_REASON_SIGN_IN_CANCELED; - } else if (statusCode == CommonStatusCodes.SIGN_IN_REQUIRED) { - return ERROR_REASON_SIGN_IN_REQUIRED; - } else if (statusCode == CommonStatusCodes.NETWORK_ERROR) { - return ERROR_REASON_NETWORK_ERROR; - } else { - return ERROR_REASON_SIGN_IN_FAILED; + switch (statusCode) { + case GoogleSignInStatusCodes.SIGN_IN_CANCELLED: + return ERROR_REASON_SIGN_IN_CANCELED; + case CommonStatusCodes.SIGN_IN_REQUIRED: + return ERROR_REASON_SIGN_IN_REQUIRED; + case CommonStatusCodes.NETWORK_ERROR: + return ERROR_REASON_NETWORK_ERROR; + case GoogleSignInStatusCodes.SIGN_IN_CURRENTLY_IN_PROGRESS: + case GoogleSignInStatusCodes.SIGN_IN_FAILED: + case CommonStatusCodes.INVALID_ACCOUNT: + case CommonStatusCodes.INTERNAL_ERROR: + return ERROR_REASON_SIGN_IN_FAILED; + default: + return ERROR_REASON_SIGN_IN_FAILED; } } diff --git a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java index 9692417390a5..78568460c9e6 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java @@ -17,7 +17,11 @@ import com.google.android.gms.auth.api.signin.GoogleSignInAccount; import com.google.android.gms.auth.api.signin.GoogleSignInClient; import com.google.android.gms.auth.api.signin.GoogleSignInOptions; +import com.google.android.gms.common.api.ApiException; +import com.google.android.gms.common.api.CommonStatusCodes; import com.google.android.gms.common.api.Scope; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.tasks.Task; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; @@ -43,6 +47,7 @@ public class GoogleSignInTest { @Mock GoogleSignInWrapper mockGoogleSignIn; @Mock GoogleSignInAccount account; @Mock GoogleSignInClient mockClient; + @Mock Task mockSignInTask; private GoogleSignInPlugin plugin; @Before @@ -204,6 +209,27 @@ public void signInThrowsWithoutActivity() { plugin.onMethodCall(new MethodCall("signIn", null), null); } + @Test + public void signInSilentlyThatImmediatelyCompletesWithoutResultFinishesWithError() + throws ApiException { + final String clientId = "fakeClientId"; + MethodCall methodCall = buildInitMethodCall(clientId, null); + initAndAssertServerClientId(methodCall, clientId); + + ApiException exception = + new ApiException(new Status(CommonStatusCodes.SIGN_IN_REQUIRED, "Error text")); + when(mockClient.silentSignIn()).thenReturn(mockSignInTask); + when(mockSignInTask.isComplete()).thenReturn(true); + when(mockSignInTask.getResult(ApiException.class)).thenThrow(exception); + + plugin.onMethodCall(new MethodCall("signInSilently", null), result); + verify(result) + .error( + "sign_in_required", + "com.google.android.gms.common.api.ApiException: 4: Error text", + null); + } + @Test public void init_LoadsServerClientIdFromResources() { final String packageName = "fakePackageName"; diff --git a/packages/google_sign_in/google_sign_in_android/example/android/gradle.properties b/packages/google_sign_in/google_sign_in_android/example/android/gradle.properties index d12b9a8297e5..5c693e744274 100644 --- a/packages/google_sign_in/google_sign_in_android/example/android/gradle.properties +++ b/packages/google_sign_in/google_sign_in_android/example/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true diff --git a/packages/google_sign_in/google_sign_in_android/example/lib/main.dart b/packages/google_sign_in/google_sign_in_android/example/lib/main.dart index 5818b6040fcc..90d7da831ef8 100644 --- a/packages/google_sign_in/google_sign_in_android/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in_android/example/lib/main.dart @@ -2,7 +2,7 @@ // 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 +// ignore_for_file: public_member_api_docs, avoid_print import 'dart:async'; import 'dart:convert' show json; diff --git a/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml index 5ac2240cbba1..72d8b82a9bf5 100644 --- a/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart index 731da3968b3f..5a2ccab3250b 100644 --- a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart +++ b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart @@ -45,6 +45,7 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { 'scopes': params.scopes, 'hostedDomain': params.hostedDomain, 'clientId': params.clientId, + 'serverClientId': params.serverClientId, 'forceCodeForRefreshToken': params.forceCodeForRefreshToken, }); } 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 086a15117c2e..4be89f27286a 100644 --- a/packages/google_sign_in/google_sign_in_android/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_android/pubspec.yaml @@ -2,11 +2,11 @@ name: google_sign_in_android description: Android implementation of the google_sign_in plugin. repository: https://github.com/flutter/plugins/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.1 +version: 6.1.6 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart index 948ced3f65bc..b70d2e7bffa6 100644 --- a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart +++ b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart @@ -50,14 +50,19 @@ void main() { setUp(() { responses = Map.from(kDefaultResponses); - channel.setMockMethodCallHandler((MethodCall methodCall) { - log.add(methodCall); - final dynamic response = responses[methodCall.method]; - if (response != null && response is Exception) { - return Future.error('$response'); - } - return Future.value(response); - }); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + channel, + (MethodCall methodCall) { + log.add(methodCall); + final dynamic response = responses[methodCall.method]; + if (response != null && response is Exception) { + return Future.error('$response'); + } + return Future.value(response); + }, + ); log.clear(); }); @@ -99,7 +104,7 @@ void main() { }); test('Other functions pass through arguments to the channel', () async { - final Map tests = { + final Map tests = { () { googleSignIn.init( hostedDomain: 'example.com', @@ -111,6 +116,7 @@ void main() { 'scopes': ['two', 'scopes'], 'signInOption': 'SignInOption.games', 'clientId': 'fakeClientId', + 'serverClientId': null, 'forceCodeForRefreshToken': false, }), () { @@ -119,12 +125,14 @@ void main() { scopes: ['two', 'scopes'], signInOption: SignInOption.games, clientId: 'fakeClientId', + serverClientId: 'fakeServerClientId', forceCodeForRefreshToken: true)); }: isMethodCall('init', arguments: { 'hostedDomain': 'example.com', 'scopes': ['two', 'scopes'], 'signInOption': 'SignInOption.games', 'clientId': 'fakeClientId', + 'serverClientId': 'fakeServerClientId', 'forceCodeForRefreshToken': true, }), () { @@ -149,10 +157,16 @@ void main() { googleSignIn.isSignedIn: isMethodCall('isSignedIn', arguments: null), }; - for (final Function f in tests.keys) { + for (final void Function() f in tests.keys) { f(); } expect(log, tests.values); }); } + +/// 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_sign_in/google_sign_in_ios/CHANGELOG.md b/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md index ecb3b6bee039..495d268bde03 100644 --- a/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md @@ -1,5 +1,10 @@ ## NEXT +* Updates minimum Flutter version to 3.0. + +## 5.5.1 + +* Fixes passing `serverClientId` via the channelled `init` call * Updates minimum Flutter version to 2.10. ## 5.5.0 diff --git a/packages/google_sign_in/google_sign_in_ios/example/lib/main.dart b/packages/google_sign_in/google_sign_in_ios/example/lib/main.dart index e23935ded1da..33deb3d388c8 100644 --- a/packages/google_sign_in/google_sign_in_ios/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in_ios/example/lib/main.dart @@ -2,7 +2,7 @@ // 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 +// ignore_for_file: public_member_api_docs, avoid_print import 'dart:async'; import 'dart:convert' show json; diff --git a/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml index aedc4b01aade..e2e643d1805d 100644 --- a/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart b/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart index 07407eaf5236..d7b6f7936b47 100644 --- a/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart +++ b/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart @@ -49,6 +49,7 @@ class GoogleSignInIOS extends GoogleSignInPlatform { 'scopes': params.scopes, 'hostedDomain': params.hostedDomain, 'clientId': params.clientId, + 'serverClientId': params.serverClientId, }); } diff --git a/packages/google_sign_in/google_sign_in_ios/pubspec.yaml b/packages/google_sign_in/google_sign_in_ios/pubspec.yaml index 24c08cdaa674..69884ca0fe70 100644 --- a/packages/google_sign_in/google_sign_in_ios/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_ios/pubspec.yaml @@ -2,11 +2,11 @@ name: google_sign_in_ios description: iOS implementation of the google_sign_in plugin. repository: https://github.com/flutter/plugins/tree/main/packages/google_sign_in/google_sign_in_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 5.5.0 +version: 5.5.1 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart b/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart index ace65092f61d..6adbdec39b74 100644 --- a/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart +++ b/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart @@ -51,14 +51,19 @@ void main() { setUp(() { responses = Map.from(kDefaultResponses); log = []; - channel.setMockMethodCallHandler((MethodCall methodCall) { - log.add(methodCall); - final dynamic response = responses[methodCall.method]; - if (response != null && response is Exception) { - return Future.error('$response'); - } - return Future.value(response); - }); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + channel, + (MethodCall methodCall) { + log.add(methodCall); + final dynamic response = responses[methodCall.method]; + if (response != null && response is Exception) { + return Future.error('$response'); + } + return Future.value(response); + }, + ); }); test('registered instance', () { @@ -114,7 +119,7 @@ void main() { }); test('Other functions pass through arguments to the channel', () async { - final Map tests = { + final Map tests = { () { googleSignIn.init( hostedDomain: 'example.com', @@ -124,16 +129,19 @@ void main() { 'hostedDomain': 'example.com', 'scopes': ['two', 'scopes'], 'clientId': 'fakeClientId', + 'serverClientId': null, }), () { googleSignIn.initWithParams(const SignInInitParameters( hostedDomain: 'example.com', scopes: ['two', 'scopes'], - clientId: 'fakeClientId')); + clientId: 'fakeClientId', + serverClientId: 'fakeServerClientId')); }: isMethodCall('init', arguments: { 'hostedDomain': 'example.com', 'scopes': ['two', 'scopes'], 'clientId': 'fakeClientId', + 'serverClientId': 'fakeServerClientId', }), () { googleSignIn.getTokens( @@ -152,10 +160,16 @@ void main() { googleSignIn.isSignedIn: isMethodCall('isSignedIn', arguments: null), }; - for (final Function f in tests.keys) { + for (final void Function() f in tests.keys) { f(); } expect(log, tests.values); }); } + +/// 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_sign_in/google_sign_in_platform_interface/CHANGELOG.md b/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md index 01d54b23dae0..8adba8aa966f 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md @@ -1,6 +1,6 @@ ## NEXT -* Updates minimum Flutter version to 2.10. +* Updates minimum Flutter version to 3.0. ## 2.3.0 diff --git a/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml b/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml index 0902069364ce..936257b9d817 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml @@ -8,7 +8,7 @@ version: 2.3.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart b/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart index 944ad3419b8e..0837f6d5d02c 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart @@ -50,7 +50,9 @@ void main() { setUp(() { responses = Map.from(kDefaultResponses); - channel.setMockMethodCallHandler((MethodCall methodCall) { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) { log.add(methodCall); final dynamic response = responses[methodCall.method]; if (response != null && response is Exception) { @@ -95,7 +97,7 @@ void main() { }); test('Other functions pass through arguments to the channel', () async { - final Map tests = { + final Map tests = { () { googleSignIn.init( hostedDomain: 'example.com', @@ -132,7 +134,7 @@ void main() { googleSignIn.isSignedIn: isMethodCall('isSignedIn', arguments: null), }; - for (final Function f in tests.keys) { + for (final void Function() f in tests.keys) { f(); } @@ -160,3 +162,9 @@ void main() { }); }); } + +/// 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_sign_in/google_sign_in_web/CHANGELOG.md b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md index 2816e7284b30..015334d77a59 100644 --- a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md @@ -1,7 +1,15 @@ -## NEXT +## 0.11.0 + +* **Breaking Change:** Migrates JS-interop to `package:google_identity_services_web` + * Uses the new Google Identity Authentication and Authorization JS SDKs. [Docs](https://developers.google.com/identity). + * Added "Migrating to v0.11" section to the `README.md`. +* Updates minimum Flutter version to 3.0. + +## 0.10.2+1 * Updates code for `no_leading_underscores_for_local_identifiers` lint. * Updates minimum Flutter version to 2.10. +* Renames generated folder to js_interop. ## 0.10.2 diff --git a/packages/google_sign_in/google_sign_in_web/README.md b/packages/google_sign_in/google_sign_in_web/README.md index 7c02379808da..64bfd7a20161 100644 --- a/packages/google_sign_in/google_sign_in_web/README.md +++ b/packages/google_sign_in/google_sign_in_web/README.md @@ -2,6 +2,122 @@ The web implementation of [google_sign_in](https://pub.dev/packages/google_sign_in) +## Migrating to v0.11 (Google Identity Services) + +The `google_sign_in_web` plugin is backed by the new Google Identity Services +(GIS) JS SDK since version 0.11.0. + +The GIS SDK is used both for [Authentication](https://developers.google.com/identity/gsi/web/guides/overview) +and [Authorization](https://developers.google.com/identity/oauth2/web/guides/overview) flows. + +The GIS SDK, however, doesn't behave exactly like the one being deprecated. +Some concepts have experienced pretty drastic changes, and that's why this +plugin required a major version update. + +### Differences between Google Identity Services SDK and Google Sign-In for Web SDK. + +The **Google Sign-In JavaScript for Web JS SDK** is set to be deprecated after +March 31, 2023. **Google Identity Services (GIS) SDK** is the new solution to +quickly and easily sign users into your app suing their Google accounts. + +* In the GIS SDK, Authentication and Authorization are now two separate concerns. + * Authentication (information about the current user) flows will not + authorize `scopes` anymore. + * Authorization (permissions for the app to access certain user information) + flows will not return authentication information. +* The GIS SDK no longer has direct access to previously-seen users upon initialization. + * `signInSilently` now displays the One Tap UX for web. +* The GIS SDK only provides an `idToken` (JWT-encoded info) when the user + successfully completes an authentication flow. In the plugin: `signInSilently`. +* The plugin `signIn` method uses the Oauth "Implicit Flow" to Authorize the requested `scopes`. + * If the user hasn't `signInSilently`, they'll have to sign in as a first step + of the Authorization popup flow. + * If `signInSilently` was unsuccessful, the plugin will add extra `scopes` to + `signIn` and retrieve basic Profile information from the People API via a + REST call immediately after a successful authorization. In this case, the + `idToken` field of the `GoogleSignInUserData` will always be null. +* The GIS SDK no longer handles sign-in state and user sessions, it only provides + Authentication credentials for the moment the user did authenticate. +* The GIS SDK no longer is able to renew Authorization sessions on the web. + Once the token expires, API requests will begin to fail with unauthorized, + and user Authorization is required again. + +See more differences in the following migration guides: + +* Authentication > [Migrating from Google Sign-In](https://developers.google.com/identity/gsi/web/guides/migration) +* Authorization > [Migrate to Google Identity Services](https://developers.google.com/identity/oauth2/web/guides/migration-to-gis) + +### New use cases to take into account in your app + +#### Enable access to the People API for your GCP project + +Since the GIS SDK is separating Authentication from Authorization, the +[Oauth Implicit pop-up flow](https://developers.google.com/identity/oauth2/web/guides/use-token-model) +used to Authorize scopes does **not** return any Authentication information +anymore (user credential / `idToken`). + +If the plugin is not able to Authenticate an user from `signInSilently` (the +OneTap UX flow), it'll add extra `scopes` to those requested by the programmer +so it can perform a [People API request](https://developers.google.com/people/api/rest/v1/people/get) +to retrieve basic profile information about the user that is signed-in. + +The information retrieved from the People API is used to complete data for the +[`GoogleSignInAccount`](https://pub.dev/documentation/google_sign_in/latest/google_sign_in/GoogleSignInAccount-class.html) +object that is returned after `signIn` completes successfully. + +#### `signInSilently` always returns `null` + +Previous versions of this plugin were able to return a `GoogleSignInAccount` +object that was fully populated (signed-in and authorized) from `signInSilently` +because the former SDK equated "is authenticated" and "is authorized". + +With the GIS SDK, `signInSilently` only deals with user Authentication, so users +retrieved "silently" will only contain an `idToken`, but not an `accessToken`. + +Only after `signIn` or `requestScopes`, a user will be fully formed. + +The GIS-backed plugin always returns `null` from `signInSilently`, to force apps +that expect the former logic to perform a full `signIn`, which will result in a +fully Authenticated and Authorized user, and making this migration easier. + +#### `idToken` is `null` in the `GoogleSignInAccount` object after `signIn` + +Since the GIS SDK is separating Authentication and Authorization, when a user +fails to Authenticate through `signInSilently` and the plugin performs the +fallback request to the People API described above, +the returned `GoogleSignInUserData` object will contain basic profile information +(name, email, photo, ID), but its `idToken` will be `null`. + +This is because JWT are cryptographically signed by Google Identity Services, and +this plugin won't spoof that signature when it retrieves the information from a +simple REST request. + +#### User Sessions + +Since the GIS SDK does _not_ manage user sessions anymore, apps that relied on +this feature might break. + +If long-lived sessions are required, consider using some user authentication +system that supports Google Sign In as a federated Authentication provider, +like [Firebase Auth](https://firebase.google.com/docs/auth/flutter/federated-auth#google), +or similar. + +#### Expired / Invalid Authorization Tokens + +Since the GIS SDK does _not_ auto-renew authorization tokens anymore, it's now +the responsibility of your app to do so. + +Apps now need to monitor the status code of their REST API requests for response +codes different to `200`. For example: + +* `401`: Missing or invalid access token. +* `403`: Expired access token. + +In either case, your app needs to prompt the end user to `signIn` or +`requestScopes`, to interactively renew the token. + +The GIS SDK limits authorization token duration to one hour (3600 seconds). + ## Usage ### Import the package @@ -12,7 +128,7 @@ normally. This package will be automatically included in your app when you do. ### Web integration -First, go through the instructions [here](https://developers.google.com/identity/sign-in/web/sign-in#before_you_begin) to create your Google Sign-In OAuth client ID. +First, go through the instructions [here](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid) to create your Google Sign-In OAuth client ID. On your `web/index.html` file, add the following `meta` tag, somewhere in the `head` of the document: @@ -29,7 +145,10 @@ You can do this by: 2. Clicking "Edit" in the OAuth 2.0 Web application client that you created above. 3. Adding the URIs you want to the **Authorized JavaScript origins**. -For local development, may add a `localhost` entry, for example: `http://localhost:7357` +For local development, you must add two `localhost` entries: + +* `http://localhost` and +* `http://localhost:7357` (or any port that is free in your machine) #### Starting flutter in http://localhost:7357 @@ -45,40 +164,11 @@ flutter run -d chrome --web-hostname localhost --web-port 7357 Read the rest of the instructions if you need to add extra APIs (like Google People API). - ### Using the plugin -Add the following import to your Dart code: - -```dart -import 'package:google_sign_in/google_sign_in.dart'; -``` - -Initialize GoogleSignIn with the scopes you want: -```dart -GoogleSignIn _googleSignIn = GoogleSignIn( - scopes: [ - 'email', - 'https://www.googleapis.com/auth/contacts.readonly', - ], -); -``` - -[Full list of available scopes](https://developers.google.com/identity/protocols/googlescopes). - -Note that the `serverClientId` parameter of the `GoogleSignIn` constructor is not supported on Web. +See the [**Usage** instructions of `package:google_sign_in`](https://pub.dev/packages/google_sign_in#usage) -You can now use the `GoogleSignIn` class to authenticate in your Dart code, e.g. - -```dart -Future _handleSignIn() async { - try { - await _googleSignIn.signIn(); - } catch (error) { - print(error); - } -} -``` +Note that the **`serverClientId` parameter of the `GoogleSignIn` constructor is not supported on Web.** ## Example @@ -86,7 +176,7 @@ Find the example wiring in the [Google sign-in example application](https://gith ## API details -See the [google_sign_in.dart](https://github.com/flutter/plugins/blob/main/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart) for more API details. +See [google_sign_in.dart](https://github.com/flutter/plugins/blob/main/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart) for more API details. ## Contributions and Testing diff --git a/packages/google_sign_in/google_sign_in_web/example/build.yaml b/packages/google_sign_in/google_sign_in_web/example/build.yaml new file mode 100644 index 000000000000..db3104bb04c6 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + sources: + - integration_test/*.dart + - lib/$lib$ + - $package$ diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_legacy_init_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_legacy_init_test.dart deleted file mode 100644 index 5dada90397fa..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_legacy_init_test.dart +++ /dev/null @@ -1,223 +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. - -// This file is a copy of `auth2_test.dart`, before it was migrated to the -// new `initWithParams` method, and is kept to ensure test coverage of the -// deprecated `init` method, until it is removed. - -import 'dart:html' as html; - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; -import 'package:google_sign_in_web/google_sign_in_web.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:js/js_util.dart' as js_util; - -import 'gapi_mocks/gapi_mocks.dart' as gapi_mocks; -import 'src/test_utils.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - final GoogleSignInTokenData expectedTokenData = - GoogleSignInTokenData(idToken: '70k3n', accessToken: 'access_70k3n'); - - final GoogleSignInUserData expectedUserData = GoogleSignInUserData( - displayName: 'Foo Bar', - email: 'foo@example.com', - id: '123', - photoUrl: 'http://example.com/img.jpg', - idToken: expectedTokenData.idToken, - ); - - late GoogleSignInPlugin plugin; - - group('plugin.initialize() throws a catchable exception', () { - setUp(() { - // The pre-configured use case for the instances of the plugin in this test - gapiUrl = toBase64Url(gapi_mocks.auth2InitError()); - plugin = GoogleSignInPlugin(); - }); - - testWidgets('initialize throws PlatformException', - (WidgetTester tester) async { - await expectLater( - plugin.init( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - ), - throwsA(isA())); - }); - - testWidgets('initialize forwards error code from JS', - (WidgetTester tester) async { - try { - await plugin.init( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - ); - fail('plugin.initialize should have thrown an exception!'); - } catch (e) { - final String code = js_util.getProperty(e, 'code'); - expect(code, 'idpiframe_initialization_failed'); - } - }); - }); - - group('other methods also throw catchable exceptions on initialize fail', () { - // This function ensures that initialize gets called, but for some reason, - // we ignored that it has thrown stuff... - Future discardInit() async { - try { - await plugin.init( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - ); - } catch (e) { - // Noop so we can call other stuff - } - } - - setUp(() { - gapiUrl = toBase64Url(gapi_mocks.auth2InitError()); - plugin = GoogleSignInPlugin(); - }); - - testWidgets('signInSilently throws', (WidgetTester tester) async { - await discardInit(); - await expectLater( - plugin.signInSilently(), throwsA(isA())); - }); - - testWidgets('signIn throws', (WidgetTester tester) async { - await discardInit(); - await expectLater(plugin.signIn(), throwsA(isA())); - }); - - testWidgets('getTokens throws', (WidgetTester tester) async { - await discardInit(); - await expectLater(plugin.getTokens(email: 'test@example.com'), - throwsA(isA())); - }); - testWidgets('requestScopes', (WidgetTester tester) async { - await discardInit(); - await expectLater(plugin.requestScopes(['newScope']), - throwsA(isA())); - }); - }); - - group('auth2 Init Successful', () { - setUp(() { - // The pre-configured use case for the instances of the plugin in this test - gapiUrl = toBase64Url(gapi_mocks.auth2InitSuccess(expectedUserData)); - plugin = GoogleSignInPlugin(); - }); - - testWidgets('Init requires clientId', (WidgetTester tester) async { - expect(plugin.init(hostedDomain: ''), throwsAssertionError); - }); - - testWidgets("Init doesn't accept spaces in scopes", - (WidgetTester tester) async { - expect( - plugin.init( - hostedDomain: '', - clientId: '', - scopes: ['scope with spaces'], - ), - throwsAssertionError); - }); - - // See: https://github.com/flutter/flutter/issues/88084 - testWidgets('Init passes plugin_name parameter with the expected value', - (WidgetTester tester) async { - await plugin.init( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - ); - - final Object? initParameters = - js_util.getProperty(html.window, 'gapi2.init.parameters'); - expect(initParameters, isNotNull); - - final Object? pluginNameParameter = - js_util.getProperty(initParameters!, 'plugin_name'); - expect(pluginNameParameter, isA()); - expect(pluginNameParameter, 'dart-google_sign_in_web'); - }); - - group('Successful .initialize, then', () { - setUp(() async { - await plugin.init( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - ); - await plugin.initialized; - }); - - testWidgets('signInSilently', (WidgetTester tester) async { - final GoogleSignInUserData actualUser = - (await plugin.signInSilently())!; - - expect(actualUser, expectedUserData); - }); - - testWidgets('signIn', (WidgetTester tester) async { - final GoogleSignInUserData actualUser = (await plugin.signIn())!; - - expect(actualUser, expectedUserData); - }); - - testWidgets('getTokens', (WidgetTester tester) async { - final GoogleSignInTokenData actualToken = - await plugin.getTokens(email: expectedUserData.email); - - expect(actualToken, expectedTokenData); - }); - - testWidgets('requestScopes', (WidgetTester tester) async { - final bool scopeGranted = - await plugin.requestScopes(['newScope']); - - expect(scopeGranted, isTrue); - }); - }); - }); - - group('auth2 Init successful, but exception on signIn() method', () { - setUp(() async { - // The pre-configured use case for the instances of the plugin in this test - gapiUrl = toBase64Url(gapi_mocks.auth2SignInError()); - plugin = GoogleSignInPlugin(); - await plugin.init( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - ); - await plugin.initialized; - }); - - testWidgets('User aborts sign in flow, throws PlatformException', - (WidgetTester tester) async { - await expectLater(plugin.signIn(), throwsA(isA())); - }); - - testWidgets('User aborts sign in flow, error code is forwarded from JS', - (WidgetTester tester) async { - try { - await plugin.signIn(); - fail('plugin.signIn() should have thrown an exception!'); - } catch (e) { - final String code = js_util.getProperty(e, 'code'); - expect(code, 'popup_closed_by_user'); - } - }); - }); -} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart deleted file mode 100644 index 3e803b83fa0c..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart +++ /dev/null @@ -1,230 +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:html' as html; - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; -import 'package:google_sign_in_web/google_sign_in_web.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:js/js_util.dart' as js_util; - -import 'gapi_mocks/gapi_mocks.dart' as gapi_mocks; -import 'src/test_utils.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - final GoogleSignInTokenData expectedTokenData = - GoogleSignInTokenData(idToken: '70k3n', accessToken: 'access_70k3n'); - - final GoogleSignInUserData expectedUserData = GoogleSignInUserData( - displayName: 'Foo Bar', - email: 'foo@example.com', - id: '123', - photoUrl: 'http://example.com/img.jpg', - idToken: expectedTokenData.idToken, - ); - - late GoogleSignInPlugin plugin; - - group('plugin.initWithParams() throws a catchable exception', () { - setUp(() { - // The pre-configured use case for the instances of the plugin in this test - gapiUrl = toBase64Url(gapi_mocks.auth2InitError()); - plugin = GoogleSignInPlugin(); - }); - - testWidgets('throws PlatformException', (WidgetTester tester) async { - await expectLater( - plugin.initWithParams(const SignInInitParameters( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - )), - throwsA(isA())); - }); - - testWidgets('forwards error code from JS', (WidgetTester tester) async { - try { - await plugin.initWithParams(const SignInInitParameters( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - )); - fail('plugin.initWithParams should have thrown an exception!'); - } catch (e) { - final String code = js_util.getProperty(e, 'code'); - expect(code, 'idpiframe_initialization_failed'); - } - }); - }); - - group('other methods also throw catchable exceptions on initWithParams fail', - () { - // This function ensures that initWithParams gets called, but for some - // reason, we ignored that it has thrown stuff... - Future discardInit() async { - try { - await plugin.initWithParams(const SignInInitParameters( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - )); - } catch (e) { - // Noop so we can call other stuff - } - } - - setUp(() { - gapiUrl = toBase64Url(gapi_mocks.auth2InitError()); - plugin = GoogleSignInPlugin(); - }); - - testWidgets('signInSilently throws', (WidgetTester tester) async { - await discardInit(); - await expectLater( - plugin.signInSilently(), throwsA(isA())); - }); - - testWidgets('signIn throws', (WidgetTester tester) async { - await discardInit(); - await expectLater(plugin.signIn(), throwsA(isA())); - }); - - testWidgets('getTokens throws', (WidgetTester tester) async { - await discardInit(); - await expectLater(plugin.getTokens(email: 'test@example.com'), - throwsA(isA())); - }); - testWidgets('requestScopes', (WidgetTester tester) async { - await discardInit(); - await expectLater(plugin.requestScopes(['newScope']), - throwsA(isA())); - }); - }); - - group('auth2 Init Successful', () { - setUp(() { - // The pre-configured use case for the instances of the plugin in this test - gapiUrl = toBase64Url(gapi_mocks.auth2InitSuccess(expectedUserData)); - plugin = GoogleSignInPlugin(); - }); - - testWidgets('Init requires clientId', (WidgetTester tester) async { - expect( - plugin.initWithParams(const SignInInitParameters(hostedDomain: '')), - throwsAssertionError); - }); - - testWidgets("Init doesn't accept serverClientId", - (WidgetTester tester) async { - expect( - plugin.initWithParams(const SignInInitParameters( - clientId: '', - serverClientId: '', - )), - throwsAssertionError); - }); - - testWidgets("Init doesn't accept spaces in scopes", - (WidgetTester tester) async { - expect( - plugin.initWithParams(const SignInInitParameters( - hostedDomain: '', - clientId: '', - scopes: ['scope with spaces'], - )), - throwsAssertionError); - }); - - // See: https://github.com/flutter/flutter/issues/88084 - testWidgets('Init passes plugin_name parameter with the expected value', - (WidgetTester tester) async { - await plugin.initWithParams(const SignInInitParameters( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - )); - - final Object? initParameters = - js_util.getProperty(html.window, 'gapi2.init.parameters'); - expect(initParameters, isNotNull); - - final Object? pluginNameParameter = - js_util.getProperty(initParameters!, 'plugin_name'); - expect(pluginNameParameter, isA()); - expect(pluginNameParameter, 'dart-google_sign_in_web'); - }); - - group('Successful .initWithParams, then', () { - setUp(() async { - await plugin.initWithParams(const SignInInitParameters( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - )); - await plugin.initialized; - }); - - testWidgets('signInSilently', (WidgetTester tester) async { - final GoogleSignInUserData actualUser = - (await plugin.signInSilently())!; - - expect(actualUser, expectedUserData); - }); - - testWidgets('signIn', (WidgetTester tester) async { - final GoogleSignInUserData actualUser = (await plugin.signIn())!; - - expect(actualUser, expectedUserData); - }); - - testWidgets('getTokens', (WidgetTester tester) async { - final GoogleSignInTokenData actualToken = - await plugin.getTokens(email: expectedUserData.email); - - expect(actualToken, expectedTokenData); - }); - - testWidgets('requestScopes', (WidgetTester tester) async { - final bool scopeGranted = - await plugin.requestScopes(['newScope']); - - expect(scopeGranted, isTrue); - }); - }); - }); - - group('auth2 Init successful, but exception on signIn() method', () { - setUp(() async { - // The pre-configured use case for the instances of the plugin in this test - gapiUrl = toBase64Url(gapi_mocks.auth2SignInError()); - plugin = GoogleSignInPlugin(); - await plugin.initWithParams(const SignInInitParameters( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - )); - await plugin.initialized; - }); - - testWidgets('User aborts sign in flow, throws PlatformException', - (WidgetTester tester) async { - await expectLater(plugin.signIn(), throwsA(isA())); - }); - - testWidgets('User aborts sign in flow, error code is forwarded from JS', - (WidgetTester tester) async { - try { - await plugin.signIn(); - fail('plugin.signIn() should have thrown an exception!'); - } catch (e) { - final String code = js_util.getProperty(e, 'code'); - expect(code, 'popup_closed_by_user'); - } - }); - }); -} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_legacy_init_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_legacy_init_test.dart deleted file mode 100644 index 7bfef53f7a23..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_legacy_init_test.dart +++ /dev/null @@ -1,51 +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. - -// This file is a copy of `gapi_load_test.dart`, before it was migrated to the -// new `initWithParams` method, and is kept to ensure test coverage of the -// deprecated `init` method, until it is removed. - -import 'dart:html' as html; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; -import 'package:google_sign_in_web/google_sign_in_web.dart'; -import 'package:integration_test/integration_test.dart'; - -import 'gapi_mocks/gapi_mocks.dart' as gapi_mocks; -import 'src/test_utils.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - gapiUrl = toBase64Url(gapi_mocks.auth2InitSuccess( - GoogleSignInUserData(email: 'test@test.com', id: '1234'))); - - testWidgets('Plugin is initialized after GAPI fully loads and init is called', - (WidgetTester tester) async { - expect( - html.querySelector('script[src^="data:"]'), - isNull, - reason: 'Mock script not present before instantiating the plugin', - ); - final GoogleSignInPlugin plugin = GoogleSignInPlugin(); - expect( - html.querySelector('script[src^="data:"]'), - isNotNull, - reason: 'Mock script should be injected', - ); - expect(() { - plugin.initialized; - }, throwsStateError, - reason: - 'The plugin should throw if checking for `initialized` before calling .init'); - await plugin.init(hostedDomain: '', clientId: ''); - await plugin.initialized; - expect( - plugin.initialized, - completes, - reason: 'The plugin should complete the future once initialized.', - ); - }); -} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_test.dart deleted file mode 100644 index fc753e20d92c..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_test.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 'dart:html' as html; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; -import 'package:google_sign_in_web/google_sign_in_web.dart'; -import 'package:integration_test/integration_test.dart'; - -import 'gapi_mocks/gapi_mocks.dart' as gapi_mocks; -import 'src/test_utils.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - gapiUrl = toBase64Url(gapi_mocks.auth2InitSuccess( - GoogleSignInUserData(email: 'test@test.com', id: '1234'))); - - testWidgets('Plugin is initialized after GAPI fully loads and init is called', - (WidgetTester tester) async { - expect( - html.querySelector('script[src^="data:"]'), - isNull, - reason: 'Mock script not present before instantiating the plugin', - ); - final GoogleSignInPlugin plugin = GoogleSignInPlugin(); - expect( - html.querySelector('script[src^="data:"]'), - isNotNull, - reason: 'Mock script should be injected', - ); - expect(() { - plugin.initialized; - }, throwsStateError, - reason: 'The plugin should throw if checking for `initialized` before ' - 'calling .initWithParams'); - await plugin.initWithParams(const SignInInitParameters( - hostedDomain: '', - clientId: '', - )); - await plugin.initialized; - expect( - plugin.initialized, - completes, - reason: 'The plugin should complete the future once initialized.', - ); - }); -} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/gapi_mocks.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/gapi_mocks.dart deleted file mode 100644 index 43eb9a55d06b..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/gapi_mocks.dart +++ /dev/null @@ -1,13 +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. - -library gapi_mocks; - -import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; - -import 'src/gapi.dart'; -import 'src/google_user.dart'; -import 'src/test_iife.dart'; - -part 'src/auth2_init.dart'; diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart deleted file mode 100644 index 84f4e6ee8ba8..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart +++ /dev/null @@ -1,109 +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. - -part of gapi_mocks; - -// JS mock of a gapi.auth2, with a successfully identified user -String auth2InitSuccess(GoogleSignInUserData userData) => testIife(''' -${gapi()} - -var mockUser = ${googleUser(userData)}; - -function GapiAuth2() {} -GapiAuth2.prototype.init = function (initOptions) { - /*Leak the initOptions so we can look at them later.*/ - window['gapi2.init.parameters'] = initOptions; - return { - then: (onSuccess, onError) => { - window.setTimeout(() => { - onSuccess(window.gapi.auth2); - }, 30); - }, - currentUser: { - listen: (cb) => { - window.setTimeout(() => { - cb(mockUser); - }, 30); - } - } - } -}; - -GapiAuth2.prototype.getAuthInstance = function () { - return { - signIn: () => { - return new Promise((resolve, reject) => { - window.setTimeout(() => { - resolve(mockUser); - }, 30); - }); - }, - currentUser: { - get: () => mockUser, - }, - } -}; - -window.gapi.auth2 = new GapiAuth2(); -'''); - -String auth2InitError() => testIife(''' -${gapi()} - -function GapiAuth2() {} -GapiAuth2.prototype.init = function (initOptions) { - return { - then: (onSuccess, onError) => { - window.setTimeout(() => { - onError({ - error: 'idpiframe_initialization_failed', - details: 'This error was raised from a test.', - }); - }, 30); - } - } -}; - -window.gapi.auth2 = new GapiAuth2(); -'''); - -String auth2SignInError([String error = 'popup_closed_by_user']) => testIife(''' -${gapi()} - -var mockUser = null; - -function GapiAuth2() {} -GapiAuth2.prototype.init = function (initOptions) { - return { - then: (onSuccess, onError) => { - window.setTimeout(() => { - onSuccess(window.gapi.auth2); - }, 30); - }, - currentUser: { - listen: (cb) => { - window.setTimeout(() => { - cb(mockUser); - }, 30); - } - } - } -}; - -GapiAuth2.prototype.getAuthInstance = function () { - return { - signIn: () => { - return new Promise((resolve, reject) => { - window.setTimeout(() => { - reject({ - error: '$error' - }); - }, 30); - }); - }, - } -}; - -window.gapi.auth2 = new GapiAuth2(); -'''); diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/gapi.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/gapi.dart deleted file mode 100644 index 0e652c647a38..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/gapi.dart +++ /dev/null @@ -1,12 +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. - -// The JS mock of the global gapi object -String gapi() => ''' -function Gapi() {}; -Gapi.prototype.load = function (script, cb) { - window.setTimeout(cb, 30); -}; -window.gapi = new Gapi(); -'''; diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/google_user.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/google_user.dart deleted file mode 100644 index e5e6eb262502..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/google_user.dart +++ /dev/null @@ -1,30 +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 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; - -// Creates the JS representation of some user data -String googleUser(GoogleSignInUserData data) => ''' -{ - getBasicProfile: () => { - return { - getName: () => '${data.displayName}', - getEmail: () => '${data.email}', - getId: () => '${data.id}', - getImageUrl: () => '${data.photoUrl}', - }; - }, - getAuthResponse: () => { - return { - id_token: '${data.idToken}', - access_token: 'access_${data.idToken}', - } - }, - getGrantedScopes: () => 'some scope', - grant: () => true, - isSignedIn: () => { - return ${data != null ? 'true' : 'false'}; - }, -} -'''; diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/test_iife.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/test_iife.dart deleted file mode 100644 index c5aac367c1de..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/test_iife.dart +++ /dev/null @@ -1,15 +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 'package:google_sign_in_web/src/load_gapi.dart' - show kGapiOnloadCallbackFunctionName; - -// Wraps some JS mock code in an IIFE that ends by calling the onLoad dart callback. -String testIife(String mock) => ''' -(function() { - $mock; - window['$kGapiOnloadCallbackFunctionName'](); -})(); -''' - .replaceAll(RegExp(r'\s{2,}'), ''); diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_utils_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_utils_test.dart deleted file mode 100644 index b341d1d6b96d..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_utils_test.dart +++ /dev/null @@ -1,70 +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 'package:flutter_test/flutter_test.dart'; -import 'package:google_sign_in_web/src/generated/gapiauth2.dart' as gapi; -import 'package:google_sign_in_web/src/utils.dart'; -import 'package:integration_test/integration_test.dart'; - -void main() { - // The non-null use cases are covered by the auth2_test.dart file. - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('gapiUserToPluginUserData', () { - late FakeGoogleUser fakeUser; - - setUp(() { - fakeUser = FakeGoogleUser(); - }); - - testWidgets('null user -> null response', (WidgetTester tester) async { - expect(gapiUserToPluginUserData(null), isNull); - }); - - testWidgets('not signed-in user -> null response', - (WidgetTester tester) async { - expect(gapiUserToPluginUserData(fakeUser), isNull); - }); - - testWidgets('signed-in, but null profile user -> null response', - (WidgetTester tester) async { - fakeUser.setIsSignedIn(true); - expect(gapiUserToPluginUserData(fakeUser), isNull); - }); - - testWidgets('signed-in, null userId in profile user -> null response', - (WidgetTester tester) async { - fakeUser.setIsSignedIn(true); - fakeUser.setBasicProfile(FakeBasicProfile()); - expect(gapiUserToPluginUserData(fakeUser), isNull); - }); - }); -} - -class FakeGoogleUser extends Fake implements gapi.GoogleUser { - bool _isSignedIn = false; - gapi.BasicProfile? _basicProfile; - - @override - bool isSignedIn() => _isSignedIn; - @override - gapi.BasicProfile? getBasicProfile() => _basicProfile; - - // ignore: use_setters_to_change_properties - void setIsSignedIn(bool isSignedIn) { - _isSignedIn = isSignedIn; - } - - // ignore: use_setters_to_change_properties - void setBasicProfile(gapi.BasicProfile basicProfile) { - _basicProfile = basicProfile; - } -} - -class FakeBasicProfile extends Fake implements gapi.BasicProfile { - String? _id; - - @override - String? getId() => _id; -} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart new file mode 100644 index 000000000000..3dcc192e8aaa --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart @@ -0,0 +1,219 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart' show PlatformException; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:google_sign_in_web/google_sign_in_web.dart'; +import 'package:google_sign_in_web/src/gis_client.dart'; +import 'package:google_sign_in_web/src/people.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart' as mockito; + +import 'google_sign_in_web_test.mocks.dart'; +import 'src/dom.dart'; +import 'src/person.dart'; + +// Mock GisSdkClient so we can simulate any response from the JS side. +@GenerateMocks([], customMocks: >[ + MockSpec(onMissingStub: OnMissingStub.returnDefault), +]) +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Constructor', () { + const String expectedClientId = '3xp3c73d_c113n7_1d'; + + testWidgets('Loads clientId when set in a meta', (_) async { + final GoogleSignInPlugin plugin = GoogleSignInPlugin( + debugOverrideLoader: true, + ); + + expect(plugin.autoDetectedClientId, isNull); + + // Add it to the test page now, and try again + final DomHtmlMetaElement meta = + document.createElement('meta') as DomHtmlMetaElement + ..name = clientIdMetaName + ..content = expectedClientId; + + document.head.appendChild(meta); + + final GoogleSignInPlugin another = GoogleSignInPlugin( + debugOverrideLoader: true, + ); + + expect(another.autoDetectedClientId, expectedClientId); + + // cleanup + meta.remove(); + }); + }); + + group('initWithParams', () { + late GoogleSignInPlugin plugin; + late MockGisSdkClient mockGis; + + setUp(() { + plugin = GoogleSignInPlugin( + debugOverrideLoader: true, + ); + mockGis = MockGisSdkClient(); + }); + + testWidgets('initializes if all is OK', (_) async { + await plugin.initWithParams( + const SignInInitParameters( + clientId: 'some-non-null-client-id', + scopes: ['ok1', 'ok2', 'ok3'], + ), + overrideClient: mockGis, + ); + + expect(plugin.initialized, completes); + }); + + testWidgets('asserts clientId is not null', (_) async { + expect(() async { + await plugin.initWithParams( + const SignInInitParameters(), + overrideClient: mockGis, + ); + }, throwsAssertionError); + }); + + testWidgets('asserts serverClientId must be null', (_) async { + expect(() async { + await plugin.initWithParams( + const SignInInitParameters( + clientId: 'some-non-null-client-id', + serverClientId: 'unexpected-non-null-client-id', + ), + overrideClient: mockGis, + ); + }, throwsAssertionError); + }); + + testWidgets('asserts no scopes have any spaces', (_) async { + expect(() async { + await plugin.initWithParams( + const SignInInitParameters( + clientId: 'some-non-null-client-id', + scopes: ['ok1', 'ok2', 'not ok', 'ok3'], + ), + overrideClient: mockGis, + ); + }, throwsAssertionError); + }); + + testWidgets('must be called for most of the API to work', (_) async { + expect(() async { + await plugin.signInSilently(); + }, throwsStateError); + + expect(() async { + await plugin.signIn(); + }, throwsStateError); + + expect(() async { + await plugin.getTokens(email: ''); + }, throwsStateError); + + expect(() async { + await plugin.signOut(); + }, throwsStateError); + + expect(() async { + await plugin.disconnect(); + }, throwsStateError); + + expect(() async { + await plugin.isSignedIn(); + }, throwsStateError); + + expect(() async { + await plugin.clearAuthCache(token: ''); + }, throwsStateError); + + expect(() async { + await plugin.requestScopes([]); + }, throwsStateError); + }); + }); + + group('(with mocked GIS)', () { + late GoogleSignInPlugin plugin; + late MockGisSdkClient mockGis; + const SignInInitParameters options = SignInInitParameters( + clientId: 'some-non-null-client-id', + scopes: ['ok1', 'ok2', 'ok3'], + ); + + setUp(() { + plugin = GoogleSignInPlugin( + debugOverrideLoader: true, + ); + mockGis = MockGisSdkClient(); + }); + + group('signInSilently', () { + setUp(() { + plugin.initWithParams(options, overrideClient: mockGis); + }); + + testWidgets('always returns null, regardless of GIS response', (_) async { + final GoogleSignInUserData someUser = extractUserData(person)!; + + mockito + .when(mockGis.signInSilently()) + .thenAnswer((_) => Future.value(someUser)); + + expect(plugin.signInSilently(), completion(isNull)); + + mockito + .when(mockGis.signInSilently()) + .thenAnswer((_) => Future.value()); + + expect(plugin.signInSilently(), completion(isNull)); + }); + }); + + group('signIn', () { + setUp(() { + plugin.initWithParams(options, overrideClient: mockGis); + }); + + testWidgets('returns the signed-in user', (_) async { + final GoogleSignInUserData someUser = extractUserData(person)!; + + mockito + .when(mockGis.signIn()) + .thenAnswer((_) => Future.value(someUser)); + + expect(await plugin.signIn(), someUser); + }); + + testWidgets('returns null if no user is signed in', (_) async { + mockito + .when(mockGis.signIn()) + .thenAnswer((_) => Future.value()); + + expect(await plugin.signIn(), isNull); + }); + + testWidgets('converts inner errors to PlatformException', (_) async { + mockito.when(mockGis.signIn()).thenThrow('popup_closed'); + + try { + await plugin.signIn(); + fail('signIn should have thrown an exception'); + } catch (exception) { + expect(exception, isA()); + expect((exception as PlatformException).code, 'popup_closed'); + } + }); + }); + }); +} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.mocks.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.mocks.dart new file mode 100644 index 000000000000..b60dac9d4b95 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.mocks.dart @@ -0,0 +1,125 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in google_sign_in_web_integration_tests/integration_test/google_sign_in_web_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i4; + +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart' + as _i2; +import 'package:google_sign_in_web/src/gis_client.dart' as _i3; +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 + +class _FakeGoogleSignInTokenData_0 extends _i1.SmartFake + implements _i2.GoogleSignInTokenData { + _FakeGoogleSignInTokenData_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [GisSdkClient]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockGisSdkClient extends _i1.Mock implements _i3.GisSdkClient { + @override + _i4.Future<_i2.GoogleSignInUserData?> signInSilently() => (super.noSuchMethod( + Invocation.method( + #signInSilently, + [], + ), + returnValue: _i4.Future<_i2.GoogleSignInUserData?>.value(), + returnValueForMissingStub: + _i4.Future<_i2.GoogleSignInUserData?>.value(), + ) as _i4.Future<_i2.GoogleSignInUserData?>); + @override + _i4.Future<_i2.GoogleSignInUserData?> signIn() => (super.noSuchMethod( + Invocation.method( + #signIn, + [], + ), + returnValue: _i4.Future<_i2.GoogleSignInUserData?>.value(), + returnValueForMissingStub: + _i4.Future<_i2.GoogleSignInUserData?>.value(), + ) as _i4.Future<_i2.GoogleSignInUserData?>); + @override + _i2.GoogleSignInTokenData getTokens() => (super.noSuchMethod( + Invocation.method( + #getTokens, + [], + ), + returnValue: _FakeGoogleSignInTokenData_0( + this, + Invocation.method( + #getTokens, + [], + ), + ), + returnValueForMissingStub: _FakeGoogleSignInTokenData_0( + this, + Invocation.method( + #getTokens, + [], + ), + ), + ) as _i2.GoogleSignInTokenData); + @override + _i4.Future signOut() => (super.noSuchMethod( + Invocation.method( + #signOut, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future disconnect() => (super.noSuchMethod( + Invocation.method( + #disconnect, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future isSignedIn() => (super.noSuchMethod( + Invocation.method( + #isSignedIn, + [], + ), + returnValue: _i4.Future.value(false), + returnValueForMissingStub: _i4.Future.value(false), + ) as _i4.Future); + @override + _i4.Future clearAuthCache() => (super.noSuchMethod( + Invocation.method( + #clearAuthCache, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future requestScopes(List? scopes) => (super.noSuchMethod( + Invocation.method( + #requestScopes, + [scopes], + ), + returnValue: _i4.Future.value(false), + returnValueForMissingStub: _i4.Future.value(false), + ) as _i4.Future); +} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/people_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/people_test.dart new file mode 100644 index 000000000000..e81ccb6e95b5 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/people_test.dart @@ -0,0 +1,132 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_identity_services_web/oauth2.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:google_sign_in_web/src/people.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart' as http_test; +import 'package:integration_test/integration_test.dart'; + +import 'src/jsify_as.dart'; +import 'src/person.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('requestUserData', () { + const String expectedAccessToken = '3xp3c73d_4cc355_70k3n'; + + final TokenResponse fakeToken = jsifyAs({ + 'token_type': 'Bearer', + 'access_token': expectedAccessToken, + }); + + testWidgets('happy case', (_) async { + final Completer accessTokenCompleter = Completer(); + + final http.Client mockClient = http_test.MockClient( + (http.Request request) async { + accessTokenCompleter.complete(request.headers['Authorization']); + + return http.Response( + jsonEncode(person), + 200, + headers: {'content-type': 'application/json'}, + ); + }, + ); + + final GoogleSignInUserData? user = await requestUserData( + fakeToken, + overrideClient: mockClient, + ); + + expect(user, isNotNull); + expect(user!.email, expectedPersonEmail); + expect(user.id, expectedPersonId); + expect(user.displayName, expectedPersonName); + expect(user.photoUrl, expectedPersonPhoto); + expect(user.idToken, isNull); + expect( + accessTokenCompleter.future, + completion('Bearer $expectedAccessToken'), + ); + }); + + testWidgets('Unauthorized request - throws exception', (_) async { + final http.Client mockClient = http_test.MockClient( + (http.Request request) async { + return http.Response( + 'Unauthorized', + 403, + ); + }, + ); + + expect(() async { + await requestUserData( + fakeToken, + overrideClient: mockClient, + ); + }, throwsA(isA())); + }); + }); + + group('extractUserData', () { + testWidgets('happy case', (_) async { + final GoogleSignInUserData? user = extractUserData(person); + + expect(user, isNotNull); + expect(user!.email, expectedPersonEmail); + expect(user.id, expectedPersonId); + expect(user.displayName, expectedPersonName); + expect(user.photoUrl, expectedPersonPhoto); + expect(user.idToken, isNull); + }); + + testWidgets('no name/photo - keeps going', (_) async { + final Map personWithoutSomeData = + mapWithoutKeys(person, { + 'names', + 'photos', + }); + + final GoogleSignInUserData? user = extractUserData(personWithoutSomeData); + + expect(user, isNotNull); + expect(user!.email, expectedPersonEmail); + expect(user.id, expectedPersonId); + expect(user.displayName, isNull); + expect(user.photoUrl, isNull); + expect(user.idToken, isNull); + }); + + testWidgets('no userId - throws assertion error', (_) async { + final Map personWithoutId = + mapWithoutKeys(person, { + 'resourceName', + }); + + expect(() { + extractUserData(personWithoutId); + }, throwsAssertionError); + }); + + testWidgets('no email - throws assertion error', (_) async { + final Map personWithoutEmail = + mapWithoutKeys(person, { + 'emailAddresses', + }); + + expect(() { + extractUserData(personWithoutEmail); + }, throwsAssertionError); + }); + }); +} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/src/dom.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/dom.dart new file mode 100644 index 000000000000..f7d3152a7e64 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/dom.dart @@ -0,0 +1,59 @@ +// 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. + +/* +// DOM shim. This file contains everything we need from the DOM API written as +// @staticInterop, so we don't need dart:html +// https://developer.mozilla.org/en-US/docs/Web/API/ +// +// (To be replaced by `package:web`) +*/ + +import 'package:js/js.dart'; + +/// Document interface +@JS() +@staticInterop +abstract class DomHtmlDocument {} + +/// Some methods of document +extension DomHtmlDocumentExtension on DomHtmlDocument { + /// document.head + external DomHtmlElement get head; + + /// document.createElement + external DomHtmlElement createElement(String tagName); +} + +/// An instance of an HTMLElement +@JS() +@staticInterop +abstract class DomHtmlElement {} + +/// (Some) methods of HtmlElement +extension DomHtmlElementExtension on DomHtmlElement { + /// Node.appendChild + external DomHtmlElement appendChild(DomHtmlElement child); + + /// Element.remove + external void remove(); +} + +/// An instance of an HTMLMetaElement +@JS() +@staticInterop +abstract class DomHtmlMetaElement extends DomHtmlElement {} + +/// Some methods exclusive of Script elements +extension DomHtmlMetaElementExtension on DomHtmlMetaElement { + external set name(String name); + external set content(String content); +} + +// Getters + +/// window.document +@JS() +@staticInterop +external DomHtmlDocument get document; diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/src/jsify_as.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/jsify_as.dart new file mode 100644 index 000000000000..82547b284fe0 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/jsify_as.dart @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:js/js_util.dart' as js_util; + +/// Converts a [data] object into a JS Object of type `T`. +T jsifyAs(Map data) { + return js_util.jsify(data) as T; +} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/src/jwt_examples.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/jwt_examples.dart new file mode 100644 index 000000000000..72841c5165ee --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/jwt_examples.dart @@ -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. + +import 'package:google_identity_services_web/id.dart'; + +import 'jsify_as.dart'; + +/// A CredentialResponse with null `credential`. +final CredentialResponse nullCredential = + jsifyAs({ + 'credential': null, +}); + +/// A CredentialResponse wrapping a known good JWT Token as its `credential`. +final CredentialResponse goodCredential = + jsifyAs({ + 'credential': goodJwtToken, +}); + +/// A JWT token with predefined values. +/// +/// 'email': 'adultman@example.com', +/// 'sub': '123456', +/// 'name': 'Vincent Adultman', +/// 'picture': 'https://thispersondoesnotexist.com/image?x=.jpg', +/// +/// Signed with HS256 and the private key: 'symmetric-encryption-is-weak' +const String goodJwtToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.$goodPayload.lqzULA_U3YzEl_-fL7YLU-kFXmdD2ttJLTv-UslaNQ4'; + +/// The payload of a JWT token that contains predefined values. +/// +/// 'email': 'adultman@example.com', +/// 'sub': '123456', +/// 'name': 'Vincent Adultman', +/// 'picture': 'https://thispersondoesnotexist.com/image?x=.jpg', +const String goodPayload = + 'eyJlbWFpbCI6ImFkdWx0bWFuQGV4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2IiwibmFtZSI6IlZpbmNlbnQgQWR1bHRtYW4iLCJwaWN0dXJlIjoiaHR0cHM6Ly90aGlzcGVyc29uZG9lc25vdGV4aXN0LmNvbS9pbWFnZT94PS5qcGcifQ'; + +// More encrypted JWT Tokens may be created on https://jwt.io. +// +// First, decode the `goodJwtToken` above, modify to your heart's +// content, and add a new credential here. +// +// (New tokens can also be created with `package:jose` and `dart:convert`.) diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/src/person.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/person.dart new file mode 100644 index 000000000000..2525596eabe9 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/person.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. + +const String expectedPersonId = '1234567890'; +const String expectedPersonName = 'Vincent Adultman'; +const String expectedPersonEmail = 'adultman@example.com'; +const String expectedPersonPhoto = + 'https://thispersondoesnotexist.com/image?x=.jpg'; + +/// A subset of https://developers.google.com/people/api/rest/v1/people#Person. +final Map person = { + 'resourceName': 'people/$expectedPersonId', + 'emailAddresses': [ + { + 'metadata': { + 'primary': false, + }, + 'value': 'bad@example.com', + }, + { + 'metadata': {}, + 'value': 'nope@example.com', + }, + { + 'metadata': { + 'primary': true, + }, + 'value': expectedPersonEmail, + }, + ], + 'names': [ + { + 'metadata': { + 'primary': true, + }, + 'displayName': expectedPersonName, + }, + { + 'metadata': { + 'primary': false, + }, + 'displayName': 'Fakey McFakeface', + }, + ], + 'photos': [ + { + 'metadata': { + 'primary': true, + }, + 'url': expectedPersonPhoto, + }, + ], +}; + +/// Returns a copy of [map] without the [keysToRemove]. +T mapWithoutKeys>( + T map, + Set keysToRemove, +) { + return Map.fromEntries( + map.entries.where((MapEntry entry) { + return !keysToRemove.contains(entry.key); + }), + ) as T; +} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/src/test_utils.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/test_utils.dart deleted file mode 100644 index 56aa61df136e..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/src/test_utils.dart +++ /dev/null @@ -1,10 +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:convert'; - -String toBase64Url(String contents) { - // Open the file - return 'data:text/javascript;base64,${base64.encode(utf8.encode(contents))}'; -} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/utils_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/utils_test.dart new file mode 100644 index 000000000000..82701e587be1 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/utils_test.dart @@ -0,0 +1,173 @@ +// 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:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_identity_services_web/id.dart'; +import 'package:google_identity_services_web/oauth2.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:google_sign_in_web/src/utils.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'src/jsify_as.dart'; +import 'src/jwt_examples.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('gisResponsesToTokenData', () { + testWidgets('null objects -> no problem', (_) async { + final GoogleSignInTokenData tokens = gisResponsesToTokenData(null, null); + + expect(tokens.accessToken, isNull); + expect(tokens.idToken, isNull); + expect(tokens.serverAuthCode, isNull); + }); + + testWidgets('non-null objects are correctly used', (_) async { + const String expectedIdToken = 'some-value-for-testing'; + const String expectedAccessToken = 'another-value-for-testing'; + + final CredentialResponse credential = + jsifyAs({ + 'credential': expectedIdToken, + }); + final TokenResponse token = jsifyAs({ + 'access_token': expectedAccessToken, + }); + final GoogleSignInTokenData tokens = + gisResponsesToTokenData(credential, token); + + expect(tokens.accessToken, expectedAccessToken); + expect(tokens.idToken, expectedIdToken); + expect(tokens.serverAuthCode, isNull); + }); + }); + + group('gisResponsesToUserData', () { + testWidgets('happy case', (_) async { + final GoogleSignInUserData data = gisResponsesToUserData(goodCredential)!; + + expect(data.displayName, 'Vincent Adultman'); + expect(data.id, '123456'); + expect(data.email, 'adultman@example.com'); + expect(data.photoUrl, 'https://thispersondoesnotexist.com/image?x=.jpg'); + expect(data.idToken, goodJwtToken); + }); + + testWidgets('null response -> null', (_) async { + expect(gisResponsesToUserData(null), isNull); + }); + + testWidgets('null response.credential -> null', (_) async { + expect(gisResponsesToUserData(nullCredential), isNull); + }); + + testWidgets('invalid payload -> null', (_) async { + final CredentialResponse response = + jsifyAs({ + 'credential': 'some-bogus.thing-that-is-not.valid-jwt', + }); + expect(gisResponsesToUserData(response), isNull); + }); + }); + + group('getJwtTokenPayload', () { + testWidgets('happy case -> data', (_) async { + final Map? data = getJwtTokenPayload(goodJwtToken); + + expect(data, isNotNull); + expect(data, containsPair('name', 'Vincent Adultman')); + expect(data, containsPair('email', 'adultman@example.com')); + expect(data, containsPair('sub', '123456')); + expect( + data, + containsPair( + 'picture', + 'https://thispersondoesnotexist.com/image?x=.jpg', + )); + }); + + testWidgets('null Token -> null', (_) async { + final Map? data = getJwtTokenPayload(null); + + expect(data, isNull); + }); + + testWidgets('Token not matching the format -> null', (_) async { + final Map? data = getJwtTokenPayload('1234.4321'); + + expect(data, isNull); + }); + + testWidgets('Bad token that matches the format -> null', (_) async { + final Map? data = getJwtTokenPayload('1234.abcd.4321'); + + expect(data, isNull); + }); + }); + + group('decodeJwtPayload', () { + testWidgets('Good payload -> data', (_) async { + final Map? data = decodeJwtPayload(goodPayload); + + expect(data, isNotNull); + expect(data, containsPair('name', 'Vincent Adultman')); + expect(data, containsPair('email', 'adultman@example.com')); + expect(data, containsPair('sub', '123456')); + expect( + data, + containsPair( + 'picture', + 'https://thispersondoesnotexist.com/image?x=.jpg', + )); + }); + + testWidgets('Proper JSON payload -> data', (_) async { + final String payload = base64.encode(utf8.encode('{"properJson": true}')); + + final Map? data = decodeJwtPayload(payload); + + expect(data, isNotNull); + expect(data, containsPair('properJson', true)); + }); + + testWidgets('Not-normalized base-64 payload -> data', (_) async { + // This is the payload generated by the "Proper JSON payload" test, but + // we remove the leading "=" symbols so it's length is not a multiple of 4 + // anymore! + final String payload = 'eyJwcm9wZXJKc29uIjogdHJ1ZX0='.replaceAll('=', ''); + + final Map? data = decodeJwtPayload(payload); + + expect(data, isNotNull); + expect(data, containsPair('properJson', true)); + }); + + testWidgets('Invalid JSON payload -> null', (_) async { + final String payload = base64.encode(utf8.encode('{properJson: false}')); + + final Map? data = decodeJwtPayload(payload); + + expect(data, isNull); + }); + + testWidgets('Non JSON payload -> null', (_) async { + final String payload = base64.encode(utf8.encode('not-json')); + + final Map? data = decodeJwtPayload(payload); + + expect(data, isNull); + }); + + testWidgets('Non base-64 payload -> null', (_) async { + const String payload = 'not-base-64-at-all'; + + final Map? data = decodeJwtPayload(payload); + + expect(data, isNull); + }); + }); +} diff --git a/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml index e5abdacf944d..c73953374696 100644 --- a/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml @@ -3,7 +3,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: @@ -12,12 +12,15 @@ dependencies: path: ../ dev_dependencies: + build_runner: ^2.1.1 flutter_driver: sdk: flutter flutter_test: sdk: flutter + google_identity_services_web: ^0.2.0 google_sign_in_platform_interface: ^2.2.0 http: ^0.13.0 integration_test: sdk: flutter js: ^0.6.3 + mockito: ^5.3.2 diff --git a/.ci/scripts/plugin_tools_tests.sh b/packages/google_sign_in/google_sign_in_web/example/regen_mocks.sh old mode 100644 new mode 100755 similarity index 56% rename from .ci/scripts/plugin_tools_tests.sh rename to packages/google_sign_in/google_sign_in_web/example/regen_mocks.sh index 96eec4349f08..78bcdc0f9e28 --- a/.ci/scripts/plugin_tools_tests.sh +++ b/packages/google_sign_in/google_sign_in_web/example/regen_mocks.sh @@ -1,7 +1,10 @@ -#!/bin/bash +#!/usr/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. -cd script/tool -dart pub run test +flutter pub get + +echo "(Re)generating mocks." + +flutter pub run build_runner build --delete-conflicting-outputs diff --git a/packages/google_sign_in/google_sign_in_web/example/run_test.sh b/packages/google_sign_in/google_sign_in_web/example/run_test.sh index 28877dce8d6e..fcac5f600acb 100755 --- a/packages/google_sign_in/google_sign_in_web/example/run_test.sh +++ b/packages/google_sign_in/google_sign_in_web/example/run_test.sh @@ -6,9 +6,11 @@ if pgrep -lf chromedriver > /dev/null; then echo "chromedriver is running." + ./regen_mocks.sh + if [ $# -eq 0 ]; then echo "No target specified, running all tests..." - find integration_test/ -iname *_test.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target='{}' + find integration_test/ -iname *_test.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target='{}' else echo "Running test target: $1..." set -x @@ -17,7 +19,6 @@ if pgrep -lf chromedriver > /dev/null; then else echo "chromedriver is not running." + echo "Please, check the README.md for instructions on how to use run_test.sh" fi - - diff --git a/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart b/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart index c305cae2a33d..827b17ca5b44 100644 --- a/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart +++ b/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart @@ -5,23 +5,22 @@ import 'dart:async'; import 'dart:html' as html; -import 'package:flutter/foundation.dart' show visibleForTesting; -import 'package:flutter/services.dart'; +import 'package:flutter/foundation.dart' show visibleForTesting, kDebugMode; +import 'package:flutter/services.dart' show PlatformException; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:google_identity_services_web/loader.dart' as loader; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; -import 'package:js/js.dart'; -import 'src/generated/gapiauth2.dart' as auth2; -import 'src/load_gapi.dart' as gapi; -import 'src/utils.dart' show gapiUserToPluginUserData; +import 'src/gis_client.dart'; -const String _kClientIdMetaSelector = 'meta[name=google-signin-client_id]'; -const String _kClientIdAttributeName = 'content'; +/// The `name` of the meta-tag to define a ClientID in HTML. +const String clientIdMetaName = 'google-signin-client_id'; -/// This is only exposed for testing. It shouldn't be accessed by users of the -/// plugin as it could break at any point. -@visibleForTesting -String gapiUrl = 'https://apis.google.com/js/platform.js'; +/// The selector used to find the meta-tag that defines a ClientID in HTML. +const String clientIdMetaSelector = 'meta[name=$clientIdMetaName]'; + +/// The attribute name that stores the Client ID in the meta-tag that defines a Client ID in HTML. +const String clientIdAttributeName = 'content'; /// Implementation of the google_sign_in plugin for Web. class GoogleSignInPlugin extends GoogleSignInPlatform { @@ -29,18 +28,24 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { /// background. /// /// The plugin is completely initialized when [initialized] completed. - GoogleSignInPlugin() { - _autoDetectedClientId = html - .querySelector(_kClientIdMetaSelector) - ?.getAttribute(_kClientIdAttributeName); - - _isGapiInitialized = gapi.inject(gapiUrl).then((_) => gapi.init()); + GoogleSignInPlugin({@visibleForTesting bool debugOverrideLoader = false}) { + autoDetectedClientId = html + .querySelector(clientIdMetaSelector) + ?.getAttribute(clientIdAttributeName); + + if (debugOverrideLoader) { + _jsSdkLoadedFuture = Future.value(true); + } else { + _jsSdkLoadedFuture = loader.loadWebSdk(); + } } - late Future _isGapiInitialized; - late Future _isAuthInitialized; + late Future _jsSdkLoadedFuture; bool _isInitCalled = false; + // The instance of [GisSdkClient] backing the plugin. + late GisSdkClient _gisClient; + // This method throws if init or initWithParams hasn't been called at some // point in the past. It is used by the [initialized] getter to ensure that // users can't await on a Future that will never resolve. @@ -53,14 +58,16 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { } } - /// A future that resolves when both GAPI and Auth2 have been correctly initialized. + /// A future that resolves when the SDK has been correctly loaded. @visibleForTesting Future get initialized { _assertIsInitCalled(); - return Future.wait(>[_isGapiInitialized, _isAuthInitialized]); + return _jsSdkLoadedFuture; } - String? _autoDetectedClientId; + /// Stores the client ID if it was set in a meta-tag of the page. + @visibleForTesting + late String? autoDetectedClientId; /// Factory method that initializes the plugin with [GoogleSignInPlatform]. static void registerWith(Registrar registrar) { @@ -83,8 +90,11 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { } @override - Future initWithParams(SignInInitParameters params) async { - final String? appClientId = params.clientId ?? _autoDetectedClientId; + Future initWithParams( + SignInInitParameters params, { + @visibleForTesting GisSdkClient? overrideClient, + }) async { + final String? appClientId = params.clientId ?? autoDetectedClientId; assert( appClientId != null, 'ClientID not set. Either set it on a ' @@ -100,141 +110,95 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { 'Check https://developers.google.com/identity/protocols/googlescopes ' 'for a list of valid OAuth 2.0 scopes.'); - await _isGapiInitialized; + await _jsSdkLoadedFuture; - final auth2.GoogleAuth auth = auth2.init(auth2.ClientConfig( - hosted_domain: params.hostedDomain, - // The js lib wants a space-separated list of values - scope: params.scopes.join(' '), - client_id: appClientId!, - plugin_name: 'dart-google_sign_in_web', - )); + _gisClient = overrideClient ?? + GisSdkClient( + clientId: appClientId!, + hostedDomain: params.hostedDomain, + initialScopes: List.from(params.scopes), + loggingEnabled: kDebugMode, + ); - final Completer isAuthInitialized = Completer(); - _isAuthInitialized = isAuthInitialized.future; _isInitCalled = true; - - auth.then(allowInterop((auth2.GoogleAuth initializedAuth) { - // onSuccess - - // TODO(ditman): https://github.com/flutter/flutter/issues/48528 - // This plugin doesn't notify the app of external changes to the - // state of the authentication, i.e: if you logout elsewhere... - - isAuthInitialized.complete(); - }), allowInterop((auth2.GoogleAuthInitFailureError reason) { - // onError - isAuthInitialized.completeError(PlatformException( - code: reason.error, - message: reason.details, - details: - 'https://developers.google.com/identity/sign-in/web/reference#error_codes', - )); - })); - - return _isAuthInitialized; } @override Future signInSilently() async { await initialized; - return gapiUserToPluginUserData( - auth2.getAuthInstance()?.currentUser?.get()); + // Since the new GIS SDK does *not* perform authorization at the same time as + // authentication (and every one of our users expects that), we need to tell + // the plugin that this failed regardless of the actual result. + // + // However, if this succeeds, we'll save a People API request later. + return _gisClient.signInSilently().then((_) => null); } @override Future signIn() async { await initialized; + + // This method mainly does oauth2 authorization, which happens to also do + // authentication if needed. However, the authentication information is not + // returned anymore. + // + // This method will synthesize authentication information from the People API + // if needed (or use the last identity seen from signInSilently). try { - return gapiUserToPluginUserData(await auth2.getAuthInstance()?.signIn()); - } on auth2.GoogleAuthSignInError catch (reason) { + return _gisClient.signIn(); + } catch (reason) { throw PlatformException( - code: reason.error, - message: 'Exception raised from GoogleAuth.signIn()', + code: reason.toString(), + message: 'Exception raised from signIn', details: - 'https://developers.google.com/identity/sign-in/web/reference#error_codes_2', + 'https://developers.google.com/identity/oauth2/web/guides/error', ); } } @override - Future getTokens( - {required String email, bool? shouldRecoverAuth}) async { + Future getTokens({ + required String email, + bool? shouldRecoverAuth, + }) async { await initialized; - final auth2.GoogleUser? currentUser = - auth2.getAuthInstance()?.currentUser?.get(); - final auth2.AuthResponse? response = currentUser?.getAuthResponse(); - - return GoogleSignInTokenData( - idToken: response?.id_token, accessToken: response?.access_token); + return _gisClient.getTokens(); } @override Future signOut() async { await initialized; - return auth2.getAuthInstance()?.signOut(); + _gisClient.signOut(); } @override Future disconnect() async { await initialized; - final auth2.GoogleUser? currentUser = - auth2.getAuthInstance()?.currentUser?.get(); - - if (currentUser == null) { - return; - } - - return currentUser.disconnect(); + _gisClient.disconnect(); } @override Future isSignedIn() async { await initialized; - final auth2.GoogleUser? currentUser = - auth2.getAuthInstance()?.currentUser?.get(); - - if (currentUser == null) { - return false; - } - - return currentUser.isSignedIn(); + return _gisClient.isSignedIn(); } @override Future clearAuthCache({required String token}) async { await initialized; - return auth2.getAuthInstance()?.disconnect(); + _gisClient.clearAuthCache(); } @override Future requestScopes(List scopes) async { await initialized; - final auth2.GoogleUser? currentUser = - auth2.getAuthInstance()?.currentUser?.get(); - - if (currentUser == null) { - return false; - } - - final String grantedScopes = currentUser.getGrantedScopes() ?? ''; - final Iterable missingScopes = - scopes.where((String scope) => !grantedScopes.contains(scope)); - - if (missingScopes.isEmpty) { - return true; - } - - final Object? response = await currentUser - .grant(auth2.SigninOptions(scope: missingScopes.join(' '))); - - return response != null; + return _gisClient.requestScopes(scopes); } } diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart deleted file mode 100644 index a6d5b9d8dbbb..000000000000 --- a/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart +++ /dev/null @@ -1,54 +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. - -/// Type definitions for Google API Client -/// Project: https://github.com/google/google-api-javascript-client -/// Definitions by: Frank M , grant -/// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -/// TypeScript Version: 2.3 - -// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/gapi - -// ignore_for_file: public_member_api_docs, unused_element, sort_constructors_first, prefer_generic_function_type_aliases - -@JS() -library gapi; - -import 'package:js/js.dart'; - -// Module gapi -typedef void LoadCallback( - [dynamic args1, - dynamic args2, - dynamic args3, - dynamic args4, - dynamic args5]); - -@anonymous -@JS() -abstract class LoadConfig { - external LoadCallback get callback; - external set callback(LoadCallback v); - external Function? get onerror; - external set onerror(Function? v); - external num? get timeout; - external set timeout(num? v); - external Function? get ontimeout; - external set ontimeout(Function? v); - external factory LoadConfig( - {LoadCallback callback, - Function? onerror, - num? timeout, - Function? ontimeout}); -} - -/*type CallbackOrConfig = LoadConfig | LoadCallback;*/ -/// Pragmatically initialize gapi class member. -/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiloadlibraries-callbackorconfig -@JS('gapi.load') -external void load( - String apiName, dynamic /*LoadConfig|LoadCallback*/ callback); -// End module gapi - -// Manually removed gapi.auth and gapi.client, unused by this plugin. diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart deleted file mode 100644 index f474e0d00f69..000000000000 --- a/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart +++ /dev/null @@ -1,503 +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. - -/// Type definitions for non-npm package Google Sign-In API 0.0 -/// Project: https://developers.google.com/identity/sign-in/web/ -/// Definitions by: Derek Lawless -/// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -/// TypeScript Version: 2.3 - -/// - -// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/gapi.auth2 - -// ignore_for_file: public_member_api_docs, unused_element, non_constant_identifier_names, sort_constructors_first, always_specify_types, strict_raw_type - -@JS() -library gapiauth2; - -import 'package:js/js.dart'; -import 'package:js/js_util.dart' show promiseToFuture; - -@anonymous -@JS() -class GoogleAuthInitFailureError { - external String get error; - external set error(String? value); - - external String get details; - external set details(String? value); -} - -@anonymous -@JS() -class GoogleAuthSignInError { - external String get error; - external set error(String value); -} - -@anonymous -@JS() -class OfflineAccessResponse { - external String? get code; - external set code(String? value); -} - -// Module gapi.auth2 -/// GoogleAuth is a singleton class that provides methods to allow the user to sign in with a Google account, -/// get the user's current sign-in status, get specific data from the user's Google profile, -/// request additional scopes, and sign out from the current account. -@JS('gapi.auth2.GoogleAuth') -class GoogleAuth { - external IsSignedIn get isSignedIn; - external set isSignedIn(IsSignedIn v); - external CurrentUser? get currentUser; - external set currentUser(CurrentUser? v); - - /// Calls the onInit function when the GoogleAuth object is fully initialized, or calls the onFailure function if - /// initialization fails. - external dynamic then(dynamic Function(GoogleAuth googleAuth) onInit, - [dynamic Function(GoogleAuthInitFailureError reason) onFailure]); - - /// Signs out all accounts from the application. - external dynamic signOut(); - - /// Revokes all of the scopes that the user granted. - external dynamic disconnect(); - - /// Attaches the sign-in flow to the specified container's click handler. - external dynamic attachClickHandler( - dynamic container, - SigninOptions options, - dynamic Function(GoogleUser googleUser) onsuccess, - dynamic Function(String reason) onfailure); -} - -@anonymous -@JS() -abstract class _GoogleAuth { - external Promise signIn( - [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]); - external Promise grantOfflineAccess( - [OfflineAccessOptions? options]); -} - -extension GoogleAuthExtensions on GoogleAuth { - Future signIn( - [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]) { - final _GoogleAuth tt = this as _GoogleAuth; - return promiseToFuture(tt.signIn(options)); - } - - Future grantOfflineAccess( - [OfflineAccessOptions? options]) { - final _GoogleAuth tt = this as _GoogleAuth; - return promiseToFuture(tt.grantOfflineAccess(options)); - } -} - -@anonymous -@JS() -abstract class IsSignedIn { - /// Returns whether the current user is currently signed in. - external bool get(); - - /// Listen for changes in the current user's sign-in state. - external void listen(dynamic Function(bool signedIn) listener); -} - -@anonymous -@JS() -abstract class CurrentUser { - /// Returns a GoogleUser object that represents the current user. Note that in a newly-initialized - /// GoogleAuth instance, the current user has not been set. Use the currentUser.listen() method or the - /// GoogleAuth.then() to get an initialized GoogleAuth instance. - external GoogleUser get(); - - /// Listen for changes in currentUser. - external void listen(dynamic Function(GoogleUser user) listener); -} - -@anonymous -@JS() -abstract class SigninOptions { - /// The package name of the Android app to install over the air. - /// See Android app installs from your web site: - /// https://developers.google.com/identity/sign-in/web/android-app-installs - external String? get app_package_name; - external set app_package_name(String? v); - - /// Fetch users' basic profile information when they sign in. - /// Adds 'profile', 'email' and 'openid' to the requested scopes. - /// True if unspecified. - external bool? get fetch_basic_profile; - external set fetch_basic_profile(bool? v); - - /// Specifies whether to prompt the user for re-authentication. - /// See OpenID Connect Request Parameters: - /// https://openid.net/specs/openid-connect-basic-1_0.html#RequestParameters - external String? get prompt; - external set prompt(String? v); - - /// The scopes to request, as a space-delimited string. - /// Optional if fetch_basic_profile is not set to false. - external String? get scope; - external set scope(String? v); - - /// The UX mode to use for the sign-in flow. - /// By default, it will open the consent flow in a popup. - external String? /*'popup'|'redirect'*/ get ux_mode; - external set ux_mode(String? /*'popup'|'redirect'*/ v); - - /// If using ux_mode='redirect', this parameter allows you to override the default redirect_uri that will be used at the end of the consent flow. - /// The default redirect_uri is the current URL stripped of query parameters and hash fragment. - external String? get redirect_uri; - external set redirect_uri(String? v); - - // When your app knows which user it is trying to authenticate, it can provide this parameter as a hint to the authentication server. - // Passing this hint suppresses the account chooser and either pre-fill the email box on the sign-in form, or select the proper session (if the user is using multiple sign-in), - // which can help you avoid problems that occur if your app logs in the wrong user account. The value can be either an email address or the sub string, - // which is equivalent to the user's Google ID. - // https://developers.google.com/identity/protocols/OpenIDConnect?hl=en#authenticationuriparameters - external String? get login_hint; - external set login_hint(String? v); - - external factory SigninOptions( - {String app_package_name, - bool fetch_basic_profile, - String prompt, - String scope, - String /*'popup'|'redirect'*/ ux_mode, - String redirect_uri, - String login_hint}); -} - -/// Definitions by: John -/// Interface that represents the different configuration parameters for the GoogleAuth.grantOfflineAccess(options) method. -/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2offlineaccessoptions -@anonymous -@JS() -abstract class OfflineAccessOptions { - external String? get scope; - external set scope(String? v); - external String? /*'select_account'|'consent'*/ get prompt; - external set prompt(String? /*'select_account'|'consent'*/ v); - external String? get app_package_name; - external set app_package_name(String? v); - external factory OfflineAccessOptions( - {String scope, - String /*'select_account'|'consent'*/ prompt, - String app_package_name}); -} - -/// Interface that represents the different configuration parameters for the gapi.auth2.init method. -/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2clientconfig -@anonymous -@JS() -abstract class ClientConfig { - /// The app's client ID, found and created in the Google Developers Console. - external String? get client_id; - external set client_id(String? v); - - /// The domains for which to create sign-in cookies. Either a URI, single_host_origin, or none. - /// Defaults to single_host_origin if unspecified. - external String? get cookie_policy; - external set cookie_policy(String? v); - - /// The scopes to request, as a space-delimited string. Optional if fetch_basic_profile is not set to false. - external String? get scope; - external set scope(String? v); - - /// Fetch users' basic profile information when they sign in. Adds 'profile' and 'email' to the requested scopes. True if unspecified. - external bool? get fetch_basic_profile; - external set fetch_basic_profile(bool? v); - - /// The Google Apps domain to which users must belong to sign in. This is susceptible to modification by clients, - /// so be sure to verify the hosted domain property of the returned user. Use GoogleUser.getHostedDomain() on the client, - /// and the hd claim in the ID Token on the server to verify the domain is what you expected. - external String? get hosted_domain; - external set hosted_domain(String? v); - - /// Used only for OpenID 2.0 client migration. Set to the value of the realm that you are currently using for OpenID 2.0, - /// as described in OpenID 2.0 (Migration). - external String? get openid_realm; - external set openid_realm(String? v); - - /// The UX mode to use for the sign-in flow. - /// By default, it will open the consent flow in a popup. - external String? /*'popup'|'redirect'*/ get ux_mode; - external set ux_mode(String? /*'popup'|'redirect'*/ v); - - /// If using ux_mode='redirect', this parameter allows you to override the default redirect_uri that will be used at the end of the consent flow. - /// The default redirect_uri is the current URL stripped of query parameters and hash fragment. - external String? get redirect_uri; - external set redirect_uri(String? v); - - /// Allows newly created Client IDs to use the Google Platform Library from now until the March 30th, 2023 deprecation date. - /// See: https://github.com/flutter/flutter/issues/88084 - external String? get plugin_name; - external set plugin_name(String? v); - - external factory ClientConfig({ - String client_id, - String cookie_policy, - String scope, - bool fetch_basic_profile, - String? hosted_domain, - String openid_realm, - String /*'popup'|'redirect'*/ ux_mode, - String redirect_uri, - String plugin_name, - }); -} - -@JS('gapi.auth2.SigninOptionsBuilder') -class SigninOptionsBuilder { - external dynamic setAppPackageName(String name); - external dynamic setFetchBasicProfile(bool fetch); - external dynamic setPrompt(String prompt); - external dynamic setScope(String scope); - external dynamic setLoginHint(String hint); -} - -@anonymous -@JS() -abstract class BasicProfile { - external String? getId(); - external String? getName(); - external String? getGivenName(); - external String? getFamilyName(); - external String? getImageUrl(); - external String? getEmail(); -} - -/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authresponse -@anonymous -@JS() -abstract class AuthResponse { - external String? get access_token; - external set access_token(String? v); - external String? get id_token; - external set id_token(String? v); - external String? get login_hint; - external set login_hint(String? v); - external String? get scope; - external set scope(String? v); - external num? get expires_in; - external set expires_in(num? v); - external num? get first_issued_at; - external set first_issued_at(num? v); - external num? get expires_at; - external set expires_at(num? v); - external factory AuthResponse( - {String? access_token, - String? id_token, - String? login_hint, - String? scope, - num? expires_in, - num? first_issued_at, - num? expires_at}); -} - -/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authorizeconfig -@anonymous -@JS() -abstract class AuthorizeConfig { - external String get client_id; - external set client_id(String v); - external String get scope; - external set scope(String v); - external String? get response_type; - external set response_type(String? v); - external String? get prompt; - external set prompt(String? v); - external String? get cookie_policy; - external set cookie_policy(String? v); - external String? get hosted_domain; - external set hosted_domain(String? v); - external String? get login_hint; - external set login_hint(String? v); - external String? get app_package_name; - external set app_package_name(String? v); - external String? get openid_realm; - external set openid_realm(String? v); - external bool? get include_granted_scopes; - external set include_granted_scopes(bool? v); - external factory AuthorizeConfig( - {String client_id, - String scope, - String response_type, - String prompt, - String cookie_policy, - String hosted_domain, - String login_hint, - String app_package_name, - String openid_realm, - bool include_granted_scopes}); -} - -/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authorizeresponse -@anonymous -@JS() -abstract class AuthorizeResponse { - external String get access_token; - external set access_token(String v); - external String get id_token; - external set id_token(String v); - external String get code; - external set code(String v); - external String get scope; - external set scope(String v); - external num get expires_in; - external set expires_in(num v); - external num get first_issued_at; - external set first_issued_at(num v); - external num get expires_at; - external set expires_at(num v); - external String get error; - external set error(String v); - external String get error_subtype; - external set error_subtype(String v); - external factory AuthorizeResponse( - {String access_token, - String id_token, - String code, - String scope, - num expires_in, - num first_issued_at, - num expires_at, - String error, - String error_subtype}); -} - -/// A GoogleUser object represents one user account. -@anonymous -@JS() -abstract class GoogleUser { - /// Get the user's unique ID string. - external String? getId(); - - /// Returns true if the user is signed in. - external bool isSignedIn(); - - /// Get the user's Google Apps domain if the user signed in with a Google Apps account. - external String? getHostedDomain(); - - /// Get the scopes that the user granted as a space-delimited string. - external String? getGrantedScopes(); - - /// Get the user's basic profile information. - external BasicProfile? getBasicProfile(); - - /// Get the response object from the user's auth session. - // This returns an empty JS object when the user hasn't attempted to sign in. - external AuthResponse getAuthResponse([bool includeAuthorizationData]); - - /// Returns true if the user granted the specified scopes. - external bool hasGrantedScopes(String scopes); - - // Has the API for grant and grantOfflineAccess changed? - /// Request additional scopes to the user. - /// - /// See GoogleAuth.signIn() for the list of parameters and the error code. - external dynamic grant( - [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]); - - /// Get permission from the user to access the specified scopes offline. - /// When you use GoogleUser.grantOfflineAccess(), the sign-in flow skips the account chooser step. - /// See GoogleUser.grantOfflineAccess(). - external void grantOfflineAccess(String scopes); - - /// Revokes all of the scopes that the user granted. - external void disconnect(); -} - -@anonymous -@JS() -abstract class _GoogleUser { - /// Forces a refresh of the access token, and then returns a Promise for the new AuthResponse. - external Promise reloadAuthResponse(); -} - -extension GoogleUserExtensions on GoogleUser { - Future reloadAuthResponse() { - final _GoogleUser tt = this as _GoogleUser; - return promiseToFuture(tt.reloadAuthResponse()); - } -} - -/// Initializes the GoogleAuth object. -/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2initparams -@JS('gapi.auth2.init') -external GoogleAuth init(ClientConfig params); - -/// Returns the GoogleAuth object. You must initialize the GoogleAuth object with gapi.auth2.init() before calling this method. -@JS('gapi.auth2.getAuthInstance') -external GoogleAuth? getAuthInstance(); - -/// Performs a one time OAuth 2.0 authorization. -/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authorizeparams-callback -@JS('gapi.auth2.authorize') -external void authorize( - AuthorizeConfig params, void Function(AuthorizeResponse response) callback); -// End module gapi.auth2 - -// Module gapi.signin2 -@JS('gapi.signin2.render') -external void render( - dynamic id, - dynamic - /*{ - /** - * The auth scope or scopes to authorize. Auth scopes for individual APIs can be found in their documentation. - */ - scope?: string; - - /** - * The width of the button in pixels (default: 120). - */ - width?: number; - - /** - * The height of the button in pixels (default: 36). - */ - height?: number; - - /** - * Display long labels such as "Sign in with Google" rather than "Sign in" (default: false). - */ - longtitle?: boolean; - - /** - * The color theme of the button: either light or dark (default: light). - */ - theme?: string; - - /** - * The callback function to call when a user successfully signs in (default: none). - */ - onsuccess?(user: auth2.GoogleUser): void; - - /** - * The callback function to call when sign-in fails (default: none). - */ - onfailure?(reason: { error: string }): void; - - /** - * The package name of the Android app to install over the air. See - * Android app installs from your web site. - * Optional. (default: none) - */ - app_package_name?: string; - }*/ - options); - -// End module gapi.signin2 -@JS() -abstract class Promise { - external factory Promise( - void Function(void Function(T result) resolve, Function reject) executor); - external Promise then(void Function(T result) onFulfilled, - [Function onRejected]); -} diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/gis_client.dart b/packages/google_sign_in/google_sign_in_web/lib/src/gis_client.dart new file mode 100644 index 000000000000..3815322e6900 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/lib/src/gis_client.dart @@ -0,0 +1,310 @@ +// 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'; + +// TODO(dit): Split `id` and `oauth2` "services" for mocking. https://github.com/flutter/flutter/issues/120657 +import 'package:google_identity_services_web/id.dart'; +import 'package:google_identity_services_web/oauth2.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +// ignore: unnecessary_import +import 'package:js/js.dart'; +import 'package:js/js_util.dart'; + +import 'people.dart' as people; +import 'utils.dart' as utils; + +/// A client to hide (most) of the interaction with the GIS SDK from the plugin. +/// +/// (Overridable for testing) +class GisSdkClient { + /// Create a GisSdkClient object. + GisSdkClient({ + required List initialScopes, + required String clientId, + bool loggingEnabled = false, + String? hostedDomain, + }) : _initialScopes = initialScopes { + if (loggingEnabled) { + id.setLogLevel('debug'); + } + // Configure the Stream objects that are going to be used by the clients. + _configureStreams(); + + // Initialize the SDK clients we need. + _initializeIdClient( + clientId, + onResponse: _onCredentialResponse, + ); + + _tokenClient = _initializeTokenClient( + clientId, + hostedDomain: hostedDomain, + onResponse: _onTokenResponse, + onError: _onTokenError, + ); + } + + // Configure the credential (authentication) and token (authorization) response streams. + void _configureStreams() { + _tokenResponses = StreamController.broadcast(); + _credentialResponses = StreamController.broadcast(); + _tokenResponses.stream.listen((TokenResponse response) { + _lastTokenResponse = response; + }, onError: (Object error) { + _lastTokenResponse = null; + }); + _credentialResponses.stream.listen((CredentialResponse response) { + _lastCredentialResponse = response; + }, onError: (Object error) { + _lastCredentialResponse = null; + }); + } + + // Initializes the `id` SDK for the silent-sign in (authentication) client. + void _initializeIdClient( + String clientId, { + required CallbackFn onResponse, + }) { + // Initialize `id` for the silent-sign in code. + final IdConfiguration idConfig = IdConfiguration( + client_id: clientId, + callback: allowInterop(onResponse), + cancel_on_tap_outside: false, + auto_select: true, // Attempt to sign-in silently. + ); + id.initialize(idConfig); + } + + // Handle a "normal" credential (authentication) response. + // + // (Normal doesn't mean successful, this might contain `error` information.) + void _onCredentialResponse(CredentialResponse response) { + if (response.error != null) { + _credentialResponses.addError(response.error!); + } else { + _credentialResponses.add(response); + } + } + + // Creates a `oauth2.TokenClient` used for authorization (scope) requests. + TokenClient _initializeTokenClient( + String clientId, { + String? hostedDomain, + required TokenClientCallbackFn onResponse, + required ErrorCallbackFn onError, + }) { + // Create a Token Client for authorization calls. + final TokenClientConfig tokenConfig = TokenClientConfig( + client_id: clientId, + hosted_domain: hostedDomain, + callback: allowInterop(_onTokenResponse), + error_callback: allowInterop(_onTokenError), + // `scope` will be modified by the `signIn` method, in case we need to + // backfill user Profile info. + scope: ' ', + ); + return oauth2.initTokenClient(tokenConfig); + } + + // Handle a "normal" token (authorization) response. + // + // (Normal doesn't mean successful, this might contain `error` information.) + void _onTokenResponse(TokenResponse response) { + if (response.error != null) { + _tokenResponses.addError(response.error!); + } else { + _tokenResponses.add(response); + } + } + + // Handle a "not-directly-related-to-authorization" error. + // + // Token clients have an additional `error_callback` for miscellaneous + // errors, like "popup couldn't open" or "popup closed by user". + void _onTokenError(Object? error) { + // This is handled in a funky (js_interop) way because of: + // https://github.com/dart-lang/sdk/issues/50899 + _tokenResponses.addError(getProperty(error!, 'type')); + } + + /// Attempts to sign-in the user using the OneTap UX flow. + /// + /// If the user consents, to OneTap, the [GoogleSignInUserData] will be + /// generated from a proper [CredentialResponse], which contains `idToken`. + /// Else, it'll be synthesized by a request to the People API later, and the + /// `idToken` will be null. + Future signInSilently() async { + final Completer userDataCompleter = + Completer(); + + // Ask the SDK to render the OneClick sign-in. + // + // And also handle its "moments". + id.prompt(allowInterop((PromptMomentNotification moment) { + _onPromptMoment(moment, userDataCompleter); + })); + + return userDataCompleter.future; + } + + // Handles "prompt moments" of the OneClick card UI. + // + // See: https://developers.google.com/identity/gsi/web/guides/receive-notifications-prompt-ui-status + Future _onPromptMoment( + PromptMomentNotification moment, + Completer completer, + ) async { + if (completer.isCompleted) { + return; // Skip once the moment has been handled. + } + + if (moment.isDismissedMoment() && + moment.getDismissedReason() == + MomentDismissedReason.credential_returned) { + // Kick this part of the handler to the bottom of the JS event queue, so + // the _credentialResponses stream has time to propagate its last value, + // and we can use _lastCredentialResponse. + return Future.delayed(Duration.zero, () { + completer + .complete(utils.gisResponsesToUserData(_lastCredentialResponse)); + }); + } + + // In any other 'failed' moments, return null and add an error to the stream. + if (moment.isNotDisplayed() || + moment.isSkippedMoment() || + moment.isDismissedMoment()) { + final String reason = moment.getNotDisplayedReason()?.toString() ?? + moment.getSkippedReason()?.toString() ?? + moment.getDismissedReason()?.toString() ?? + 'unknown_error'; + + _credentialResponses.addError(reason); + completer.complete(null); + } + } + + /// Starts an oauth2 "implicit" flow to authorize requests. + /// + /// The new GIS SDK does not return user authentication from this flow, so: + /// * If [_lastCredentialResponse] is **not** null (the user has successfully + /// `signInSilently`), we return that after this method completes. + /// * If [_lastCredentialResponse] is null, we add [people.scopes] to the + /// [_initialScopes], so we can retrieve User Profile information back + /// from the People API (without idToken). See [people.requestUserData]. + Future signIn() async { + // If we already know the user, use their `email` as a `hint`, so they don't + // have to pick their user again in the Authorization popup. + final GoogleSignInUserData? knownUser = + utils.gisResponsesToUserData(_lastCredentialResponse); + // This toggles a popup, so `signIn` *must* be called with + // user activation. + _tokenClient.requestAccessToken(OverridableTokenClientConfig( + prompt: knownUser == null ? 'select_account' : '', + hint: knownUser?.email, + scope: [ + ..._initialScopes, + // If the user hasn't gone through the auth process, + // the plugin will attempt to `requestUserData` after, + // so we need extra scopes to retrieve that info. + if (_lastCredentialResponse == null) ...people.scopes, + ].join(' '), + )); + + await _tokenResponses.stream.first; + + return _computeUserDataForLastToken(); + } + + // This function returns the currently signed-in [GoogleSignInUserData]. + // + // It'll do a request to the People API (if needed). + Future _computeUserDataForLastToken() async { + // If the user hasn't authenticated, request their basic profile info + // from the People API. + // + // This synthetic response will *not* contain an `idToken` field. + if (_lastCredentialResponse == null && _requestedUserData == null) { + assert(_lastTokenResponse != null); + _requestedUserData = await people.requestUserData(_lastTokenResponse!); + } + // Complete user data either with the _lastCredentialResponse seen, + // or the synthetic _requestedUserData from above. + return utils.gisResponsesToUserData(_lastCredentialResponse) ?? + _requestedUserData; + } + + /// Returns a [GoogleSignInTokenData] from the latest seen responses. + GoogleSignInTokenData getTokens() { + return utils.gisResponsesToTokenData( + _lastCredentialResponse, + _lastTokenResponse, + ); + } + + /// Revokes the current authentication. + Future signOut() async { + clearAuthCache(); + id.disableAutoSelect(); + } + + /// Revokes the current authorization and authentication. + Future disconnect() async { + if (_lastTokenResponse != null) { + oauth2.revoke(_lastTokenResponse!.access_token); + } + signOut(); + } + + /// Returns true if the client has recognized this user before. + Future isSignedIn() async { + return _lastCredentialResponse != null || _requestedUserData != null; + } + + /// Clears all the cached results from authentication and authorization. + Future clearAuthCache() async { + _lastCredentialResponse = null; + _lastTokenResponse = null; + _requestedUserData = null; + } + + /// Requests the list of [scopes] passed in to the client. + /// + /// Keeps the previously granted scopes. + Future requestScopes(List scopes) async { + _tokenClient.requestAccessToken(OverridableTokenClientConfig( + scope: scopes.join(' '), + include_granted_scopes: true, + )); + + await _tokenResponses.stream.first; + + return oauth2.hasGrantedAllScopes(_lastTokenResponse!, scopes); + } + + // The scopes initially requested by the developer. + // + // We store this because we might need to add more at `signIn`. If the user + // doesn't `silentSignIn`, we expand this list to consult the People API to + // return some basic Authentication information. + final List _initialScopes; + + // The Google Identity Services client for oauth requests. + late TokenClient _tokenClient; + + // Streams of credential and token responses. + late StreamController _credentialResponses; + late StreamController _tokenResponses; + + // The last-seen credential and token responses + CredentialResponse? _lastCredentialResponse; + TokenResponse? _lastTokenResponse; + + // If the user *authenticates* (signs in) through oauth2, the SDK doesn't return + // identity information anymore, so we synthesize it by calling the PeopleAPI + // (if needed) + // + // (This is a synthetic _lastCredentialResponse) + GoogleSignInUserData? _requestedUserData; +} diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart b/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart deleted file mode 100644 index f60d6cd57e56..000000000000 --- a/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart +++ /dev/null @@ -1,59 +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. - -@JS() -library gapi_onload; - -import 'dart:async'; - -import 'package:flutter/foundation.dart' show visibleForTesting; -import 'package:js/js.dart'; - -import 'generated/gapi.dart' as gapi; -import 'utils.dart' show injectJSLibraries; - -@JS() -external set gapiOnloadCallback(Function callback); - -// This name must match the external setter above -/// This is only exposed for testing. It shouldn't be accessed by users of the -/// plugin as it could break at any point. -@visibleForTesting -const String kGapiOnloadCallbackFunctionName = 'gapiOnloadCallback'; -String _addOnloadToScript(String url) => url.startsWith('data:') - ? url - : '$url?onload=$kGapiOnloadCallbackFunctionName'; - -/// Injects the GAPI library by its [url], and other additional [libraries]. -/// -/// GAPI has an onload API where it'll call a callback when it's ready, JSONP style. -Future inject(String url, {List libraries = const []}) { - // Inject the GAPI library, and configure the onload global - final Completer gapiOnLoad = Completer(); - gapiOnloadCallback = allowInterop(() { - // Funnel the GAPI onload to a Dart future - gapiOnLoad.complete(); - }); - - // Attach the onload callback to the main url - final List allLibraries = [ - _addOnloadToScript(url), - ...libraries - ]; - - return Future.wait( - >[injectJSLibraries(allLibraries), gapiOnLoad.future]); -} - -/// Initialize the global gapi object so 'auth2' can be used. -/// Returns a promise that resolves when 'auth2' is ready. -Future init() { - final Completer gapiLoadCompleter = Completer(); - gapi.load('auth2', allowInterop(() { - gapiLoadCompleter.complete(); - })); - - // After this resolves, we can use gapi.auth2! - return gapiLoadCompleter.future; -} diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/people.dart b/packages/google_sign_in/google_sign_in_web/lib/src/people.dart new file mode 100644 index 000000000000..528dc89b1a75 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/lib/src/people.dart @@ -0,0 +1,152 @@ +// 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:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:google_identity_services_web/oauth2.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:http/http.dart' as http; + +/// Basic scopes for self-id +const List scopes = [ + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/userinfo.email', +]; + +/// People API to return my profile info... +const String MY_PROFILE = 'https://content-people.googleapis.com/v1/people/me' + '?sources=READ_SOURCE_TYPE_PROFILE' + '&personFields=photos%2Cnames%2CemailAddresses'; + +/// Requests user data from the People API using the given [tokenResponse]. +Future requestUserData( + TokenResponse tokenResponse, { + @visibleForTesting http.Client? overrideClient, +}) async { + // Request my profile from the People API. + final Map person = await _doRequest( + MY_PROFILE, + tokenResponse, + overrideClient: overrideClient, + ); + + // Now transform the Person response into a GoogleSignInUserData. + return extractUserData(person); +} + +/// Extracts user data from a Person resource. +/// +/// See: https://developers.google.com/people/api/rest/v1/people#Person +GoogleSignInUserData? extractUserData(Map json) { + final String? userId = _extractUserId(json); + final String? email = _extractPrimaryField( + json['emailAddresses'] as List?, + 'value', + ); + + assert(userId != null); + assert(email != null); + + return GoogleSignInUserData( + id: userId!, + email: email!, + displayName: _extractPrimaryField( + json['names'] as List?, + 'displayName', + ), + photoUrl: _extractPrimaryField( + json['photos'] as List?, + 'url', + ), + // Synthetic user data doesn't contain an idToken! + ); +} + +/// Extracts the ID from a Person resource. +/// +/// The User ID looks like this: +/// { +/// 'resourceName': 'people/PERSON_ID', +/// ... +/// } +String? _extractUserId(Map profile) { + final String? resourceName = profile['resourceName'] as String?; + return resourceName?.split('/').last; +} + +/// Extracts the [fieldName] marked as 'primary' from a list of [values]. +/// +/// Values can be one of: +/// * `emailAddresses` +/// * `names` +/// * `photos` +/// +/// From a Person object. +T? _extractPrimaryField(List? values, String fieldName) { + if (values != null) { + for (final Object? value in values) { + if (value != null && value is Map) { + final bool isPrimary = _extractPath( + value, + path: ['metadata', 'primary'], + defaultValue: false, + ); + if (isPrimary) { + return value[fieldName] as T?; + } + } + } + } + + return null; +} + +/// Attempts to get the property in [path] of type `T` from a deeply nested [source]. +/// +/// Returns [default] if the property is not found. +T _extractPath( + Map source, { + required List path, + required T defaultValue, +}) { + final String valueKey = path.removeLast(); + Object? data = source; + for (final String key in path) { + if (data != null && data is Map) { + data = data[key]; + } else { + break; + } + } + if (data != null && data is Map) { + return (data[valueKey] ?? defaultValue) as T; + } else { + return defaultValue; + } +} + +/// Gets from [url] with an authorization header defined by [token]. +/// +/// Attempts to [jsonDecode] the result. +Future> _doRequest( + String url, + TokenResponse token, { + http.Client? overrideClient, +}) async { + final Uri uri = Uri.parse(url); + final http.Client client = overrideClient ?? http.Client(); + try { + final http.Response response = + await client.get(uri, headers: { + 'Authorization': '${token.token_type} ${token.access_token}', + }); + if (response.statusCode != 200) { + throw http.ClientException(response.body, uri); + } + return jsonDecode(response.body) as Map; + } finally { + client.close(); + } +} diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart index 72424d8ea15b..c4bb9d403d2d 100644 --- a/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart +++ b/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart @@ -2,59 +2,87 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; -import 'dart:html' as html; +import 'dart:convert'; +import 'package:google_identity_services_web/id.dart'; +import 'package:google_identity_services_web/oauth2.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; -import 'generated/gapiauth2.dart' as auth2; - -/// Injects a list of JS [libraries] as `script` tags into a [target] [html.HtmlElement]. -/// -/// If [target] is not provided, it defaults to the web app's `head` tag (see `web/index.html`). -/// [libraries] is a list of URLs that are used as the `src` attribute of `script` tags -/// to which an `onLoad` listener is attached (one per URL). -/// -/// Returns a [Future] that resolves when all of the `script` tags `onLoad` events trigger. -Future injectJSLibraries( - List libraries, { - html.HtmlElement? target, -}) { - final List> loading = >[]; - final List tags = []; - - final html.Element targetElement = target ?? html.querySelector('head')!; - - for (final String library in libraries) { - final html.ScriptElement script = html.ScriptElement() - ..async = true - ..defer = true - // ignore: unsafe_html - ..src = library; - // TODO(ditman): add a timeout race to fail this future - loading.add(script.onLoad.first); - tags.add(script); +/// A codec that can encode/decode JWT payloads. +/// +/// See https://www.rfc-editor.org/rfc/rfc7519#section-3 +final Codec jwtCodec = json.fuse(utf8).fuse(base64); + +/// A RegExp that can match, and extract parts from a JWT Token. +/// +/// A JWT token consists of 3 base-64 encoded parts of data separated by periods: +/// +/// header.payload.signature +/// +/// More info: https://regexr.com/789qc +final RegExp jwtTokenRegexp = RegExp( + r'^(?

[^\.\s]+)\.(?[^\.\s]+)\.(?[^\.\s]+)$'); + +/// Decodes the `claims` of a JWT token and returns them as a Map. +/// +/// JWT `claims` are stored as a JSON object in the `payload` part of the token. +/// +/// (This method does not validate the signature of the token.) +/// +/// See https://www.rfc-editor.org/rfc/rfc7519#section-3 +Map? getJwtTokenPayload(String? token) { + if (token != null) { + final RegExpMatch? match = jwtTokenRegexp.firstMatch(token); + if (match != null) { + return decodeJwtPayload(match.namedGroup('payload')); + } } - targetElement.children.addAll(tags); - return Future.wait(loading); + return null; } -/// Utility method that converts `currentUser` to the equivalent [GoogleSignInUserData]. +/// Decodes a JWT payload using the [jwtCodec]. +Map? decodeJwtPayload(String? payload) { + try { + // Payload must be normalized before passing it to the codec + return jwtCodec.decode(base64.normalize(payload!)) as Map?; + } catch (_) { + // Do nothing, we always return null for any failure. + } + return null; +} + +/// Converts a [CredentialResponse] into a [GoogleSignInUserData]. /// -/// This method returns `null` when the [currentUser] is not signed in. -GoogleSignInUserData? gapiUserToPluginUserData(auth2.GoogleUser? currentUser) { - final bool isSignedIn = currentUser?.isSignedIn() ?? false; - final auth2.BasicProfile? profile = currentUser?.getBasicProfile(); - if (!isSignedIn || profile?.getId() == null) { +/// May return `null`, if the `credentialResponse` is null, or its `credential` +/// cannot be decoded. +GoogleSignInUserData? gisResponsesToUserData( + CredentialResponse? credentialResponse) { + if (credentialResponse == null || credentialResponse.credential == null) { + return null; + } + + final Map? payload = + getJwtTokenPayload(credentialResponse.credential); + + if (payload == null) { return null; } return GoogleSignInUserData( - displayName: profile?.getName(), - email: profile?.getEmail() ?? '', - id: profile?.getId() ?? '', - photoUrl: profile?.getImageUrl(), - idToken: currentUser?.getAuthResponse().id_token, + email: payload['email']! as String, + id: payload['sub']! as String, + displayName: payload['name']! as String, + photoUrl: payload['picture']! as String, + idToken: credentialResponse.credential, + ); +} + +/// Converts responses from the GIS library into TokenData for the plugin. +GoogleSignInTokenData gisResponsesToTokenData( + CredentialResponse? credentialResponse, TokenResponse? tokenResponse) { + return GoogleSignInTokenData( + idToken: credentialResponse?.credential, + accessToken: tokenResponse?.access_token, ); } diff --git a/packages/google_sign_in/google_sign_in_web/pubspec.yaml b/packages/google_sign_in/google_sign_in_web/pubspec.yaml index 42413e091e6e..40e8b0381e67 100644 --- a/packages/google_sign_in/google_sign_in_web/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android, iOS and Web. repository: https://github.com/flutter/plugins/tree/main/packages/google_sign_in/google_sign_in_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 0.10.2 +version: 0.11.0 environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + sdk: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" flutter: plugin: @@ -22,7 +22,9 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter + google_identity_services_web: ^0.2.0 google_sign_in_platform_interface: ^2.2.0 + http: ^0.13.5 js: ^0.6.3 dev_dependencies: diff --git a/packages/google_sign_in/google_sign_in_web/test/tests_exist_elsewhere_test.dart b/packages/google_sign_in/google_sign_in_web/test/tests_exist_elsewhere_test.dart index 442c50144727..cc32e6c72f1e 100644 --- a/packages/google_sign_in/google_sign_in_web/test/tests_exist_elsewhere_test.dart +++ b/packages/google_sign_in/google_sign_in_web/test/tests_exist_elsewhere_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: avoid_print + import 'package:flutter_test/flutter_test.dart'; void main() { diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 76192566b18b..1ac6a8d77ba2 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.8.6+2 + +* Updates `NSPhotoLibraryUsageDescription` description in README. + +* Updates minimum Flutter version to 3.0. + +## 0.8.6+1 + +* Updates code for stricter lint checks. + ## 0.8.6 * Updates minimum Flutter version to 2.10. diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index aadfc83ff5e6..8fff8920054c 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -23,7 +23,7 @@ As a result of implementing PHPicker it becomes impossible to pick HEIC images o Add the following keys to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: * `NSPhotoLibraryUsageDescription` - describe why your app needs permission for the photo library. This is called _Privacy - Photo Library Usage Description_ in the visual editor. - * This permission is not required for image picking on iOS 11+ if you pass `false` for `requestFullMetadata`. + * This permission will not be requested if you always pass `false` for `requestFullMetadata`, but App Store policy requires including the plist entry. * `NSCameraUsageDescription` - describe why your app needs access to the camera. This is called _Privacy - Camera Usage Description_ in the visual editor. * `NSMicrophoneUsageDescription` - describe why your app needs access to the microphone, if you intend to record videos. This is called _Privacy - Microphone Usage Description_ in the visual editor. diff --git a/packages/image_picker/image_picker/example/android/gradle.properties b/packages/image_picker/image_picker/example/android/gradle.properties index 38c8d4544ff1..2f3603c9ff62 100755 --- a/packages/image_picker/image_picker/example/android/gradle.properties +++ b/packages/image_picker/image_picker/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/packages/image_picker/image_picker/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index 5e448ddbee68..f4f6546b1a98 100755 --- a/packages/image_picker/image_picker/example/lib/main.dart +++ b/packages/image_picker/image_picker/example/lib/main.dart @@ -260,7 +260,7 @@ class _MyHomePageState extends State { ); case ConnectionState.done: return _handlePreview(); - default: + case ConnectionState.active: if (snapshot.hasError) { return Text( 'Pick image/video error: ${snapshot.error}}', diff --git a/packages/image_picker/image_picker/example/pubspec.yaml b/packages/image_picker/image_picker/example/pubspec.yaml index e9511e27ab6d..3d97877498dc 100755 --- a/packages/image_picker/image_picker/example/pubspec.yaml +++ b/packages/image_picker/image_picker/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index 7fed3bf4637b..0d6308198891 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,11 +3,11 @@ 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/plugins/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.6 +version: 0.8.6+2 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/image_picker/image_picker_android/CHANGELOG.md b/packages/image_picker/image_picker_android/CHANGELOG.md index 3075f5b1d976..1ab21108d70f 100644 --- a/packages/image_picker/image_picker_android/CHANGELOG.md +++ b/packages/image_picker/image_picker_android/CHANGELOG.md @@ -1,3 +1,16 @@ +## 0.8.5+6 + +* Updates minimum Flutter version to 3.0. +* Fixes names of picked files to match original filenames where possible. + +## 0.8.5+5 + +* Updates code for stricter lint checks. + +## 0.8.5+4 + +* Fixes null cast exception when restoring a cancelled selection. + ## 0.8.5+3 * Updates minimum Flutter version to 2.10. diff --git a/packages/image_picker/image_picker_android/android/build.gradle b/packages/image_picker/image_picker_android/android/build.gradle index aed1ad5174ea..e61f3161d0f5 100644 --- a/packages/image_picker/image_picker_android/android/build.gradle +++ b/packages/image_picker/image_picker_android/android/build.gradle @@ -29,9 +29,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' } dependencies { implementation 'androidx.core:core:1.8.0' @@ -39,7 +37,7 @@ android { implementation 'androidx.exifinterface:exifinterface:1.3.3' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:4.8.0' + testImplementation 'org.mockito:mockito-core:5.1.1' testImplementation 'androidx.test:core:1.4.0' testImplementation "org.robolectric:robolectric:4.8.1" } diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java index 1f51a226c7e2..449480c19d9c 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java @@ -25,55 +25,60 @@ import android.content.ContentResolver; import android.content.Context; +import android.database.Cursor; import android.net.Uri; +import android.provider.MediaStore; import android.webkit.MimeTypeMap; +import io.flutter.Log; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.UUID; class FileUtils { - + /** + * Copies the file from the given content URI to a temporary directory, retaining the original + * file name if possible. + * + *

Each file is placed in its own directory to avoid conflicts according to the following + * scheme: {cacheDir}/{randomUuid}/{fileName} + * + *

If the original file name is unknown, a predefined "image_picker" filename is used and the + * file extension is deduced from the mime type (with fallback to ".jpg" in case of failure). + */ String getPathFromUri(final Context context, final Uri uri) { - File file = null; - InputStream inputStream = null; - OutputStream outputStream = null; - boolean success = false; - try { - String extension = getImageExtension(context, uri); - inputStream = context.getContentResolver().openInputStream(uri); - file = File.createTempFile("image_picker", extension, context.getCacheDir()); - file.deleteOnExit(); - outputStream = new FileOutputStream(file); - if (inputStream != null) { - copy(inputStream, outputStream); - success = true; + try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) { + String uuid = UUID.randomUUID().toString(); + File targetDirectory = new File(context.getCacheDir(), uuid); + targetDirectory.mkdir(); + // TODO(SynSzakala) according to the docs, `deleteOnExit` does not work reliably on Android; we should preferably + // just clear the picked files after the app startup. + targetDirectory.deleteOnExit(); + String fileName = getImageName(context, uri); + if (fileName == null) { + Log.w("FileUtils", "Cannot get file name for " + uri); + fileName = "image_picker" + getImageExtension(context, uri); } - } catch (IOException ignored) { - } finally { - try { - if (inputStream != null) inputStream.close(); - } catch (IOException ignored) { - } - try { - if (outputStream != null) outputStream.close(); - } catch (IOException ignored) { - // If closing the output stream fails, we cannot be sure that the - // target file was written in full. Flushing the stream merely moves - // the bytes into the OS, not necessarily to the file. - success = false; + File file = new File(targetDirectory, fileName); + try (OutputStream outputStream = new FileOutputStream(file)) { + copy(inputStream, outputStream); + return file.getPath(); } + } catch (IOException e) { + // If closing the output stream fails, we cannot be sure that the + // target file was written in full. Flushing the stream merely moves + // the bytes into the OS, not necessarily to the file. + return null; } - return success ? file.getPath() : null; } /** @return extension of image with dot, or default .jpg if it none. */ private static String getImageExtension(Context context, Uri uriImage) { - String extension = null; + String extension; try { - String imagePath = uriImage.getPath(); if (uriImage.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { final MimeTypeMap mime = MimeTypeMap.getSingleton(); extension = mime.getExtensionFromMimeType(context.getContentResolver().getType(uriImage)); @@ -94,6 +99,20 @@ private static String getImageExtension(Context context, Uri uriImage) { return "." + extension; } + /** @return name of the image provided by ContentResolver; this may be null. */ + private static String getImageName(Context context, Uri uriImage) { + try (Cursor cursor = queryImageName(context, uriImage)) { + if (cursor == null || !cursor.moveToFirst() || cursor.getColumnCount() < 1) return null; + return cursor.getString(0); + } + } + + private static Cursor queryImageName(Context context, Uri uriImage) { + return context + .getContentResolver() + .query(uriImage, new String[] {MediaStore.MediaColumns.DISPLAY_NAME}, null, null, null); + } + private static void copy(InputStream in, OutputStream out) throws IOException { final byte[] buffer = new byte[4 * 1024]; int bytesRead; 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 dddf67e6a382..cb4beacf9df4 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 @@ -15,6 +15,7 @@ import android.net.Uri; import android.os.Build; import android.provider.MediaStore; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.core.app.ActivityCompat; import androidx.core.content.FileProvider; @@ -621,11 +622,18 @@ private boolean setPendingMethodCallAndResult( return true; } - private void finishWithSuccess(String imagePath) { + // Handles completion of selection with a single result. + // + // A null imagePath indicates that the image picker was cancelled without + // selection. + private void finishWithSuccess(@Nullable String imagePath) { if (pendingResult == null) { - ArrayList pathList = new ArrayList<>(); - pathList.add(imagePath); - cache.saveResult(pathList, null, null); + // Only save data for later retrieval if something was actually selected. + if (imagePath != null) { + ArrayList pathList = new ArrayList<>(); + pathList.add(imagePath); + cache.saveResult(pathList, null, null); + } return; } pendingResult.success(imagePath); diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java index 32e3ebc6183d..0ea0173fa954 100644 --- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java +++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java @@ -8,8 +8,15 @@ import static org.junit.Assert.assertTrue; import static org.robolectric.Shadows.shadowOf; +import android.content.ContentProvider; +import android.content.ContentValues; import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; import android.net.Uri; +import android.provider.MediaStore; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; @@ -19,6 +26,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.shadows.ShadowContentResolver; @@ -63,4 +71,62 @@ public void FileUtil_getImageExtension() throws IOException { String path = fileUtils.getPathFromUri(context, uri); assertTrue(path.endsWith(".jpg")); } + + @Test + public void FileUtil_getImageName() throws IOException { + Uri uri = Uri.parse("content://dummy/dummy.png"); + Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); + shadowContentResolver.registerInputStream( + uri, new ByteArrayInputStream("imageStream".getBytes(UTF_8))); + String path = fileUtils.getPathFromUri(context, uri); + assertTrue(path.endsWith("dummy.png")); + } + + private static class MockContentProvider extends ContentProvider { + + @Override + public boolean onCreate() { + return true; + } + + @Nullable + @Override + public Cursor query( + @NonNull Uri uri, + @Nullable String[] projection, + @Nullable String selection, + @Nullable String[] selectionArgs, + @Nullable String sortOrder) { + MatrixCursor cursor = new MatrixCursor(new String[] {MediaStore.MediaColumns.DISPLAY_NAME}); + cursor.addRow(new Object[] {"dummy.png"}); + return cursor; + } + + @Nullable + @Override + public String getType(@NonNull Uri uri) { + return "image/png"; + } + + @Nullable + @Override + public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { + return null; + } + + @Override + public int delete( + @NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { + return 0; + } + + @Override + public int update( + @NonNull Uri uri, + @Nullable ContentValues values, + @Nullable String selection, + @Nullable String[] selectionArgs) { + return 0; + } + } } 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 d2ee7b0b7d61..6d1e73c49eb9 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 @@ -13,6 +13,7 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -275,6 +276,16 @@ public void onActivityResult_WhenPickFromGalleryCanceled_FinishesWithNull() { verifyNoMoreInteractions(mockResult); } + @Test + public void onActivityResult_WhenPickFromGalleryCanceled_StoresNothingInCache() { + ImagePickerDelegate delegate = createDelegate(); + + delegate.onActivityResult( + ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY, Activity.RESULT_CANCELED, null); + + verify(cache, never()).saveResult(any(), any(), any()); + } + @Test public void onActivityResult_WhenImagePickedFromGallery_AndNoResizeNeeded_FinishesWithImagePath() { @@ -287,6 +298,18 @@ public void onActivityResult_WhenPickFromGalleryCanceled_FinishesWithNull() { verifyNoMoreInteractions(mockResult); } + @Test + public void onActivityResult_WhenImagePickedFromGallery_AndNoResizeNeeded_StoresImageInCache() { + ImagePickerDelegate delegate = createDelegate(); + + delegate.onActivityResult( + ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY, Activity.RESULT_OK, mockIntent); + + ArgumentCaptor> pathListCapture = ArgumentCaptor.forClass(ArrayList.class); + verify(cache, times(1)).saveResult(pathListCapture.capture(), any(), any()); + assertEquals("pathFromUri", pathListCapture.getValue().get(0)); + } + @Test public void onActivityResult_WhenImagePickedFromGallery_AndResizeNeeded_FinishesWithScaledImagePath() { diff --git a/packages/image_picker/image_picker_android/example/android/app/build.gradle b/packages/image_picker/image_picker_android/example/android/app/build.gradle index 31d8c82a0a9d..f8487c7959f1 100755 --- a/packages/image_picker/image_picker_android/example/android/app/build.gradle +++ b/packages/image_picker/image_picker_android/example/android/app/build.gradle @@ -63,5 +63,7 @@ dependencies { testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + implementation project(':image_picker_android') + implementation project(':espresso') api 'androidx.test:core:1.4.0' } diff --git a/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerPickTest.java b/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerPickTest.java new file mode 100644 index 000000000000..8b7ae11d5c2d --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerPickTest.java @@ -0,0 +1,43 @@ +// 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.imagepickerexample; + +import static androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget; +import static androidx.test.espresso.flutter.action.FlutterActions.click; +import static androidx.test.espresso.flutter.assertion.FlutterAssertions.matches; +import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withText; +import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withValueKey; +import static androidx.test.espresso.intent.Intents.intended; +import static androidx.test.espresso.intent.Intents.intending; +import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction; + +import android.app.Activity; +import android.app.Instrumentation; +import android.content.Intent; +import android.net.Uri; +import androidx.test.espresso.intent.rule.IntentsTestRule; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +public class ImagePickerPickTest { + + @Rule public TestRule rule = new IntentsTestRule<>(DriverExtensionActivity.class); + + @Test + @Ignore("Doesn't run in Firebase Test Lab: https://github.com/flutter/flutter/issues/94748") + public void imageIsPickedWithOriginalName() { + Instrumentation.ActivityResult result = + new Instrumentation.ActivityResult( + Activity.RESULT_OK, new Intent().setData(Uri.parse("content://dummy/dummy.png"))); + intending(hasAction(Intent.ACTION_GET_CONTENT)).respondWith(result); + onFlutterWidget(withValueKey("image_picker_example_from_gallery")).perform(click()); + onFlutterWidget(withText("PICK")).perform(click()); + intended(hasAction(Intent.ACTION_GET_CONTENT)); + onFlutterWidget(withValueKey("image_picker_example_picked_image_name")) + .check(matches(withText("dummy.png"))); + } +} diff --git a/packages/image_picker/image_picker_android/example/android/app/src/debug/AndroidManifest.xml b/packages/image_picker/image_picker_android/example/android/app/src/debug/AndroidManifest.xml index 6f85cefded34..317af1d1a371 100644 --- a/packages/image_picker/image_picker_android/example/android/app/src/debug/AndroidManifest.xml +++ b/packages/image_picker/image_picker_android/example/android/app/src/debug/AndroidManifest.xml @@ -13,5 +13,17 @@ android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> + + + diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/DriverExtensionActivity.java b/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/DriverExtensionActivity.java new file mode 100644 index 000000000000..b35a6c4b0e49 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/DriverExtensionActivity.java @@ -0,0 +1,16 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.imagepickerexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; + +public class DriverExtensionActivity extends FlutterActivity { + @NonNull + @Override + public String getDartEntrypointFunctionName() { + return "appMain"; + } +} diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/DummyContentProvider.java b/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/DummyContentProvider.java new file mode 100644 index 000000000000..8967318ee977 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/DummyContentProvider.java @@ -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. + +package io.flutter.plugins.imagepickerexample; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.provider.MediaStore; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class DummyContentProvider extends ContentProvider { + @Override + public boolean onCreate() { + return true; + } + + @Nullable + @Override + public AssetFileDescriptor openAssetFile(@NonNull Uri uri, @NonNull String mode) { + return getContext().getResources().openRawResourceFd(R.raw.ic_launcher); + } + + @Nullable + @Override + public Cursor query( + @NonNull Uri uri, + @Nullable String[] projection, + @Nullable String selection, + @Nullable String[] selectionArgs, + @Nullable String sortOrder) { + MatrixCursor cursor = new MatrixCursor(new String[] {MediaStore.MediaColumns.DISPLAY_NAME}); + cursor.addRow(new Object[] {"dummy.png"}); + return cursor; + } + + @Nullable + @Override + public String getType(@NonNull Uri uri) { + return "image/png"; + } + + @Nullable + @Override + public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { + return null; + } + + @Override + public int delete( + @NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { + return 0; + } + + @Override + public int update( + @NonNull Uri uri, + @Nullable ContentValues values, + @Nullable String selection, + @Nullable String[] selectionArgs) { + return 0; + } +} diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/res/raw/ic_launcher.png b/packages/image_picker/image_picker_android/example/android/app/src/main/res/raw/ic_launcher.png new file mode 100755 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@U { } } - Future _onImageButtonPressed(ImageSource source, - {BuildContext? context, bool isMultiImage = false}) async { + Future _onImageButtonPressed( + BuildContext context, { + required ImageSource source, + bool isMultiImage = false, + }) async { if (_controller != null) { await _controller!.setVolume(0.0); } if (isVideo) { final XFile? file = await _picker.getVideo( source: source, maxDuration: const Duration(seconds: 10)); + if (file != null && context.mounted) { + _showPickedSnackBar(context, [file]); + } await _playVideo(file); - } else if (isMultiImage) { - await _displayPickImageDialog(context!, + } else if (isMultiImage && context.mounted) { + await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { final List? pickedFileList = await _picker.getMultiImage( @@ -98,17 +110,16 @@ class _MyHomePageState extends State { maxHeight: maxHeight, imageQuality: quality, ); - setState(() { - _imageFileList = pickedFileList; - }); + if (pickedFileList != null && context.mounted) { + _showPickedSnackBar(context, pickedFileList); + } + setState(() => _imageFileList = pickedFileList); } catch (e) { - setState(() { - _pickImageError = e; - }); + setState(() => _pickImageError = e); } }); } else { - await _displayPickImageDialog(context!, + await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { final XFile? pickedFile = await _picker.getImage( @@ -117,13 +128,12 @@ class _MyHomePageState extends State { maxHeight: maxHeight, imageQuality: quality, ); - setState(() { - _setImageFileListFromFile(pickedFile); - }); + if (pickedFile != null && context.mounted) { + _showPickedSnackBar(context, [pickedFile]); + } + setState(() => _setImageFileListFromFile(pickedFile)); } catch (e) { - setState(() { - _pickImageError = e; - }); + setState(() => _pickImageError = e); } }); } @@ -183,13 +193,21 @@ class _MyHomePageState extends State { 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 - return Semantics( - label: 'image_picker_example_picked_image', - child: kIsWeb - ? Image.network(_imageFileList![index].path) - : Image.file(File(_imageFileList![index].path)), + final XFile image = _imageFileList![index]; + 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)), + ), + ], ); }, itemCount: _imageFileList!.length, @@ -260,7 +278,7 @@ class _MyHomePageState extends State { ); case ConnectionState.done: return _handlePreview(); - default: + case ConnectionState.active: if (snapshot.hasError) { return Text( 'Pick image/video error: ${snapshot.error}}', @@ -283,9 +301,10 @@ 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); + _onImageButtonPressed(context, source: ImageSource.gallery); }, heroTag: 'image0', tooltip: 'Pick Image from gallery', @@ -298,8 +317,8 @@ class _MyHomePageState extends State { onPressed: () { isVideo = false; _onImageButtonPressed( - ImageSource.gallery, - context: context, + context, + source: ImageSource.gallery, isMultiImage: true, ); }, @@ -313,7 +332,7 @@ class _MyHomePageState extends State { child: FloatingActionButton( onPressed: () { isVideo = false; - _onImageButtonPressed(ImageSource.camera, context: context); + _onImageButtonPressed(context, source: ImageSource.camera); }, heroTag: 'image2', tooltip: 'Take a Photo', @@ -326,7 +345,7 @@ class _MyHomePageState extends State { backgroundColor: Colors.red, onPressed: () { isVideo = true; - _onImageButtonPressed(ImageSource.gallery); + _onImageButtonPressed(context, source: ImageSource.gallery); }, heroTag: 'video0', tooltip: 'Pick Video from gallery', @@ -339,7 +358,7 @@ class _MyHomePageState extends State { backgroundColor: Colors.red, onPressed: () { isVideo = true; - _onImageButtonPressed(ImageSource.camera); + _onImageButtonPressed(context, source: ImageSource.camera); }, heroTag: 'video1', tooltip: 'Take a Video', @@ -417,6 +436,13 @@ class _MyHomePageState extends State { ); }); } + + void _showPickedSnackBar(BuildContext context, List files) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Picked: ${files.map((XFile it) => it.name).join(',')}'), + duration: const Duration(seconds: 2), + )); + } } typedef OnPickImageCallback = void Function( diff --git a/packages/image_picker/image_picker_android/example/pubspec.yaml b/packages/image_picker/image_picker_android/example/pubspec.yaml index 02ef8a02af4c..bfeac3de14d5 100755 --- a/packages/image_picker/image_picker_android/example/pubspec.yaml +++ b/packages/image_picker/image_picker_android/example/pubspec.yaml @@ -4,11 +4,13 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: sdk: flutter + flutter_driver: + sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 image_picker_android: # When depending on this package from a real application you should use: @@ -22,8 +24,6 @@ dependencies: dev_dependencies: espresso: ^0.2.0 - flutter_driver: - sdk: flutter flutter_test: sdk: flutter integration_test: diff --git a/packages/image_picker/image_picker_android/pubspec.yaml b/packages/image_picker/image_picker_android/pubspec.yaml index c0092493b715..a0516685964c 100755 --- a/packages/image_picker/image_picker_android/pubspec.yaml +++ b/packages/image_picker/image_picker_android/pubspec.yaml @@ -2,11 +2,11 @@ name: image_picker_android description: Android implementation of the image_picker plugin. repository: https://github.com/flutter/plugins/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.5+3 +version: 0.8.5+6 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: 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 ee1eb79f1045..d6680ce44dd5 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 @@ -18,7 +18,10 @@ void main() { setUp(() { returnValue = ''; - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { log.add(methodCall); return returnValue; }); @@ -189,7 +192,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.pickImage(source: ImageSource.gallery), isNull); expect(await picker.pickImage(source: ImageSource.camera), isNull); @@ -347,7 +353,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.pickMultiImage(), isNull); expect(await picker.pickMultiImage(), isNull); @@ -418,7 +427,10 @@ void main() { }); test('handles a null video path response gracefully', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.pickVideo(source: ImageSource.gallery), isNull); expect(await picker.pickVideo(source: ImageSource.camera), isNull); @@ -460,7 +472,10 @@ void main() { group('#retrieveLostData', () { test('retrieveLostData get success response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'image', 'path': '/example/path', @@ -473,7 +488,10 @@ void main() { }); test('retrieveLostData get error response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'video', 'errorCode': 'test_error_code', @@ -488,14 +506,20 @@ void main() { }); test('retrieveLostData get null response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return null; }); expect((await picker.retrieveLostData()).isEmpty, true); }); test('retrieveLostData get both path and error should throw', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'video', 'errorCode': 'test_error_code', @@ -665,7 +689,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.getImage(source: ImageSource.gallery), isNull); expect(await picker.getImage(source: ImageSource.camera), isNull); @@ -823,7 +850,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.getMultiImage(), isNull); expect(await picker.getMultiImage(), isNull); @@ -894,7 +924,10 @@ void main() { }); test('handles a null video path response gracefully', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.getVideo(source: ImageSource.gallery), isNull); expect(await picker.getVideo(source: ImageSource.camera), isNull); @@ -936,7 +969,10 @@ void main() { group('#getLostData', () { test('getLostData get success response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'image', 'path': '/example/path', @@ -949,7 +985,10 @@ void main() { }); test('getLostData should successfully retrieve multiple files', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'image', 'path': '/example/path1', @@ -965,7 +1004,10 @@ void main() { }); test('getLostData get error response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'video', 'errorCode': 'test_error_code', @@ -980,14 +1022,20 @@ void main() { }); test('getLostData get null response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return null; }); expect((await picker.getLostData()).isEmpty, true); }); test('getLostData get both path and error should throw', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'video', 'errorCode': 'test_error_code', @@ -1183,7 +1231,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect( await picker.getImageFromSource(source: ImageSource.gallery), isNull); @@ -1254,3 +1305,9 @@ void main() { }); }); } + +/// 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/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md index 8a5c089ef807..86c1bea873ae 100644 --- a/packages/image_picker/image_picker_for_web/CHANGELOG.md +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 2.1.10 * Updates code for `no_leading_underscores_for_local_identifiers` lint. 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 c39bd81f9de0..96ce0dfa70c7 100644 --- a/packages/image_picker/image_picker_for_web/example/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/example/pubspec.yaml @@ -3,7 +3,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index c2e0975dda57..03c0fb3e3056 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -6,7 +6,7 @@ version: 2.1.10 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/image_picker/image_picker_for_web/test/tests_exist_elsewhere_test.dart b/packages/image_picker/image_picker_for_web/test/tests_exist_elsewhere_test.dart index 442c50144727..cc32e6c72f1e 100644 --- a/packages/image_picker/image_picker_for_web/test/tests_exist_elsewhere_test.dart +++ b/packages/image_picker/image_picker_for_web/test/tests_exist_elsewhere_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: avoid_print + import 'package:flutter_test/flutter_test.dart'; void main() { diff --git a/packages/image_picker/image_picker_ios/CHANGELOG.md b/packages/image_picker/image_picker_ios/CHANGELOG.md index 986f5c0ff6ca..dbd5160edd7d 100644 --- a/packages/image_picker/image_picker_ios/CHANGELOG.md +++ b/packages/image_picker/image_picker_ios/CHANGELOG.md @@ -1,3 +1,32 @@ +## 0.8.6+8 + +* Fixes issue with images sometimes changing to incorrect orientation. + +## 0.8.6+7 + +* Fixes issue where GIF file would not animate without `Photo Library Usage` permissions. Fixes issue where PNG and GIF files were converted to JPG, but only when they are do not have `Photo Library Usage` permissions. +* Updates minimum Flutter version to 3.0. + +## 0.8.6+6 + +* Updates code for stricter lint checks. + +## 0.8.6+5 + +* Fixes crash when `imageQuality` is set. + +## 0.8.6+4 + +* Fixes authorization status check for iOS14+ so it includes `PHAuthorizationStatusLimited`. + +## 0.8.6+3 + +* Returns error on image load failure. + +## 0.8.6+2 + +* Fixes issue where selectable images of certain types (such as ProRAW images) could not be loaded. + ## 0.8.6+1 * Fixes issue with crashing the app when picking images with PHPicker without providing `Photo Library Usage` permission. diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/project.pbxproj index 2847bfd85046..ddbc856d6aa7 100644 --- a/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -18,6 +18,22 @@ 680049382280F2B9006DD6AB /* pngImage.png in Resources */ = {isa = PBXBuildFile; fileRef = 680049352280F2B8006DD6AB /* pngImage.png */; }; 680049392280F2B9006DD6AB /* jpgImage.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 680049362280F2B8006DD6AB /* jpgImage.jpg */; }; 6801C8392555D726009DAF8D /* ImagePickerFromGalleryUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6801C8382555D726009DAF8D /* ImagePickerFromGalleryUITests.m */; }; + 782C2B45299ECE33008DC703 /* jpgImageWithRightOrientation.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 782C2B44299ECE33008DC703 /* jpgImageWithRightOrientation.jpg */; }; + 782C2B46299ECE33008DC703 /* jpgImageWithRightOrientation.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 782C2B44299ECE33008DC703 /* jpgImageWithRightOrientation.jpg */; }; + 7865C5E12941326F0010E17F /* bmpImage.bmp in Resources */ = {isa = PBXBuildFile; fileRef = 7865C5E02941326F0010E17F /* bmpImage.bmp */; }; + 7865C5E22941326F0010E17F /* bmpImage.bmp in Resources */ = {isa = PBXBuildFile; fileRef = 7865C5E02941326F0010E17F /* bmpImage.bmp */; }; + 7865C5E4294132D50010E17F /* svgImage.svg in Resources */ = {isa = PBXBuildFile; fileRef = 7865C5E3294132D50010E17F /* svgImage.svg */; }; + 7865C5E5294132D50010E17F /* svgImage.svg in Resources */ = {isa = PBXBuildFile; fileRef = 7865C5E3294132D50010E17F /* svgImage.svg */; }; + 7865C5E72941374F0010E17F /* heicImage.heic in Resources */ = {isa = PBXBuildFile; fileRef = 7865C5E62941374F0010E17F /* heicImage.heic */; }; + 7865C5E82941374F0010E17F /* heicImage.heic in Resources */ = {isa = PBXBuildFile; fileRef = 7865C5E62941374F0010E17F /* heicImage.heic */; }; + 7865C5EA294137960010E17F /* icoImage.ico in Resources */ = {isa = PBXBuildFile; fileRef = 7865C5E9294137960010E17F /* icoImage.ico */; }; + 7865C5EB294137960010E17F /* icoImage.ico in Resources */ = {isa = PBXBuildFile; fileRef = 7865C5E9294137960010E17F /* icoImage.ico */; }; + 7865C5ED294137AB0010E17F /* tiffImage.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 7865C5EC294137AB0010E17F /* tiffImage.tiff */; }; + 7865C5EE294137AB0010E17F /* tiffImage.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 7865C5EC294137AB0010E17F /* tiffImage.tiff */; }; + 7865C5FC294157BC0010E17F /* icnsImage.icns in Resources */ = {isa = PBXBuildFile; fileRef = 7865C5FB294157BB0010E17F /* icnsImage.icns */; }; + 7865C5FD294157BC0010E17F /* icnsImage.icns in Resources */ = {isa = PBXBuildFile; fileRef = 7865C5FB294157BB0010E17F /* icnsImage.icns */; }; + 7865C5FF294252A60010E17F /* proRawImage.dng in Resources */ = {isa = PBXBuildFile; fileRef = 7865C5FE294252A60010E17F /* proRawImage.dng */; }; + 7865C600294252A60010E17F /* proRawImage.dng in Resources */ = {isa = PBXBuildFile; fileRef = 7865C5FE294252A60010E17F /* proRawImage.dng */; }; 86430DF9272D71E9002D9D6C /* gifImage.gif in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */; }; 86E9A893272754860017E6E0 /* PickerSaveImageToPathOperationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 86E9A892272754860017E6E0 /* PickerSaveImageToPathOperationTests.m */; }; 86E9A894272754A30017E6E0 /* webpImage.webp in Resources */ = {isa = PBXBuildFile; fileRef = 86E9A88F272747B90017E6E0 /* webpImage.webp */; }; @@ -81,6 +97,14 @@ 6801C83A2555D726009DAF8D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 68B9AF71243E4B3F00927CE4 /* ImagePickerPluginTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ImagePickerPluginTests.m; sourceTree = ""; }; 68F4B463228B3AB500C25614 /* PhotoAssetUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PhotoAssetUtilTests.m; sourceTree = ""; }; + 782C2B44299ECE33008DC703 /* jpgImageWithRightOrientation.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = jpgImageWithRightOrientation.jpg; sourceTree = ""; }; + 7865C5E02941326F0010E17F /* bmpImage.bmp */ = {isa = PBXFileReference; lastKnownFileType = image.bmp; path = bmpImage.bmp; sourceTree = ""; }; + 7865C5E3294132D50010E17F /* svgImage.svg */ = {isa = PBXFileReference; lastKnownFileType = text; path = svgImage.svg; sourceTree = ""; }; + 7865C5E62941374F0010E17F /* heicImage.heic */ = {isa = PBXFileReference; lastKnownFileType = file; path = heicImage.heic; sourceTree = ""; }; + 7865C5E9294137960010E17F /* icoImage.ico */ = {isa = PBXFileReference; lastKnownFileType = image.ico; path = icoImage.ico; sourceTree = ""; }; + 7865C5EC294137AB0010E17F /* tiffImage.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = tiffImage.tiff; sourceTree = ""; }; + 7865C5FB294157BB0010E17F /* icnsImage.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = icnsImage.icns; sourceTree = ""; }; + 7865C5FE294252A60010E17F /* proRawImage.dng */ = {isa = PBXFileReference; lastKnownFileType = file; path = proRawImage.dng; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -148,10 +172,18 @@ 680049282280E33D006DD6AB /* TestImages */ = { isa = PBXGroup; children = ( + 782C2B44299ECE33008DC703 /* jpgImageWithRightOrientation.jpg */, 86E9A88F272747B90017E6E0 /* webpImage.webp */, 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */, 680049362280F2B8006DD6AB /* jpgImage.jpg */, 680049352280F2B8006DD6AB /* pngImage.png */, + 7865C5E02941326F0010E17F /* bmpImage.bmp */, + 7865C5E62941374F0010E17F /* heicImage.heic */, + 7865C5FB294157BB0010E17F /* icnsImage.icns */, + 7865C5E9294137960010E17F /* icoImage.ico */, + 7865C5FE294252A60010E17F /* proRawImage.dng */, + 7865C5E3294132D50010E17F /* svgImage.svg */, + 7865C5EC294137AB0010E17F /* tiffImage.tiff */, ); path = TestImages; sourceTree = ""; @@ -361,10 +393,18 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7865C5E12941326F0010E17F /* bmpImage.bmp in Resources */, + 7865C5E4294132D50010E17F /* svgImage.svg in Resources */, 86430DF9272D71E9002D9D6C /* gifImage.gif in Resources */, + 7865C5FF294252A60010E17F /* proRawImage.dng in Resources */, + 7865C5EA294137960010E17F /* icoImage.ico in Resources */, + 7865C5E72941374F0010E17F /* heicImage.heic in Resources */, 86E9A894272754A30017E6E0 /* webpImage.webp in Resources */, 86E9A895272769130017E6E0 /* pngImage.png in Resources */, + 7865C5FC294157BC0010E17F /* icnsImage.icns in Resources */, + 782C2B45299ECE33008DC703 /* jpgImageWithRightOrientation.jpg in Resources */, 86E9A896272769150017E6E0 /* jpgImage.jpg in Resources */, + 7865C5ED294137AB0010E17F /* tiffImage.tiff in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -373,8 +413,16 @@ buildActionMask = 2147483647; files = ( 9FC8F0EC229FA68500C8D58F /* gifImage.gif in Resources */, + 7865C5EE294137AB0010E17F /* tiffImage.tiff in Resources */, + 782C2B46299ECE33008DC703 /* jpgImageWithRightOrientation.jpg in Resources */, + 7865C5E82941374F0010E17F /* heicImage.heic in Resources */, + 7865C5FD294157BC0010E17F /* icnsImage.icns in Resources */, 680049382280F2B9006DD6AB /* pngImage.png in Resources */, 680049392280F2B9006DD6AB /* jpgImage.jpg in Resources */, + 7865C5EB294137960010E17F /* icoImage.ico in Resources */, + 7865C5E22941326F0010E17F /* bmpImage.bmp in Resources */, + 7865C600294252A60010E17F /* proRawImage.dng in Resources */, + 7865C5E5294132D50010E17F /* svgImage.svg in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 9b24f28c25cc..3504e6812840 100755 --- a/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -47,16 +47,6 @@ ReferencedContainer = "container:Runner.xcodeproj"> - - - - @interface MockViewController : UIViewController @@ -54,7 +56,7 @@ - (void)testPluginPickImageDeviceBack { camera:FLTSourceCameraRear] maxSize:[[FLTMaxSize alloc] init] quality:nil - fullMetadata:@(YES) + fullMetadata:@YES completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ }]; @@ -87,7 +89,7 @@ - (void)testPluginPickImageDeviceFront { camera:FLTSourceCameraFront] maxSize:[[FLTMaxSize alloc] init] quality:nil - fullMetadata:@(YES) + fullMetadata:@YES completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ }]; @@ -172,7 +174,7 @@ - (void)testPickMultiImageShouldUseUIImagePickerControllerOnPreiOS14 { [plugin pickMultiImageWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)] quality:@(50) - fullMetadata:@(YES) + fullMetadata:@YES completion:^(NSArray *_Nullable result, FlutterError *_Nullable error){ }]; @@ -191,7 +193,7 @@ - (void)testPickImageWithoutFullMetadata API_AVAILABLE(ios(11)) { camera:FLTSourceCameraFront] maxSize:[[FLTMaxSize alloc] init] quality:nil - fullMetadata:@(NO) + fullMetadata:@NO completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ }]; @@ -207,7 +209,7 @@ - (void)testPickMultiImageWithoutFullMetadata API_AVAILABLE(ios(11)) { [plugin pickMultiImageWithMaxSize:[[FLTMaxSize alloc] init] quality:nil - fullMetadata:@(NO) + fullMetadata:@NO completion:^(NSArray *_Nullable result, FlutterError *_Nullable error){ }]; @@ -229,7 +231,7 @@ - (void)testPluginPickImageDeviceCancelClickMultipleTimes { camera:FLTSourceCameraRear] maxSize:[[FLTMaxSize alloc] init] quality:nil - fullMetadata:@(YES) + fullMetadata:@YES completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ }]; @@ -269,37 +271,172 @@ - (void)testViewController { - (void)testPluginMultiImagePathHasNullItem { FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; - dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0); - __block FlutterError *pickImageResult = nil; + XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; plugin.callContext = [[FLTImagePickerMethodCallContext alloc] initWithResult:^(NSArray *_Nullable result, FlutterError *_Nullable error) { - pickImageResult = error; - dispatch_semaphore_signal(resultSemaphore); + XCTAssertEqualObjects(error.code, @"create_error"); + [resultExpectation fulfill]; }]; [plugin sendCallResultWithSavedPathList:@[ [NSNull null] ]]; - dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); - - XCTAssertEqualObjects(pickImageResult.code, @"create_error"); + [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)testPluginMultiImagePathHasItem { FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; NSArray *pathList = @[ @"test" ]; - dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0); - __block id pickImageResult = nil; + XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; plugin.callContext = [[FLTImagePickerMethodCallContext alloc] initWithResult:^(NSArray *_Nullable result, FlutterError *_Nullable error) { - pickImageResult = result; - dispatch_semaphore_signal(resultSemaphore); + XCTAssertEqualObjects(result, pathList); + [resultExpectation fulfill]; }]; [plugin sendCallResultWithSavedPathList:pathList]; - dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)testSendsImageInvalidSourceError API_AVAILABLE(ios(14)) { + id mockPickerViewController = OCMClassMock([PHPickerViewController class]); + + id mockItemProvider = OCMClassMock([NSItemProvider class]); + // Does not conform to image, invalid source. + OCMStub([mockItemProvider hasItemConformingToTypeIdentifier:OCMOCK_ANY]).andReturn(NO); + + PHPickerResult *failResult1 = OCMClassMock([PHPickerResult class]); + OCMStub([failResult1 itemProvider]).andReturn(mockItemProvider); + + PHPickerResult *failResult2 = OCMClassMock([PHPickerResult class]); + OCMStub([failResult2 itemProvider]).andReturn(mockItemProvider); + + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + + XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; + + plugin.callContext = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^(NSArray *result, FlutterError *error) { + XCTAssertTrue(NSThread.isMainThread); + XCTAssertNil(result); + XCTAssertEqualObjects(error.code, @"invalid_source"); + [resultExpectation fulfill]; + }]; + + [plugin picker:mockPickerViewController didFinishPicking:@[ failResult1, failResult2 ]]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)testSendsImageInvalidErrorWhenOneFails API_AVAILABLE(ios(14)) { + id mockPickerViewController = OCMClassMock([PHPickerViewController class]); + NSError *loadDataError = [NSError errorWithDomain:@"PHPickerDomain" code:1234 userInfo:nil]; + + id mockFailItemProvider = OCMClassMock([NSItemProvider class]); + OCMStub([mockFailItemProvider hasItemConformingToTypeIdentifier:OCMOCK_ANY]).andReturn(YES); + [[mockFailItemProvider stub] + loadDataRepresentationForTypeIdentifier:OCMOCK_ANY + completionHandler:[OCMArg invokeBlockWithArgs:[NSNull null], + loadDataError, nil]]; + + PHPickerResult *failResult = OCMClassMock([PHPickerResult class]); + OCMStub([failResult itemProvider]).andReturn(mockFailItemProvider); + + NSURL *tiffURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"tiffImage" + withExtension:@"tiff"]; + NSItemProvider *tiffItemProvider = [[NSItemProvider alloc] initWithContentsOfURL:tiffURL]; + PHPickerResult *tiffResult = OCMClassMock([PHPickerResult class]); + OCMStub([tiffResult itemProvider]).andReturn(tiffItemProvider); + + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + + XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; + + plugin.callContext = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^(NSArray *result, FlutterError *error) { + XCTAssertTrue(NSThread.isMainThread); + XCTAssertNil(result); + XCTAssertEqualObjects(error.code, @"invalid_image"); + [resultExpectation fulfill]; + }]; + + [plugin picker:mockPickerViewController didFinishPicking:@[ failResult, tiffResult ]]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)testSavesImages API_AVAILABLE(ios(14)) { + id mockPickerViewController = OCMClassMock([PHPickerViewController class]); - XCTAssertEqual(pickImageResult, pathList); + NSURL *tiffURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"tiffImage" + withExtension:@"tiff"]; + NSItemProvider *tiffItemProvider = [[NSItemProvider alloc] initWithContentsOfURL:tiffURL]; + PHPickerResult *tiffResult = OCMClassMock([PHPickerResult class]); + OCMStub([tiffResult itemProvider]).andReturn(tiffItemProvider); + + NSURL *pngURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"pngImage" + withExtension:@"png"]; + NSItemProvider *pngItemProvider = [[NSItemProvider alloc] initWithContentsOfURL:pngURL]; + PHPickerResult *pngResult = OCMClassMock([PHPickerResult class]); + OCMStub([pngResult itemProvider]).andReturn(pngItemProvider); + + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + + XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; + + plugin.callContext = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^(NSArray *result, FlutterError *error) { + XCTAssertTrue(NSThread.isMainThread); + XCTAssertEqual(result.count, 2); + XCTAssertNil(error); + [resultExpectation fulfill]; + }]; + + [plugin picker:mockPickerViewController didFinishPicking:@[ tiffResult, pngResult ]]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)testPickImageRequestAuthorization API_AVAILABLE(ios(14)) { + id mockPhotoLibrary = OCMClassMock([PHPhotoLibrary class]); + OCMStub([mockPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite]) + .andReturn(PHAuthorizationStatusNotDetermined); + OCMExpect([mockPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelReadWrite + handler:OCMOCK_ANY]); + + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + + [plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeGallery + camera:FLTSourceCameraFront] + maxSize:[[FLTMaxSize alloc] init] + quality:nil + fullMetadata:@YES + completion:^(NSString *result, FlutterError *error){ + }]; + OCMVerifyAll(mockPhotoLibrary); +} + +- (void)testPickImageAuthorizationDenied API_AVAILABLE(ios(14)) { + id mockPhotoLibrary = OCMClassMock([PHPhotoLibrary class]); + OCMStub([mockPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite]) + .andReturn(PHAuthorizationStatusDenied); + + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + + XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; + + [plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeGallery + camera:FLTSourceCameraFront] + maxSize:[[FLTMaxSize alloc] init] + quality:nil + fullMetadata:@YES + completion:^(NSString *result, FlutterError *error) { + XCTAssertNil(result); + XCTAssertEqualObjects(error.code, @"photo_access_denied"); + XCTAssertEqualObjects(error.message, @"The user did not allow photo access."); + [resultExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:30 handler:nil]; } @end diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImageUtilTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImageUtilTests.m index e449a84b80bb..1dc807a15dba 100644 --- a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImageUtilTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImageUtilTests.m @@ -36,12 +36,21 @@ - (void)testScaledImage_ShouldBeScaledWithNoMetadata { } - (void)testScaledImage_ShouldBeCorrectRotation { - UIImage *image = [UIImage imageWithData:ImagePickerTestImages.JPGTestData]; + NSURL *imageURL = + [[NSBundle bundleForClass:[self class]] URLForResource:@"jpgImageWithRightOrientation" + withExtension:@"jpg"]; + NSData *imageData = [NSData dataWithContentsOfURL:imageURL]; + UIImage *image = [UIImage imageWithData:imageData]; + XCTAssertEqual(image.size.width, 130); + XCTAssertEqual(image.size.height, 174); + XCTAssertEqual(image.imageOrientation, UIImageOrientationRight); + UIImage *newImage = [FLTImagePickerImageUtil scaledImage:image - maxWidth:@3 - maxHeight:@2 + maxWidth:@10 + maxHeight:@10 isMetadataAvailable:YES]; - + XCTAssertEqual(newImage.size.width, 10); + XCTAssertEqual(newImage.size.height, 7); XCTAssertEqual(newImage.imageOrientation, UIImageOrientationUp); } diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PhotoAssetUtilTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PhotoAssetUtilTests.m index d211ea3f91df..41398bf7d3e3 100644 --- a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PhotoAssetUtilTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PhotoAssetUtilTests.m @@ -39,8 +39,7 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveWithTheCorrectExtentionAndM maxWidth:nil maxHeight:nil imageQuality:nil]; - XCTAssertNotNil(savedPathJPG); - XCTAssertEqualObjects([savedPathJPG substringFromIndex:savedPathJPG.length - 4], @".jpg"); + XCTAssertEqualObjects([NSURL URLWithString:savedPathJPG].pathExtension, @"jpg"); NSDictionary *originalMetaDataJPG = [FLTImagePickerMetaDataUtil getMetaDataFromImageData:dataJPG]; NSData *newDataJPG = [NSData dataWithContentsOfFile:savedPathJPG]; @@ -55,8 +54,7 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveWithTheCorrectExtentionAndM maxWidth:nil maxHeight:nil imageQuality:nil]; - XCTAssertNotNil(savedPathPNG); - XCTAssertEqualObjects([savedPathPNG substringFromIndex:savedPathPNG.length - 4], @".png"); + XCTAssertEqualObjects([NSURL URLWithString:savedPathPNG].pathExtension, @"png"); NSDictionary *originalMetaDataPNG = [FLTImagePickerMetaDataUtil getMetaDataFromImageData:dataPNG]; NSData *newDataPNG = [NSData dataWithContentsOfFile:savedPathPNG]; @@ -69,8 +67,6 @@ - (void)testSaveImageWithPickerInfo_ShouldSaveWithDefaultExtention { NSString *savedPathJPG = [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:nil image:imageJPG imageQuality:nil]; - - XCTAssertNotNil(savedPathJPG); // should be saved as XCTAssertEqualObjects([savedPathJPG substringFromIndex:savedPathJPG.length - 4], kFLTImagePickerDefaultSuffix); @@ -98,7 +94,7 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveAsGifAnimation { // test gif NSData *dataGIF = ImagePickerTestImages.GIFTestData; UIImage *imageGIF = [UIImage imageWithData:dataGIF]; - CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)dataGIF, nil); + CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)dataGIF, nil); size_t numberOfFrames = CGImageSourceGetCount(imageSource); @@ -107,12 +103,12 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveAsGifAnimation { maxWidth:nil maxHeight:nil imageQuality:nil]; - XCTAssertNotNil(savedPathGIF); - XCTAssertEqualObjects([savedPathGIF substringFromIndex:savedPathGIF.length - 4], @".gif"); + XCTAssertEqualObjects([NSURL URLWithString:savedPathGIF].pathExtension, @"gif"); NSData *newDataGIF = [NSData dataWithContentsOfFile:savedPathGIF]; - CGImageSourceRef newImageSource = CGImageSourceCreateWithData((CFDataRef)newDataGIF, nil); + CGImageSourceRef newImageSource = + CGImageSourceCreateWithData((__bridge CFDataRef)newDataGIF, nil); size_t newNumberOfFrames = CGImageSourceGetCount(newImageSource); @@ -124,7 +120,7 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveAsScalledGifAnimation { NSData *dataGIF = ImagePickerTestImages.GIFTestData; UIImage *imageGIF = [UIImage imageWithData:dataGIF]; - CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)dataGIF, nil); + CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)dataGIF, nil); size_t numberOfFrames = CGImageSourceGetCount(imageSource); @@ -139,7 +135,8 @@ - (void)testSaveImageWithOriginalImageData_ShouldSaveAsScalledGifAnimation { XCTAssertEqual(newImage.size.width, 3); XCTAssertEqual(newImage.size.height, 2); - CGImageSourceRef newImageSource = CGImageSourceCreateWithData((CFDataRef)newDataGIF, nil); + CGImageSourceRef newImageSource = + CGImageSourceCreateWithData((__bridge CFDataRef)newDataGIF, nil); size_t newNumberOfFrames = CGImageSourceGetCount(newImageSource); diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m index e04c4f2abb50..57ccb86c0060 100644 --- a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m @@ -3,10 +3,10 @@ // found in the LICENSE file. #import -#import @import image_picker_ios; @import image_picker_ios.Test; +@import UniformTypeIdentifiers; @import XCTest; @interface PickerSaveImageToPathOperationTests : XCTestCase @@ -19,40 +19,221 @@ - (void)testSaveWebPImage API_AVAILABLE(ios(14)) { NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"webpImage" withExtension:@"webp"]; NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; - PHPickerResult *result = [self createPickerResultWithProvider:itemProvider - withIdentifier:UTTypeWebP.identifier]; + PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; - [self verifySavingImageWithPickerResult:result fullMetadata:YES]; + [self verifySavingImageWithPickerResult:result fullMetadata:YES withExtension:@"jpg"]; } - (void)testSavePNGImage API_AVAILABLE(ios(14)) { NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"pngImage" withExtension:@"png"]; NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; - PHPickerResult *result = [self createPickerResultWithProvider:itemProvider - withIdentifier:UTTypeWebP.identifier]; + PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; - [self verifySavingImageWithPickerResult:result fullMetadata:YES]; + [self verifySavingImageWithPickerResult:result fullMetadata:YES withExtension:@"png"]; } - (void)testSaveJPGImage API_AVAILABLE(ios(14)) { NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"jpgImage" withExtension:@"jpg"]; NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; - PHPickerResult *result = [self createPickerResultWithProvider:itemProvider - withIdentifier:UTTypeWebP.identifier]; + PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; - [self verifySavingImageWithPickerResult:result fullMetadata:YES]; + [self verifySavingImageWithPickerResult:result fullMetadata:YES withExtension:@"jpg"]; } - (void)testSaveGIFImage API_AVAILABLE(ios(14)) { NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"gifImage" withExtension:@"gif"]; NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; - PHPickerResult *result = [self createPickerResultWithProvider:itemProvider - withIdentifier:UTTypeWebP.identifier]; + PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; - [self verifySavingImageWithPickerResult:result fullMetadata:YES]; + NSData *dataGIF = [NSData dataWithContentsOfURL:imageURL]; + CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)dataGIF, nil); + size_t numberOfFrames = CGImageSourceGetCount(imageSource); + + XCTestExpectation *pathExpectation = [self expectationWithDescription:@"Path was created"]; + XCTestExpectation *operationExpectation = + [self expectationWithDescription:@"Operation completed"]; + + FLTPHPickerSaveImageToPathOperation *operation = [[FLTPHPickerSaveImageToPathOperation alloc] + initWithResult:result + maxHeight:@100 + maxWidth:@100 + desiredImageQuality:@100 + fullMetadata:NO + savedPathBlock:^(NSString *savedPath, FlutterError *error) { + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:savedPath]); + + // Ensure gif is animated. + XCTAssertEqualObjects([NSURL URLWithString:savedPath].pathExtension, @"gif"); + NSData *newDataGIF = [NSData dataWithContentsOfFile:savedPath]; + CGImageSourceRef newImageSource = + CGImageSourceCreateWithData((__bridge CFDataRef)newDataGIF, nil); + size_t newNumberOfFrames = CGImageSourceGetCount(newImageSource); + XCTAssertEqual(numberOfFrames, newNumberOfFrames); + [pathExpectation fulfill]; + }]; + operation.completionBlock = ^{ + [operationExpectation fulfill]; + }; + + [operation start]; + [self waitForExpectationsWithTimeout:30 handler:nil]; + XCTAssertTrue(operation.isFinished); +} + +- (void)testSaveBMPImage API_AVAILABLE(ios(14)) { + NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"bmpImage" + withExtension:@"bmp"]; + NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; + PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; + + [self verifySavingImageWithPickerResult:result fullMetadata:YES withExtension:@"jpg"]; +} + +- (void)testSaveHEICImage API_AVAILABLE(ios(14)) { + NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"heicImage" + withExtension:@"heic"]; + NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; + PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; + + [self verifySavingImageWithPickerResult:result fullMetadata:YES withExtension:@"jpg"]; +} + +- (void)testSaveWithOrientation API_AVAILABLE(ios(14)) { + NSURL *imageURL = + [[NSBundle bundleForClass:[self class]] URLForResource:@"jpgImageWithRightOrientation" + withExtension:@"jpg"]; + NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; + PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; + + XCTestExpectation *pathExpectation = [self expectationWithDescription:@"Path was created"]; + XCTestExpectation *operationExpectation = + [self expectationWithDescription:@"Operation completed"]; + + FLTPHPickerSaveImageToPathOperation *operation = [[FLTPHPickerSaveImageToPathOperation alloc] + initWithResult:result + maxHeight:@10 + maxWidth:@10 + desiredImageQuality:@100 + fullMetadata:NO + savedPathBlock:^(NSString *savedPath, FlutterError *error) { + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:savedPath]); + + // Ensure image retained it's orientation data. + XCTAssertEqualObjects([NSURL URLWithString:savedPath].pathExtension, @"jpg"); + UIImage *image = [UIImage imageWithContentsOfFile:savedPath]; + XCTAssertEqual(image.imageOrientation, UIImageOrientationRight); + XCTAssertEqual(image.size.width, 7); + XCTAssertEqual(image.size.height, 10); + [pathExpectation fulfill]; + }]; + operation.completionBlock = ^{ + [operationExpectation fulfill]; + }; + + [operation start]; + [self waitForExpectationsWithTimeout:30 handler:nil]; + XCTAssertTrue(operation.isFinished); +} + +- (void)testSaveICNSImage API_AVAILABLE(ios(14)) { + NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"icnsImage" + withExtension:@"icns"]; + NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; + PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; + + [self verifySavingImageWithPickerResult:result fullMetadata:YES withExtension:@"jpg"]; +} + +- (void)testSaveICOImage API_AVAILABLE(ios(14)) { + NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"icoImage" + withExtension:@"ico"]; + NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; + PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; + + [self verifySavingImageWithPickerResult:result fullMetadata:YES withExtension:@"jpg"]; +} + +- (void)testSaveProRAWImage API_AVAILABLE(ios(14)) { + NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"proRawImage" + withExtension:@"dng"]; + NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; + PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; + + [self verifySavingImageWithPickerResult:result fullMetadata:YES withExtension:@"jpg"]; +} + +- (void)testSaveSVGImage API_AVAILABLE(ios(14)) { + NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"svgImage" + withExtension:@"svg"]; + NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; + PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; + + [self verifySavingImageWithPickerResult:result fullMetadata:YES withExtension:@"jpg"]; +} + +- (void)testSaveTIFFImage API_AVAILABLE(ios(14)) { + NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"tiffImage" + withExtension:@"tiff"]; + NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; + PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; + [self verifySavingImageWithPickerResult:result fullMetadata:YES withExtension:@"jpg"]; +} + +- (void)testNonexistentImage API_AVAILABLE(ios(14)) { + NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"bogus" + withExtension:@"png"]; + NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; + PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; + + XCTestExpectation *errorExpectation = [self expectationWithDescription:@"invalid source error"]; + FLTPHPickerSaveImageToPathOperation *operation = [[FLTPHPickerSaveImageToPathOperation alloc] + initWithResult:result + maxHeight:@100 + maxWidth:@100 + desiredImageQuality:@100 + fullMetadata:YES + savedPathBlock:^(NSString *savedPath, FlutterError *error) { + XCTAssertEqualObjects(error.code, @"invalid_source"); + [errorExpectation fulfill]; + }]; + + [operation start]; + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)testFailingImageLoad API_AVAILABLE(ios(14)) { + NSError *loadDataError = [NSError errorWithDomain:@"PHPickerDomain" code:1234 userInfo:nil]; + + id mockItemProvider = OCMClassMock([NSItemProvider class]); + OCMStub([mockItemProvider hasItemConformingToTypeIdentifier:OCMOCK_ANY]).andReturn(YES); + [[mockItemProvider stub] + loadDataRepresentationForTypeIdentifier:OCMOCK_ANY + completionHandler:[OCMArg invokeBlockWithArgs:[NSNull null], + loadDataError, nil]]; + + id pickerResult = OCMClassMock([PHPickerResult class]); + OCMStub([pickerResult itemProvider]).andReturn(mockItemProvider); + + XCTestExpectation *errorExpectation = [self expectationWithDescription:@"invalid image error"]; + + FLTPHPickerSaveImageToPathOperation *operation = [[FLTPHPickerSaveImageToPathOperation alloc] + initWithResult:pickerResult + maxHeight:@100 + maxWidth:@100 + desiredImageQuality:@100 + fullMetadata:YES + savedPathBlock:^(NSString *savedPath, FlutterError *error) { + XCTAssertEqualObjects(error.code, @"invalid_image"); + XCTAssertEqualObjects(error.message, loadDataError.localizedDescription); + XCTAssertEqualObjects(error.details, @"PHPickerDomain"); + [errorExpectation fulfill]; + }]; + + [operation start]; + [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)testSavePNGImageWithoutFullMetadata API_AVAILABLE(ios(14)) { @@ -61,26 +242,24 @@ - (void)testSavePNGImageWithoutFullMetadata API_AVAILABLE(ios(14)) { NSURL *imageURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"pngImage" withExtension:@"png"]; NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; - PHPickerResult *result = [self createPickerResultWithProvider:itemProvider - withIdentifier:UTTypeWebP.identifier]; + PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; + OCMReject([photoAssetUtil fetchAssetsWithLocalIdentifiers:OCMOCK_ANY options:OCMOCK_ANY]); - [self verifySavingImageWithPickerResult:result fullMetadata:NO]; - OCMVerify(times(0), [photoAssetUtil fetchAssetsWithLocalIdentifiers:[OCMArg any] - options:[OCMArg any]]); + [self verifySavingImageWithPickerResult:result fullMetadata:NO withExtension:@"png"]; + OCMVerifyAll(photoAssetUtil); } /** * Creates a mock picker result using NSItemProvider. * * @param itemProvider an item provider that will be used as picker result - * @param identifier local identifier of the asset */ - (PHPickerResult *)createPickerResultWithProvider:(NSItemProvider *)itemProvider - withIdentifier:(NSString *)identifier API_AVAILABLE(ios(14)) { + API_AVAILABLE(ios(14)) { PHPickerResult *result = OCMClassMock([PHPickerResult class]); OCMStub([result itemProvider]).andReturn(itemProvider); - OCMStub([result assetIdentifier]).andReturn(identifier); + OCMStub([result assetIdentifier]).andReturn(itemProvider.registeredTypeIdentifiers.firstObject); return result; } @@ -94,8 +273,11 @@ - (PHPickerResult *)createPickerResultWithProvider:(NSItemProvider *)itemProvide * @param result the picker result */ - (void)verifySavingImageWithPickerResult:(PHPickerResult *)result - fullMetadata:(BOOL)fullMetadata API_AVAILABLE(ios(14)) { + fullMetadata:(BOOL)fullMetadata + withExtension:(NSString *)extension API_AVAILABLE(ios(14)) { XCTestExpectation *pathExpectation = [self expectationWithDescription:@"Path was created"]; + XCTestExpectation *operationExpectation = + [self expectationWithDescription:@"Operation completed"]; FLTPHPickerSaveImageToPathOperation *operation = [[FLTPHPickerSaveImageToPathOperation alloc] initWithResult:result @@ -103,14 +285,18 @@ - (void)verifySavingImageWithPickerResult:(PHPickerResult *)result maxWidth:@100 desiredImageQuality:@100 fullMetadata:fullMetadata - savedPathBlock:^(NSString *savedPath) { - if ([[NSFileManager defaultManager] fileExistsAtPath:savedPath]) { - [pathExpectation fulfill]; - } + savedPathBlock:^(NSString *savedPath, FlutterError *error) { + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:savedPath]); + XCTAssertEqualObjects([NSURL URLWithString:savedPath].pathExtension, extension); + [pathExpectation fulfill]; }]; + operation.completionBlock = ^{ + [operationExpectation fulfill]; + }; [operation start]; - [self waitForExpectations:@[ pathExpectation ] timeout:30]; + [self waitForExpectationsWithTimeout:30 handler:nil]; + XCTAssertTrue(operation.isFinished); } @end diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m index e081cee9cce4..dc5693b28603 100644 --- a/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m @@ -59,41 +59,25 @@ - (void)tearDown { [self.app terminate]; } -- (void)testPickingFromGallery { - [self launchPickerAndPick]; -} - - (void)testCancel { - [self launchPickerAndCancel]; -} - -- (void)launchPickerAndCancel { // Find and tap on the pick from gallery button. - NSPredicate *predicateToFindImageFromGalleryButton = - [NSPredicate predicateWithFormat:@"label == %@", @"image_picker_example_from_gallery"]; - XCUIElement *imageFromGalleryButton = - [self.app.otherElements elementMatchingPredicate:predicateToFindImageFromGalleryButton]; + self.app.otherElements[@"image_picker_example_from_gallery"].firstMatch; if (![imageFromGalleryButton waitForExistenceWithTimeout:kElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTFail(@"Failed due to not able to find image from gallery button with %@ seconds", @(kElementWaitingTime)); } - XCTAssertTrue(imageFromGalleryButton.exists); [imageFromGalleryButton tap]; // Find and tap on the `pick` button. - NSPredicate *predicateToFindPickButton = - [NSPredicate predicateWithFormat:@"label == %@", @"PICK"]; - - XCUIElement *pickButton = [self.app.buttons elementMatchingPredicate:predicateToFindPickButton]; + XCUIElement *pickButton = self.app.buttons[@"PICK"].firstMatch; if (![pickButton waitForExistenceWithTimeout:kElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTFail(@"Failed due to not able to find pick button with %@ seconds", @(kElementWaitingTime)); } - XCTAssertTrue(pickButton.exists); [pickButton tap]; // There is a known bug where the permission popups interruption won't get fired until a tap @@ -101,61 +85,70 @@ - (void)launchPickerAndCancel { [self.app tap]; // Find and tap on the `Cancel` button. - NSPredicate *predicateToFindCancelButton = - [NSPredicate predicateWithFormat:@"label == %@", @"Cancel"]; - - XCUIElement *cancelButton = - [self.app.buttons elementMatchingPredicate:predicateToFindCancelButton]; + XCUIElement *cancelButton = self.app.buttons[@"Cancel"].firstMatch; if (![cancelButton waitForExistenceWithTimeout:kElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTFail(@"Failed due to not able to find Cancel button with %@ seconds", @(kElementWaitingTime)); } - XCTAssertTrue(cancelButton.exists); [cancelButton tap]; // Find the "not picked image text". - XCUIElement *imageNotPickedText = [self.app.staticTexts - elementMatchingPredicate:[NSPredicate - predicateWithFormat:@"label == %@", - @"You have not yet picked an image."]]; + XCUIElement *imageNotPickedText = + self.app.staticTexts[@"You have not yet picked an image."].firstMatch; if (![imageNotPickedText waitForExistenceWithTimeout:kElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTFail(@"Failed due to not able to find imageNotPickedText with %@ seconds", @(kElementWaitingTime)); } +} - XCTAssertTrue(imageNotPickedText.exists); +- (void)testPickingFromGallery { + [self launchPickerAndPickWithMaxWidth:nil maxHeight:nil quality:nil]; } -- (void)launchPickerAndPick { - // Find and tap on the pick from gallery button. - NSPredicate *predicateToFindImageFromGalleryButton = - [NSPredicate predicateWithFormat:@"label == %@", @"image_picker_example_from_gallery"]; +- (void)testPickingWithContraintsFromGallery { + [self launchPickerAndPickWithMaxWidth:@200 maxHeight:@100 quality:@50]; +} +- (void)launchPickerAndPickWithMaxWidth:(NSNumber *)maxWidth + maxHeight:(NSNumber *)maxHeight + quality:(NSNumber *)quality { + // Find and tap on the pick from gallery button. XCUIElement *imageFromGalleryButton = - [self.app.otherElements elementMatchingPredicate:predicateToFindImageFromGalleryButton]; + self.app.otherElements[@"image_picker_example_from_gallery"].firstMatch; if (![imageFromGalleryButton waitForExistenceWithTimeout:kElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTFail(@"Failed due to not able to find image from gallery button with %@ seconds", @(kElementWaitingTime)); } - - XCTAssertTrue(imageFromGalleryButton.exists); [imageFromGalleryButton tap]; - // Find and tap on the `pick` button. - NSPredicate *predicateToFindPickButton = - [NSPredicate predicateWithFormat:@"label == %@", @"PICK"]; + if (maxWidth != nil) { + XCUIElement *field = self.app.textFields[@"Enter maxWidth if desired"].firstMatch; + [field tap]; + [field typeText:maxWidth.stringValue]; + } + + if (maxHeight != nil) { + XCUIElement *field = self.app.textFields[@"Enter maxHeight if desired"].firstMatch; + [field tap]; + [field typeText:maxHeight.stringValue]; + } - XCUIElement *pickButton = [self.app.buttons elementMatchingPredicate:predicateToFindPickButton]; + if (quality != nil) { + XCUIElement *field = self.app.textFields[@"Enter quality if desired"].firstMatch; + [field tap]; + [field typeText:quality.stringValue]; + } + + // Find and tap on the `pick` button. + XCUIElement *pickButton = self.app.buttons[@"PICK"].firstMatch; if (![pickButton waitForExistenceWithTimeout:kElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTFail(@"Failed due to not able to find pick button with %@ seconds", @(kElementWaitingTime)); } - - XCTAssertTrue(pickButton.exists); [pickButton tap]; // There is a known bug where the permission popups interruption won't get fired until a tap @@ -167,8 +160,7 @@ - (void)launchPickerAndPick { if (@available(iOS 14, *)) { aImage = [self.app.scrollViews.firstMatch.images elementBoundByIndex:1]; } else { - XCUIElement *allPhotosCell = [self.app.cells - elementMatchingPredicate:[NSPredicate predicateWithFormat:@"label == %@", @"All Photos"]]; + XCUIElement *allPhotosCell = self.app.cells[@"All Photos"].firstMatch; if (![allPhotosCell waitForExistenceWithTimeout:kElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTFail(@"Failed due to not able to find \"All Photos\" cell with %@ seconds", @@ -184,20 +176,14 @@ - (void)launchPickerAndPick { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTFail(@"Failed due to not able to find an image with %@ seconds", @(kElementWaitingTime)); } - XCTAssertTrue(aImage.exists); [aImage tap]; // Find the picked image. - NSPredicate *predicateToFindPickedImage = - [NSPredicate predicateWithFormat:@"label == %@", @"image_picker_example_picked_image"]; - - XCUIElement *pickedImage = [self.app.images elementMatchingPredicate:predicateToFindPickedImage]; + XCUIElement *pickedImage = self.app.images[@"image_picker_example_picked_image"].firstMatch; if (![pickedImage waitForExistenceWithTimeout:kElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTFail(@"Failed due to not able to find pickedImage with %@ seconds", @(kElementWaitingTime)); } - - XCTAssertTrue(pickedImage.exists); } @end diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m index 455fd6269d4b..7cce0520215b 100644 --- a/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m @@ -46,126 +46,67 @@ - (void)tearDown { [self.app terminate]; } -- (void)testSelectingFromGallery { - // Test the `Select Photos` button which is available after iOS 14. - if (@available(iOS 14, *)) { - [self launchPickerAndSelect]; - } else { - return; - } -} - -- (void)launchPickerAndSelect { +// Test the `Select Photos` button which is available after iOS 14. +- (void)testSelectingFromGallery API_AVAILABLE(ios(14)) { // Find and tap on the pick from gallery button. - NSPredicate *predicateToFindImageFromGalleryButton = - [NSPredicate predicateWithFormat:@"label == %@", @"image_picker_example_from_gallery"]; - XCUIElement *imageFromGalleryButton = - [self.app.otherElements elementMatchingPredicate:predicateToFindImageFromGalleryButton]; + self.app.otherElements[@"image_picker_example_from_gallery"].firstMatch; if (![imageFromGalleryButton waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTFail(@"Failed due to not able to find image from gallery button with %@ seconds", @(kLimitedElementWaitingTime)); } - - XCTAssertTrue(imageFromGalleryButton.exists); [imageFromGalleryButton tap]; // Find and tap on the `pick` button. - NSPredicate *predicateToFindPickButton = - [NSPredicate predicateWithFormat:@"label == %@", @"PICK"]; - - XCUIElement *pickButton = [self.app.buttons elementMatchingPredicate:predicateToFindPickButton]; + XCUIElement *pickButton = self.app.buttons[@"PICK"].firstMatch; if (![pickButton waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTSkip(@"Pick button isn't found so the test is skipped..."); } - - XCTAssertTrue(pickButton.exists); [pickButton tap]; // There is a known bug where the permission popups interruption won't get fired until a tap // happened in the app. We expect a permission popup so we do a tap here. [self.app tap]; - // Find an image and tap on it. (IOS 14 UI, images are showing directly) - XCUIElement *aImage; - if (@available(iOS 14, *)) { - aImage = [self.app.scrollViews.firstMatch.images elementBoundByIndex:1]; - } else { - XCUIElement *selectedPhotosCell = [self.app.cells - elementMatchingPredicate:[NSPredicate - predicateWithFormat:@"label == %@", @"Selected Photos"]]; - if (![selectedPhotosCell waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { - os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); - XCTFail(@"Failed due to not able to find \"Selected Photos\" cell with %@ seconds", - @(kLimitedElementWaitingTime)); - } - [selectedPhotosCell tap]; - aImage = [self.app.collectionViews elementMatchingType:XCUIElementTypeCollectionView - identifier:@"PhotosGridView"] - .cells.firstMatch; - } + // Find an image and tap on it. + XCUIElement *aImage = [self.app.scrollViews.firstMatch.images elementBoundByIndex:1]; os_log_error(OS_LOG_DEFAULT, "description before picking image %@", self.app.debugDescription); if (![aImage waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTFail(@"Failed due to not able to find an image with %@ seconds", @(kLimitedElementWaitingTime)); } - XCTAssertTrue(aImage.exists); + [aImage tap]; // Find and tap on the `Done` button. - NSPredicate *predicateToFindDoneButton = - [NSPredicate predicateWithFormat:@"label == %@", @"Done"]; - - XCUIElement *doneButton = [self.app.buttons elementMatchingPredicate:predicateToFindDoneButton]; + XCUIElement *doneButton = self.app.buttons[@"Done"].firstMatch; if (![doneButton waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTSkip(@"Permissions popup could not fired so the test is skipped..."); } - - XCTAssertTrue(doneButton.exists); [doneButton tap]; // Find an image and tap on it to have access to selected photos. - if (@available(iOS 14, *)) { - aImage = [self.app.scrollViews.firstMatch.images elementBoundByIndex:1]; - } else { - XCUIElement *selectedPhotosCell = [self.app.cells - elementMatchingPredicate:[NSPredicate - predicateWithFormat:@"label == %@", @"Selected Photos"]]; - if (![selectedPhotosCell waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { - os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); - XCTFail(@"Failed due to not able to find \"Selected Photos\" cell with %@ seconds", - @(kLimitedElementWaitingTime)); - } - [selectedPhotosCell tap]; - aImage = [self.app.collectionViews elementMatchingType:XCUIElementTypeCollectionView - identifier:@"PhotosGridView"] - .cells.firstMatch; - } + aImage = [self.app.scrollViews.firstMatch.images elementBoundByIndex:1]; + os_log_error(OS_LOG_DEFAULT, "description before picking image %@", self.app.debugDescription); if (![aImage waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTFail(@"Failed due to not able to find an image with %@ seconds", @(kLimitedElementWaitingTime)); } - XCTAssertTrue(aImage.exists); [aImage tap]; // Find the picked image. - NSPredicate *predicateToFindPickedImage = - [NSPredicate predicateWithFormat:@"label == %@", @"image_picker_example_picked_image"]; - - XCUIElement *pickedImage = [self.app.images elementMatchingPredicate:predicateToFindPickedImage]; + XCUIElement *pickedImage = self.app.images[@"image_picker_example_picked_image"].firstMatch; if (![pickedImage waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTFail(@"Failed due to not able to find pickedImage with %@ seconds", @(kLimitedElementWaitingTime)); } - - XCTAssertTrue(pickedImage.exists); } @end diff --git a/packages/image_picker/image_picker_ios/example/ios/TestImages/bmpImage.bmp b/packages/image_picker/image_picker_ios/example/ios/TestImages/bmpImage.bmp new file mode 100644 index 0000000000000000000000000000000000000000..553e765fb01836d630da6c16871d7711e7d96b70 GIT binary patch literal 92 rcmZ?rjbVTQJ0PV2#5_RE4#bQ=2m*3IEW{1LU;!Xu>huc6#6trBzKH`M literal 0 HcmV?d00001 diff --git a/packages/image_picker/image_picker_ios/example/ios/TestImages/heicImage.heic b/packages/image_picker/image_picker_ios/example/ios/TestImages/heicImage.heic new file mode 100644 index 0000000000000000000000000000000000000000..03f41f69cc82bcbf8dbdf4bca815de36783b05f3 GIT binary patch literal 3207 zcmeHJ%WD%s7@y7BG}Ta~4_d3#Wm_RC&88I#2~Ar~d{Br;8?^03w!52T=sw2Xt=Z;c zeIZp5grWxzMtjkdp1gQ43W6veJoTWLN-rW}p_dj~;y0UZ>i=K{zI^X*e*2pV3__?q zW6zSDf+YltY7J+45%L|<6kDV*SbR=a&5DT-!IHggS9Q0H^$d6iWK*G!SP!*4@c#IDdlR*74CXxZVSu#{pmsAJpeaJrtq04|M!LYRI z6$nC|t7vm;eI21aY!Sw^4KqQeT&4vqtpH>!>LlGyA=h=1aXbG>6kE6xrALiFI#(vr z-vfbm-!rIdNHQhS{5U+;4f9*DMHF`@pb&HbBqXUKML>N@ZM~;KA*Nh{-#HM38n9`& z2eg-92h#`}2+#rhFwwS86o~L&yUyOc<%dKJtA>00Ns61!1_=tF$AJOY^QA!FryoXh zzHhKin%bHydY57M2>AZWDWy{A2zUWh$2568(8YA1x=KG9VfI(;Q6}Woz+Sg8+U3oX^=di3vhJpAYAcg$*++^hTpmA<`%G_4PnSk2R~? zqSK>WUE33qiY1wt&e*@Z-MhHY3mgH(s>R@em@mJCg(riI>c1mITv zSi)3f41?ak;LaFER#kWbFw_4G1x?NB*AH^FRb!yWK#hSK12qO}4E%o#?7W{89Ue`2 zcpcGaRXW7HT>LQCacQwMvbOY~ekJki_1gT^t%nnnIpK3?ZL{3^Wn%r)-BK(&-+b_Q q$Jo#A3(ZX%@{QokyT`bHVdKv8x_jj(OJ5hV%Z145P@82hto{Y%%Lo1d literal 0 HcmV?d00001 diff --git a/packages/image_picker/image_picker_ios/example/ios/TestImages/icnsImage.icns b/packages/image_picker/image_picker_ios/example/ios/TestImages/icnsImage.icns new file mode 100644 index 0000000000000000000000000000000000000000..db0fbb07a69b930b74ec1275a6c551fb66bbca9a GIT binary patch literal 354 zcmc~y&MRhMU`)zPHZWmeV32VPa(DXw|M&m@uYSM!@BG{Ozv*w&%)GRGpc=cRf}G6a z5(9%Pj7-cdtZeLD5h`V=Ma7xt&}_7DoYT zs6k;W$vKI|#ft=m8kicG7m0{Aur#nXu!ZC$=BCCAhykTiOOg{y5_9s?b&FEdQj1da zl2b!6lk@XHW=hM5Em+XNc1d1AQ7H;y5=dhNgqd0#;}`6bnOp+&NMcdtB1Pp0zhHl` z2RHz=*)4jjF3^Vv&ar~v^ZBZOv9fzmMQ F695C(b}9e> literal 0 HcmV?d00001 diff --git a/packages/image_picker/image_picker_ios/example/ios/TestImages/icoImage.ico b/packages/image_picker/image_picker_ios/example/ios/TestImages/icoImage.ico new file mode 100644 index 0000000000000000000000000000000000000000..30923c7b6435c1117ed56a8426fecaf4856acd22 GIT binary patch literal 139 zcmZQzU<5)Qb_O7+z);G-z#s<1odJICyj)UTKqjxJhf5HU<^f`MMrI(%R%4eAq!^2X z+?^P2p46!aaySb-B8!0vgh80`oB}@wP>?ymC&cxYlj-!&jUXl`J!(~ScZ($vD57o^m5PtH*6OwvJ9}sHK-xo3cmM0n-1&a_ za_{}_{U-DMF4MzY2X9qeVjK_zKmvY%xxv4pT&>FiNJ@fG0B{2V;R_TZ0t9#)3w#a( zq!TZ?@M1#Zupt3RKL^I38+5>P*kepMg^H_SEaxcP3?RgBys(If@G8QN36cV{b3QyQ z%uLuLG7Ny3bVWR5rd+t9LEu;wgK;7tD96mOya}(Nyou4n@+O?oh;gKA72aV-ykU7W zJd3d=C_yX7buLjID%vr>o z0v|V64Q7g%2&4}|`4G%mq7L_&&n3uiHz$}UCMI-q7kPLCNl+BYqxhIFTE4+Kh>?m5ZT;=o*+MYHIw^REOYwD+W~RLo?$p9>l^ zC~vZ2vA4xklA{LpL6~aymKrC0zKK7pw4>C1U(8s9NbJ#b|IojVo z=lw~y-S*N{HrD-T^_%3C@n`Z|Po$Lg2268WjWCLqW*f5bw?WjfEPYWio<&m-t5+54 zS^O8obP*b}@kuQgVJT@_j!N#_lWS#N4uJhczlmkm9$eX^D`PHcMgtwlienEFc)8_uBQ&VM|zO_`% zX$#%Vmyhf0)tqjk?nbuHoLE!XTRu-EsVm>0D)~y4PZLbXATef8nsSmp5gOT^d|evQ zV6L^M5lYImxXBnOZxGiI=LNfKJMS7dnIs3nGOZB_WXrDF#}=wavG?OG(E(KR^a_JA z%*8Rt9Wxp0AiN~o*-k&EKOm-CkJ@&}FZf$|5a#lpyeaEM`gM6tbYY@$G}M`-MwTj#tv@#*yfdokvPU zcL(9E)z4^{l)1UqmezwzaS&IRaM~upqFvPNr0DTX=aB19Ia5;KY-c+*dxsKC*9Z=Q z*62Rtq`WL|-)!e^Be&8cA@*Hhx4pH+Z_Yzf69n_uIQVZzy{neEj?(HG8b`0~`P_Uoo XujyXbU5>g#?Q4=WIXfA9H*@*F9kTcL literal 0 HcmV?d00001 diff --git a/packages/image_picker/image_picker_ios/example/ios/TestImages/proRawImage.dng b/packages/image_picker/image_picker_ios/example/ios/TestImages/proRawImage.dng new file mode 100644 index 0000000000000000000000000000000000000000..7c3de76c86e2794297025958629b06e693881c5b GIT binary patch literal 8100 zcmeHMc|276`~S?Av6M)cicBbNV-^f1`x=!kOSHHXgUO6xj4f@Hw8$E_C`m+{Ho79A zqJ=i-@_O93!juGX>xv!t7ZIKV$(G;wkV*!Auun zd58~TJfE9_7vr-j5@YMan59UL;pq@diDipY6h!kd8rW+oThO%# zr4ZjrNfz0FXHfM)O3DPz9=DlBrDkKsbi(*#W5**ZTbYd;$I_2-6vsI5%A6SW1%rk& z7dtkOV%#K57{jyS{8)BC3dp64Y=(9cx8-V!?17m=%*xdfVR%i5xhM|~RJ|-b(9FXA z>`MJ?4uBjw0E>bF*mfBJ?n-2&D;|JZpP>$PU;z2>Rv>|Vmv~O-AZ8ONqKqMN99F|~ zFrT#`LsG;8U>dXt#t}_stRb`ly~h6VG8@9EjB{X^#dsae`SE!)LKB!Mx|Yxnd@hg? zVW<>;4NMhtRN!jkd;q44F$O4zu?$dzXGC$q*MV7M^Y9h}v&Z9ze*#p+d;mzI`T20I zCNOV2pYRA!7jd8s1PL&ITqi*mED+1$faW-#pa_mjHD@;FD<2}Wz&W(=T+F#@P! zSxdkW&jX+*#u7kZj3t4A7z40u5;hcLJTRJsjYZc&1KPh5n2N9+#6iGJ%mKiB61JFx zEhk~CN!WT4UOoxiPQrF#j3&kZkN@Kl*cim+gb1nBC-9+;Crptby83ciej%=5EN&>< zL&)K=L%dmBwktb`@9jSwGXvo5tMZ22+U1vJ+=F)=QBF}slT6V!8V#f9af(9po|-&N z5adY;z`Qd*$;7T2rB9ITRo9hCl`aSNILbk)_D6E)f7tik`WJg{6+R z1=&)^%8aZ-)wL#@XwvLs)6@y_tDuWu(#y0`r*D?V1K%X9z1Jk|m_Zvg7 zBgF{pD7y|y@=X3AW>X}bw4G>9bSLU`poS`%jv>m;n5j5x4rx@aI4aPbzYH~iPx=h7 zJ&AC3{3#@QQ^4QT;6eDCT7a~XJ z@j^d!?Po%)Q0U&z;=zVhpeP`aLHU_vkUw9@5Ao;!&7$JdA|+y{pv{Ibyi5xLn1DqJ0UD0 zPEsI_EEDx+G>|?&kK)g%W4a6}XhKa&u+i)l&BF9GmVz-IzZHz7kPZNo-FjkQfV(iM z%DjLXuODk2Sa>-)J6PMUw1OKF zSd%HSV6_hmNx%yf3Y=}sNozbjNfNK&a|o>*;b3`(1erNH!aWJx8H|0Scw!9xG0JTq zqq^P=pGeS;wYkYFDtFl2q(qtE{S-<0z#9*qL7&4^?;wE?J_6w$2{?uegHXH$Vg&&- z3gRRb`;B6>zE#*bic3Ts7iSB|se_p0?a%Uo_#MPcH%^-On}m7XP;^~%pq|RG16^VH z3M)kblz}DSfDjM_xPS%U3MAkNwEu>$BjQ^CK4eD%0hIHD^{o)fn86uT0~=yn5D2_s zrMDDfI;`yIfR4r)%EB@DbS!>LC+2p-t7udKUs7cgb9NN~T+{gvSYLVW<_qye~d zHHal(iPVXn-@b5X`7s}WsU85#cm!qEhWKiaW(h{d96s1zBFMl3m{_R*3@0JT_Z$Qn zJ^@qc1pt+SNHf?a+OJ|TSonv-V&S_P2M3;jM+adFK{Td}4GGa8qC-+V(ETnH#KS+y zDTFEF`F|N8Eil&!NF|UX!T{RhFlqoRhr!8VkXx7>c-08#3TP6Kj`8|IQc4Wn2T`nrV@_WhE(i=B#RoC3UbN60-!;`0fJZpU3)ZX!` zv+H$tPw$%#9|t~t9{lok=o{QUj=CfEXp%p2&>t)wkHbr#{$Q}-@W9F83G--EaA8>PyrZn}BKamT8Be9IXj%hS8;>%H`q^l#sj9A4t(wBIRn!${4OLQk6* zFZH@DM>FjkqWco-Pd7KVaB6vHH@Q9Fsf?^V8ndcAxbKbW;JK5R_AT%0`<(k-qfa?O z0|DtN%LJKKJ+dpao~ALK1s~ODWjeaw%YRwk(NNU8>vBrXo43b<%HvdsZ=V@|PRqa5 zRNZ%dXU+}PZ9OivH7hz}Oy5g5WS{1HPdVHs$E4? zWO(TD>qiIY?{2!AWMFPT-$2EFTmRJCU!{auGoF3w3mK7%`m|!%b9HO1yUUPAIHqiw zb(@W|iRX*QMO&k8xL;qY>%XPnQ-_PL$R6S<-9XT(my(rKILn!VqZN zrgEV#P-)d^ucpe3Ko=apeE(HL#dYz!UcFDf&3%#@AsAd`7@#On%1?L1mJV&Y^~J4C zUUtJFolo)WIB&g|oPK$C(LVRouWI&Y@9ke}Y6zu_0!p2__P@4jxE!`{h4F!@PvbKz zopmx5AG+V%_UXgNX|g5V^`%p7ZeD5PUiJ(S=sw8TF}qQ7ldFXQ`rbqGhBr)hs2=Q( zR8r>F@MPUmjEvpfcO7)EsZl8nNvs@RlHdQ^-eMEkqVxmuEBw@b{aR4pYMnSFD< zS)18%k-oVx(XoHz%$7MW%iL?%)HOG%pK8MB8=0yfEjL;;BG;esHAXr!^J<#$+SSCvVu-%gS^s|EQ=xx4xyekduBTJvdU(d~!=c z(&7uV2w&g4*u46T(m+Vy`tvykAN+Kt?bR@MC|aO?)5j-^xml=Q9Ga1SJXCA1YFuHi zp5t287Q6i)VnEjxJ%5;L;9^?R#&F-E7%!Y@V^@Ct zTG4*Wgw6^yNdQqz`5JD}`r=I&xfR2b`*3A94D&#beF>*T;U9ZR(I#e^IzL z|6Xm@uCN-~29Nbtg~h>9;XMAi|wAp1V4o36EdX^B#$ zx^l;r!uDexvS$s?@OWLhKH8l}OE6&x4(dkKB^%aTKdV`i-$QFw%8Ral*`ij1=WgSs zwwlm1hukes1?1m3M4q--`tWRr;+OoS9W#{%c6*gd8pY-3*=NN6#@z4qCaQ3QJKgTq zkNe33Q6cN6)t4C{`9Rd^ViFFyI=qxr#$w7%QtT$%MLoN^PZo=vao7iLO(7#e=p zB*;g8M|{e3Fp6G%*|$4PB`ir#mRIcDG1$7PI)CV~ zd1OR}sZINiihHFiX4Q8czQ#OZ>pv7(cfYb{M%a`W2*^r{wF?$>nGYtelDcG((_32V zb*MXF(Ki47Fn#sY-6}30sso0y6d}fuYx$6@rjZj%o}K#aNMfg1 zVK{d~{zuhU>jm+@rtkmLCY+w=a0jU$nM!(;T@=36k($bX&%6Hi?(LoL)yjJl5;X=4l4ee^eT_&49L#vdV+K@hHqx${)d_tmNVx~xsA?11A$ZhhpzfeEt3`6e{C`js02zjAe7QKVk=)5i>|CVffd)rd5IDUB3@HLP4(6NOeiVeg0>a^b0GL{A)&h-VvRyCRTv3j*Et05zrm)_4Nw^X0F{$ zrmvV2wJgN&3oV_qfp+y5_2({qhM`6hx~k1Z=Z8LR{}GOWJ%!i(Ywx6QM;bf;!tN^v-jki|?lN9=EqITqG46kxxYB)2 z_{c?*w;9`e)XK=JBbLn-Us@4h=Gk-ep>uZaY`-{-hPe-K=_&q^)5so}5f^;uv<+b6WwcGS+|tXa7;uo+adjvHwo=FJ~u-PI%3nfNpF-2yDC= zGjw&e8HSXJ6F=dM>H1GMRhIsxse=8xrV6^3gxg<>|MvD*ynB@#Pgwn9 zqaU^pzkF8PFmp1+CbCdRV?g+5%g!$6%kcg@_ZGhr!+Wt63+_tqb>puDWq zUGS=LnttxDL@8#X=PbCnUr+(718f4AWBsSYNU^n~X7Yez^TEz8&%8wk&g?L0D!B1} smZnK8`nteO3rh^PmkH~Bv@y5J;Wq=k@6iitzw~9Lxo^*|-i|!~H*9306#xJL literal 0 HcmV?d00001 diff --git a/packages/image_picker/image_picker_ios/example/ios/TestImages/svgImage.svg b/packages/image_picker/image_picker_ios/example/ios/TestImages/svgImage.svg new file mode 100644 index 000000000000..19d6af9f660e --- /dev/null +++ b/packages/image_picker/image_picker_ios/example/ios/TestImages/svgImage.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/image_picker/image_picker_ios/example/ios/TestImages/tiffImage.tiff b/packages/image_picker/image_picker_ios/example/ios/TestImages/tiffImage.tiff new file mode 100644 index 0000000000000000000000000000000000000000..2431333c02e78de9435639a29ab4405e787cfbc3 GIT binary patch literal 4192 zcmeH}cUTkI7QpXJdI&WkbQpRw383`eK|rb?*a%602q}hyViyq=T)T*1MOH;!bae&9 zjs**gy_ZFKh^Xu0DmGkwcR+#nzHj%vZ@>5FyYtQDH|L&v&bjBDn`yF$AQ+ilZfn(GV|1j7Qk9`^vcA zgc0(@0mkFpfF8m72N@5!Wx-*9cZ><-+C}1Tz5sPm9xi7@06^5oI|>diyy3d9pHwh- z;k^hiVo7zEGAS z@`8+uc^24!iZaL7VnR$D7A#YUFgLCf*U7?@d5G*_m`;yHVIdh(oHcrGp+sKLoAk}COy9BH2AppkEOLPU82g;=f>%cK}?m!Bb1 zDSa(0a7RO9Fiv^4OexFDmdS_QN%Ob^k#hwy32$hc=gf8D4S9h^e1@jHf)yg6Sm`&E z4WHp<-{!u0_Tk~-j0kUV?*me}c|W=jZzz#|bXW>TM6_5cL$v^qNR^6&@E~kLY8pmq zhb5o^GH?cfFUXU}B!(qHWe^z}jO9TkoQKETn*&t^*EkVz7&a{buSi=US14gy#6j)R zLYP;`C6IFqlyY3&3z?OXgEI^_Firr1W%440I4fI;*$M11*i;@^gs31_q*QX^_<|h1LJ0d)BIiqs02tge zz6$8zxy4|T-JCt$+&C^=nEs*hmy<(Ue+_tT2Q^;D57Z1{w++jN*AB~MH2`?@!Q8AE zmSwC5pm8Ap`gezAR$BnjECHb5(un=A@qQU`$iyN67fT%hntu)>{Bd!AL%G93 zM45b5t`ftODv;&MR0=Fl&KHO<&c8SD-(EN(tPydHN)Tm=6e1~fl?=N~EX{)Dmck>9 ze_x2De^%kYxokuXI1ctT1ZM4Lz?#YhT9;UW@M{|&GSvWK(k3{A4Am`~mJINdmtxjE z*!Pg({J`^H9_VuTisp&4FkBp*kc0_TihP{mnZO?;KnI$D1q^^GumpC116+VR@CE@O z6pRJ2APJ;`DL??SfdnW(0hk5mgHliqR)Dpj3Ty_opbpf7{h$#X0Vly(a1mSvH^4p6 z0lL6*&J)xPJgCaIH*Nsc5hQY2{t zDT`D)G%rqRZ1y}oZ zHdEVKJ62nwy;OUb_Id56IusonolqU2PKnMool`oUOajxA8O#(g7cgs?XP8|qGRu|~ z$(qJ0XZ^rxXZ7mp=#J4%)-BYn);+4*!6vY6*pcj9_Hy=q_D%LjJrlh^J(1oLy?VW? zdhhg&^aJ!o`epii^>65ZG{6i(4RQ=t8Z;U_FeDl}7{(hG7;Z8=WB9^|W#nxnFe)?J zZ*s0G>&soQLtaGt*v-9sR4lV+hYM1My)klYoo;$i}^s6zB zV?<*%jk)d0bd7OckoqA?=|$q2oiVLLY`%gk^>84(knf z4WAi)B7zhV6|pknR-{R!AhIs9cdYx^*<(*fF`^Qqs-hl8J47p@n`6+Ju`#P-?vJw` zCmq)q3t}TTue1g6{j|(QPPsrwx{(?44t@kV%H?sNu`r+PPUy~ zIQje(!zsB_j!xB_%Aa~5os^!EzAOC`KbBv^@5_kH*pSgH2o|gpJQoHC*9dz=zM@s4 z?o8jzRhdt-e6v<(^Pet;nHhNm2KVuo>UaB+eAfY?);~YyPZ9vjb;uorBEDnA0-Xa&F1ou6g0}cFw2I z&zb*ou~YG?;x`LY7BrWbl*}vXTo}G^cd16HqV(1x??syz6BmmYw=WsJWc`xQWx}!x z`T5q*}#rjWG*;O~HgR1v#Fy63a!@G^bjaN4XY^vXE zvblWo$1UP5x3`9GZK$!Ssn|x|mcQ*uZE|hv_a5KZZ8zLrzWt9K(j6T;6Ly}ibFZu0 zWxQ+oZgjV5ch8y|Fq#C z^I%y6(oodU+nC+h(UjVB<J(9r244t(dEbJ$4ZWUIX>ff-wFAN z7bmk$cD77yxp!*9sq3fXPq&|mI&7Cp=ukPmG?Y}qgKIwkh1FZ)Y4~-wz{9^yh-VV2pqmM!!wLMOJe6Lf``TQSw|M=8Z z@=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.m index 2d370aa2e6c8..6adfd50402af 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerImageUtil.m @@ -116,11 +116,11 @@ + (GIFInfo *)scaledGIFImage:(NSData *)data maxWidth:(NSNumber *)maxWidth maxHeight:(NSNumber *)maxHeight { NSMutableDictionary *options = [NSMutableDictionary dictionary]; - options[(NSString *)kCGImageSourceShouldCache] = @(YES); + options[(NSString *)kCGImageSourceShouldCache] = @YES; options[(NSString *)kCGImageSourceTypeIdentifierHint] = (NSString *)kUTTypeGIF; CGImageSourceRef imageSource = - CGImageSourceCreateWithData((CFDataRef)data, (CFDictionaryRef)options); + CGImageSourceCreateWithData((__bridge CFDataRef)data, (__bridge CFDictionaryRef)options); size_t numberOfFrames = CGImageSourceGetCount(imageSource); NSMutableArray *images = [NSMutableArray arrayWithCapacity:numberOfFrames]; @@ -128,7 +128,7 @@ + (GIFInfo *)scaledGIFImage:(NSData *)data NSTimeInterval interval = 0.0; for (size_t index = 0; index < numberOfFrames; index++) { CGImageRef imageRef = - CGImageSourceCreateImageAtIndex(imageSource, index, (CFDictionaryRef)options); + CGImageSourceCreateImageAtIndex(imageSource, index, (__bridge CFDictionaryRef)options); NSDictionary *properties = (NSDictionary *)CFBridgingRelease( CGImageSourceCopyPropertiesAtIndex(imageSource, index, NULL)); diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerMetaDataUtil.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerMetaDataUtil.m index 45bcaa7191f7..195462533544 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerMetaDataUtil.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerMetaDataUtil.m @@ -42,7 +42,7 @@ + (NSString *)imageTypeSuffixFromType:(FLTImagePickerMIMEType)type { } + (NSDictionary *)getMetaDataFromImageData:(NSData *)imageData { - CGImageSourceRef source = CGImageSourceCreateWithData((CFDataRef)imageData, NULL); + CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL); NSDictionary *metadata = (NSDictionary *)CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(source, 0, NULL)); CFRelease(source); 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 37a1a9897cd3..fef94ad30bea 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m @@ -103,7 +103,7 @@ + (NSString *)saveImageWithMetaData:(NSDictionary *)metaData gifInfo:(GIFInfo *)gifInfo path:(NSString *)path { CGImageDestinationRef destination = CGImageDestinationCreateWithURL( - (CFURLRef)[NSURL fileURLWithPath:path], kUTTypeGIF, gifInfo.images.count, NULL); + (__bridge CFURLRef)[NSURL fileURLWithPath:path], kUTTypeGIF, gifInfo.images.count, NULL); NSDictionary *frameProperties = @{ (__bridge NSString *)kCGImagePropertyGIFDictionary : @{ @@ -120,11 +120,12 @@ + (NSString *)saveImageWithMetaData:(NSDictionary *)metaData gifProperties[(__bridge NSString *)kCGImagePropertyGIFLoopCount] = @0; - CGImageDestinationSetProperties(destination, (CFDictionaryRef)gifMetaProperties); + CGImageDestinationSetProperties(destination, (__bridge CFDictionaryRef)gifMetaProperties); for (NSInteger index = 0; index < gifInfo.images.count; index++) { UIImage *image = (UIImage *)[gifInfo.images objectAtIndex:index]; - CGImageDestinationAddImage(destination, image.CGImage, (CFDictionaryRef)frameProperties); + CGImageDestinationAddImage(destination, image.CGImage, + (__bridge CFDictionaryRef)frameProperties); } CGImageDestinationFinalize(destination); diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.h index c88db0bad72f..626e2ba77d67 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.h +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.h @@ -5,9 +5,9 @@ #import #import -@interface FLTImagePickerPlugin : NSObject - -// For testing only. -- (UIViewController *)viewControllerWithWindow:(UIWindow *)window; +NS_ASSUME_NONNULL_BEGIN +@interface FLTImagePickerPlugin : NSObject @end + +NS_ASSUME_NONNULL_END 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 27b06ba994ef..e910f8fc333b 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m @@ -29,10 +29,7 @@ - (instancetype)initWithResult:(nonnull FlutterResultAdapter)result { #pragma mark - -@interface FLTImagePickerPlugin () +@interface FLTImagePickerPlugin () /** * The PHPickerViewController instance used to pick multiple @@ -368,11 +365,13 @@ - (void)checkPhotoAuthorizationWithImagePicker:(UIImagePickerController *)imageP } - (void)checkPhotoAuthorizationForAccessLevel API_AVAILABLE(ios(14)) { - PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus]; + PHAccessLevel requestedAccessLevel = PHAccessLevelReadWrite; + PHAuthorizationStatus status = + [PHPhotoLibrary authorizationStatusForAccessLevel:requestedAccessLevel]; switch (status) { case PHAuthorizationStatusNotDetermined: { [PHPhotoLibrary - requestAuthorizationForAccessLevel:PHAccessLevelReadWrite + requestAuthorizationForAccessLevel:requestedAccessLevel handler:^(PHAuthorizationStatus status) { dispatch_async(dispatch_get_main_queue(), ^{ if (status == PHAuthorizationStatusAuthorized) { @@ -478,52 +477,55 @@ - (void)picker:(PHPickerViewController *)picker [self sendCallResultWithSavedPathList:nil]; return; } - dispatch_queue_t backgroundQueue = - dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); - dispatch_async(backgroundQueue, ^{ - NSNumber *maxWidth = self.callContext.maxSize.width; - NSNumber *maxHeight = self.callContext.maxSize.height; - NSNumber *imageQuality = self.callContext.imageQuality; - NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality]; - NSOperationQueue *operationQueue = [NSOperationQueue new]; - NSMutableArray *pathList = [self createNSMutableArrayWithSize:results.count]; - - for (int i = 0; i < results.count; i++) { - PHPickerResult *result = results[i]; - FLTPHPickerSaveImageToPathOperation *operation = [[FLTPHPickerSaveImageToPathOperation alloc] - initWithResult:result - maxHeight:maxHeight - maxWidth:maxWidth - desiredImageQuality:desiredImageQuality - fullMetadata:self.callContext.requestFullMetadata - savedPathBlock:^(NSString *savedPath) { - pathList[i] = savedPath; - }]; - [operationQueue addOperation:operation]; + __block NSOperationQueue *saveQueue = [[NSOperationQueue alloc] init]; + saveQueue.name = @"Flutter Save Image Queue"; + saveQueue.qualityOfService = NSQualityOfServiceUserInitiated; + + FLTImagePickerMethodCallContext *currentCallContext = self.callContext; + NSNumber *maxWidth = currentCallContext.maxSize.width; + NSNumber *maxHeight = currentCallContext.maxSize.height; + NSNumber *imageQuality = currentCallContext.imageQuality; + NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality]; + BOOL requestFullMetadata = currentCallContext.requestFullMetadata; + NSMutableArray *pathList = [[NSMutableArray alloc] initWithCapacity:results.count]; + __block FlutterError *saveError = nil; + __weak typeof(self) weakSelf = self; + // This operation will be executed on the main queue after + // all selected files have been saved. + NSBlockOperation *sendListOperation = [NSBlockOperation blockOperationWithBlock:^{ + if (saveError != nil) { + [weakSelf sendCallResultWithError:saveError]; + } else { + [weakSelf sendCallResultWithSavedPathList:pathList]; } - [operationQueue waitUntilAllOperationsAreFinished]; - dispatch_async(dispatch_get_main_queue(), ^{ - [self sendCallResultWithSavedPathList:pathList]; - }); - }); -} - -#pragma mark - - -/** - * Creates an NSMutableArray of a certain size filled with NSNull objects. - * - * The difference with initWithCapacity is that initWithCapacity still gives an empty array making - * it impossible to add objects on an index larger than the size. - * - * @param size The length of the required array - * @return NSMutableArray An array of a specified size - */ -- (NSMutableArray *)createNSMutableArrayWithSize:(NSUInteger)size { - NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithCapacity:size]; - for (int i = 0; i < size; [mutableArray addObject:[NSNull null]], i++) - ; - return mutableArray; + // Retain queue until here. + saveQueue = nil; + }]; + + [results enumerateObjectsUsingBlock:^(PHPickerResult *result, NSUInteger index, BOOL *stop) { + // NSNull means it hasn't saved yet. + [pathList addObject:[NSNull null]]; + FLTPHPickerSaveImageToPathOperation *saveOperation = + [[FLTPHPickerSaveImageToPathOperation alloc] + initWithResult:result + maxHeight:maxHeight + maxWidth:maxWidth + desiredImageQuality:desiredImageQuality + fullMetadata:requestFullMetadata + savedPathBlock:^(NSString *savedPath, FlutterError *error) { + if (savedPath != nil) { + pathList[index] = savedPath; + } else { + saveError = error; + } + }]; + [sendListOperation addDependency:saveOperation]; + [saveQueue addOperation:saveOperation]; + }]; + + // Schedule the final Flutter callback on the main queue + // to be run after all images have been saved. + [NSOperationQueue.mainQueue addOperation:sendListOperation]; } #pragma mark - UIImagePickerControllerDelegate 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 d73a54d245f6..f84921160a31 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 @@ -54,13 +54,19 @@ typedef void (^FlutterResultAdapter)(NSArray *_Nullable, FlutterErro #pragma mark - /** Methods exposed for unit testing. */ -@interface FLTImagePickerPlugin () +@interface FLTImagePickerPlugin () /** * The context of the Flutter method call that is currently being handled, if any. */ @property(strong, nonatomic, nullable) FLTImagePickerMethodCallContext *callContext; +- (UIViewController *)viewControllerWithWindow:(nullable UIWindow *)window; + /** * Validates the provided paths list, then sends it via `callContext.result` as the result of the * original platform channel method call, clearing the in-progress call state. diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.h index 8e0970725e90..00c1f1dacd6c 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.h +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.h @@ -9,6 +9,11 @@ #import "FLTImagePickerMetaDataUtil.h" #import "FLTImagePickerPhotoAssetUtil.h" +NS_ASSUME_NONNULL_BEGIN + +/// Returns either the saved path, or an error. Both cannot be set. +typedef void (^FLTGetSavedPath)(NSString *_Nullable savedPath, FlutterError *_Nullable error); + /*! @class FLTPHPickerSaveImageToPathOperation @@ -27,6 +32,8 @@ maxWidth:(NSNumber *)maxWidth desiredImageQuality:(NSNumber *)desiredImageQuality fullMetadata:(BOOL)fullMetadata - savedPathBlock:(void (^)(NSString *))savedPathBlock API_AVAILABLE(ios(14)); + savedPathBlock:(FLTGetSavedPath)savedPathBlock API_AVAILABLE(ios(14)); @end + +NS_ASSUME_NONNULL_END 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 7c8fbc9ca7cf..80e03ddd6578 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m @@ -2,27 +2,28 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#import #import #import "FLTPHPickerSaveImageToPathOperation.h" +#import + API_AVAILABLE(ios(14)) @interface FLTPHPickerSaveImageToPathOperation () @property(strong, nonatomic) PHPickerResult *result; -@property(assign, nonatomic) NSNumber *maxHeight; -@property(assign, nonatomic) NSNumber *maxWidth; -@property(assign, nonatomic) NSNumber *desiredImageQuality; +@property(strong, nonatomic) NSNumber *maxHeight; +@property(strong, nonatomic) NSNumber *maxWidth; +@property(strong, nonatomic) NSNumber *desiredImageQuality; @property(assign, nonatomic) BOOL requestFullMetadata; @end -typedef void (^GetSavedPath)(NSString *); - @implementation FLTPHPickerSaveImageToPathOperation { BOOL executing; BOOL finished; - GetSavedPath getSavedPath; + FLTGetSavedPath getSavedPath; } - (instancetype)initWithResult:(PHPickerResult *)result @@ -30,7 +31,7 @@ - (instancetype)initWithResult:(PHPickerResult *)result maxWidth:(NSNumber *)maxWidth desiredImageQuality:(NSNumber *)desiredImageQuality fullMetadata:(BOOL)fullMetadata - savedPathBlock:(GetSavedPath)savedPathBlock API_AVAILABLE(ios(14)) { + savedPathBlock:(FLTGetSavedPath)savedPathBlock API_AVAILABLE(ios(14)) { if (self = [super init]) { if (result) { self.result = result; @@ -74,10 +75,10 @@ - (void)setExecuting:(BOOL)isExecuting { [self didChangeValueForKey:@"isExecuting"]; } -- (void)completeOperationWithPath:(NSString *)savedPath { +- (void)completeOperationWithPath:(NSString *)savedPath error:(FlutterError *)error { + getSavedPath(savedPath, error); [self setExecuting:NO]; [self setFinished:YES]; - getSavedPath(savedPath); } - (void)start { @@ -88,25 +89,30 @@ - (void)start { if (@available(iOS 14, *)) { [self setExecuting:YES]; - if ([self.result.itemProvider hasItemConformingToTypeIdentifier:UTTypeWebP.identifier]) { + // This supports uniform types that conform to UTTypeImage. + // This includes UTTypeHEIC, UTTypeHEIF, UTTypeLivePhoto, UTTypeICO, UTTypeICNS, UTTypePNG + // UTTypeGIF, UTTypeJPEG, UTTypeWebP, UTTypeTIFF, UTTypeBMP, UTTypeSVG, UTTypeRAWImage + if ([self.result.itemProvider hasItemConformingToTypeIdentifier:UTTypeImage.identifier]) { [self.result.itemProvider - loadDataRepresentationForTypeIdentifier:UTTypeWebP.identifier + loadDataRepresentationForTypeIdentifier:UTTypeImage.identifier completionHandler:^(NSData *_Nullable data, NSError *_Nullable error) { - UIImage *image = [[UIImage alloc] initWithData:data]; - [self processImage:image]; + if (data != nil) { + [self processImage:data]; + } else { + FlutterError *flutterError = + [FlutterError errorWithCode:@"invalid_image" + message:error.localizedDescription + details:error.domain]; + [self completeOperationWithPath:nil error:flutterError]; + } }]; - return; + } else { + FlutterError *flutterError = [FlutterError errorWithCode:@"invalid_source" + message:@"Invalid image source." + details:nil]; + [self completeOperationWithPath:nil error:flutterError]; } - - [self.result.itemProvider - loadObjectOfClass:[UIImage class] - completionHandler:^(__kindof id _Nullable image, - NSError *_Nullable error) { - if ([image isKindOfClass:[UIImage class]]) { - [self processImage:image]; - } - }]; } else { [self setFinished:YES]; } @@ -115,7 +121,9 @@ - (void)start { /** * Processes the image. */ -- (void)processImage:(UIImage *)localImage API_AVAILABLE(ios(14)) { +- (void)processImage:(NSData *)pickerImageData API_AVAILABLE(ios(14)) { + UIImage *localImage = [[UIImage alloc] initWithData:pickerImageData]; + PHAsset *originalAsset; // Only if requested, fetch the full "PHAsset" metadata, which requires "Photo Library Usage" // permissions. @@ -127,7 +135,7 @@ - (void)processImage:(UIImage *)localImage API_AVAILABLE(ios(14)) { localImage = [FLTImagePickerImageUtil scaledImage:localImage maxWidth:self.maxWidth maxHeight:self.maxHeight - isMetadataAvailable:originalAsset != nil]; + isMetadataAvailable:YES]; } if (originalAsset) { void (^resultHandler)(NSData *imageData, NSString *dataUTI, NSDictionary *info) = @@ -139,7 +147,7 @@ - (void)processImage:(UIImage *)localImage API_AVAILABLE(ios(14)) { maxWidth:self.maxWidth maxHeight:self.maxHeight imageQuality:self.desiredImageQuality]; - [self completeOperationWithPath:savedPath]; + [self completeOperationWithPath:savedPath error:nil]; }; if (@available(iOS 13.0, *)) { [[PHImageManager defaultManager] @@ -165,11 +173,14 @@ - (void)processImage:(UIImage *)localImage API_AVAILABLE(ios(14)) { } } else { // Image picked without an original asset (e.g. User pick image without permission) + // maxWidth and maxHeight are used only for GIF images. NSString *savedPath = - [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:nil - image:localImage - imageQuality:self.desiredImageQuality]; - [self completeOperationWithPath:savedPath]; + [FLTImagePickerPhotoAssetUtil saveImageWithOriginalImageData:pickerImageData + image:localImage + maxWidth:self.maxWidth + maxHeight:self.maxHeight + imageQuality:self.desiredImageQuality]; + [self completeOperationWithPath:savedPath error: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 fbc356f212b8..3f76784ff07c 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 @@ -14,9 +14,14 @@ SourceType _convertSource(ImageSource source) { return SourceType.camera; case ImageSource.gallery: return SourceType.gallery; - default: - throw UnimplementedError('Unknown source: $source'); } + // The enum comes from a different package, which could get a new value at + // any time, so a fallback case is necessary. Since there is no reasonable + // default behavior, throw to alert the client that they need an updated + // version. This is deliberately outside the switch rather than a `default` + // so that the linter will flag the switch as needing an update. + // ignore: dead_code + throw UnimplementedError('Unknown source: $source'); } // Converts a [CameraDevice] to the corresponding Pigeon API enum value. @@ -26,9 +31,14 @@ SourceCamera _convertCamera(CameraDevice camera) { return SourceCamera.front; case CameraDevice.rear: return SourceCamera.rear; - default: - throw UnimplementedError('Unknown camera: $camera'); } + // The enum comes from a different package, which could get a new value at + // any time, so a fallback case is necessary. Since there is no reasonable + // default behavior, throw to alert the client that they need an updated + // version. This is deliberately outside the switch rather than a `default` + // so that the linter will flag the switch as needing an update. + // ignore: dead_code + throw UnimplementedError('Unknown camera: $camera'); } /// An implementation of [ImagePickerPlatform] for iOS. diff --git a/packages/image_picker/image_picker_ios/pigeons/messages.dart b/packages/image_picker/image_picker_ios/pigeons/messages.dart index dd8a0f0c0834..d04841b0fde9 100644 --- a/packages/image_picker/image_picker_ios/pigeons/messages.dart +++ b/packages/image_picker/image_picker_ios/pigeons/messages.dart @@ -6,7 +6,7 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions( dartOut: 'lib/src/messages.g.dart', - dartTestOut: 'test/test_api.dart', + dartTestOut: 'test/test_api.g.dart', objcHeaderOut: 'ios/Classes/messages.g.h', objcSourceOut: 'ios/Classes/messages.g.m', objcOptions: ObjcOptions( diff --git a/packages/image_picker/image_picker_ios/pubspec.yaml b/packages/image_picker/image_picker_ios/pubspec.yaml index 6c78b2340ed0..b188055087c7 100755 --- a/packages/image_picker/image_picker_ios/pubspec.yaml +++ b/packages/image_picker/image_picker_ios/pubspec.yaml @@ -2,11 +2,11 @@ name: image_picker_ios description: iOS implementation of the image_picker plugin. repository: https://github.com/flutter/plugins/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.6+1 +version: 0.8.6+8 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: 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 b20025770ad1..2c9d52509f26 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 @@ -8,7 +8,7 @@ import 'package:image_picker_ios/image_picker_ios.dart'; import 'package:image_picker_ios/src/messages.g.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; -import 'test_api.dart'; +import 'test_api.g.dart'; @immutable class _LoggedMethodCall { diff --git a/packages/image_picker/image_picker_ios/test/test_api.dart b/packages/image_picker/image_picker_ios/test/test_api.g.dart similarity index 100% rename from packages/image_picker/image_picker_ios/test/test_api.dart rename to packages/image_picker/image_picker_ios/test/test_api.g.dart diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index 05b03a37cb98..91d6d80e6c23 100644 --- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md +++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 2.6.2 * Updates imports for `prefer_relative_imports`. diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index eb4d2b649eac..2f34ee2b349c 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -8,7 +8,7 @@ version: 2.6.2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: cross_file: ^0.3.1+1 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/new_method_channel_image_picker_test.dart index 44980100742a..244af3982672 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/new_method_channel_image_picker_test.dart @@ -18,7 +18,10 @@ void main() { setUp(() { returnValue = ''; - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { log.add(methodCall); return returnValue; }); @@ -185,8 +188,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel - .setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.pickImage(source: ImageSource.gallery), isNull); expect(await picker.pickImage(source: ImageSource.camera), isNull); @@ -352,8 +357,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel - .setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.pickMultiImage(), isNull); expect(await picker.pickMultiImage(), isNull); @@ -424,8 +431,10 @@ void main() { }); test('handles a null video path response gracefully', () async { - picker.channel - .setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.pickVideo(source: ImageSource.gallery), isNull); expect(await picker.pickVideo(source: ImageSource.camera), isNull); @@ -467,7 +476,10 @@ void main() { group('#retrieveLostData', () { test('retrieveLostData get success response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'image', 'path': '/example/path', @@ -480,7 +492,10 @@ void main() { }); test('retrieveLostData get error response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'video', 'errorCode': 'test_error_code', @@ -495,14 +510,20 @@ void main() { }); test('retrieveLostData get null response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return null; }); expect((await picker.retrieveLostData()).isEmpty, true); }); test('retrieveLostData get both path and error should throw', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'video', 'errorCode': 'test_error_code', @@ -672,8 +693,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel - .setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.getImage(source: ImageSource.gallery), isNull); expect(await picker.getImage(source: ImageSource.camera), isNull); @@ -839,8 +862,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel - .setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.getMultiImage(), isNull); expect(await picker.getMultiImage(), isNull); @@ -911,8 +936,10 @@ void main() { }); test('handles a null video path response gracefully', () async { - picker.channel - .setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.getVideo(source: ImageSource.gallery), isNull); expect(await picker.getVideo(source: ImageSource.camera), isNull); @@ -954,7 +981,10 @@ void main() { group('#getLostData', () { test('getLostData get success response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'image', 'path': '/example/path', @@ -967,7 +997,10 @@ void main() { }); test('getLostData should successfully retrieve multiple files', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'image', 'path': '/example/path1', @@ -983,7 +1016,10 @@ void main() { }); test('getLostData get error response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'video', 'errorCode': 'test_error_code', @@ -998,14 +1034,20 @@ void main() { }); test('getLostData get null response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return null; }); expect((await picker.getLostData()).isEmpty, true); }); test('getLostData get both path and error should throw', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'video', 'errorCode': 'test_error_code', @@ -1201,8 +1243,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel - .setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.getImageFromSource(source: ImageSource.gallery), isNull); @@ -1431,8 +1475,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel - .setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.getMultiImage(), isNull); expect(await picker.getMultiImage(), isNull); @@ -1478,3 +1524,9 @@ void main() { }); }); } + +/// 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/image_picker/image_picker_windows/CHANGELOG.md b/packages/image_picker/image_picker_windows/CHANGELOG.md index 427598760a4b..e739db71363e 100644 --- a/packages/image_picker/image_picker_windows/CHANGELOG.md +++ b/packages/image_picker/image_picker_windows/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.1.0+4 + +* Updates example code for `use_build_context_synchronously` lint. +* Updates minimum Flutter version to 3.0. + ## 0.1.0+3 * Changes XTypeGroup initialization from final to const. 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 e340a185bf3d..dae45a5e2957 100644 --- a/packages/image_picker/image_picker_windows/example/lib/main.dart +++ b/packages/image_picker/image_picker_windows/example/lib/main.dart @@ -70,8 +70,8 @@ class _MyHomePageState extends State { } } - Future _handleMultiImagePicked(BuildContext? context) async { - await _displayPickImageDialog(context!, + Future _handleMultiImagePicked(BuildContext context) async { + await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { final List? pickedFileList = await _picker.pickMultiImage( @@ -91,8 +91,8 @@ class _MyHomePageState extends State { } Future _handleSingleImagePicked( - BuildContext? context, ImageSource source) async { - await _displayPickImageDialog(context!, + BuildContext context, ImageSource source) async { + await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { final PickedFile? pickedFile = await _picker.pickImage( @@ -113,18 +113,20 @@ class _MyHomePageState extends State { } Future _onImageButtonPressed(ImageSource source, - {BuildContext? context, bool isMultiImage = false}) async { + {required BuildContext context, bool isMultiImage = false}) async { if (_controller != null) { await _controller!.setVolume(0.0); } - if (_isVideo) { - final PickedFile? file = await _picker.pickVideo( - source: source, maxDuration: const Duration(seconds: 10)); - await _playVideo(file); - } else if (isMultiImage) { - await _handleMultiImagePicked(context); - } else { - await _handleSingleImagePicked(context, source); + if (context.mounted) { + if (_isVideo) { + final PickedFile? file = await _picker.pickVideo( + source: source, maxDuration: const Duration(seconds: 10)); + await _playVideo(file); + } else if (isMultiImage) { + await _handleMultiImagePicked(context); + } else { + await _handleSingleImagePicked(context, source); + } } } @@ -269,7 +271,7 @@ class _MyHomePageState extends State { backgroundColor: Colors.red, onPressed: () { _isVideo = true; - _onImageButtonPressed(ImageSource.gallery); + _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'video0', tooltip: 'Pick Video from gallery', @@ -282,7 +284,7 @@ class _MyHomePageState extends State { backgroundColor: Colors.red, onPressed: () { _isVideo = true; - _onImageButtonPressed(ImageSource.camera); + _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'video1', tooltip: 'Take a Video', diff --git a/packages/image_picker/image_picker_windows/example/pubspec.yaml b/packages/image_picker/image_picker_windows/example/pubspec.yaml index b87000a6caff..bdbd182d3fc5 100644 --- a/packages/image_picker/image_picker_windows/example/pubspec.yaml +++ b/packages/image_picker/image_picker_windows/example/pubspec.yaml @@ -5,7 +5,7 @@ version: 1.0.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/image_picker/image_picker_windows/pubspec.yaml b/packages/image_picker/image_picker_windows/pubspec.yaml index 5d6988cc2931..07fa673649de 100644 --- a/packages/image_picker/image_picker_windows/pubspec.yaml +++ b/packages/image_picker/image_picker_windows/pubspec.yaml @@ -2,11 +2,11 @@ name: image_picker_windows description: Windows platform implementation of image_picker repository: https://github.com/flutter/plugins/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+3 +version: 0.1.0+4 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: 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 c3df2d80679f..f8adde4051c7 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 @@ -15,6 +15,12 @@ import 'image_picker_windows_test.mocks.dart'; 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; + } + group('$ImagePickerWindows()', () { final ImagePickerWindows plugin = ImagePickerWindows(); late MockFileSelectorPlatform mockFileSelectorPlatform; @@ -42,48 +48,42 @@ void main() { test('pickImage passes the accepted type groups correctly', () async { await plugin.pickImage(source: ImageSource.gallery); - expect( - verify(mockFileSelectorPlatform.openFile( - acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))) - .captured - .single[0] - .extensions, + final VerificationResult result = verify( + mockFileSelectorPlatform.openFile( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].extensions, ImagePickerWindows.imageFormats); }); test('pickImage throws UnimplementedError when source is camera', () async { - expect(() async => await plugin.pickImage(source: ImageSource.camera), + expect(() async => plugin.pickImage(source: ImageSource.camera), throwsA(isA())); }); test('getImage passes the accepted type groups correctly', () async { await plugin.getImage(source: ImageSource.gallery); - expect( - verify(mockFileSelectorPlatform.openFile( - acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))) - .captured - .single[0] - .extensions, + final VerificationResult result = verify( + mockFileSelectorPlatform.openFile( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].extensions, ImagePickerWindows.imageFormats); }); test('getImage throws UnimplementedError when source is camera', () async { - expect(() async => await plugin.getImage(source: ImageSource.camera), + expect(() async => plugin.getImage(source: ImageSource.camera), throwsA(isA())); }); test('getMultiImage passes the accepted type groups correctly', () async { await plugin.getMultiImage(); - expect( - verify(mockFileSelectorPlatform.openFiles( - acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))) - .captured - .single[0] - .extensions, + final VerificationResult result = verify( + mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].extensions, ImagePickerWindows.imageFormats); }); }); @@ -91,36 +91,32 @@ void main() { test('pickVideo passes the accepted type groups correctly', () async { await plugin.pickVideo(source: ImageSource.gallery); - expect( - verify(mockFileSelectorPlatform.openFile( - acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))) - .captured - .single[0] - .extensions, + final VerificationResult result = verify( + mockFileSelectorPlatform.openFile( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].extensions, ImagePickerWindows.videoFormats); }); test('pickVideo throws UnimplementedError when source is camera', () async { - expect(() async => await plugin.pickVideo(source: ImageSource.camera), + expect(() async => plugin.pickVideo(source: ImageSource.camera), throwsA(isA())); }); test('getVideo passes the accepted type groups correctly', () async { await plugin.getVideo(source: ImageSource.gallery); - expect( - verify(mockFileSelectorPlatform.openFile( - acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))) - .captured - .single[0] - .extensions, + final VerificationResult result = verify( + mockFileSelectorPlatform.openFile( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].extensions, ImagePickerWindows.videoFormats); }); test('getVideo throws UnimplementedError when source is camera', () async { - expect(() async => await plugin.getVideo(source: ImageSource.camera), + expect(() async => plugin.getVideo(source: ImageSource.camera), throwsA(isA())); }); }); diff --git a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md index f38fe18a25d4..19e65372662d 100644 --- a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md @@ -1,3 +1,24 @@ +## 3.1.4 + +* Updates iOS minimum version in README. + +## 3.1.3 + +* Ignores a lint in the example app for backwards compatibility. + +## 3.1.2 + +* Updates example code for `use_build_context_synchronously` lint. +* Updates minimum Flutter version to 3.0. + +## 3.1.1 + +* Adds screenshots to pubspec.yaml. + +## 3.1.0 + +* Adds macOS as a supported platform. + ## 3.0.8 * Updates minimum Flutter version to 2.10. diff --git a/packages/in_app_purchase/in_app_purchase/README.md b/packages/in_app_purchase/in_app_purchase/README.md index 8986b9dea809..6df0ebaccaa0 100644 --- a/packages/in_app_purchase/in_app_purchase/README.md +++ b/packages/in_app_purchase/in_app_purchase/README.md @@ -3,11 +3,11 @@ A storefront-independent API for purchases in Flutter apps. This plugin supports in-app purchases (_IAP_) through an _underlying store_, -which can be the App Store (on iOS) or Google Play (on Android). +which can be the App Store (on iOS and macOS) or Google Play (on Android). -| | Android | iOS | -|-------------|---------|------| -| **Support** | SDK 16+ | 9.0+ | +| | Android | iOS | macOS | +|-------------|---------|-------|--------| +| **Support** | SDK 16+ | 11.0+ | 10.15+ |

CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 11.0 diff --git a/packages/in_app_purchase/in_app_purchase/example/ios/Podfile b/packages/in_app_purchase/in_app_purchase/example/ios/Podfile index 310b9b498ba6..cad555de0518 100644 --- a/packages/in_app_purchase/in_app_purchase/example/ios/Podfile +++ b/packages/in_app_purchase/in_app_purchase/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/project.pbxproj b/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/project.pbxproj index df13d20ae61d..8b83bba96707 100644 --- a/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -176,7 +176,7 @@ isa = PBXProject; attributes = { DefaultBuildSystemTypeForWorkspace = Original; - LastUpgradeCheck = 1100; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -224,10 +224,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -256,6 +258,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -351,7 +354,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -401,7 +404,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3bb3697ef41c..50a8cfc99f50 100644 --- a/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/in_app_purchase/in_app_purchase/example/lib/main.dart b/packages/in_app_purchase/in_app_purchase/example/lib/main.dart index 9e53b4bf8b8e..21268d4e7e8a 100644 --- a/packages/in_app_purchase/in_app_purchase/example/lib/main.dart +++ b/packages/in_app_purchase/in_app_purchase/example/lib/main.dart @@ -164,6 +164,8 @@ class _MyAppState extends State<_MyApp> { } if (_purchasePending) { stack.add( + // TODO(goderbauer): Make this const when that's available on stable. + // ignore: prefer_const_constructors Stack( children: const [ Opacity( @@ -468,17 +470,19 @@ class _MyAppState extends State<_MyApp> { await androidAddition.launchPriceChangeConfirmationFlow( sku: 'purchaseId', ); - if (priceChangeConfirmationResult.responseCode == BillingResponse.ok) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Price change accepted'), - )); - } else { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text( - priceChangeConfirmationResult.debugMessage ?? - 'Price change failed with code ${priceChangeConfirmationResult.responseCode}', - ), - )); + if (context.mounted) { + if (priceChangeConfirmationResult.responseCode == BillingResponse.ok) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Price change accepted'), + )); + } else { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + priceChangeConfirmationResult.debugMessage ?? + 'Price change failed with code ${priceChangeConfirmationResult.responseCode}', + ), + )); + } } } if (Platform.isIOS) { diff --git a/packages/in_app_purchase/in_app_purchase/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/in_app_purchase/in_app_purchase/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000000..4b81f9b2d200 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/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/in_app_purchase/in_app_purchase/example/macos/Flutter/Flutter-Release.xcconfig b/packages/in_app_purchase/in_app_purchase/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000000..5caa9d1579e4 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/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/in_app_purchase/in_app_purchase/example/macos/Podfile b/packages/in_app_purchase/in_app_purchase/example/macos/Podfile new file mode 100644 index 000000000000..9ec46f8cd53c --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.15' + +# 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/in_app_purchase/in_app_purchase/example/macos/Runner.xcodeproj/project.pbxproj b/packages/in_app_purchase/in_app_purchase/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..113455d6e179 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,631 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + 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 */ + 1936C695A67BE3AC115E6938 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91FC7522CD18DEF95EA2F630 /* Pods_Runner.framework */; }; + 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 */ + 0B65A69DF81DCCDA43899BF5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 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 = ""; }; + 684854F04C44509BBCC60AB6 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 91FC7522CD18DEF95EA2F630 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + BB0D94179B02A04FD98DA5BE /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1936C695A67BE3AC115E6938 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 27C7C1505089764F862054EC /* Pods */ = { + isa = PBXGroup; + children = ( + 684854F04C44509BBCC60AB6 /* Pods-Runner.debug.xcconfig */, + BB0D94179B02A04FD98DA5BE /* Pods-Runner.release.xcconfig */, + 0B65A69DF81DCCDA43899BF5 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 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 */, + 27C7C1505089764F862054EC /* Pods */, + ); + 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 = ( + 91FC7522CD18DEF95EA2F630 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + D69320C4691D46AC439743E0 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 4225BF8FBCA00DC792D87EF7 /* [CP] Embed Pods Frameworks */, + ); + 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; + 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"; + }; + 4225BF8FBCA00DC792D87EF7 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + D69320C4691D46AC439743E0 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 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.15; + 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.15; + 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.15; + 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/path_provider/path_provider_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/in_app_purchase/in_app_purchase/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/path_provider/path_provider_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/in_app_purchase/in_app_purchase/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/in_app_purchase/in_app_purchase/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/in_app_purchase/in_app_purchase/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..fb7259e17785 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/in_app_purchase/in_app_purchase/example/macos/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/path_provider/path_provider_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/in_app_purchase/in_app_purchase/example/macos/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/in_app_purchase/in_app_purchase/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/in_app_purchase/in_app_purchase/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner/AppDelegate.swift b/packages/in_app_purchase/in_app_purchase/example/macos/Runner/AppDelegate.swift similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner/AppDelegate.swift rename to packages/in_app_purchase/in_app_purchase/example/macos/Runner/AppDelegate.swift diff --git a/packages/in_app_purchase/in_app_purchase/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/in_app_purchase/in_app_purchase/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000000..80e867a4e06b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/in_app_purchase/in_app_purchase/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/in_app_purchase/in_app_purchase/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000000..3c916dec7ec9 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/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.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2022 dev.flutter.plugins. All rights reserved. diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner/Configs/Debug.xcconfig b/packages/in_app_purchase/in_app_purchase/example/macos/Runner/Configs/Debug.xcconfig similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner/Configs/Debug.xcconfig rename to packages/in_app_purchase/in_app_purchase/example/macos/Runner/Configs/Debug.xcconfig diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner/Configs/Release.xcconfig b/packages/in_app_purchase/in_app_purchase/example/macos/Runner/Configs/Release.xcconfig similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner/Configs/Release.xcconfig rename to packages/in_app_purchase/in_app_purchase/example/macos/Runner/Configs/Release.xcconfig diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner/Configs/Warnings.xcconfig b/packages/in_app_purchase/in_app_purchase/example/macos/Runner/Configs/Warnings.xcconfig similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner/Configs/Warnings.xcconfig rename to packages/in_app_purchase/in_app_purchase/example/macos/Runner/Configs/Warnings.xcconfig diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/DebugProfile.entitlements b/packages/in_app_purchase/in_app_purchase/example/macos/Runner/DebugProfile.entitlements similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner/DebugProfile.entitlements rename to packages/in_app_purchase/in_app_purchase/example/macos/Runner/DebugProfile.entitlements diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner/Info.plist b/packages/in_app_purchase/in_app_purchase/example/macos/Runner/Info.plist similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner/Info.plist rename to packages/in_app_purchase/in_app_purchase/example/macos/Runner/Info.plist diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner/MainFlutterWindow.swift b/packages/in_app_purchase/in_app_purchase/example/macos/Runner/MainFlutterWindow.swift similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner/MainFlutterWindow.swift rename to packages/in_app_purchase/in_app_purchase/example/macos/Runner/MainFlutterWindow.swift diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Release.entitlements b/packages/in_app_purchase/in_app_purchase/example/macos/Runner/Release.entitlements similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Release.entitlements rename to packages/in_app_purchase/in_app_purchase/example/macos/Runner/Release.entitlements diff --git a/packages/in_app_purchase/in_app_purchase/example/pubspec.yaml b/packages/in_app_purchase/in_app_purchase/example/pubspec.yaml index 74ad9aafb768..8037b1a4c1ef 100644 --- a/packages/in_app_purchase/in_app_purchase/example/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: @@ -17,7 +17,7 @@ dependencies: # the parent directory to use the current plugin's version. path: ../ in_app_purchase_android: ^0.2.1 - in_app_purchase_storekit: ^0.3.0+1 + in_app_purchase_storekit: ^0.3.4 shared_preferences: ^2.0.0 dev_dependencies: diff --git a/packages/in_app_purchase/in_app_purchase/lib/in_app_purchase.dart b/packages/in_app_purchase/in_app_purchase/lib/in_app_purchase.dart index d550e48ebc3a..64e0095dcbb7 100644 --- a/packages/in_app_purchase/in_app_purchase/lib/in_app_purchase.dart +++ b/packages/in_app_purchase/in_app_purchase/lib/in_app_purchase.dart @@ -34,7 +34,8 @@ class InAppPurchase implements InAppPurchasePlatformAdditionProvider { if (defaultTargetPlatform == TargetPlatform.android) { InAppPurchaseAndroidPlatform.registerPlatform(); - } else if (defaultTargetPlatform == TargetPlatform.iOS) { + } else if (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS) { InAppPurchaseStoreKitPlatform.registerPlatform(); } diff --git a/packages/in_app_purchase/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/in_app_purchase/pubspec.yaml index 40b0ea9a152b..483fe2c3b691 100644 --- a/packages/in_app_purchase/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase/pubspec.yaml @@ -2,11 +2,11 @@ name: in_app_purchase description: A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store and Google Play. repository: https://github.com/flutter/plugins/tree/main/packages/in_app_purchase/in_app_purchase issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 3.0.8 +version: 3.1.4 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -15,13 +15,15 @@ flutter: default_package: in_app_purchase_android ios: default_package: in_app_purchase_storekit + macos: + default_package: in_app_purchase_storekit dependencies: flutter: sdk: flutter in_app_purchase_android: ^0.2.3 in_app_purchase_platform_interface: ^1.0.0 - in_app_purchase_storekit: ^0.3.0+1 + in_app_purchase_storekit: ^0.3.4 dev_dependencies: flutter_driver: @@ -32,3 +34,9 @@ dev_dependencies: sdk: flutter plugin_platform_interface: ^2.0.0 test: ^1.16.0 + +screenshots: + - description: 'Example of in-app purchase on ios' + path: doc/iap_ios.gif + - description: 'Example of in-app purchase on android' + path: doc/iap_android.gif 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 b595d7e148d9..76c94cbab35c 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,25 @@ +## 0.2.4+1 + +* Updates Google Play Billing Library to 5.1.0. +* Updates androidx.annotation to 1.5.0. + +## 0.2.4 + +* Updates minimum Flutter version to 3.0. +* Ignores a lint in the example app for backwards compatibility. + +## 0.2.3+9 + +* Updates `androidx.test.espresso:espresso-core` to 3.5.1. + +## 0.2.3+8 + +* Updates code for stricter lint checks. + +## 0.2.3+7 + +* Updates code for new analysis options. + ## 0.2.3+6 * Updates android gradle plugin to 7.3.1. 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 704ab36c253b..0d4bde6183cd 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 @@ -29,10 +29,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - // TODO(stuartmorgan): Enable when gradle is updated. - // disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -54,11 +51,11 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.3.0' - implementation 'com.android.billingclient:billing:5.0.0' + implementation 'androidx.annotation:annotation:1.5.0' + implementation 'com.android.billingclient:billing:5.1.0' testImplementation 'junit:junit:4.13.2' testImplementation 'org.json:json:20220924' testImplementation 'org.mockito:mockito-core:4.7.0' androidTestImplementation 'androidx.test:runner:1.4.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' } diff --git a/packages/in_app_purchase/in_app_purchase_android/example/android/app/build.gradle b/packages/in_app_purchase/in_app_purchase_android/example/android/app/build.gradle index 281f349989be..511091df086d 100644 --- a/packages/in_app_purchase/in_app_purchase_android/example/android/app/build.gradle +++ b/packages/in_app_purchase/in_app_purchase_android/example/android/app/build.gradle @@ -108,7 +108,7 @@ flutter { dependencies { implementation 'com.android.billingclient:billing:5.0.0' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:4.7.0' + testImplementation 'org.mockito:mockito-core:5.1.1' testImplementation 'org.json:json:20220924' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' diff --git a/packages/in_app_purchase/in_app_purchase_android/example/android/gradle.properties b/packages/in_app_purchase/in_app_purchase_android/example/android/gradle.properties index 38c8d4544ff1..2f3603c9ff62 100644 --- a/packages/in_app_purchase/in_app_purchase_android/example/android/gradle.properties +++ b/packages/in_app_purchase/in_app_purchase_android/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true android.enableJetifier=true 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 4eea725b089f..97e71b038be3 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 @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: avoid_print + import 'dart:async'; import 'package:flutter/material.dart'; @@ -154,6 +156,8 @@ class _MyAppState extends State<_MyApp> { } if (_purchasePending) { stack.add( + // TODO(goderbauer): Make this const when that's available on stable. + // ignore: prefer_const_constructors Stack( children: const [ Opacity( diff --git a/packages/in_app_purchase/in_app_purchase_android/example/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_android/example/pubspec.yaml index af760a3ada46..d5a76b848093 100644 --- a/packages/in_app_purchase/in_app_purchase_android/example/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart index b64eaab49a9d..2d4a3f96b50e 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart @@ -343,8 +343,12 @@ class BillingClient { (call.arguments as Map).cast())); break; case _kOnBillingServiceDisconnected: - final int handle = call.arguments['handle'] as int; - await _callbacks[_kOnBillingServiceDisconnected]![handle](); + final int handle = + (call.arguments as Map)['handle']! as int; + final List onDisconnected = + _callbacks[_kOnBillingServiceDisconnected]! + .cast(); + onDisconnected[handle](); break; } } diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart index d73ca8ef2e00..c8046d6e655a 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart @@ -171,7 +171,7 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { 'completePurchase unsuccessful. The `purchase.verificationData` is not valid'); } - return await billingClient + return billingClient .acknowledgePurchase(purchase.verificationData.serverVerificationData); } 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 555773a47910..397e82a82446 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -2,11 +2,11 @@ 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/plugins/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.2.3+6 +version: 0.2.4+1 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index 4dae957e21eb..98219dc9d4e5 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -17,8 +17,9 @@ void main() { final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); late BillingClient billingClient; - setUpAll(() => - channel.setMockMethodCallHandler(stubPlatform.fakeMethodCallHandler)); + setUpAll(() => _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, stubPlatform.fakeMethodCallHandler)); setUp(() { billingClient = BillingClient((PurchasesResultWrapper _) {}); @@ -651,3 +652,9 @@ void main() { }); }); } + +/// 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/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart index 9737282e27b7..a97c69608a3a 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart @@ -22,7 +22,9 @@ void main() { const String endConnectionCall = 'BillingClient#endConnection()'; setUpAll(() { - channel.setMockMethodCallHandler(stubPlatform.fakeMethodCallHandler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, stubPlatform.fakeMethodCallHandler); }); setUp(() { @@ -213,3 +215,9 @@ void main() { }); }); } + +/// 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/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart index b6055cc9a8bb..347bacde20b1 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart @@ -26,7 +26,9 @@ void main() { const String endConnectionCall = 'BillingClient#endConnection()'; setUpAll(() { - channel.setMockMethodCallHandler(stubPlatform.fakeMethodCallHandler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, stubPlatform.fakeMethodCallHandler); }); setUp(() { @@ -426,7 +428,8 @@ void main() { name: consumeMethodName, value: buildBillingResultMap(expectedBillingResultForConsume), additionalStepBeforeReturn: (dynamic args) { - final String purchaseToken = args['purchaseToken'] as String; + final String purchaseToken = + (args as Map)['purchaseToken']! as String; consumeCompleter.complete(purchaseToken); }); @@ -540,7 +543,8 @@ void main() { name: consumeMethodName, value: buildBillingResultMap(expectedBillingResultForConsume), additionalStepBeforeReturn: (dynamic args) { - final String purchaseToken = args['purchaseToken'] as String; + final String purchaseToken = + (args as Map)['purchaseToken']! as String; consumeCompleter.complete(purchaseToken); }); @@ -617,7 +621,8 @@ void main() { name: consumeMethodName, value: buildBillingResultMap(expectedBillingResultForConsume), additionalStepBeforeReturn: (dynamic args) { - final String purchaseToken = args['purchaseToken'] as String; + final String purchaseToken = + (args as Map)['purchaseToken']! as String; consumeCompleter.complete(purchaseToken); }); @@ -682,7 +687,8 @@ void main() { name: consumeMethodName, value: buildBillingResultMap(expectedBillingResultForConsume), additionalStepBeforeReturn: (dynamic args) { - final String purchaseToken = args['purchaseToken'] as String; + final String purchaseToken = + (args as Map)['purchaseToken']! as String; consumeCompleter.complete(purchaseToken); }); @@ -782,3 +788,9 @@ void main() { }); }); } + +/// 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/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md index 17ba02986088..a408c2db2cd7 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 1.3.2 * Updates imports for `prefer_relative_imports`. diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml index 46e38b0a03fa..b3420161530b 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml @@ -8,7 +8,7 @@ version: 1.3.2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md index 324e0608b7f9..6314bdc323f5 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md @@ -1,6 +1,31 @@ +## 0.3.6 + +* Updates minimum Flutter version to 3.3 and iOS 11. + +## 0.3.5+2 + +* Fix a crash when `appStoreReceiptURL` is nil. + +## 0.3.5+1 + +* Uses the new `sharedDarwinSource` flag when available. + +## 0.3.5 + +* Updates minimum Flutter version to 3.0. +* Ignores a lint in the example app for backwards compatibility. + +## 0.3.4+1 + +* Updates code for stricter lint checks. + +## 0.3.4 + +* Adds macOS as a supported platform. + ## 0.3.3 -* Supports adding discount information to AppStorePurchaseParam. +* Supports adding discount information to AppStorePurchaseParam. * Fixes iOS Promotional Offers bug which prevents them from working. ## 0.3.2+2 diff --git a/packages/in_app_purchase/in_app_purchase_storekit/README.md b/packages/in_app_purchase/in_app_purchase_storekit/README.md index 76e2854c26e1..d58efd1e298c 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/README.md +++ b/packages/in_app_purchase/in_app_purchase_storekit/README.md @@ -1,6 +1,6 @@ # in\_app\_purchase\_storekit -The iOS implementation of [`in_app_purchase`][1]. +The iOS and macOS implementation of [`in_app_purchase`][1]. ## Usage diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAObjectTranslator.h b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAObjectTranslator.h new file mode 100644 index 000000000000..eb97ceb44754 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAObjectTranslator.h @@ -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. + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FIAObjectTranslator : NSObject + +// Converts an instance of SKProduct into a dictionary. ++ (NSDictionary *)getMapFromSKProduct:(SKProduct *)product; + +// Converts an instance of SKProductSubscriptionPeriod into a dictionary. ++ (NSDictionary *)getMapFromSKProductSubscriptionPeriod:(SKProductSubscriptionPeriod *)period + API_AVAILABLE(ios(11.2)); + +// Converts an instance of SKProductDiscount into a dictionary. ++ (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount + API_AVAILABLE(ios(11.2)); + +// Converts an array of SKProductDiscount instances into an array of dictionaries. ++ (nonnull NSArray *)getMapArrayFromSKProductDiscounts: + (nonnull NSArray *)productDiscounts API_AVAILABLE(ios(12.2)); + +// Converts an instance of SKProductsResponse into a dictionary. ++ (NSDictionary *)getMapFromSKProductsResponse:(SKProductsResponse *)productResponse; + +// Converts an instance of SKPayment into a dictionary. ++ (NSDictionary *)getMapFromSKPayment:(SKPayment *)payment; + +// Converts an instance of NSLocale into a dictionary. ++ (NSDictionary *)getMapFromNSLocale:(NSLocale *)locale; + +// Creates an instance of the SKMutablePayment class based on the supplied dictionary. ++ (SKMutablePayment *)getSKMutablePaymentFromMap:(NSDictionary *)map; + +// Converts an instance of SKPaymentTransaction into a dictionary. ++ (NSDictionary *)getMapFromSKPaymentTransaction:(SKPaymentTransaction *)transaction; + +// Converts an instance of NSError into a dictionary. ++ (NSDictionary *)getMapFromNSError:(NSError *)error; + +// Converts an instance of SKStorefront into a dictionary. ++ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront + API_AVAILABLE(ios(13), macos(10.15), watchos(6.2)); + +// Converts the supplied instances of SKStorefront and SKPaymentTransaction into a dictionary. ++ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront + andSKPaymentTransaction:(SKPaymentTransaction *)transaction + API_AVAILABLE(ios(13), macos(10.15), watchos(6.2)); + +// Creates an instance of the SKPaymentDiscount class based on the supplied dictionary. ++ (nullable SKPaymentDiscount *)getSKPaymentDiscountFromMap:(NSDictionary *)map + withError:(NSString *_Nullable *_Nullable)error + API_AVAILABLE(ios(12.2)); + +@end +; + +NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAObjectTranslator.m new file mode 100644 index 000000000000..c656b58808b3 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAObjectTranslator.m @@ -0,0 +1,297 @@ +// 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 "FIAObjectTranslator.h" + +#pragma mark - SKProduct Coders + +@implementation FIAObjectTranslator + ++ (NSDictionary *)getMapFromSKProduct:(SKProduct *)product { + if (!product) { + return nil; + } + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"localizedDescription" : product.localizedDescription ?: [NSNull null], + @"localizedTitle" : product.localizedTitle ?: [NSNull null], + @"productIdentifier" : product.productIdentifier ?: [NSNull null], + @"price" : product.price.description ?: [NSNull null] + + }]; + // TODO(cyanglaz): NSLocale is a complex object, want to see the actual need of getting this + // expanded to a map. Matching android to only get the currencySymbol for now. + // https://github.com/flutter/flutter/issues/26610 + [map setObject:[FIAObjectTranslator getMapFromNSLocale:product.priceLocale] ?: [NSNull null] + forKey:@"priceLocale"]; + if (@available(iOS 11.2, *)) { + [map setObject:[FIAObjectTranslator + getMapFromSKProductSubscriptionPeriod:product.subscriptionPeriod] + ?: [NSNull null] + forKey:@"subscriptionPeriod"]; + } + if (@available(iOS 11.2, *)) { + [map setObject:[FIAObjectTranslator getMapFromSKProductDiscount:product.introductoryPrice] + ?: [NSNull null] + forKey:@"introductoryPrice"]; + } + if (@available(iOS 12.2, *)) { + [map setObject:[FIAObjectTranslator getMapArrayFromSKProductDiscounts:product.discounts] + forKey:@"discounts"]; + } + if (@available(iOS 12.0, *)) { + [map setObject:product.subscriptionGroupIdentifier ?: [NSNull null] + forKey:@"subscriptionGroupIdentifier"]; + } + return map; +} + ++ (NSDictionary *)getMapFromSKProductSubscriptionPeriod:(SKProductSubscriptionPeriod *)period { + if (!period) { + return nil; + } + return @{@"numberOfUnits" : @(period.numberOfUnits), @"unit" : @(period.unit)}; +} + ++ (nonnull NSArray *)getMapArrayFromSKProductDiscounts: + (nonnull NSArray *)productDiscounts { + NSMutableArray *discountsMapArray = [[NSMutableArray alloc] init]; + + for (SKProductDiscount *productDiscount in productDiscounts) { + [discountsMapArray addObject:[FIAObjectTranslator getMapFromSKProductDiscount:productDiscount]]; + } + + return discountsMapArray; +} + ++ (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount { + if (!discount) { + return nil; + } + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"price" : discount.price.description ?: [NSNull null], + @"numberOfPeriods" : @(discount.numberOfPeriods), + @"subscriptionPeriod" : + [FIAObjectTranslator getMapFromSKProductSubscriptionPeriod:discount.subscriptionPeriod] + ?: [NSNull null], + @"paymentMode" : @(discount.paymentMode), + }]; + if (@available(iOS 12.2, *)) { + [map setObject:discount.identifier ?: [NSNull null] forKey:@"identifier"]; + [map setObject:@(discount.type) forKey:@"type"]; + } + + // TODO(cyanglaz): NSLocale is a complex object, want to see the actual need of getting this + // expanded to a map. Matching android to only get the currencySymbol for now. + // https://github.com/flutter/flutter/issues/26610 + [map setObject:[FIAObjectTranslator getMapFromNSLocale:discount.priceLocale] ?: [NSNull null] + forKey:@"priceLocale"]; + return map; +} + ++ (NSDictionary *)getMapFromSKProductsResponse:(SKProductsResponse *)productResponse { + if (!productResponse) { + return nil; + } + NSMutableArray *productsMapArray = [NSMutableArray new]; + for (SKProduct *product in productResponse.products) { + [productsMapArray addObject:[FIAObjectTranslator getMapFromSKProduct:product]]; + } + return @{ + @"products" : productsMapArray, + @"invalidProductIdentifiers" : productResponse.invalidProductIdentifiers ?: @[] + }; +} + ++ (NSDictionary *)getMapFromSKPayment:(SKPayment *)payment { + if (!payment) { + return nil; + } + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"productIdentifier" : payment.productIdentifier ?: [NSNull null], + @"requestData" : payment.requestData ? [[NSString alloc] initWithData:payment.requestData + encoding:NSUTF8StringEncoding] + : [NSNull null], + @"quantity" : @(payment.quantity), + @"applicationUsername" : payment.applicationUsername ?: [NSNull null] + }]; + [map setObject:@(payment.simulatesAskToBuyInSandbox) forKey:@"simulatesAskToBuyInSandbox"]; + return map; +} + ++ (NSDictionary *)getMapFromNSLocale:(NSLocale *)locale { + if (!locale) { + return nil; + } + NSMutableDictionary *map = [[NSMutableDictionary alloc] init]; + [map setObject:[locale objectForKey:NSLocaleCurrencySymbol] ?: [NSNull null] + forKey:@"currencySymbol"]; + [map setObject:[locale objectForKey:NSLocaleCurrencyCode] ?: [NSNull null] + forKey:@"currencyCode"]; + [map setObject:[locale objectForKey:NSLocaleCountryCode] ?: [NSNull null] forKey:@"countryCode"]; + return map; +} + ++ (SKMutablePayment *)getSKMutablePaymentFromMap:(NSDictionary *)map { + if (!map) { + return nil; + } + SKMutablePayment *payment = [[SKMutablePayment alloc] init]; + payment.productIdentifier = map[@"productIdentifier"]; + NSString *utf8String = map[@"requestData"]; + payment.requestData = [utf8String dataUsingEncoding:NSUTF8StringEncoding]; + payment.quantity = [map[@"quantity"] integerValue]; + payment.applicationUsername = map[@"applicationUsername"]; + payment.simulatesAskToBuyInSandbox = [map[@"simulatesAskToBuyInSandbox"] boolValue]; + return payment; +} + ++ (NSDictionary *)getMapFromSKPaymentTransaction:(SKPaymentTransaction *)transaction { + if (!transaction) { + return nil; + } + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"error" : [FIAObjectTranslator getMapFromNSError:transaction.error] ?: [NSNull null], + @"payment" : transaction.payment ? [FIAObjectTranslator getMapFromSKPayment:transaction.payment] + : [NSNull null], + @"originalTransaction" : transaction.originalTransaction + ? [FIAObjectTranslator getMapFromSKPaymentTransaction:transaction.originalTransaction] + : [NSNull null], + @"transactionTimeStamp" : transaction.transactionDate + ? @(transaction.transactionDate.timeIntervalSince1970) + : [NSNull null], + @"transactionIdentifier" : transaction.transactionIdentifier ?: [NSNull null], + @"transactionState" : @(transaction.transactionState) + }]; + + return map; +} + ++ (NSDictionary *)getMapFromNSError:(NSError *)error { + if (!error) { + return nil; + } + + NSMutableDictionary *userInfo = [NSMutableDictionary new]; + for (NSErrorUserInfoKey key in error.userInfo) { + id value = error.userInfo[key]; + userInfo[key] = [FIAObjectTranslator encodeNSErrorUserInfo:value]; + } + return @{@"code" : @(error.code), @"domain" : error.domain ?: @"", @"userInfo" : userInfo}; +} + ++ (id)encodeNSErrorUserInfo:(id)value { + if ([value isKindOfClass:[NSError class]]) { + return [FIAObjectTranslator getMapFromNSError:value]; + } else if ([value isKindOfClass:[NSURL class]]) { + return [value absoluteString]; + } else if ([value isKindOfClass:[NSNumber class]]) { + return value; + } else if ([value isKindOfClass:[NSString class]]) { + return value; + } else if ([value isKindOfClass:[NSArray class]]) { + NSMutableArray *errors = [[NSMutableArray alloc] init]; + for (id error in value) { + [errors addObject:[FIAObjectTranslator encodeNSErrorUserInfo:error]]; + } + return errors; + } else { + return [NSString + stringWithFormat: + @"Unable to encode native userInfo object of type %@ to map. Please submit an issue at " + @"https://github.com/flutter/flutter/issues/new with the title " + @"\"[in_app_purchase_storekit] " + @"Unable to encode userInfo of type %@\" and add reproduction steps and the error " + @"details in " + @"the description field.", + [value class], [value class]]; + } +} + ++ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront { + if (!storefront) { + return nil; + } + + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"countryCode" : storefront.countryCode, + @"identifier" : storefront.identifier + }]; + + return map; +} + ++ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront + andSKPaymentTransaction:(SKPaymentTransaction *)transaction { + if (!storefront || !transaction) { + return nil; + } + + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"storefront" : [FIAObjectTranslator getMapFromSKStorefront:storefront], + @"transaction" : [FIAObjectTranslator getMapFromSKPaymentTransaction:transaction] + }]; + + return map; +} + ++ (SKPaymentDiscount *)getSKPaymentDiscountFromMap:(NSDictionary *)map + withError:(NSString **)error { + if (!map || map.count <= 0) { + return nil; + } + + NSString *identifier = map[@"identifier"]; + NSString *keyIdentifier = map[@"keyIdentifier"]; + NSString *nonce = map[@"nonce"]; + NSString *signature = map[@"signature"]; + NSNumber *timestamp = map[@"timestamp"]; + + if (!identifier || ![identifier isKindOfClass:NSString.class] || + [identifier isEqualToString:@""]) { + if (error) { + *error = @"When specifying a payment discount the 'identifier' field is mandatory."; + } + return nil; + } + + if (!keyIdentifier || ![keyIdentifier isKindOfClass:NSString.class] || + [keyIdentifier isEqualToString:@""]) { + if (error) { + *error = @"When specifying a payment discount the 'keyIdentifier' field is mandatory."; + } + return nil; + } + + if (!nonce || ![nonce isKindOfClass:NSString.class] || [nonce isEqualToString:@""]) { + if (error) { + *error = @"When specifying a payment discount the 'nonce' field is mandatory."; + } + return nil; + } + + if (!signature || ![signature isKindOfClass:NSString.class] || [signature isEqualToString:@""]) { + if (error) { + *error = @"When specifying a payment discount the 'signature' field is mandatory."; + } + return nil; + } + + if (!timestamp || ![timestamp isKindOfClass:NSNumber.class] || [timestamp longLongValue] <= 0) { + if (error) { + *error = @"When specifying a payment discount the 'timestamp' field is mandatory."; + } + return nil; + } + + SKPaymentDiscount *discount = + [[SKPaymentDiscount alloc] initWithIdentifier:identifier + keyIdentifier:keyIdentifier + nonce:[[NSUUID alloc] initWithUUIDString:nonce] + signature:signature + timestamp:timestamp]; + + return discount; +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPPaymentQueueDelegate.h b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPPaymentQueueDelegate.h new file mode 100644 index 000000000000..4347846f54ca --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPPaymentQueueDelegate.h @@ -0,0 +1,21 @@ +// 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. + +#if TARGET_OS_OSX +#import +#else +#import +#endif +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +API_AVAILABLE(ios(13)) +API_UNAVAILABLE(tvos, macos, watchos) +@interface FIAPPaymentQueueDelegate : NSObject +- (id)initWithMethodChannel:(FlutterMethodChannel *)methodChannel; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPPaymentQueueDelegate.m b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPPaymentQueueDelegate.m new file mode 100644 index 000000000000..cb18d9b86d66 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPPaymentQueueDelegate.m @@ -0,0 +1,80 @@ +// 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 "FIAPPaymentQueueDelegate.h" +#import "FIAObjectTranslator.h" + +@interface FIAPPaymentQueueDelegate () + +@property(strong, nonatomic, readonly) FlutterMethodChannel *callbackChannel; + +@end + +@implementation FIAPPaymentQueueDelegate + +- (id)initWithMethodChannel:(FlutterMethodChannel *)methodChannel { + self = [super init]; + if (self) { + _callbackChannel = methodChannel; + } + + return self; +} + +- (BOOL)paymentQueue:(SKPaymentQueue *)paymentQueue + shouldContinueTransaction:(SKPaymentTransaction *)transaction + inStorefront:(SKStorefront *)newStorefront { + // Default return value for this method is true (see + // https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate/3521328-paymentqueueshouldshowpriceconse?language=objc) + __block BOOL shouldContinue = YES; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + [self.callbackChannel invokeMethod:@"shouldContinueTransaction" + arguments:[FIAObjectTranslator getMapFromSKStorefront:newStorefront + andSKPaymentTransaction:transaction] + result:^(id _Nullable result) { + // When result is a valid instance of NSNumber use it to determine + // if the transaction should continue. Otherwise use the default + // value. + if (result && [result isKindOfClass:[NSNumber class]]) { + shouldContinue = [(NSNumber *)result boolValue]; + } + + dispatch_semaphore_signal(semaphore); + }]; + + // The client should respond within 1 second otherwise continue + // with default value. + dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)); + + return shouldContinue; +} + +#if TARGET_OS_IOS +- (BOOL)paymentQueueShouldShowPriceConsent:(SKPaymentQueue *)paymentQueue { + // Default return value for this method is true (see + // https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate/3521328-paymentqueueshouldshowpriceconse?language=objc) + __block BOOL shouldShowPriceConsent = YES; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + [self.callbackChannel invokeMethod:@"shouldShowPriceConsent" + arguments:nil + result:^(id _Nullable result) { + // When result is a valid instance of NSNumber use it to determine + // if the transaction should continue. Otherwise use the default + // value. + if (result && [result isKindOfClass:[NSNumber class]]) { + shouldShowPriceConsent = [(NSNumber *)result boolValue]; + } + + dispatch_semaphore_signal(semaphore); + }]; + + // The client should respond within 1 second otherwise continue + // with default value. + dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)); + + return shouldShowPriceConsent; +} +#endif + +@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPReceiptManager.h b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPReceiptManager.h new file mode 100644 index 000000000000..94020ff2348b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPReceiptManager.h @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class FlutterError; + +@interface FIAPReceiptManager : NSObject + +- (nullable NSString *)retrieveReceiptWithError:(FlutterError *_Nullable *_Nullable)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPReceiptManager.m b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPReceiptManager.m new file mode 100644 index 000000000000..320e6072d046 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPReceiptManager.m @@ -0,0 +1,45 @@ +// 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 "FIAPReceiptManager.h" +#if TARGET_OS_OSX +#import +#else +#import +#endif +#import "FIAObjectTranslator.h" + +@interface FIAPReceiptManager () +// Gets the receipt file data from the location of the url. Can be nil if +// there is an error. This interface is defined so it can be stubbed for testing. +- (NSData *)getReceiptData:(NSURL *)url error:(NSError **)error; + +@end + +@implementation FIAPReceiptManager + +- (NSString *)retrieveReceiptWithError:(FlutterError **)flutterError { + NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; + if (!receiptURL) { + return nil; + } + NSError *receiptError; + NSData *receipt = [self getReceiptData:receiptURL error:&receiptError]; + if (!receipt || receiptError) { + if (flutterError) { + NSDictionary *errorMap = [FIAObjectTranslator getMapFromNSError:receiptError]; + *flutterError = [FlutterError errorWithCode:errorMap[@"code"] + message:errorMap[@"domain"] + details:errorMap[@"userInfo"]]; + } + return nil; + } + return [receipt base64EncodedStringWithOptions:kNilOptions]; +} + +- (NSData *)getReceiptData:(NSURL *)url error:(NSError **)error { + return [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:error]; +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPRequestHandler.h b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPRequestHandler.h new file mode 100644 index 000000000000..cbf21d6e161f --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPRequestHandler.h @@ -0,0 +1,20 @@ +// 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 +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^ProductRequestCompletion)(SKProductsResponse *_Nullable response, + NSError *_Nullable errror); + +@interface FIAPRequestHandler : NSObject + +- (instancetype)initWithRequest:(SKRequest *)request; +- (void)startProductRequestWithCompletionHandler:(ProductRequestCompletion)completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPRequestHandler.m b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPRequestHandler.m new file mode 100644 index 000000000000..8767265d8544 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPRequestHandler.m @@ -0,0 +1,55 @@ +// 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 "FIAPRequestHandler.h" +#import + +#pragma mark - Main Handler + +@interface FIAPRequestHandler () + +@property(copy, nonatomic) ProductRequestCompletion completion; +@property(strong, nonatomic) SKRequest *request; + +@end + +@implementation FIAPRequestHandler + +- (instancetype)initWithRequest:(SKRequest *)request { + self = [super init]; + if (self) { + self.request = request; + request.delegate = self; + } + return self; +} + +- (void)startProductRequestWithCompletionHandler:(ProductRequestCompletion)completion { + self.completion = completion; + [self.request start]; +} + +- (void)productsRequest:(SKProductsRequest *)request + didReceiveResponse:(SKProductsResponse *)response { + if (self.completion) { + self.completion(response, nil); + // set the completion to nil here so self.completion won't be triggered again in + // requestDidFinish for SKProductRequest. + self.completion = nil; + } +} + +- (void)requestDidFinish:(SKRequest *)request { + if (self.completion) { + self.completion(nil, nil); + } +} + +- (void)request:(SKRequest *)request didFailWithError:(NSError *)error { + if (self.completion) { + self.completion(nil, error); + } +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPaymentQueueHandler.h new file mode 100644 index 000000000000..fdc042655fd7 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPaymentQueueHandler.h @@ -0,0 +1,132 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import "FIATransactionCache.h" + +@class SKPaymentTransaction; + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^TransactionsUpdated)(NSArray *transactions); +typedef void (^TransactionsRemoved)(NSArray *transactions); +typedef void (^RestoreTransactionFailed)(NSError *error); +typedef void (^RestoreCompletedTransactionsFinished)(void); +typedef BOOL (^ShouldAddStorePayment)(SKPayment *payment, SKProduct *product); +typedef void (^UpdatedDownloads)(NSArray *downloads); + +@interface FIAPaymentQueueHandler : NSObject + +@property(NS_NONATOMIC_IOSONLY, weak, nullable) id delegate API_AVAILABLE( + ios(13.0), macos(10.15), watchos(6.2)); + +/// Creates a new FIAPaymentQueueHandler initialized with an empty +/// FIATransactionCache. +/// +/// @param queue The SKPaymentQueue instance connected to the App Store and +/// responsible for processing transactions. +/// @param transactionsUpdated Callback method that is called each time the App +/// Store indicates transactions are updated. +/// @param transactionsRemoved Callback method that is called each time the App +/// Store indicates transactions are removed. +/// @param restoreTransactionFailed Callback method that is called each time +/// the App Store indicates transactions failed +/// to restore. +/// @param restoreCompletedTransactionsFinished Callback method that is called +/// each time the App Store +/// indicates restoring of +/// transactions has finished. +/// @param shouldAddStorePayment Callback method that is called each time an +/// in-app purchase has been initiated from the +/// App Store. +/// @param updatedDownloads Callback method that is called each time the App +/// Store indicates downloads are updated. +- (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue + transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated + transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved + restoreTransactionFailed:(nullable RestoreTransactionFailed)restoreTransactionFailed + restoreCompletedTransactionsFinished: + (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished + shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment + updatedDownloads:(nullable UpdatedDownloads)updatedDownloads + DEPRECATED_MSG_ATTRIBUTE( + "Use the " + "'initWithQueue:transactionsUpdated:transactionsRemoved:restoreTransactionsFinished:" + "shouldAddStorePayment:updatedDownloads:transactionCache:' message instead."); + +/// Creates a new FIAPaymentQueueHandler. +/// +/// The "transactionsUpdated", "transactionsRemoved" and "updatedDownloads" +/// callbacks are only called while actively observing transactions. To start +/// observing transactions send the "startObservingPaymentQueue" message. +/// Sending the "stopObservingPaymentQueue" message will stop actively +/// observing transactions. When transactions are not observed they are cached +/// to the "transactionCache" and will be delivered via the +/// "transactionsUpdated", "transactionsRemoved" and "updatedDownloads" +/// callbacks as soon as the "startObservingPaymentQueue" message arrives. +/// +/// Note: cached transactions that are not processed when the application is +/// killed will be delivered again by the App Store as soon as the application +/// starts again. +/// +/// @param queue The SKPaymentQueue instance connected to the App Store and +/// responsible for processing transactions. +/// @param transactionsUpdated Callback method that is called each time the App +/// Store indicates transactions are updated. +/// @param transactionsRemoved Callback method that is called each time the App +/// Store indicates transactions are removed. +/// @param restoreTransactionFailed Callback method that is called each time +/// the App Store indicates transactions failed +/// to restore. +/// @param restoreCompletedTransactionsFinished Callback method that is called +/// each time the App Store +/// indicates restoring of +/// transactions has finished. +/// @param shouldAddStorePayment Callback method that is called each time an +/// in-app purchase has been initiated from the +/// App Store. +/// @param updatedDownloads Callback method that is called each time the App +/// Store indicates downloads are updated. +/// @param transactionCache An empty [FIATransactionCache] instance that is +/// responsible for keeping track of transactions that +/// arrive when not actively observing transactions. +- (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue + transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated + transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved + restoreTransactionFailed:(nullable RestoreTransactionFailed)restoreTransactionFailed + restoreCompletedTransactionsFinished: + (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished + shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment + updatedDownloads:(nullable UpdatedDownloads)updatedDownloads + transactionCache:(nonnull FIATransactionCache *)transactionCache; +// Can throw exceptions if the transaction type is purchasing, should always used in a @try block. +- (void)finishTransaction:(nonnull SKPaymentTransaction *)transaction; +- (void)restoreTransactions:(nullable NSString *)applicationName; +- (void)presentCodeRedemptionSheet API_UNAVAILABLE(tvos, macos, watchos); +- (NSArray *)getUnfinishedTransactions; + +// This method needs to be called before any other methods. +- (void)startObservingPaymentQueue; +// Call this method when the Flutter app is no longer listening +- (void)stopObservingPaymentQueue; + +// Appends a payment to the SKPaymentQueue. +// +// @param payment Payment object to be added to the payment queue. +// @return whether "addPayment" was successful. +- (BOOL)addPayment:(SKPayment *)payment; + +// Displays the price consent sheet. +// +// The price consent sheet is only displayed when the following +// is true: +// - You have increased the price of the subscription in App Store Connect. +// - The subscriber has not yet responded to a price consent query. +// Otherwise the method has no effect. +- (void)showPriceConsentIfNeeded API_AVAILABLE(ios(13.4))API_UNAVAILABLE(tvos, macos, watchos); + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPaymentQueueHandler.m new file mode 100644 index 000000000000..d18a09cfa405 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPaymentQueueHandler.m @@ -0,0 +1,236 @@ +// 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 "FIAPaymentQueueHandler.h" +#import "FIAPPaymentQueueDelegate.h" +#import "FIATransactionCache.h" + +@interface FIAPaymentQueueHandler () + +/// The SKPaymentQueue instance connected to the App Store and responsible for processing +/// transactions. +@property(strong, nonatomic) SKPaymentQueue *queue; + +/// Callback method that is called each time the App Store indicates transactions are updated. +@property(nullable, copy, nonatomic) TransactionsUpdated transactionsUpdated; + +/// Callback method that is called each time the App Store indicates transactions are removed. +@property(nullable, copy, nonatomic) TransactionsRemoved transactionsRemoved; + +/// Callback method that is called each time the App Store indicates transactions failed to restore. +@property(nullable, copy, nonatomic) RestoreTransactionFailed restoreTransactionFailed; + +/// Callback method that is called each time the App Store indicates restoring of transactions has +/// finished. +@property(nullable, copy, nonatomic) + RestoreCompletedTransactionsFinished paymentQueueRestoreCompletedTransactionsFinished; + +/// Callback method that is called each time an in-app purchase has been initiated from the App +/// Store. +@property(nullable, copy, nonatomic) ShouldAddStorePayment shouldAddStorePayment; + +/// Callback method that is called each time the App Store indicates downloads are updated. +@property(nullable, copy, nonatomic) UpdatedDownloads updatedDownloads; + +/// The transaction cache responsible for caching transactions. +/// +/// Keeps track of transactions that arrive when the Flutter client is not +/// actively observing for transactions. +@property(strong, nonatomic, nonnull) FIATransactionCache *transactionCache; + +/// Indicates if the Flutter client is observing transactions. +/// +/// When the client is not observing, transactions are cached and send to the +/// client as soon as it starts observing. The Flutter client can start +/// observing by sending a startObservingPaymentQueue message and stop by +/// sending a stopObservingPaymentQueue message. +@property(atomic, assign, readwrite, getter=isObservingTransactions) BOOL observingTransactions; + +@end + +@implementation FIAPaymentQueueHandler + +- (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue + transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated + transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved + restoreTransactionFailed:(nullable RestoreTransactionFailed)restoreTransactionFailed + restoreCompletedTransactionsFinished: + (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished + shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment + updatedDownloads:(nullable UpdatedDownloads)updatedDownloads { + return [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:transactionsUpdated + transactionRemoved:transactionsRemoved + restoreTransactionFailed:restoreTransactionFailed + restoreCompletedTransactionsFinished:restoreCompletedTransactionsFinished + shouldAddStorePayment:shouldAddStorePayment + updatedDownloads:updatedDownloads + transactionCache:[[FIATransactionCache alloc] init]]; +} + +- (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue + transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated + transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved + restoreTransactionFailed:(nullable RestoreTransactionFailed)restoreTransactionFailed + restoreCompletedTransactionsFinished: + (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished + shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment + updatedDownloads:(nullable UpdatedDownloads)updatedDownloads + transactionCache:(nonnull FIATransactionCache *)transactionCache { + self = [super init]; + if (self) { + _queue = queue; + _transactionsUpdated = transactionsUpdated; + _transactionsRemoved = transactionsRemoved; + _restoreTransactionFailed = restoreTransactionFailed; + _paymentQueueRestoreCompletedTransactionsFinished = restoreCompletedTransactionsFinished; + _shouldAddStorePayment = shouldAddStorePayment; + _updatedDownloads = updatedDownloads; + _transactionCache = transactionCache; + + [_queue addTransactionObserver:self]; + if (@available(iOS 13.0, macOS 10.15, *)) { + queue.delegate = self.delegate; + } + } + return self; +} + +- (void)startObservingPaymentQueue { + self.observingTransactions = YES; + + [self processCachedTransactions]; +} + +- (void)stopObservingPaymentQueue { + // When the client stops observing transaction, the transaction observer is + // not removed from the SKPaymentQueue. The FIAPaymentQueueHandler will cache + // trasnactions in memory when the client is not observing, allowing the app + // to process these transactions if it starts observing again during the same + // lifetime of the app. + // + // If the app is killed, cached transactions will be removed from memory; + // however, the App Store will re-deliver the transactions as soon as the app + // is started again, since the cached transactions have not been acknowledged + // by the client (by sending the `finishTransaction` message). + self.observingTransactions = NO; +} + +- (void)processCachedTransactions { + NSArray *cachedObjects = + [self.transactionCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]; + if (cachedObjects.count != 0) { + self.transactionsUpdated(cachedObjects); + } + + cachedObjects = [self.transactionCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]; + if (cachedObjects.count != 0) { + self.updatedDownloads(cachedObjects); + } + + cachedObjects = [self.transactionCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]; + if (cachedObjects.count != 0) { + self.transactionsRemoved(cachedObjects); + } + + [self.transactionCache clear]; +} + +- (BOOL)addPayment:(SKPayment *)payment { + for (SKPaymentTransaction *transaction in self.queue.transactions) { + if ([transaction.payment.productIdentifier isEqualToString:payment.productIdentifier]) { + return NO; + } + } + [self.queue addPayment:payment]; + return YES; +} + +- (void)finishTransaction:(SKPaymentTransaction *)transaction { + [self.queue finishTransaction:transaction]; +} + +- (void)restoreTransactions:(nullable NSString *)applicationName { + if (applicationName) { + [self.queue restoreCompletedTransactionsWithApplicationUsername:applicationName]; + } else { + [self.queue restoreCompletedTransactions]; + } +} + +#if TARGET_OS_IOS +- (void)presentCodeRedemptionSheet { + if (@available(iOS 14, *)) { + [self.queue presentCodeRedemptionSheet]; + } else { + NSLog(@"presentCodeRedemptionSheet is only available on iOS 14 or newer"); + } +} +#endif + +#if TARGET_OS_IOS +- (void)showPriceConsentIfNeeded { + [self.queue showPriceConsentIfNeeded]; +} +#endif + +#pragma mark - observing + +// Sent when the transaction array has changed (additions or state changes). Client should check +// state of transactions and finish as appropriate. +- (void)paymentQueue:(SKPaymentQueue *)queue + updatedTransactions:(NSArray *)transactions { + if (!self.observingTransactions) { + [_transactionCache addObjects:transactions forKey:TransactionCacheKeyUpdatedTransactions]; + return; + } + + // notify dart through callbacks. + self.transactionsUpdated(transactions); +} + +// Sent when transactions are removed from the queue (via finishTransaction:). +- (void)paymentQueue:(SKPaymentQueue *)queue + removedTransactions:(NSArray *)transactions { + if (!self.observingTransactions) { + [_transactionCache addObjects:transactions forKey:TransactionCacheKeyRemovedTransactions]; + return; + } + self.transactionsRemoved(transactions); +} + +// Sent when an error is encountered while adding transactions from the user's purchase history back +// to the queue. +- (void)paymentQueue:(SKPaymentQueue *)queue + restoreCompletedTransactionsFailedWithError:(NSError *)error { + self.restoreTransactionFailed(error); +} + +// Sent when all transactions from the user's purchase history have successfully been added back to +// the queue. +- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue { + self.paymentQueueRestoreCompletedTransactionsFinished(); +} + +// Sent when the download state has changed. +- (void)paymentQueue:(SKPaymentQueue *)queue updatedDownloads:(NSArray *)downloads { + if (!self.observingTransactions) { + [_transactionCache addObjects:downloads forKey:TransactionCacheKeyUpdatedDownloads]; + return; + } + self.updatedDownloads(downloads); +} + +// Sent when a user initiates an IAP buy from the App Store +- (BOOL)paymentQueue:(SKPaymentQueue *)queue + shouldAddStorePayment:(SKPayment *)payment + forProduct:(SKProduct *)product { + return (self.shouldAddStorePayment(payment, product)); +} + +- (NSArray *)getUnfinishedTransactions { + return self.queue.transactions; +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIATransactionCache.h b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIATransactionCache.h new file mode 100644 index 000000000000..dea3c2d85d14 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIATransactionCache.h @@ -0,0 +1,31 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, TransactionCacheKey) { + TransactionCacheKeyUpdatedDownloads, + TransactionCacheKeyUpdatedTransactions, + TransactionCacheKeyRemovedTransactions +}; + +@interface FIATransactionCache : NSObject + +/// Adds objects to the transaction cache. +/// +/// If the cache already contains an array of objects on the specified key, the supplied +/// array will be appended to the existing array. +- (void)addObjects:(NSArray *)objects forKey:(TransactionCacheKey)key; + +/// Gets the array of objects stored at the given key. +/// +/// If there are no objects associated with the given key nil is returned. +- (NSArray *)getObjectsForKey:(TransactionCacheKey)key; + +/// Removes all objects from the transaction cache. +- (void)clear; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIATransactionCache.m b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIATransactionCache.m new file mode 100644 index 000000000000..f80b9c40c7bc --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIATransactionCache.m @@ -0,0 +1,40 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FIATransactionCache.h" + +@interface FIATransactionCache () + +/// A NSMutableDictionary storing the objects that are cached. +@property(nonatomic, strong, nonnull) NSMutableDictionary *cache; + +@end + +@implementation FIATransactionCache + +- (instancetype)init { + self = [super init]; + if (self) { + self.cache = [[NSMutableDictionary alloc] init]; + } + + return self; +} + +- (void)addObjects:(NSArray *)objects forKey:(TransactionCacheKey)key { + NSArray *cachedObjects = self.cache[@(key)]; + + self.cache[@(key)] = + cachedObjects ? [cachedObjects arrayByAddingObjectsFromArray:objects] : objects; +} + +- (NSArray *)getObjectsForKey:(TransactionCacheKey)key { + return self.cache[@(key)]; +} + +- (void)clear { + [self.cache removeAllObjects]; +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.h b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.h new file mode 100644 index 000000000000..eeab0a706683 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.h @@ -0,0 +1,21 @@ +// 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. + +#if TARGET_OS_OSX +#import +#else +#import +#endif +@class FIAPaymentQueueHandler; +@class FIAPReceiptManager; + +@interface InAppPurchasePlugin : NSObject + +@property(strong, nonatomic) FIAPaymentQueueHandler *paymentQueueHandler; + +- (instancetype)initWithReceiptManager:(FIAPReceiptManager *)receiptManager + NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.m new file mode 100644 index 000000000000..1ecb0fc1dc68 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.m @@ -0,0 +1,451 @@ +// 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 "InAppPurchasePlugin.h" +#import +#import "FIAObjectTranslator.h" +#import "FIAPPaymentQueueDelegate.h" +#import "FIAPReceiptManager.h" +#import "FIAPRequestHandler.h" +#import "FIAPaymentQueueHandler.h" + +@interface InAppPurchasePlugin () + +// Holding strong references to FIAPRequestHandlers. Remove the handlers from the set after +// the request is finished. +@property(strong, nonatomic, readonly) NSMutableSet *requestHandlers; + +// After querying the product, the available products will be saved in the map to be used +// for purchase. +@property(strong, nonatomic, readonly) NSMutableDictionary *productsCache; + +// Callback channel to dart used for when a function from the transaction observer is triggered. +@property(strong, nonatomic, readonly) FlutterMethodChannel *transactionObserverCallbackChannel; + +// Callback channel to dart used for when a function from the payment queue delegate is triggered. +@property(strong, nonatomic, readonly) FlutterMethodChannel *paymentQueueDelegateCallbackChannel; +@property(strong, nonatomic, readonly) NSObject *registrar; + +@property(strong, nonatomic, readonly) FIAPReceiptManager *receiptManager; +@property(strong, nonatomic, readonly) + FIAPPaymentQueueDelegate *paymentQueueDelegate API_AVAILABLE(ios(13)) + API_UNAVAILABLE(tvos, macos, watchos); + +@end + +@implementation InAppPurchasePlugin + ++ (void)registerWithRegistrar:(NSObject *)registrar { + FlutterMethodChannel *channel = + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" + binaryMessenger:[registrar messenger]]; + InAppPurchasePlugin *instance = [[InAppPurchasePlugin alloc] initWithRegistrar:registrar]; + [registrar addMethodCallDelegate:instance channel:channel]; +} + +- (instancetype)initWithReceiptManager:(FIAPReceiptManager *)receiptManager { + self = [super init]; + _receiptManager = receiptManager; + _requestHandlers = [NSMutableSet new]; + _productsCache = [NSMutableDictionary new]; + return self; +} + +- (instancetype)initWithRegistrar:(NSObject *)registrar { + self = [self initWithReceiptManager:[FIAPReceiptManager new]]; + _registrar = registrar; + + __weak typeof(self) weakSelf = self; + _paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueue defaultQueue] + transactionsUpdated:^(NSArray *_Nonnull transactions) { + [weakSelf handleTransactionsUpdated:transactions]; + } + transactionRemoved:^(NSArray *_Nonnull transactions) { + [weakSelf handleTransactionsRemoved:transactions]; + } + restoreTransactionFailed:^(NSError *_Nonnull error) { + [weakSelf handleTransactionRestoreFailed:error]; + } + restoreCompletedTransactionsFinished:^{ + [weakSelf restoreCompletedTransactionsFinished]; + } + shouldAddStorePayment:^BOOL(SKPayment *payment, SKProduct *product) { + return [weakSelf shouldAddStorePayment:payment product:product]; + } + updatedDownloads:^void(NSArray *_Nonnull downloads) { + [weakSelf updatedDownloads:downloads]; + } + transactionCache:[[FIATransactionCache alloc] init]]; + + _transactionObserverCallbackChannel = + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" + binaryMessenger:[registrar messenger]]; + return self; +} + +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + if ([@"-[SKPaymentQueue canMakePayments:]" isEqualToString:call.method]) { + [self canMakePayments:result]; + } else if ([@"-[SKPaymentQueue transactions]" isEqualToString:call.method]) { + [self getPendingTransactions:result]; + } else if ([@"-[InAppPurchasePlugin startProductRequest:result:]" isEqualToString:call.method]) { + [self handleProductRequestMethodCall:call result:result]; + } else if ([@"-[InAppPurchasePlugin addPayment:result:]" isEqualToString:call.method]) { + [self addPayment:call result:result]; + } else if ([@"-[InAppPurchasePlugin finishTransaction:result:]" isEqualToString:call.method]) { + [self finishTransaction:call result:result]; + } else if ([@"-[InAppPurchasePlugin restoreTransactions:result:]" isEqualToString:call.method]) { + [self restoreTransactions:call result:result]; +#if TARGET_OS_IOS + } else if ([@"-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]" + isEqualToString:call.method]) { + [self presentCodeRedemptionSheet:call result:result]; +#endif + } else if ([@"-[InAppPurchasePlugin retrieveReceiptData:result:]" isEqualToString:call.method]) { + [self retrieveReceiptData:call result:result]; + } else if ([@"-[InAppPurchasePlugin refreshReceipt:result:]" isEqualToString:call.method]) { + [self refreshReceipt:call result:result]; + } else if ([@"-[SKPaymentQueue startObservingTransactionQueue]" isEqualToString:call.method]) { + [self startObservingPaymentQueue:result]; + } else if ([@"-[SKPaymentQueue stopObservingTransactionQueue]" isEqualToString:call.method]) { + [self stopObservingPaymentQueue:result]; +#if TARGET_OS_IOS + } else if ([@"-[SKPaymentQueue registerDelegate]" isEqualToString:call.method]) { + [self registerPaymentQueueDelegate:result]; +#endif + } else if ([@"-[SKPaymentQueue removeDelegate]" isEqualToString:call.method]) { + [self removePaymentQueueDelegate:result]; +#if TARGET_OS_IOS + } else if ([@"-[SKPaymentQueue showPriceConsentIfNeeded]" isEqualToString:call.method]) { + [self showPriceConsentIfNeeded:result]; +#endif + } else { + result(FlutterMethodNotImplemented); + } +} + +- (void)canMakePayments:(FlutterResult)result { + result(@([SKPaymentQueue canMakePayments])); +} + +- (void)getPendingTransactions:(FlutterResult)result { + NSArray *transactions = + [self.paymentQueueHandler getUnfinishedTransactions]; + NSMutableArray *transactionMaps = [[NSMutableArray alloc] init]; + for (SKPaymentTransaction *transaction in transactions) { + [transactionMaps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; + } + result(transactionMaps); +} + +- (void)handleProductRequestMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + if (![call.arguments isKindOfClass:[NSArray class]]) { + result([FlutterError errorWithCode:@"storekit_invalid_argument" + message:@"Argument type of startRequest is not array" + details:call.arguments]); + return; + } + NSArray *productIdentifiers = (NSArray *)call.arguments; + SKProductsRequest *request = + [self getProductRequestWithIdentifiers:[NSSet setWithArray:productIdentifiers]]; + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + [self.requestHandlers addObject:handler]; + __weak typeof(self) weakSelf = self; + [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + result([FlutterError errorWithCode:@"storekit_getproductrequest_platform_error" + message:error.localizedDescription + details:error.description]); + return; + } + if (!response) { + result([FlutterError errorWithCode:@"storekit_platform_no_response" + message:@"Failed to get SKProductResponse in startRequest " + @"call. Error occured on iOS platform" + details:call.arguments]); + return; + } + for (SKProduct *product in response.products) { + [self.productsCache setObject:product forKey:product.productIdentifier]; + } + result([FIAObjectTranslator getMapFromSKProductsResponse:response]); + [weakSelf.requestHandlers removeObject:handler]; + }]; +} + +- (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { + if (![call.arguments isKindOfClass:[NSDictionary class]]) { + result([FlutterError errorWithCode:@"storekit_invalid_argument" + message:@"Argument type of addPayment is not a Dictionary" + details:call.arguments]); + return; + } + NSDictionary *paymentMap = (NSDictionary *)call.arguments; + NSString *productID = [paymentMap objectForKey:@"productIdentifier"]; + // When a product is already fetched, we create a payment object with + // the product to process the payment. + SKProduct *product = [self getProduct:productID]; + if (!product) { + result([FlutterError + errorWithCode:@"storekit_invalid_payment_object" + message: + @"You have requested a payment for an invalid product. Either the " + @"`productIdentifier` of the payment is not valid or the product has not been " + @"fetched before adding the payment to the payment queue." + details:call.arguments]); + return; + } + SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product]; + payment.applicationUsername = [paymentMap objectForKey:@"applicationUsername"]; + NSNumber *quantity = [paymentMap objectForKey:@"quantity"]; + payment.quantity = (quantity != nil) ? quantity.integerValue : 1; + NSNumber *simulatesAskToBuyInSandbox = [paymentMap objectForKey:@"simulatesAskToBuyInSandbox"]; + payment.simulatesAskToBuyInSandbox = (id)simulatesAskToBuyInSandbox == (id)[NSNull null] + ? NO + : [simulatesAskToBuyInSandbox boolValue]; + + if (@available(iOS 12.2, *)) { + NSDictionary *paymentDiscountMap = [self getNonNullValueFromDictionary:paymentMap + forKey:@"paymentDiscount"]; + NSString *error = nil; + SKPaymentDiscount *paymentDiscount = + [FIAObjectTranslator getSKPaymentDiscountFromMap:paymentDiscountMap withError:&error]; + + if (error) { + result([FlutterError + errorWithCode:@"storekit_invalid_payment_discount_object" + message:[NSString stringWithFormat:@"You have requested a payment and specified a " + @"payment discount with invalid properties. %@", + error] + details:call.arguments]); + return; + } + + payment.paymentDiscount = paymentDiscount; + } + + if (![self.paymentQueueHandler addPayment:payment]) { + result([FlutterError + errorWithCode:@"storekit_duplicate_product_object" + message:@"There is a pending transaction for the same product identifier. Please " + @"either wait for it to be finished or finish it manually using " + @"`completePurchase` to avoid edge cases." + + details:call.arguments]); + return; + } + result(nil); +} + +- (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result { + if (![call.arguments isKindOfClass:[NSDictionary class]]) { + result([FlutterError errorWithCode:@"storekit_invalid_argument" + message:@"Argument type of finishTransaction is not a Dictionary" + details:call.arguments]); + return; + } + NSDictionary *paymentMap = (NSDictionary *)call.arguments; + NSString *transactionIdentifier = [paymentMap objectForKey:@"transactionIdentifier"]; + NSString *productIdentifier = [paymentMap objectForKey:@"productIdentifier"]; + + NSArray *pendingTransactions = + [self.paymentQueueHandler getUnfinishedTransactions]; + + for (SKPaymentTransaction *transaction in pendingTransactions) { + // If the user cancels the purchase dialog we won't have a transactionIdentifier. + // So if it is null AND a transaction in the pendingTransactions list has + // also a null transactionIdentifier we check for equal product identifiers. + if ([transaction.transactionIdentifier isEqualToString:transactionIdentifier] || + ([transactionIdentifier isEqual:[NSNull null]] && + transaction.transactionIdentifier == nil && + [transaction.payment.productIdentifier isEqualToString:productIdentifier])) { + @try { + [self.paymentQueueHandler finishTransaction:transaction]; + } @catch (NSException *e) { + result([FlutterError errorWithCode:@"storekit_finish_transaction_exception" + message:e.name + details:e.description]); + return; + } + } + } + + result(nil); +} + +- (void)restoreTransactions:(FlutterMethodCall *)call result:(FlutterResult)result { + if (call.arguments && ![call.arguments isKindOfClass:[NSString class]]) { + result([FlutterError + errorWithCode:@"storekit_invalid_argument" + message:@"Argument is not nil and the type of finishTransaction is not a string." + details:call.arguments]); + return; + } + [self.paymentQueueHandler restoreTransactions:call.arguments]; + result(nil); +} + +#if TARGET_OS_IOS +- (void)presentCodeRedemptionSheet:(FlutterMethodCall *)call result:(FlutterResult)result { + [self.paymentQueueHandler presentCodeRedemptionSheet]; + result(nil); +} +#endif + +- (void)retrieveReceiptData:(FlutterMethodCall *)call result:(FlutterResult)result { + FlutterError *error = nil; + NSString *receiptData = [self.receiptManager retrieveReceiptWithError:&error]; + if (error) { + result(error); + return; + } + result(receiptData); +} + +- (void)refreshReceipt:(FlutterMethodCall *)call result:(FlutterResult)result { + NSDictionary *arguments = call.arguments; + SKReceiptRefreshRequest *request; + if (arguments) { + if (![arguments isKindOfClass:[NSDictionary class]]) { + result([FlutterError errorWithCode:@"storekit_invalid_argument" + message:@"Argument type of startRequest is not array" + details:call.arguments]); + return; + } + NSMutableDictionary *properties = [NSMutableDictionary new]; + properties[SKReceiptPropertyIsExpired] = arguments[@"isExpired"]; + properties[SKReceiptPropertyIsRevoked] = arguments[@"isRevoked"]; + properties[SKReceiptPropertyIsVolumePurchase] = arguments[@"isVolumePurchase"]; + request = [self getRefreshReceiptRequest:properties]; + } else { + request = [self getRefreshReceiptRequest:nil]; + } + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + [self.requestHandlers addObject:handler]; + __weak typeof(self) weakSelf = self; + [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + result([FlutterError errorWithCode:@"storekit_refreshreceiptrequest_platform_error" + message:error.localizedDescription + details:error.description]); + return; + } + result(nil); + [weakSelf.requestHandlers removeObject:handler]; + }]; +} + +- (void)startObservingPaymentQueue:(FlutterResult)result { + [_paymentQueueHandler startObservingPaymentQueue]; + result(nil); +} + +- (void)stopObservingPaymentQueue:(FlutterResult)result { + [_paymentQueueHandler stopObservingPaymentQueue]; + result(nil); +} + +#if TARGET_OS_IOS +- (void)registerPaymentQueueDelegate:(FlutterResult)result { + if (@available(iOS 13.0, *)) { + _paymentQueueDelegateCallbackChannel = [FlutterMethodChannel + methodChannelWithName:@"plugins.flutter.io/in_app_purchase_payment_queue_delegate" + binaryMessenger:[_registrar messenger]]; + + _paymentQueueDelegate = [[FIAPPaymentQueueDelegate alloc] + initWithMethodChannel:_paymentQueueDelegateCallbackChannel]; + _paymentQueueHandler.delegate = _paymentQueueDelegate; + } + result(nil); +} +#endif + +- (void)removePaymentQueueDelegate:(FlutterResult)result { + if (@available(iOS 13.0, *)) { + _paymentQueueHandler.delegate = nil; + } + _paymentQueueDelegate = nil; + _paymentQueueDelegateCallbackChannel = nil; + result(nil); +} + +#if TARGET_OS_IOS +- (void)showPriceConsentIfNeeded:(FlutterResult)result { + if (@available(iOS 13.4, *)) { + [_paymentQueueHandler showPriceConsentIfNeeded]; + } + result(nil); +} +#endif + +- (id)getNonNullValueFromDictionary:(NSDictionary *)dictionary forKey:(NSString *)key { + id value = dictionary[key]; + return [value isKindOfClass:[NSNull class]] ? nil : value; +} + +#pragma mark - transaction observer: + +- (void)handleTransactionsUpdated:(NSArray *)transactions { + NSMutableArray *maps = [NSMutableArray new]; + for (SKPaymentTransaction *transaction in transactions) { + [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; + } + [self.transactionObserverCallbackChannel invokeMethod:@"updatedTransactions" arguments:maps]; +} + +- (void)handleTransactionsRemoved:(NSArray *)transactions { + NSMutableArray *maps = [NSMutableArray new]; + for (SKPaymentTransaction *transaction in transactions) { + [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; + } + [self.transactionObserverCallbackChannel invokeMethod:@"removedTransactions" arguments:maps]; +} + +- (void)handleTransactionRestoreFailed:(NSError *)error { + [self.transactionObserverCallbackChannel + invokeMethod:@"restoreCompletedTransactionsFailed" + arguments:[FIAObjectTranslator getMapFromNSError:error]]; +} + +- (void)restoreCompletedTransactionsFinished { + [self.transactionObserverCallbackChannel + invokeMethod:@"paymentQueueRestoreCompletedTransactionsFinished" + arguments:nil]; +} + +- (void)updatedDownloads:(NSArray *)downloads { + NSLog(@"Received an updatedDownloads callback, but downloads are not supported."); +} + +- (BOOL)shouldAddStorePayment:(SKPayment *)payment product:(SKProduct *)product { + // We always return NO here. And we send the message to dart to process the payment; and we will + // have a interception method that deciding if the payment should be processed (implemented by the + // programmer). + [self.productsCache setObject:product forKey:product.productIdentifier]; + [self.transactionObserverCallbackChannel + invokeMethod:@"shouldAddStorePayment" + arguments:@{ + @"payment" : [FIAObjectTranslator getMapFromSKPayment:payment], + @"product" : [FIAObjectTranslator getMapFromSKProduct:product] + }]; + return NO; +} + +#pragma mark - dependency injection (for unit testing) + +- (SKProductsRequest *)getProductRequestWithIdentifiers:(NSSet *)identifiers { + return [[SKProductsRequest alloc] initWithProductIdentifiers:identifiers]; +} + +- (SKProduct *)getProduct:(NSString *)productID { + return [self.productsCache objectForKey:productID]; +} + +- (SKReceiptRefreshRequest *)getRefreshReceiptRequest:(NSDictionary *)properties { + return [[SKReceiptRefreshRequest alloc] initWithReceiptProperties:properties]; +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit.podspec b/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit.podspec new file mode 100644 index 000000000000..57a24bd674ab --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit.podspec @@ -0,0 +1,26 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'in_app_purchase_storekit' + s.version = '0.0.1' + s.summary = 'Flutter In App Purchase iOS and macOS' + s.description = <<-DESC +A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store. +Downloaded by pub (not CocoaPods). + DESC + s.homepage = 'https://github.com/flutter/plugins' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/in_app_purchase/in_app_purchase_storekit' } + # TODO(mvanbeusekom): update URL when in_app_purchase_storekit package is published. + # Updating it before the package is published will cause a lint error and block the tree. + s.documentation_url = 'https://pub.dev/packages/in_app_purchase' + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.ios.dependency 'Flutter' + s.osx.dependency 'FlutterMacOS' + s.ios.deployment_target = '11.0' + s.osx.deployment_target = '10.15' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } +end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Flutter/AppFrameworkInfo.plist b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Flutter/AppFrameworkInfo.plist index 8d4492f977ad..9625e105df39 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Podfile b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Podfile index 5200b9fa5045..4f563887c820 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Podfile +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj index 3977d549af12..4b24d767a226 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -318,10 +318,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -354,6 +356,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -493,7 +496,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -543,7 +546,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; 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 a8f31ba92572..3c493732947a 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 @@ -41,5 +41,9 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m deleted file mode 100644 index ea8787f55a0a..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m +++ /dev/null @@ -1,120 +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 -#import -#import "FIAObjectTranslator.h" -#import "FIAPaymentQueueHandler.h" -#import "Stubs.h" - -@import in_app_purchase_storekit; - -API_AVAILABLE(ios(13.0)) -@interface FIAPPaymentQueueDelegateTests : XCTestCase - -@property(strong, nonatomic) FlutterMethodChannel *channel; -@property(strong, nonatomic) SKPaymentTransaction *transaction; -@property(strong, nonatomic) SKStorefront *storefront; - -@end - -@implementation FIAPPaymentQueueDelegateTests - -- (void)setUp { - self.channel = OCMClassMock(FlutterMethodChannel.class); - - NSDictionary *transactionMap = @{ - @"transactionIdentifier" : [NSNull null], - @"transactionState" : @(SKPaymentTransactionStatePurchasing), - @"payment" : [NSNull null], - @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" - code:123 - userInfo:@{}]], - @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), - @"originalTransaction" : [NSNull null], - }; - self.transaction = [[SKPaymentTransactionStub alloc] initWithMap:transactionMap]; - - NSDictionary *storefrontMap = @{ - @"countryCode" : @"USA", - @"identifier" : @"unique_identifier", - }; - self.storefront = [[SKStorefrontStub alloc] initWithMap:storefrontMap]; -} - -- (void)tearDown { - self.channel = nil; -} - -- (void)testShouldContinueTransaction { - if (@available(iOS 13.0, *)) { - FIAPPaymentQueueDelegate *delegate = - [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; - - OCMStub([self.channel - invokeMethod:@"shouldContinueTransaction" - arguments:[FIAObjectTranslator getMapFromSKStorefront:self.storefront - andSKPaymentTransaction:self.transaction] - result:([OCMArg invokeBlockWithArgs:[NSNumber numberWithBool:NO], nil])]); - - BOOL shouldContinue = [delegate paymentQueue:OCMClassMock(SKPaymentQueue.class) - shouldContinueTransaction:self.transaction - inStorefront:self.storefront]; - - XCTAssertFalse(shouldContinue); - } -} - -- (void)testShouldContinueTransaction_should_default_to_yes { - if (@available(iOS 13.0, *)) { - FIAPPaymentQueueDelegate *delegate = - [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; - - OCMStub([self.channel invokeMethod:@"shouldContinueTransaction" - arguments:[FIAObjectTranslator getMapFromSKStorefront:self.storefront - andSKPaymentTransaction:self.transaction] - result:[OCMArg any]]); - - BOOL shouldContinue = [delegate paymentQueue:OCMClassMock(SKPaymentQueue.class) - shouldContinueTransaction:self.transaction - inStorefront:self.storefront]; - - XCTAssertTrue(shouldContinue); - } -} - -- (void)testShouldShowPriceConsentIfNeeded { - if (@available(iOS 13.4, *)) { - FIAPPaymentQueueDelegate *delegate = - [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; - - OCMStub([self.channel - invokeMethod:@"shouldShowPriceConsent" - arguments:nil - result:([OCMArg invokeBlockWithArgs:[NSNumber numberWithBool:NO], nil])]); - - BOOL shouldShow = - [delegate paymentQueueShouldShowPriceConsent:OCMClassMock(SKPaymentQueue.class)]; - - XCTAssertFalse(shouldShow); - } -} - -- (void)testShouldShowPriceConsentIfNeeded_should_default_to_yes { - if (@available(iOS 13.4, *)) { - FIAPPaymentQueueDelegate *delegate = - [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; - - OCMStub([self.channel invokeMethod:@"shouldShowPriceConsent" - arguments:nil - result:[OCMArg any]]); - - BOOL shouldShow = - [delegate paymentQueueShouldShowPriceConsent:OCMClassMock(SKPaymentQueue.class)]; - - XCTAssertTrue(shouldShow); - } -} - -@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m new file mode 120000 index 000000000000..7c8e7691c6d4 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m @@ -0,0 +1 @@ +../../shared/RunnerTests/FIAPPaymentQueueDeleteTests.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIATransactionCacheTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIATransactionCacheTests.m deleted file mode 100644 index 1ba0aea76e39..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIATransactionCacheTests.m +++ /dev/null @@ -1,63 +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 - -@import in_app_purchase_storekit; - -@interface FIATransactionCacheTests : XCTestCase - -@end - -@implementation FIATransactionCacheTests - -- (void)testAddObjectsForNewKey { - NSArray *dummyArray = @[ @1, @2, @3 ]; - FIATransactionCache *cache = [[FIATransactionCache alloc] init]; - [cache addObjects:dummyArray forKey:TransactionCacheKeyUpdatedTransactions]; - - XCTAssertEqual(dummyArray, [cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); -} - -- (void)testAddObjectsForExistingKey { - NSArray *dummyArray = @[ @1, @2, @3 ]; - FIATransactionCache *cache = [[FIATransactionCache alloc] init]; - [cache addObjects:dummyArray forKey:TransactionCacheKeyUpdatedTransactions]; - - XCTAssertEqual(dummyArray, [cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); - - [cache addObjects:@[ @4, @5, @6 ] forKey:TransactionCacheKeyUpdatedTransactions]; - - NSArray *expected = @[ @1, @2, @3, @4, @5, @6 ]; - XCTAssertEqualObjects(expected, [cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); -} - -- (void)testGetObjectsForNonExistingKey { - FIATransactionCache *cache = [[FIATransactionCache alloc] init]; - XCTAssertNil([cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); -} - -- (void)testClear { - NSArray *fakeUpdatedTransactions = @[ @1, @2, @3 ]; - NSArray *fakeRemovedTransactions = @[ @"Remove 1", @"Remove 2", @"Remove 3" ]; - NSArray *fakeUpdatedDownloads = @[ @"Download 1", @"Download 2" ]; - FIATransactionCache *cache = [[FIATransactionCache alloc] init]; - [cache addObjects:fakeUpdatedTransactions forKey:TransactionCacheKeyUpdatedTransactions]; - [cache addObjects:fakeRemovedTransactions forKey:TransactionCacheKeyRemovedTransactions]; - [cache addObjects:fakeUpdatedDownloads forKey:TransactionCacheKeyUpdatedDownloads]; - - XCTAssertEqual(fakeUpdatedTransactions, - [cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); - XCTAssertEqual(fakeRemovedTransactions, - [cache getObjectsForKey:TransactionCacheKeyRemovedTransactions]); - XCTAssertEqual(fakeUpdatedDownloads, - [cache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]); - - [cache clear]; - - XCTAssertNil([cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); - XCTAssertNil([cache getObjectsForKey:TransactionCacheKeyRemovedTransactions]); - XCTAssertNil([cache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]); -} -@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIATransactionCacheTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIATransactionCacheTests.m new file mode 120000 index 000000000000..5c7c87fd1aea --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/FIATransactionCacheTests.m @@ -0,0 +1 @@ +../../shared/RunnerTests/FIATransactionCacheTests.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.m deleted file mode 100644 index c89589c6a9e5..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.m +++ /dev/null @@ -1,520 +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 -#import -#import "FIAPaymentQueueHandler.h" -#import "Stubs.h" - -@import in_app_purchase_storekit; - -@interface InAppPurchasePluginTest : XCTestCase - -@property(strong, nonatomic) FIAPReceiptManagerStub *receiptManagerStub; -@property(strong, nonatomic) InAppPurchasePlugin *plugin; - -@end - -@implementation InAppPurchasePluginTest - -- (void)setUp { - self.receiptManagerStub = [FIAPReceiptManagerStub new]; - self.plugin = [[InAppPurchasePluginStub alloc] initWithReceiptManager:self.receiptManagerStub]; -} - -- (void)tearDown { -} - -- (void)testInvalidMethodCall { - XCTestExpectation *expectation = - [self expectationWithDescription:@"expect result to be not implemented"]; - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"invalid" arguments:NULL]; - __block id result; - [self.plugin handleMethodCall:call - result:^(id r) { - [expectation fulfill]; - result = r; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(result, FlutterMethodNotImplemented); -} - -- (void)testCanMakePayments { - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result to be YES"]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue canMakePayments:]" - arguments:NULL]; - __block id result; - [self.plugin handleMethodCall:call - result:^(id r) { - [expectation fulfill]; - result = r; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(result, @YES); -} - -- (void)testGetProductResponse { - XCTestExpectation *expectation = - [self expectationWithDescription:@"expect response contains 1 item"]; - FlutterMethodCall *call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin startProductRequest:result:]" - arguments:@[ @"123" ]]; - __block id result; - [self.plugin handleMethodCall:call - result:^(id r) { - [expectation fulfill]; - result = r; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssert([result isKindOfClass:[NSDictionary class]]); - NSArray *resultArray = [result objectForKey:@"products"]; - XCTAssertEqual(resultArray.count, 1); - XCTAssertTrue([resultArray.firstObject[@"productIdentifier"] isEqualToString:@"123"]); -} - -- (void)testAddPaymentShouldReturnFlutterErrorWhenArgumentsAreInvalid { - XCTestExpectation *expectation = - [self expectationWithDescription: - @"Result should contain a FlutterError when invalid parameters are passed in."]; - NSString *argument = @"Invalid argument"; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" - arguments:argument]; - [self.plugin handleMethodCall:call - result:^(id _Nullable result) { - FlutterError *error = result; - XCTAssertEqualObjects(@"storekit_invalid_argument", error.code); - XCTAssertEqualObjects(@"Argument type of addPayment is not a Dictionary", - error.message); - XCTAssertEqualObjects(argument, error.details); - [expectation fulfill]; - }]; - - [self waitForExpectations:@[ expectation ] timeout:5]; -} - -- (void)testAddPaymentShouldReturnFlutterErrorWhenPaymentFails { - NSDictionary *arguments = @{ - @"productIdentifier" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandbox" : @YES, - }; - XCTestExpectation *expectation = - [self expectationWithDescription:@"Result should return failed state."]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" - arguments:arguments]; - - FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); - OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(NO); - self.plugin.paymentQueueHandler = mockHandler; - - [self.plugin handleMethodCall:call - result:^(id _Nullable result) { - FlutterError *error = result; - XCTAssertEqualObjects(@"storekit_duplicate_product_object", error.code); - XCTAssertEqualObjects( - @"There is a pending transaction for the same product identifier. " - @"Please either wait for it to be finished or finish it manually " - @"using `completePurchase` to avoid edge cases.", - error.message); - XCTAssertEqualObjects(arguments, error.details); - [expectation fulfill]; - }]; - - [self waitForExpectations:@[ expectation ] timeout:5]; - OCMVerify(times(1), [mockHandler addPayment:[OCMArg any]]); -} - -- (void)testAddPaymentSuccessWithoutPaymentDiscount { - XCTestExpectation *expectation = - [self expectationWithDescription:@"Result should return success state"]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" - arguments:@{ - @"productIdentifier" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandbox" : @YES, - }]; - FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); - OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(YES); - self.plugin.paymentQueueHandler = mockHandler; - [self.plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertNil(result); - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; -} - -- (void)testAddPaymentSuccessWithPaymentDiscount { - XCTestExpectation *expectation = - [self expectationWithDescription:@"Result should return success state"]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" - arguments:@{ - @"productIdentifier" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandbox" : @YES, - @"paymentDiscount" : @{ - @"identifier" : @"test_identifier", - @"keyIdentifier" : @"test_key_identifier", - @"nonce" : @"4a11a9cc-3bc3-11ec-8d3d-0242ac130003", - @"signature" : @"test_signature", - @"timestamp" : @(1635847102), - } - }]; - - FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); - OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(YES); - self.plugin.paymentQueueHandler = mockHandler; - [self.plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertNil(result); - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - OCMVerify( - times(1), - [mockHandler - addPayment:[OCMArg checkWithBlock:^BOOL(id obj) { - SKPayment *payment = obj; - if (@available(iOS 12.2, *)) { - SKPaymentDiscount *discount = payment.paymentDiscount; - - return [discount.identifier isEqual:@"test_identifier"] && - [discount.keyIdentifier isEqual:@"test_key_identifier"] && - [discount.nonce - isEqual:[[NSUUID alloc] - initWithUUIDString:@"4a11a9cc-3bc3-11ec-8d3d-0242ac130003"]] && - [discount.signature isEqual:@"test_signature"] && - [discount.timestamp isEqual:@(1635847102)]; - } - - return YES; - }]]); -} - -- (void)testAddPaymentFailureWithInvalidPaymentDiscount { - // Support for payment discount is only available on iOS 12.2 and higher. - if (@available(iOS 12.2, *)) { - XCTestExpectation *expectation = - [self expectationWithDescription:@"Result should return success state"]; - NSDictionary *arguments = @{ - @"productIdentifier" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandbox" : @YES, - @"paymentDiscount" : @{ - @"keyIdentifier" : @"test_key_identifier", - @"nonce" : @"4a11a9cc-3bc3-11ec-8d3d-0242ac130003", - @"signature" : @"test_signature", - @"timestamp" : @(1635847102), - } - }; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" - arguments:arguments]; - - FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); - id translator = OCMClassMock(FIAObjectTranslator.class); - - NSString *error = @"Some error occurred"; - OCMStub(ClassMethod([translator - getSKPaymentDiscountFromMap:[OCMArg any] - withError:(NSString __autoreleasing **)[OCMArg setTo:error]])) - .andReturn(nil); - self.plugin.paymentQueueHandler = mockHandler; - [self.plugin - handleMethodCall:call - result:^(id _Nullable result) { - FlutterError *error = result; - XCTAssertEqualObjects(@"storekit_invalid_payment_discount_object", error.code); - XCTAssertEqualObjects( - @"You have requested a payment and specified a " - @"payment discount with invalid properties. Some error occurred", - error.message); - XCTAssertEqualObjects(arguments, error.details); - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - OCMVerify(never(), [mockHandler addPayment:[OCMArg any]]); - } -} - -- (void)testAddPaymentWithNullSandboxArgument { - XCTestExpectation *expectation = - [self expectationWithDescription:@"result should return success state"]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" - arguments:@{ - @"productIdentifier" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandbox" : [NSNull null], - }]; - FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); - OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(YES); - self.plugin.paymentQueueHandler = mockHandler; - [self.plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertNil(result); - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - OCMVerify(times(1), [mockHandler addPayment:[OCMArg checkWithBlock:^BOOL(id obj) { - SKPayment *payment = obj; - return !payment.simulatesAskToBuyInSandbox; - }]]); -} - -- (void)testRestoreTransactions { - XCTestExpectation *expectation = - [self expectationWithDescription:@"result successfully restore transactions"]; - FlutterMethodCall *call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin restoreTransactions:result:]" - arguments:nil]; - SKPaymentQueueStub *queue = [SKPaymentQueueStub new]; - queue.testState = SKPaymentTransactionStatePurchased; - __block BOOL callbackInvoked = NO; - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:^() { - callbackInvoked = YES; - [expectation fulfill]; - } - shouldAddStorePayment:nil - updatedDownloads:nil - transactionCache:OCMClassMock(FIATransactionCache.class)]; - [queue addTransactionObserver:self.plugin.paymentQueueHandler]; - [self.plugin handleMethodCall:call - result:^(id r){ - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertTrue(callbackInvoked); -} - -- (void)testRetrieveReceiptDataSuccess { - XCTestExpectation *expectation = [self expectationWithDescription:@"receipt data retrieved"]; - FlutterMethodCall *call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" - arguments:nil]; - __block NSDictionary *result; - [self.plugin handleMethodCall:call - result:^(id r) { - result = r; - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertNotNil(result); - XCTAssert([result isKindOfClass:[NSString class]]); -} - -- (void)testRetrieveReceiptDataError { - XCTestExpectation *expectation = [self expectationWithDescription:@"receipt data retrieved"]; - FlutterMethodCall *call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" - arguments:nil]; - __block NSDictionary *result; - self.receiptManagerStub.returnError = YES; - [self.plugin handleMethodCall:call - result:^(id r) { - result = r; - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertNotNil(result); - XCTAssert([result isKindOfClass:[FlutterError class]]); - NSDictionary *details = ((FlutterError *)result).details; - XCTAssertNotNil(details[@"error"]); - NSNumber *errorCode = (NSNumber *)details[@"error"][@"code"]; - XCTAssertEqual(errorCode, [NSNumber numberWithInteger:99]); -} - -- (void)testRefreshReceiptRequest { - XCTestExpectation *expectation = [self expectationWithDescription:@"expect success"]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin refreshReceipt:result:]" - arguments:nil]; - __block BOOL result = NO; - [self.plugin handleMethodCall:call - result:^(id r) { - result = YES; - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertTrue(result); -} - -- (void)testPresentCodeRedemptionSheet { - XCTestExpectation *expectation = - [self expectationWithDescription:@"expect successfully present Code Redemption Sheet"]; - FlutterMethodCall *call = [FlutterMethodCall - methodCallWithMethodName:@"-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]" - arguments:nil]; - __block BOOL callbackInvoked = NO; - [self.plugin handleMethodCall:call - result:^(id r) { - callbackInvoked = YES; - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertTrue(callbackInvoked); -} - -- (void)testGetPendingTransactions { - XCTestExpectation *expectation = [self expectationWithDescription:@"expect success"]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue transactions]" arguments:nil]; - SKPaymentQueue *mockQueue = OCMClassMock(SKPaymentQueue.class); - NSDictionary *transactionMap = @{ - @"transactionIdentifier" : [NSNull null], - @"transactionState" : @(SKPaymentTransactionStatePurchasing), - @"payment" : [NSNull null], - @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" - code:123 - userInfo:@{}]], - @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), - @"originalTransaction" : [NSNull null], - }; - OCMStub(mockQueue.transactions).andReturn(@[ [[SKPaymentTransactionStub alloc] - initWithMap:transactionMap] ]); - - __block NSArray *resultArray; - self.plugin.paymentQueueHandler = - [[FIAPaymentQueueHandler alloc] initWithQueue:mockQueue - transactionsUpdated:nil - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:nil - updatedDownloads:nil - transactionCache:OCMClassMock(FIATransactionCache.class)]; - [self.plugin handleMethodCall:call - result:^(id r) { - resultArray = r; - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqualObjects(resultArray, @[ transactionMap ]); -} - -- (void)testStartObservingPaymentQueue { - XCTestExpectation *expectation = - [self expectationWithDescription:@"Should return success result"]; - FlutterMethodCall *startCall = [FlutterMethodCall - methodCallWithMethodName:@"-[SKPaymentQueue startObservingTransactionQueue]" - arguments:nil]; - FIAPaymentQueueHandler *mockHandler = OCMClassMock([FIAPaymentQueueHandler class]); - self.plugin.paymentQueueHandler = mockHandler; - [self.plugin handleMethodCall:startCall - result:^(id _Nullable result) { - XCTAssertNil(result); - [expectation fulfill]; - }]; - - [self waitForExpectations:@[ expectation ] timeout:5]; - OCMVerify(times(1), [mockHandler startObservingPaymentQueue]); -} - -- (void)testStopObservingPaymentQueue { - XCTestExpectation *expectation = - [self expectationWithDescription:@"Should return success result"]; - FlutterMethodCall *stopCall = - [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue stopObservingTransactionQueue]" - arguments:nil]; - FIAPaymentQueueHandler *mockHandler = OCMClassMock([FIAPaymentQueueHandler class]); - self.plugin.paymentQueueHandler = mockHandler; - [self.plugin handleMethodCall:stopCall - result:^(id _Nullable result) { - XCTAssertNil(result); - [expectation fulfill]; - }]; - - [self waitForExpectations:@[ expectation ] timeout:5]; - OCMVerify(times(1), [mockHandler stopObservingPaymentQueue]); -} - -- (void)testRegisterPaymentQueueDelegate { - if (@available(iOS 13, *)) { - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue registerDelegate]" - arguments:nil]; - - self.plugin.paymentQueueHandler = - [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueueStub new] - transactionsUpdated:nil - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:nil - updatedDownloads:nil - transactionCache:OCMClassMock(FIATransactionCache.class)]; - - // Verify the delegate is nil before we register one. - XCTAssertNil(self.plugin.paymentQueueHandler.delegate); - - [self.plugin handleMethodCall:call - result:^(id r){ - }]; - - // Verify the delegate is not nil after we registered one. - XCTAssertNotNil(self.plugin.paymentQueueHandler.delegate); - } -} - -- (void)testRemovePaymentQueueDelegate { - if (@available(iOS 13, *)) { - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue removeDelegate]" - arguments:nil]; - - self.plugin.paymentQueueHandler = - [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueueStub new] - transactionsUpdated:nil - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:nil - updatedDownloads:nil - transactionCache:OCMClassMock(FIATransactionCache.class)]; - self.plugin.paymentQueueHandler.delegate = OCMProtocolMock(@protocol(SKPaymentQueueDelegate)); - - // Verify the delegate is not nil before removing it. - XCTAssertNotNil(self.plugin.paymentQueueHandler.delegate); - - [self.plugin handleMethodCall:call - result:^(id r){ - }]; - - // Verify the delegate is nill after removing it. - XCTAssertNil(self.plugin.paymentQueueHandler.delegate); - } -} - -- (void)testShowPriceConsentIfNeeded { - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue showPriceConsentIfNeeded]" - arguments:nil]; - - FIAPaymentQueueHandler *mockQueueHandler = OCMClassMock(FIAPaymentQueueHandler.class); - self.plugin.paymentQueueHandler = mockQueueHandler; - - [self.plugin handleMethodCall:call - result:^(id r){ - }]; - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wpartial-availability" - if (@available(iOS 13.4, *)) { - OCMVerify(times(1), [mockQueueHandler showPriceConsentIfNeeded]); - } else { - OCMVerify(never(), [mockQueueHandler showPriceConsentIfNeeded]); - } -#pragma clang diagnostic pop -} - -@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.m new file mode 120000 index 000000000000..495146dde20b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/InAppPurchasePluginTests.m @@ -0,0 +1 @@ +../../shared/RunnerTests/InAppPurchasePluginTests.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Info.plist b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Info.plist deleted file mode 100644 index 6c40a6cd0c4a..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Info.plist b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Info.plist new file mode 120000 index 000000000000..55acf210929a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Info.plist @@ -0,0 +1 @@ +../../shared/RunnerTests/Info.plist \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/PaymentQueueTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/PaymentQueueTests.m deleted file mode 100644 index 2f8d5857c8d8..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/PaymentQueueTests.m +++ /dev/null @@ -1,420 +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 -#import -#import "Stubs.h" - -@import in_app_purchase_storekit; - -@interface PaymentQueueTest : XCTestCase - -@property(strong, nonatomic) NSDictionary *periodMap; -@property(strong, nonatomic) NSDictionary *discountMap; -@property(strong, nonatomic) NSDictionary *productMap; -@property(strong, nonatomic) NSDictionary *productResponseMap; - -@end - -@implementation PaymentQueueTest - -- (void)setUp { - self.periodMap = @{@"numberOfUnits" : @(0), @"unit" : @(0)}; - self.discountMap = @{ - @"price" : @1.0, - @"currencyCode" : @"USD", - @"numberOfPeriods" : @1, - @"subscriptionPeriod" : self.periodMap, - @"paymentMode" : @1 - }; - self.productMap = @{ - @"price" : @1.0, - @"currencyCode" : @"USD", - @"productIdentifier" : @"123", - @"localizedTitle" : @"title", - @"localizedDescription" : @"des", - @"subscriptionPeriod" : self.periodMap, - @"introductoryPrice" : self.discountMap, - @"subscriptionGroupIdentifier" : @"com.group" - }; - self.productResponseMap = - @{@"products" : @[ self.productMap ], @"invalidProductIdentifiers" : [NSNull null]}; -} - -- (void)testTransactionPurchased { - XCTestExpectation *expectation = - [self expectationWithDescription:@"expect to get purchased transcation."]; - SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; - queue.testState = SKPaymentTransactionStatePurchased; - __block SKPaymentTransactionStub *tran; - FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - SKPaymentTransaction *transaction = transactions[0]; - tran = (SKPaymentTransactionStub *)transaction; - [expectation fulfill]; - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:nil - transactionCache:OCMClassMock(FIATransactionCache.class)]; - SKPayment *payment = - [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; - [handler startObservingPaymentQueue]; - [handler addPayment:payment]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(tran.transactionState, SKPaymentTransactionStatePurchased); - XCTAssertEqual(tran.transactionIdentifier, @"fakeID"); -} - -- (void)testTransactionFailed { - XCTestExpectation *expectation = - [self expectationWithDescription:@"expect to get failed transcation."]; - SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; - queue.testState = SKPaymentTransactionStateFailed; - __block SKPaymentTransactionStub *tran; - FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - SKPaymentTransaction *transaction = transactions[0]; - tran = (SKPaymentTransactionStub *)transaction; - [expectation fulfill]; - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:nil - transactionCache:OCMClassMock(FIATransactionCache.class)]; - - SKPayment *payment = - [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; - [handler startObservingPaymentQueue]; - [handler addPayment:payment]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateFailed); - XCTAssertEqual(tran.transactionIdentifier, nil); -} - -- (void)testTransactionRestored { - XCTestExpectation *expectation = - [self expectationWithDescription:@"expect to get restored transcation."]; - SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; - queue.testState = SKPaymentTransactionStateRestored; - __block SKPaymentTransactionStub *tran; - FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - SKPaymentTransaction *transaction = transactions[0]; - tran = (SKPaymentTransactionStub *)transaction; - [expectation fulfill]; - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:nil - transactionCache:OCMClassMock(FIATransactionCache.class)]; - - SKPayment *payment = - [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; - [handler startObservingPaymentQueue]; - [handler addPayment:payment]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateRestored); - XCTAssertEqual(tran.transactionIdentifier, @"fakeID"); -} - -- (void)testTransactionPurchasing { - XCTestExpectation *expectation = - [self expectationWithDescription:@"expect to get purchasing transcation."]; - SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; - queue.testState = SKPaymentTransactionStatePurchasing; - __block SKPaymentTransactionStub *tran; - FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - SKPaymentTransaction *transaction = transactions[0]; - tran = (SKPaymentTransactionStub *)transaction; - [expectation fulfill]; - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:nil - transactionCache:OCMClassMock(FIATransactionCache.class)]; - - SKPayment *payment = - [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; - [handler startObservingPaymentQueue]; - [handler addPayment:payment]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(tran.transactionState, SKPaymentTransactionStatePurchasing); - XCTAssertEqual(tran.transactionIdentifier, nil); -} - -- (void)testTransactionDeferred { - XCTestExpectation *expectation = - [self expectationWithDescription:@"expect to get deffered transcation."]; - SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; - queue.testState = SKPaymentTransactionStateDeferred; - __block SKPaymentTransactionStub *tran; - FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - SKPaymentTransaction *transaction = transactions[0]; - tran = (SKPaymentTransactionStub *)transaction; - [expectation fulfill]; - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:nil - transactionCache:OCMClassMock(FIATransactionCache.class)]; - SKPayment *payment = - [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; - [handler startObservingPaymentQueue]; - [handler addPayment:payment]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateDeferred); - XCTAssertEqual(tran.transactionIdentifier, nil); -} - -- (void)testFinishTransaction { - XCTestExpectation *expectation = - [self expectationWithDescription:@"handler.transactions should be empty."]; - SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; - queue.testState = SKPaymentTransactionStateDeferred; - __block FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - XCTAssertEqual(transactions.count, 1); - SKPaymentTransaction *transaction = transactions[0]; - [handler finishTransaction:transaction]; - } - transactionRemoved:^(NSArray *_Nonnull transactions) { - XCTAssertEqual(transactions.count, 1); - [expectation fulfill]; - } - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:nil - transactionCache:OCMClassMock(FIATransactionCache.class)]; - SKPayment *payment = - [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; - [handler startObservingPaymentQueue]; - [handler addPayment:payment]; - [self waitForExpectations:@[ expectation ] timeout:5]; -} - -- (void)testStartObservingPaymentQueueShouldNotProcessTransactionsWhenCacheIsEmpty { - FIATransactionCache *mockCache = OCMClassMock(FIATransactionCache.class); - FIAPaymentQueueHandler *handler = - [[FIAPaymentQueueHandler alloc] initWithQueue:[[SKPaymentQueueStub alloc] init] - transactionsUpdated:^(NSArray *_Nonnull transactions) { - XCTFail("transactionsUpdated callback should not be called when cache is empty."); - } - transactionRemoved:^(NSArray *_Nonnull transactions) { - XCTFail("transactionRemoved callback should not be called when cache is empty."); - } - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:^(NSArray *_Nonnull downloads) { - XCTFail("updatedDownloads callback should not be called when cache is empty."); - } - transactionCache:mockCache]; - - [handler startObservingPaymentQueue]; - - OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); - OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]); - OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]); -} - -- (void) - testStartObservingPaymentQueueShouldNotProcessTransactionsWhenCacheContainsEmptyTransactionArrays { - FIATransactionCache *mockCache = OCMClassMock(FIATransactionCache.class); - FIAPaymentQueueHandler *handler = - [[FIAPaymentQueueHandler alloc] initWithQueue:[[SKPaymentQueueStub alloc] init] - transactionsUpdated:^(NSArray *_Nonnull transactions) { - XCTFail("transactionsUpdated callback should not be called when cache is empty."); - } - transactionRemoved:^(NSArray *_Nonnull transactions) { - XCTFail("transactionRemoved callback should not be called when cache is empty."); - } - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:^(NSArray *_Nonnull downloads) { - XCTFail("updatedDownloads callback should not be called when cache is empty."); - } - transactionCache:mockCache]; - - OCMStub([mockCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]).andReturn(@[]); - OCMStub([mockCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]).andReturn(@[]); - OCMStub([mockCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]).andReturn(@[]); - - [handler startObservingPaymentQueue]; - - OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); - OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]); - OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]); -} - -- (void)testStartObservingPaymentQueueShouldProcessTransactionsForItemsInCache { - XCTestExpectation *updateTransactionsExpectation = - [self expectationWithDescription: - @"transactionsUpdated callback should be called with one transaction."]; - XCTestExpectation *removeTransactionsExpectation = - [self expectationWithDescription: - @"transactionsRemoved callback should be called with one transaction."]; - XCTestExpectation *updateDownloadsExpectation = - [self expectationWithDescription: - @"downloadsUpdated callback should be called with one transaction."]; - SKPaymentTransaction *mockTransaction = OCMClassMock(SKPaymentTransaction.class); - SKDownload *mockDownload = OCMClassMock(SKDownload.class); - FIATransactionCache *mockCache = OCMClassMock(FIATransactionCache.class); - FIAPaymentQueueHandler *handler = - [[FIAPaymentQueueHandler alloc] initWithQueue:[[SKPaymentQueueStub alloc] init] - transactionsUpdated:^(NSArray *_Nonnull transactions) { - XCTAssertEqualObjects(transactions, @[ mockTransaction ]); - [updateTransactionsExpectation fulfill]; - } - transactionRemoved:^(NSArray *_Nonnull transactions) { - XCTAssertEqualObjects(transactions, @[ mockTransaction ]); - [removeTransactionsExpectation fulfill]; - } - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:^(NSArray *_Nonnull downloads) { - XCTAssertEqualObjects(downloads, @[ mockDownload ]); - [updateDownloadsExpectation fulfill]; - } - transactionCache:mockCache]; - - OCMStub([mockCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]).andReturn(@[ - mockTransaction - ]); - OCMStub([mockCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]).andReturn(@[ - mockDownload - ]); - OCMStub([mockCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]).andReturn(@[ - mockTransaction - ]); - - [handler startObservingPaymentQueue]; - - [self waitForExpectations:@[ - updateTransactionsExpectation, removeTransactionsExpectation, updateDownloadsExpectation - ] - timeout:5]; - OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); - OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]); - OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]); - OCMVerify(times(1), [mockCache clear]); -} - -- (void)testTransactionsShouldBeCachedWhenNotObserving { - SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; - FIATransactionCache *mockCache = OCMClassMock(FIATransactionCache.class); - FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - XCTFail("transactionsUpdated callback should not be called when cache is empty."); - } - transactionRemoved:^(NSArray *_Nonnull transactions) { - XCTFail("transactionRemoved callback should not be called when cache is empty."); - } - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:^(NSArray *_Nonnull downloads) { - XCTFail("updatedDownloads callback should not be called when cache is empty."); - } - transactionCache:mockCache]; - - SKPayment *payment = - [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; - [handler addPayment:payment]; - - OCMVerify(times(1), [mockCache addObjects:[OCMArg any] - forKey:TransactionCacheKeyUpdatedTransactions]); - OCMVerify(never(), [mockCache addObjects:[OCMArg any] - forKey:TransactionCacheKeyUpdatedDownloads]); - OCMVerify(never(), [mockCache addObjects:[OCMArg any] - forKey:TransactionCacheKeyRemovedTransactions]); -} - -- (void)testTransactionsShouldNotBeCachedWhenObserving { - XCTestExpectation *updateTransactionsExpectation = - [self expectationWithDescription: - @"transactionsUpdated callback should be called with one transaction."]; - XCTestExpectation *removeTransactionsExpectation = - [self expectationWithDescription: - @"transactionsRemoved callback should be called with one transaction."]; - XCTestExpectation *updateDownloadsExpectation = - [self expectationWithDescription: - @"downloadsUpdated callback should be called with one transaction."]; - SKPaymentTransaction *mockTransaction = OCMClassMock(SKPaymentTransaction.class); - SKDownload *mockDownload = OCMClassMock(SKDownload.class); - SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; - queue.testState = SKPaymentTransactionStatePurchased; - FIATransactionCache *mockCache = OCMClassMock(FIATransactionCache.class); - FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - XCTAssertEqualObjects(transactions, @[ mockTransaction ]); - [updateTransactionsExpectation fulfill]; - } - transactionRemoved:^(NSArray *_Nonnull transactions) { - XCTAssertEqualObjects(transactions, @[ mockTransaction ]); - [removeTransactionsExpectation fulfill]; - } - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { - return YES; - } - updatedDownloads:^(NSArray *_Nonnull downloads) { - XCTAssertEqualObjects(downloads, @[ mockDownload ]); - [updateDownloadsExpectation fulfill]; - } - transactionCache:mockCache]; - - [handler startObservingPaymentQueue]; - [handler paymentQueue:queue updatedTransactions:@[ mockTransaction ]]; - [handler paymentQueue:queue removedTransactions:@[ mockTransaction ]]; - [handler paymentQueue:queue updatedDownloads:@[ mockDownload ]]; - - [self waitForExpectations:@[ - updateTransactionsExpectation, removeTransactionsExpectation, updateDownloadsExpectation - ] - timeout:5]; - OCMVerify(never(), [mockCache addObjects:[OCMArg any] - forKey:TransactionCacheKeyUpdatedTransactions]); - OCMVerify(never(), [mockCache addObjects:[OCMArg any] - forKey:TransactionCacheKeyUpdatedDownloads]); - OCMVerify(never(), [mockCache addObjects:[OCMArg any] - forKey:TransactionCacheKeyRemovedTransactions]); -} -@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/PaymentQueueTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/PaymentQueueTests.m new file mode 120000 index 000000000000..f207cda68945 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/PaymentQueueTests.m @@ -0,0 +1 @@ +../../shared/RunnerTests/PaymentQueueTests.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/ProductRequestHandlerTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/ProductRequestHandlerTests.m deleted file mode 100644 index ac36aae5acb5..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/ProductRequestHandlerTests.m +++ /dev/null @@ -1,89 +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 -#import "Stubs.h" - -@import in_app_purchase_storekit; - -#pragma tests start here - -@interface RequestHandlerTest : XCTestCase - -@end - -@implementation RequestHandlerTest - -- (void)testRequestHandlerWithProductRequestSuccess { - SKProductRequestStub *request = - [[SKProductRequestStub alloc] initWithProductIdentifiers:[NSSet setWithArray:@[ @"123" ]]]; - FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; - XCTestExpectation *expectation = - [self expectationWithDescription:@"expect to get response with 1 product"]; - __block SKProductsResponse *response; - [handler - startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *error) { - response = r; - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertNotNil(response); - XCTAssertEqual(response.products.count, 1); - SKProduct *product = response.products.firstObject; - XCTAssertTrue([product.productIdentifier isEqualToString:@"123"]); -} - -- (void)testRequestHandlerWithProductRequestFailure { - SKProductRequestStub *request = [[SKProductRequestStub alloc] - initWithFailureError:[NSError errorWithDomain:@"test" code:123 userInfo:@{}]]; - FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; - XCTestExpectation *expectation = - [self expectationWithDescription:@"expect to get response with 1 product"]; - __block NSError *error; - __block SKProductsResponse *response; - [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *e) { - error = e; - response = r; - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertNotNil(error); - XCTAssertEqual(error.domain, @"test"); - XCTAssertNil(response); -} - -- (void)testRequestHandlerWithRefreshReceiptSuccess { - SKReceiptRefreshRequestStub *request = - [[SKReceiptRefreshRequestStub alloc] initWithReceiptProperties:nil]; - FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; - XCTestExpectation *expectation = [self expectationWithDescription:@"expect no error"]; - __block NSError *e; - [handler - startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *error) { - e = error; - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertNil(e); -} - -- (void)testRequestHandlerWithRefreshReceiptFailure { - SKReceiptRefreshRequestStub *request = [[SKReceiptRefreshRequestStub alloc] - initWithFailureError:[NSError errorWithDomain:@"test" code:123 userInfo:@{}]]; - FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; - XCTestExpectation *expectation = [self expectationWithDescription:@"expect error"]; - __block NSError *error; - __block SKProductsResponse *response; - [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *e) { - error = e; - response = r; - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertNotNil(error); - XCTAssertEqual(error.domain, @"test"); - XCTAssertNil(response); -} - -@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/ProductRequestHandlerTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/ProductRequestHandlerTests.m new file mode 120000 index 000000000000..f186e1122526 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/ProductRequestHandlerTests.m @@ -0,0 +1 @@ +../../shared/RunnerTests/ProductRequestHandlerTests.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.h b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.h deleted file mode 100644 index d4e8df3eba72..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.h +++ /dev/null @@ -1,71 +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 -#import - -@import in_app_purchase_storekit; - -NS_ASSUME_NONNULL_BEGIN -API_AVAILABLE(ios(11.2), macos(10.13.2)) -@interface SKProductSubscriptionPeriodStub : SKProductSubscriptionPeriod -- (instancetype)initWithMap:(NSDictionary *)map; -@end - -API_AVAILABLE(ios(11.2), macos(10.13.2)) -@interface SKProductDiscountStub : SKProductDiscount -- (instancetype)initWithMap:(NSDictionary *)map; -@end - -@interface SKProductStub : SKProduct -- (instancetype)initWithMap:(NSDictionary *)map; -@end - -@interface SKProductRequestStub : SKProductsRequest -- (instancetype)initWithProductIdentifiers:(NSSet *)productIdentifiers; -- (instancetype)initWithFailureError:(NSError *)error; -@end - -@interface SKProductsResponseStub : SKProductsResponse -- (instancetype)initWithMap:(NSDictionary *)map; -@end - -@interface InAppPurchasePluginStub : InAppPurchasePlugin -@end - -@interface SKPaymentQueueStub : SKPaymentQueue -@property(assign, nonatomic) SKPaymentTransactionState testState; -@property(strong, nonatomic, nullable) id observer; -@end - -@interface SKPaymentTransactionStub : SKPaymentTransaction -- (instancetype)initWithMap:(NSDictionary *)map; -- (instancetype)initWithState:(SKPaymentTransactionState)state; -- (instancetype)initWithState:(SKPaymentTransactionState)state payment:(SKPayment *)payment; -@end - -@interface SKMutablePaymentStub : SKMutablePayment -- (instancetype)initWithMap:(NSDictionary *)map; -@end - -@interface NSErrorStub : NSError -- (instancetype)initWithMap:(NSDictionary *)map; -@end - -@interface FIAPReceiptManagerStub : FIAPReceiptManager -// Indicates whether getReceiptData of this stub is going to return an error. -// Setting this to true will let getReceiptData give a basic NSError and return nil. -@property(assign, nonatomic) BOOL returnError; -@end - -@interface SKReceiptRefreshRequestStub : SKReceiptRefreshRequest -- (instancetype)initWithFailureError:(NSError *)error; -@end - -API_AVAILABLE(ios(13.0), macos(10.15)) -@interface SKStorefrontStub : SKStorefront -- (instancetype)initWithMap:(NSDictionary *)map; -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.h b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.h new file mode 120000 index 000000000000..420bd56538d1 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.h @@ -0,0 +1 @@ +../../shared/RunnerTests/Stubs.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.m deleted file mode 100644 index f5e44d78b157..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.m +++ /dev/null @@ -1,330 +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 "Stubs.h" - -@implementation SKProductSubscriptionPeriodStub - -- (instancetype)initWithMap:(NSDictionary *)map { - self = [super init]; - if (self) { - [self setValue:map[@"numberOfUnits"] ?: @(0) forKey:@"numberOfUnits"]; - [self setValue:map[@"unit"] ?: @(0) forKey:@"unit"]; - } - return self; -} - -@end - -@implementation SKProductDiscountStub - -- (instancetype)initWithMap:(NSDictionary *)map { - self = [super init]; - if (self) { - [self setValue:[[NSDecimalNumber alloc] initWithString:map[@"price"]] ?: [NSNull null] - forKey:@"price"]; - NSLocale *locale = NSLocale.systemLocale; - [self setValue:locale ?: [NSNull null] forKey:@"priceLocale"]; - [self setValue:map[@"numberOfPeriods"] ?: @(0) forKey:@"numberOfPeriods"]; - SKProductSubscriptionPeriodStub *subscriptionPeriodSub = - [[SKProductSubscriptionPeriodStub alloc] initWithMap:map[@"subscriptionPeriod"]]; - [self setValue:subscriptionPeriodSub forKey:@"subscriptionPeriod"]; - [self setValue:map[@"paymentMode"] ?: @(0) forKey:@"paymentMode"]; - if (@available(iOS 12.2, *)) { - [self setValue:map[@"identifier"] ?: [NSNull null] forKey:@"identifier"]; - [self setValue:map[@"type"] ?: @(0) forKey:@"type"]; - } - } - return self; -} - -@end - -@implementation SKProductStub - -- (instancetype)initWithMap:(NSDictionary *)map { - self = [super init]; - if (self) { - [self setValue:map[@"productIdentifier"] ?: [NSNull null] forKey:@"productIdentifier"]; - [self setValue:map[@"localizedDescription"] ?: [NSNull null] forKey:@"localizedDescription"]; - [self setValue:map[@"localizedTitle"] ?: [NSNull null] forKey:@"localizedTitle"]; - [self setValue:map[@"downloadable"] ?: @NO forKey:@"downloadable"]; - [self setValue:[[NSDecimalNumber alloc] initWithString:map[@"price"]] ?: [NSNull null] - forKey:@"price"]; - NSLocale *locale = NSLocale.systemLocale; - [self setValue:locale ?: [NSNull null] forKey:@"priceLocale"]; - [self setValue:map[@"downloadContentLengths"] ?: @(0) forKey:@"downloadContentLengths"]; - if (@available(iOS 11.2, *)) { - SKProductSubscriptionPeriodStub *period = - [[SKProductSubscriptionPeriodStub alloc] initWithMap:map[@"subscriptionPeriod"]]; - [self setValue:period ?: [NSNull null] forKey:@"subscriptionPeriod"]; - SKProductDiscountStub *discount = - [[SKProductDiscountStub alloc] initWithMap:map[@"introductoryPrice"]]; - [self setValue:discount ?: [NSNull null] forKey:@"introductoryPrice"]; - [self setValue:map[@"subscriptionGroupIdentifier"] ?: [NSNull null] - forKey:@"subscriptionGroupIdentifier"]; - } - if (@available(iOS 12.2, *)) { - NSMutableArray *discounts = [[NSMutableArray alloc] init]; - for (NSDictionary *discountMap in map[@"discounts"]) { - [discounts addObject:[[SKProductDiscountStub alloc] initWithMap:discountMap]]; - } - - [self setValue:discounts forKey:@"discounts"]; - } - } - return self; -} - -- (instancetype)initWithProductID:(NSString *)productIdentifier { - self = [super init]; - if (self) { - [self setValue:productIdentifier forKey:@"productIdentifier"]; - } - return self; -} - -@end - -@interface SKProductRequestStub () - -@property(strong, nonatomic) NSSet *identifers; -@property(strong, nonatomic) NSError *error; - -@end - -@implementation SKProductRequestStub - -- (instancetype)initWithProductIdentifiers:(NSSet *)productIdentifiers { - self = [super initWithProductIdentifiers:productIdentifiers]; - self.identifers = productIdentifiers; - return self; -} - -- (instancetype)initWithFailureError:(NSError *)error { - self = [super init]; - self.error = error; - return self; -} - -- (void)start { - NSMutableArray *productArray = [NSMutableArray new]; - for (NSString *identifier in self.identifers) { - [productArray addObject:@{@"productIdentifier" : identifier}]; - } - SKProductsResponseStub *response = - [[SKProductsResponseStub alloc] initWithMap:@{@"products" : productArray}]; - if (self.error) { - [self.delegate request:self didFailWithError:self.error]; - } else { - [self.delegate productsRequest:self didReceiveResponse:response]; - } -} - -@end - -@implementation SKProductsResponseStub - -- (instancetype)initWithMap:(NSDictionary *)map { - self = [super init]; - if (self) { - NSMutableArray *products = [NSMutableArray new]; - for (NSDictionary *productMap in map[@"products"]) { - SKProductStub *product = [[SKProductStub alloc] initWithMap:productMap]; - [products addObject:product]; - } - [self setValue:products forKey:@"products"]; - } - return self; -} - -@end - -@interface InAppPurchasePluginStub () - -@end - -@implementation InAppPurchasePluginStub - -- (SKProductRequestStub *)getProductRequestWithIdentifiers:(NSSet *)identifiers { - return [[SKProductRequestStub alloc] initWithProductIdentifiers:identifiers]; -} - -- (SKProduct *)getProduct:(NSString *)productID { - return [[SKProductStub alloc] initWithProductID:productID]; -} - -- (SKReceiptRefreshRequestStub *)getRefreshReceiptRequest:(NSDictionary *)properties { - return [[SKReceiptRefreshRequestStub alloc] initWithReceiptProperties:properties]; -} - -@end - -@interface SKPaymentQueueStub () - -@end - -@implementation SKPaymentQueueStub - -- (void)addTransactionObserver:(id)observer { - self.observer = observer; -} - -- (void)removeTransactionObserver:(id)observer { - self.observer = nil; -} - -- (void)addPayment:(SKPayment *)payment { - SKPaymentTransactionStub *transaction = - [[SKPaymentTransactionStub alloc] initWithState:self.testState payment:payment]; - [self.observer paymentQueue:self updatedTransactions:@[ transaction ]]; -} - -- (void)restoreCompletedTransactions { - if ([self.observer - respondsToSelector:@selector(paymentQueueRestoreCompletedTransactionsFinished:)]) { - [self.observer paymentQueueRestoreCompletedTransactionsFinished:self]; - } -} - -- (void)finishTransaction:(SKPaymentTransaction *)transaction { - if ([self.observer respondsToSelector:@selector(paymentQueue:removedTransactions:)]) { - [self.observer paymentQueue:self removedTransactions:@[ transaction ]]; - } -} - -@end - -@implementation SKPaymentTransactionStub { - SKPayment *_payment; -} - -- (instancetype)initWithID:(NSString *)identifier { - self = [super init]; - if (self) { - [self setValue:identifier forKey:@"transactionIdentifier"]; - } - return self; -} - -- (instancetype)initWithMap:(NSDictionary *)map { - self = [super init]; - if (self) { - [self setValue:map[@"transactionIdentifier"] forKey:@"transactionIdentifier"]; - [self setValue:map[@"transactionState"] forKey:@"transactionState"]; - if (![map[@"originalTransaction"] isKindOfClass:[NSNull class]] && - map[@"originalTransaction"]) { - [self setValue:[[SKPaymentTransactionStub alloc] initWithMap:map[@"originalTransaction"]] - forKey:@"originalTransaction"]; - } - [self setValue:map[@"error"] ? [[NSErrorStub alloc] initWithMap:map[@"error"]] : [NSNull null] - forKey:@"error"]; - [self setValue:[NSDate dateWithTimeIntervalSince1970:[map[@"transactionTimeStamp"] doubleValue]] - forKey:@"transactionDate"]; - } - return self; -} - -- (instancetype)initWithState:(SKPaymentTransactionState)state { - self = [super init]; - if (self) { - // Only purchased and restored transactions have transactionIdentifier: - // https://developer.apple.com/documentation/storekit/skpaymenttransaction/1411288-transactionidentifier?language=objc - if (state == SKPaymentTransactionStatePurchased || state == SKPaymentTransactionStateRestored) { - [self setValue:@"fakeID" forKey:@"transactionIdentifier"]; - } - [self setValue:@(state) forKey:@"transactionState"]; - } - return self; -} - -- (instancetype)initWithState:(SKPaymentTransactionState)state payment:(SKPayment *)payment { - self = [super init]; - if (self) { - // Only purchased and restored transactions have transactionIdentifier: - // https://developer.apple.com/documentation/storekit/skpaymenttransaction/1411288-transactionidentifier?language=objc - if (state == SKPaymentTransactionStatePurchased || state == SKPaymentTransactionStateRestored) { - [self setValue:@"fakeID" forKey:@"transactionIdentifier"]; - } - [self setValue:@(state) forKey:@"transactionState"]; - _payment = payment; - } - return self; -} - -- (SKPayment *)payment { - return _payment; -} - -@end - -@implementation NSErrorStub - -- (instancetype)initWithMap:(NSDictionary *)map { - return [self initWithDomain:[map objectForKey:@"domain"] - code:[[map objectForKey:@"code"] integerValue] - userInfo:[map objectForKey:@"userInfo"]]; -} - -@end - -@implementation FIAPReceiptManagerStub : FIAPReceiptManager - -- (NSData *)getReceiptData:(NSURL *)url error:(NSError **)error { - if (self.returnError) { - *error = [NSError errorWithDomain:@"test" - code:1 - userInfo:@{ - @"name" : @"test", - @"houseNr" : @5, - @"error" : [[NSError alloc] initWithDomain:@"internalTestDomain" - code:99 - userInfo:nil] - }]; - return nil; - } - NSString *originalString = [NSString stringWithFormat:@"test"]; - return [[NSData alloc] initWithBase64EncodedString:originalString options:kNilOptions]; -} - -@end - -@implementation SKReceiptRefreshRequestStub { - NSError *_error; -} - -- (instancetype)initWithReceiptProperties:(NSDictionary *)properties { - self = [super initWithReceiptProperties:properties]; - return self; -} - -- (instancetype)initWithFailureError:(NSError *)error { - self = [super init]; - _error = error; - return self; -} - -- (void)start { - if (_error) { - [self.delegate request:self didFailWithError:_error]; - } else { - [self.delegate requestDidFinish:self]; - } -} - -@end - -@implementation SKStorefrontStub - -- (instancetype)initWithMap:(NSDictionary *)map { - self = [super init]; - if (self) { - // Set stub values - [self setValue:map[@"countryCode"] forKey:@"countryCode"]; - [self setValue:map[@"identifier"] forKey:@"identifier"]; - } - return self; -} - -@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.m new file mode 120000 index 000000000000..eee9d6b331a9 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/Stubs.m @@ -0,0 +1 @@ +../../shared/RunnerTests/Stubs.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m deleted file mode 100644 index 34d686753762..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m +++ /dev/null @@ -1,416 +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 -#import "Stubs.h" - -@import in_app_purchase_storekit; - -@interface TranslatorTest : XCTestCase - -@property(strong, nonatomic) NSDictionary *periodMap; -@property(strong, nonatomic) NSMutableDictionary *discountMap; -@property(strong, nonatomic) NSMutableDictionary *discountMissingIdentifierMap; -@property(strong, nonatomic) NSMutableDictionary *productMap; -@property(strong, nonatomic) NSDictionary *productResponseMap; -@property(strong, nonatomic) NSDictionary *paymentMap; -@property(copy, nonatomic) NSDictionary *paymentDiscountMap; -@property(strong, nonatomic) NSDictionary *transactionMap; -@property(strong, nonatomic) NSDictionary *errorMap; -@property(strong, nonatomic) NSDictionary *localeMap; -@property(strong, nonatomic) NSDictionary *storefrontMap; -@property(strong, nonatomic) NSDictionary *storefrontAndPaymentTransactionMap; - -@end - -@implementation TranslatorTest - -- (void)setUp { - self.periodMap = @{@"numberOfUnits" : @(0), @"unit" : @(0)}; - - self.discountMap = [[NSMutableDictionary alloc] initWithDictionary:@{ - @"price" : @"1", - @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], - @"numberOfPeriods" : @1, - @"subscriptionPeriod" : self.periodMap, - @"paymentMode" : @1, - }]; - if (@available(iOS 12.2, *)) { - self.discountMap[@"identifier"] = @"test offer id"; - self.discountMap[@"type"] = @(SKProductDiscountTypeIntroductory); - } - self.discountMissingIdentifierMap = [[NSMutableDictionary alloc] initWithDictionary:@{ - @"price" : @"1", - @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], - @"numberOfPeriods" : @1, - @"subscriptionPeriod" : self.periodMap, - @"paymentMode" : @1, - @"identifier" : [NSNull null], - @"type" : @0, - }]; - - self.productMap = [[NSMutableDictionary alloc] initWithDictionary:@{ - @"price" : @"1", - @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], - @"productIdentifier" : @"123", - @"localizedTitle" : @"title", - @"localizedDescription" : @"des", - }]; - if (@available(iOS 11.2, *)) { - self.productMap[@"subscriptionPeriod"] = self.periodMap; - self.productMap[@"introductoryPrice"] = self.discountMap; - } - if (@available(iOS 12.2, *)) { - self.productMap[@"discounts"] = @[ self.discountMap ]; - } - - if (@available(iOS 12.0, *)) { - self.productMap[@"subscriptionGroupIdentifier"] = @"com.group"; - } - - self.productResponseMap = - @{@"products" : @[ self.productMap ], @"invalidProductIdentifiers" : @[]}; - self.paymentMap = @{ - @"productIdentifier" : @"123", - @"requestData" : @"abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh", - @"quantity" : @(2), - @"applicationUsername" : @"app user name", - @"simulatesAskToBuyInSandbox" : @(NO) - }; - self.paymentDiscountMap = @{ - @"identifier" : @"payment_discount_identifier", - @"keyIdentifier" : @"payment_discount_key_identifier", - @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", - @"signature" : @"this is a encrypted signature", - @"timestamp" : @([NSDate date].timeIntervalSince1970), - }; - NSDictionary *originalTransactionMap = @{ - @"transactionIdentifier" : @"567", - @"transactionState" : @(SKPaymentTransactionStatePurchasing), - @"payment" : [NSNull null], - @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" - code:123 - userInfo:@{}]], - @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), - @"originalTransaction" : [NSNull null], - }; - self.transactionMap = @{ - @"transactionIdentifier" : @"567", - @"transactionState" : @(SKPaymentTransactionStatePurchasing), - @"payment" : [NSNull null], - @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" - code:123 - userInfo:@{}]], - @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), - @"originalTransaction" : originalTransactionMap, - }; - self.errorMap = @{ - @"code" : @(123), - @"domain" : @"test_domain", - @"userInfo" : @{ - @"key" : @"value", - } - }; - self.storefrontMap = @{ - @"countryCode" : @"USA", - @"identifier" : @"unique_identifier", - }; - - self.storefrontAndPaymentTransactionMap = @{ - @"storefront" : self.storefrontMap, - @"transaction" : self.transactionMap, - }; -} - -- (void)testSKProductSubscriptionPeriodStubToMap { - if (@available(iOS 11.2, *)) { - SKProductSubscriptionPeriodStub *period = - [[SKProductSubscriptionPeriodStub alloc] initWithMap:self.periodMap]; - NSDictionary *map = [FIAObjectTranslator getMapFromSKProductSubscriptionPeriod:period]; - XCTAssertEqualObjects(map, self.periodMap); - } -} - -- (void)testSKProductDiscountStubToMap { - if (@available(iOS 11.2, *)) { - SKProductDiscountStub *discount = [[SKProductDiscountStub alloc] initWithMap:self.discountMap]; - NSDictionary *map = [FIAObjectTranslator getMapFromSKProductDiscount:discount]; - XCTAssertEqualObjects(map, self.discountMap); - } -} - -- (void)testProductToMap { - SKProductStub *product = [[SKProductStub alloc] initWithMap:self.productMap]; - NSDictionary *map = [FIAObjectTranslator getMapFromSKProduct:product]; - XCTAssertEqualObjects(map, self.productMap); -} - -- (void)testProductResponseToMap { - SKProductsResponseStub *response = - [[SKProductsResponseStub alloc] initWithMap:self.productResponseMap]; - NSDictionary *map = [FIAObjectTranslator getMapFromSKProductsResponse:response]; - XCTAssertEqualObjects(map, self.productResponseMap); -} - -- (void)testPaymentToMap { - SKMutablePayment *payment = [FIAObjectTranslator getSKMutablePaymentFromMap:self.paymentMap]; - NSDictionary *map = [FIAObjectTranslator getMapFromSKPayment:payment]; - XCTAssertEqualObjects(map, self.paymentMap); -} - -- (void)testPaymentTransactionToMap { - // payment is not KVC, cannot test payment field. - SKPaymentTransactionStub *paymentTransaction = - [[SKPaymentTransactionStub alloc] initWithMap:self.transactionMap]; - NSDictionary *map = [FIAObjectTranslator getMapFromSKPaymentTransaction:paymentTransaction]; - XCTAssertEqualObjects(map, self.transactionMap); -} - -- (void)testError { - NSErrorStub *error = [[NSErrorStub alloc] initWithMap:self.errorMap]; - NSDictionary *map = [FIAObjectTranslator getMapFromNSError:error]; - XCTAssertEqualObjects(map, self.errorMap); -} - -- (void)testErrorWithNSNumberAsUserInfo { - NSError *error = [NSError errorWithDomain:SKErrorDomain code:3 userInfo:@{@"key" : @42}]; - NSDictionary *expectedMap = - @{@"domain" : SKErrorDomain, @"code" : @3, @"userInfo" : @{@"key" : @42}}; - NSDictionary *map = [FIAObjectTranslator getMapFromNSError:error]; - XCTAssertEqualObjects(expectedMap, map); -} - -- (void)testErrorWithMultipleUnderlyingErrors { - NSError *underlyingErrorOne = [NSError errorWithDomain:SKErrorDomain code:2 userInfo:nil]; - NSError *underlyingErrorTwo = [NSError errorWithDomain:SKErrorDomain code:1 userInfo:nil]; - NSError *mainError = [NSError - errorWithDomain:SKErrorDomain - code:3 - userInfo:@{@"underlyingErrors" : @[ underlyingErrorOne, underlyingErrorTwo ]}]; - NSDictionary *expectedMap = @{ - @"domain" : SKErrorDomain, - @"code" : @3, - @"userInfo" : @{ - @"underlyingErrors" : @[ - @{@"domain" : SKErrorDomain, @"code" : @2, @"userInfo" : @{}}, - @{@"domain" : SKErrorDomain, @"code" : @1, @"userInfo" : @{}} - ] - } - }; - NSDictionary *map = [FIAObjectTranslator getMapFromNSError:mainError]; - XCTAssertEqualObjects(expectedMap, map); -} - -- (void)testErrorWithUnsupportedUserInfo { - NSError *error = [NSError errorWithDomain:SKErrorDomain - code:3 - userInfo:@{@"user_info" : [[NSObject alloc] init]}]; - NSDictionary *expectedMap = @{ - @"domain" : SKErrorDomain, - @"code" : @3, - @"userInfo" : @{ - @"user_info" : [NSString - stringWithFormat: - @"Unable to encode native userInfo object of type %@ to map. Please submit an " - @"issue at https://github.com/flutter/flutter/issues/new with the title " - @"\"[in_app_purchase_storekit] Unable to encode userInfo of type %@\" and add " - @"reproduction steps and the error details in the description field.", - [NSObject class], [NSObject class]] - } - }; - NSDictionary *map = [FIAObjectTranslator getMapFromNSError:error]; - XCTAssertEqualObjects(expectedMap, map); -} - -- (void)testLocaleToMap { - if (@available(iOS 10.0, *)) { - NSLocale *system = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; - NSDictionary *map = [FIAObjectTranslator getMapFromNSLocale:system]; - XCTAssertEqualObjects(map[@"currencySymbol"], system.currencySymbol); - XCTAssertEqualObjects(map[@"countryCode"], system.countryCode); - } -} - -- (void)testSKStorefrontToMap { - if (@available(iOS 13.0, *)) { - SKStorefront *storefront = [[SKStorefrontStub alloc] initWithMap:self.storefrontMap]; - NSDictionary *map = [FIAObjectTranslator getMapFromSKStorefront:storefront]; - XCTAssertEqualObjects(map, self.storefrontMap); - } -} - -- (void)testSKStorefrontAndSKPaymentTransactionToMap { - if (@available(iOS 13.0, *)) { - SKStorefront *storefront = [[SKStorefrontStub alloc] initWithMap:self.storefrontMap]; - SKPaymentTransaction *transaction = - [[SKPaymentTransactionStub alloc] initWithMap:self.transactionMap]; - NSDictionary *map = [FIAObjectTranslator getMapFromSKStorefront:storefront - andSKPaymentTransaction:transaction]; - XCTAssertEqualObjects(map, self.storefrontAndPaymentTransactionMap); - } -} - -- (void)testSKPaymentDiscountFromMap { - if (@available(iOS 12.2, *)) { - NSString *error = nil; - SKPaymentDiscount *paymentDiscount = - [FIAObjectTranslator getSKPaymentDiscountFromMap:self.paymentDiscountMap withError:&error]; - - XCTAssertEqual(paymentDiscount.identifier, self.paymentDiscountMap[@"identifier"]); - XCTAssertEqual(paymentDiscount.keyIdentifier, self.paymentDiscountMap[@"keyIdentifier"]); - XCTAssertEqualObjects(paymentDiscount.nonce, - [[NSUUID alloc] initWithUUIDString:self.paymentDiscountMap[@"nonce"]]); - XCTAssertEqual(paymentDiscount.signature, self.paymentDiscountMap[@"signature"]); - XCTAssertEqual(paymentDiscount.timestamp, self.paymentDiscountMap[@"timestamp"]); - } -} - -- (void)testSKPaymentDiscountFromMapMissingIdentifier { - if (@available(iOS 12.2, *)) { - NSArray *invalidValues = @[ [NSNull null], @(1), @"" ]; - - for (id value in invalidValues) { - NSDictionary *discountMap = @{ - @"identifier" : value, - @"keyIdentifier" : @"payment_discount_key_identifier", - @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", - @"signature" : @"this is a encrypted signature", - @"timestamp" : @([NSDate date].timeIntervalSince1970), - }; - - NSString *error = nil; - [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; - - XCTAssertNotNil(error); - XCTAssertEqualObjects( - error, @"When specifying a payment discount the 'identifier' field is mandatory."); - } - } -} - -- (void)testGetMapFromSKProductDiscountMissingIdentifier { - if (@available(iOS 12.2, *)) { - SKProductDiscountStub *discount = - [[SKProductDiscountStub alloc] initWithMap:self.discountMissingIdentifierMap]; - NSDictionary *map = [FIAObjectTranslator getMapFromSKProductDiscount:discount]; - XCTAssertEqualObjects(map, self.discountMissingIdentifierMap); - } -} - -- (void)testSKPaymentDiscountFromMapMissingKeyIdentifier { - if (@available(iOS 12.2, *)) { - NSArray *invalidValues = @[ [NSNull null], @(1), @"" ]; - - for (id value in invalidValues) { - NSDictionary *discountMap = @{ - @"identifier" : @"payment_discount_identifier", - @"keyIdentifier" : value, - @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", - @"signature" : @"this is a encrypted signature", - @"timestamp" : @([NSDate date].timeIntervalSince1970), - }; - - NSString *error = nil; - [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; - - XCTAssertNotNil(error); - XCTAssertEqualObjects( - error, @"When specifying a payment discount the 'keyIdentifier' field is mandatory."); - } - } -} - -- (void)testSKPaymentDiscountFromMapMissingNonce { - if (@available(iOS 12.2, *)) { - NSArray *invalidValues = @[ [NSNull null], @(1), @"" ]; - - for (id value in invalidValues) { - NSDictionary *discountMap = @{ - @"identifier" : @"payment_discount_identifier", - @"keyIdentifier" : @"payment_discount_key_identifier", - @"nonce" : value, - @"signature" : @"this is a encrypted signature", - @"timestamp" : @([NSDate date].timeIntervalSince1970), - }; - - NSString *error = nil; - [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; - - XCTAssertNotNil(error); - XCTAssertEqualObjects(error, - @"When specifying a payment discount the 'nonce' field is mandatory."); - } - } -} - -- (void)testSKPaymentDiscountFromMapMissingSignature { - if (@available(iOS 12.2, *)) { - NSArray *invalidValues = @[ [NSNull null], @(1), @"" ]; - - for (id value in invalidValues) { - NSDictionary *discountMap = @{ - @"identifier" : @"payment_discount_identifier", - @"keyIdentifier" : @"payment_discount_key_identifier", - @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", - @"signature" : value, - @"timestamp" : @([NSDate date].timeIntervalSince1970), - }; - - NSString *error = nil; - [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; - - XCTAssertNotNil(error); - XCTAssertEqualObjects( - error, @"When specifying a payment discount the 'signature' field is mandatory."); - } - } -} - -- (void)testSKPaymentDiscountFromMapMissingTimestamp { - if (@available(iOS 12.2, *)) { - NSArray *invalidValues = @[ [NSNull null], @"", @(-1) ]; - - for (id value in invalidValues) { - NSDictionary *discountMap = @{ - @"identifier" : @"payment_discount_identifier", - @"keyIdentifier" : @"payment_discount_key_identifier", - @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", - @"signature" : @"this is a encrypted signature", - @"timestamp" : value, - }; - - NSString *error = nil; - [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; - - XCTAssertNotNil(error); - XCTAssertEqualObjects( - error, @"When specifying a payment discount the 'timestamp' field is mandatory."); - } - } -} - -- (void)testSKPaymentDiscountFromMapOverflowingTimestamp { - if (@available(iOS 12.2, *)) { - NSDictionary *discountMap = @{ - @"identifier" : @"payment_discount_identifier", - @"keyIdentifier" : @"payment_discount_key_identifier", - @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", - @"signature" : @"this is a encrypted signature", - @"timestamp" : @1665044583595, // timestamp 2022 Oct - }; - NSString *error = nil; - SKPaymentDiscount *paymentDiscount = - [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; - XCTAssertNil(error); - XCTAssertNotNil(paymentDiscount); - XCTAssertEqual(paymentDiscount.identifier, discountMap[@"identifier"]); - XCTAssertEqual(paymentDiscount.keyIdentifier, discountMap[@"keyIdentifier"]); - XCTAssertEqualObjects(paymentDiscount.nonce, - [[NSUUID alloc] initWithUUIDString:discountMap[@"nonce"]]); - XCTAssertEqual(paymentDiscount.signature, discountMap[@"signature"]); - XCTAssertEqual(paymentDiscount.timestamp, discountMap[@"timestamp"]); - } -} - -@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m new file mode 120000 index 000000000000..ac58ed96972e --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/RunnerTests/TranslatorTests.m @@ -0,0 +1 @@ +../../shared/RunnerTests/TranslatorTests.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/lib/main.dart b/packages/in_app_purchase/in_app_purchase_storekit/example/lib/main.dart index 09058ea2e89a..ce06aa1d1ab6 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/lib/main.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/lib/main.dart @@ -156,6 +156,8 @@ class _MyAppState extends State<_MyApp> { } if (_purchasePending) { stack.add( + // TODO(goderbauer): Make this const when that's available on stable. + // ignore: prefer_const_constructors Stack( children: const [ Opacity( diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000000..4b81f9b2d200 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/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/in_app_purchase/in_app_purchase_storekit/example/macos/Flutter/Flutter-Release.xcconfig b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000000..5caa9d1579e4 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/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/in_app_purchase/in_app_purchase_storekit/example/macos/Podfile b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Podfile new file mode 100644 index 000000000000..04238b6a5f2c --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Podfile @@ -0,0 +1,46 @@ +platform :osx, '10.15' + +# 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__)) + + target 'RunnerTests' do + inherit! :search_paths + + pod 'OCMock', '~> 3.6' + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcodeproj/project.pbxproj b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..7e30d1fa4c1d --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,883 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + 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 */; }; + A2C6CD5797E6A6721FDBCA1C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 36DEEA66738F64D983F76848 /* Pods_Runner.framework */; }; + C51E64432925727D7AC7BBFF /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE8A421F08C80BE6E90142D5 /* Pods_RunnerTests.framework */; }; + F79BDC102905FBE300E3999D /* FIAPPaymentQueueDeleteTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F79BDC0F2905FBE300E3999D /* FIAPPaymentQueueDeleteTests.m */; }; + F79BDC122905FBF700E3999D /* FIATransactionCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F79BDC112905FBF700E3999D /* FIATransactionCacheTests.m */; }; + F79BDC142905FBFE00E3999D /* InAppPurchasePluginTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F79BDC132905FBFE00E3999D /* InAppPurchasePluginTests.m */; }; + F79BDC182905FC1800E3999D /* PaymentQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F79BDC172905FC1800E3999D /* PaymentQueueTests.m */; }; + F79BDC1A2905FC1F00E3999D /* ProductRequestHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F79BDC192905FC1F00E3999D /* ProductRequestHandlerTests.m */; }; + F79BDC1C2905FC3200E3999D /* Stubs.m in Sources */ = {isa = PBXBuildFile; fileRef = F79BDC1B2905FC3200E3999D /* Stubs.m */; }; + F79BDC1E2905FC3900E3999D /* TranslatorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F79BDC1D2905FC3900E3999D /* TranslatorTests.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; + F700DD0628E652A10004836B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; +/* 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 */ + 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 = ""; }; + 36DEEA66738F64D983F76848 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4E423AE82F466005587C3567 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 5E5D46173E3025B0DB32A1BE /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 5EBC5A8BA44B08330BA605AB /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 62F1680C5AE033907C1DF7AB /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.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 = ""; }; + 9A4FEABF1DEF0D106FEB7974 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + B6C8FD76BB3278AA51FED870 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + EE8A421F08C80BE6E90142D5 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F700DD0228E652A10004836B /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F79BDC0F2905FBE300E3999D /* FIAPPaymentQueueDeleteTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIAPPaymentQueueDeleteTests.m; path = ../../shared/RunnerTests/FIAPPaymentQueueDeleteTests.m; sourceTree = ""; }; + F79BDC112905FBF700E3999D /* FIATransactionCacheTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIATransactionCacheTests.m; path = ../../shared/RunnerTests/FIATransactionCacheTests.m; sourceTree = ""; }; + F79BDC132905FBFE00E3999D /* InAppPurchasePluginTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = InAppPurchasePluginTests.m; path = ../../shared/RunnerTests/InAppPurchasePluginTests.m; sourceTree = ""; }; + F79BDC152905FC0500E3999D /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ../../shared/RunnerTests/Info.plist; sourceTree = ""; }; + F79BDC172905FC1800E3999D /* PaymentQueueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = PaymentQueueTests.m; path = ../../shared/RunnerTests/PaymentQueueTests.m; sourceTree = ""; }; + F79BDC192905FC1F00E3999D /* ProductRequestHandlerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ProductRequestHandlerTests.m; path = ../../shared/RunnerTests/ProductRequestHandlerTests.m; sourceTree = ""; }; + F79BDC1B2905FC3200E3999D /* Stubs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Stubs.m; path = ../../shared/RunnerTests/Stubs.m; sourceTree = ""; }; + F79BDC1D2905FC3900E3999D /* TranslatorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TranslatorTests.m; path = ../../shared/RunnerTests/TranslatorTests.m; sourceTree = ""; }; + F79BDC1F2906023C00E3999D /* Stubs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = Stubs.h; path = ../../shared/RunnerTests/Stubs.h; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A2C6CD5797E6A6721FDBCA1C /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F700DCFF28E652A10004836B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C51E64432925727D7AC7BBFF /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 09D47623A8E19B84FF0453EE /* Pods */ = { + isa = PBXGroup; + children = ( + B6C8FD76BB3278AA51FED870 /* Pods-Runner.debug.xcconfig */, + 9A4FEABF1DEF0D106FEB7974 /* Pods-Runner.release.xcconfig */, + 62F1680C5AE033907C1DF7AB /* Pods-Runner.profile.xcconfig */, + 5E5D46173E3025B0DB32A1BE /* Pods-RunnerTests.debug.xcconfig */, + 5EBC5A8BA44B08330BA605AB /* Pods-RunnerTests.release.xcconfig */, + 4E423AE82F466005587C3567 /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + F700DD0328E652A10004836B /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 09D47623A8E19B84FF0453EE /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + F700DD0228E652A10004836B /* RunnerTests.xctest */, + ); + 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 = ( + 36DEEA66738F64D983F76848 /* Pods_Runner.framework */, + EE8A421F08C80BE6E90142D5 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + F700DD0328E652A10004836B /* RunnerTests */ = { + isa = PBXGroup; + children = ( + F79BDC0F2905FBE300E3999D /* FIAPPaymentQueueDeleteTests.m */, + F79BDC132905FBFE00E3999D /* InAppPurchasePluginTests.m */, + F79BDC172905FC1800E3999D /* PaymentQueueTests.m */, + F79BDC1F2906023C00E3999D /* Stubs.h */, + F79BDC152905FC0500E3999D /* Info.plist */, + F79BDC1B2905FC3200E3999D /* Stubs.m */, + F79BDC1D2905FC3900E3999D /* TranslatorTests.m */, + F79BDC192905FC1F00E3999D /* ProductRequestHandlerTests.m */, + F79BDC112905FBF700E3999D /* FIATransactionCacheTests.m */, + ); + path = RunnerTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 377E3E3C5CA24E98C4B6A4BB /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 23A80E9A6DAA80757416464A /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; + F700DD0128E652A10004836B /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F700DD0B28E652A10004836B /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 959FA4942EA5DA018C52D3DA /* [CP] Check Pods Manifest.lock */, + F700DCFE28E652A10004836B /* Sources */, + F700DCFF28E652A10004836B /* Frameworks */, + F700DD0028E652A10004836B /* Resources */, + 1FAA0D39365CA43DED71E657 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + F700DD0728E652A10004836B /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = F700DD0228E652A10004836B /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1400; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + F700DD0128E652A10004836B = { + CreatedOnToolsVersion = 14.0.1; + LastSwiftMigration = 1400; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + }; + }; + 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 */, + F700DD0128E652A10004836B /* RunnerTests */, + ); + }; +/* 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; + }; + F700DD0028E652A10004836B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 1FAA0D39365CA43DED71E657 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-RunnerTests/Pods-RunnerTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-RunnerTests/Pods-RunnerTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-RunnerTests/Pods-RunnerTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 23A80E9A6DAA80757416464A /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + 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"; + }; + 377E3E3C5CA24E98C4B6A4BB /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 959FA4942EA5DA018C52D3DA /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F700DCFE28E652A10004836B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F79BDC1A2905FC1F00E3999D /* ProductRequestHandlerTests.m in Sources */, + F79BDC1E2905FC3900E3999D /* TranslatorTests.m in Sources */, + F79BDC182905FC1800E3999D /* PaymentQueueTests.m in Sources */, + F79BDC1C2905FC3200E3999D /* Stubs.m in Sources */, + F79BDC102905FBE300E3999D /* FIAPPaymentQueueDeleteTests.m in Sources */, + F79BDC142905FBFE00E3999D /* InAppPurchasePluginTests.m in Sources */, + F79BDC122905FBF700E3999D /* FIATransactionCacheTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; + F700DD0728E652A10004836B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = F700DD0628E652A10004836B /* 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.15; + 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", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + 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.15; + 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.15; + 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", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + 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", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + 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; + }; + F700DD0828E652A10004836B /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5E5D46173E3025B0DB32A1BE /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS -Xcc -fmodule-map-file=\"${PODS_ROOT}/Headers/Public/in_app_purchase_storekit/in_app_purchase_storekit.modulemap\" -Xcc -fmodule-map-file=\"${PODS_ROOT}/Headers/Public/integration_test/integration_test.modulemap\" -Xcc -fmodule-map-file=\"${PODS_ROOT}/Headers/Public/shared_preferences_macos/shared_preferences_macos.modulemap\""; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Debug; + }; + F700DD0928E652A10004836B /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5EBC5A8BA44B08330BA605AB /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS -Xcc -fmodule-map-file=\"${PODS_ROOT}/Headers/Public/in_app_purchase_storekit/in_app_purchase_storekit.modulemap\" -Xcc -fmodule-map-file=\"${PODS_ROOT}/Headers/Public/integration_test/integration_test.modulemap\" -Xcc -fmodule-map-file=\"${PODS_ROOT}/Headers/Public/shared_preferences_macos/shared_preferences_macos.modulemap\""; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Release; + }; + F700DD0A28E652A10004836B /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4E423AE82F466005587C3567 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D COCOAPODS -Xcc -fmodule-map-file=\"${PODS_ROOT}/Headers/Public/in_app_purchase_storekit/in_app_purchase_storekit.modulemap\" -Xcc -fmodule-map-file=\"${PODS_ROOT}/Headers/Public/integration_test/integration_test.modulemap\" -Xcc -fmodule-map-file=\"${PODS_ROOT}/Headers/Public/shared_preferences_macos/shared_preferences_macos.modulemap\""; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Profile; + }; +/* 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; + }; + F700DD0B28E652A10004836B /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F700DD0828E652A10004836B /* Debug */, + F700DD0928E652A10004836B /* Release */, + F700DD0A28E652A10004836B /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..cd370a07dfcb --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata rename to packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/AppDelegate.swift b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/AppDelegate.swift similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner/AppDelegate.swift rename to packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/AppDelegate.swift diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000000..80e867a4e06b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000000..3c916dec7ec9 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/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.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2022 dev.flutter.plugins. All rights reserved. diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Configs/Debug.xcconfig b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000000..3e2524adcdd6 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Configs/Release.xcconfig b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000000..77ac7613be91 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1 @@ +#include "../../Flutter/Flutter-Release.xcconfig" diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/DebugProfile.entitlements b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000000..dddb8a30c851 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/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/shared_preferences/shared_preferences_macos/example/macos/Runner/Info.plist b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Info.plist similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Info.plist rename to packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Info.plist diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/MainFlutterWindow.swift b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/MainFlutterWindow.swift similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner/MainFlutterWindow.swift rename to packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/MainFlutterWindow.swift diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Release.entitlements b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Release.entitlements new file mode 100644 index 000000000000..852fa1a4728a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/FIAPPaymentQueueDeleteTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/FIAPPaymentQueueDeleteTests.m new file mode 120000 index 000000000000..7c8e7691c6d4 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/FIAPPaymentQueueDeleteTests.m @@ -0,0 +1 @@ +../../shared/RunnerTests/FIAPPaymentQueueDeleteTests.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/FIATransactionCacheTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/FIATransactionCacheTests.m new file mode 120000 index 000000000000..5c7c87fd1aea --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/FIATransactionCacheTests.m @@ -0,0 +1 @@ +../../shared/RunnerTests/FIATransactionCacheTests.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/InAppPurchasePluginTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/InAppPurchasePluginTests.m new file mode 120000 index 000000000000..495146dde20b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/InAppPurchasePluginTests.m @@ -0,0 +1 @@ +../../shared/RunnerTests/InAppPurchasePluginTests.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/Info.plist b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/Info.plist new file mode 120000 index 000000000000..55acf210929a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/Info.plist @@ -0,0 +1 @@ +../../shared/RunnerTests/Info.plist \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/PaymentQueueTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/PaymentQueueTests.m new file mode 120000 index 000000000000..f207cda68945 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/PaymentQueueTests.m @@ -0,0 +1 @@ +../../shared/RunnerTests/PaymentQueueTests.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/ProductRequestHandlerTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/ProductRequestHandlerTests.m new file mode 120000 index 000000000000..f186e1122526 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/ProductRequestHandlerTests.m @@ -0,0 +1 @@ +../../shared/RunnerTests/ProductRequestHandlerTests.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/Stubs.h b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/Stubs.h new file mode 120000 index 000000000000..420bd56538d1 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/Stubs.h @@ -0,0 +1 @@ +../../shared/RunnerTests/Stubs.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/Stubs.m b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/Stubs.m new file mode 120000 index 000000000000..eee9d6b331a9 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/Stubs.m @@ -0,0 +1 @@ +../../shared/RunnerTests/Stubs.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/TranslatorTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/TranslatorTests.m new file mode 120000 index 000000000000..ac58ed96972e --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/macos/RunnerTests/TranslatorTests.m @@ -0,0 +1 @@ +../../shared/RunnerTests/TranslatorTests.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_storekit/example/pubspec.yaml index e71b85d4b447..b06dd6a9a594 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/FIAPPaymentQueueDeleteTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/FIAPPaymentQueueDeleteTests.m new file mode 100644 index 000000000000..187cc6e37bf6 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/FIAPPaymentQueueDeleteTests.m @@ -0,0 +1,125 @@ +// 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 +#import +#import "FIAObjectTranslator.h" +#import "FIAPaymentQueueHandler.h" +#import "Stubs.h" + +@import in_app_purchase_storekit; + +API_AVAILABLE(ios(13.0)) +API_UNAVAILABLE(tvos, macos, watchos) +@interface FIAPPaymentQueueDelegateTests : XCTestCase + +@property(strong, nonatomic) FlutterMethodChannel *channel; +@property(strong, nonatomic) SKPaymentTransaction *transaction; +@property(strong, nonatomic) SKStorefront *storefront; + +@end + +@implementation FIAPPaymentQueueDelegateTests + +- (void)setUp { + self.channel = OCMClassMock(FlutterMethodChannel.class); + + NSDictionary *transactionMap = @{ + @"transactionIdentifier" : [NSNull null], + @"transactionState" : @(SKPaymentTransactionStatePurchasing), + @"payment" : [NSNull null], + @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" + code:123 + userInfo:@{}]], + @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), + @"originalTransaction" : [NSNull null], + }; + self.transaction = [[SKPaymentTransactionStub alloc] initWithMap:transactionMap]; + + NSDictionary *storefrontMap = @{ + @"countryCode" : @"USA", + @"identifier" : @"unique_identifier", + }; + self.storefront = [[SKStorefrontStub alloc] initWithMap:storefrontMap]; +} + +- (void)tearDown { + self.channel = nil; +} + +- (void)testShouldContinueTransaction { + if (@available(iOS 13.0, *)) { + FIAPPaymentQueueDelegate *delegate = + [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; + + OCMStub([self.channel + invokeMethod:@"shouldContinueTransaction" + arguments:[FIAObjectTranslator getMapFromSKStorefront:self.storefront + andSKPaymentTransaction:self.transaction] + result:([OCMArg invokeBlockWithArgs:[NSNumber numberWithBool:NO], nil])]); + + BOOL shouldContinue = [delegate paymentQueue:OCMClassMock(SKPaymentQueue.class) + shouldContinueTransaction:self.transaction + inStorefront:self.storefront]; + + XCTAssertFalse(shouldContinue); + } +} + +- (void)testShouldContinueTransaction_should_default_to_yes { + if (@available(iOS 13.0, *)) { + FIAPPaymentQueueDelegate *delegate = + [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; + + OCMStub([self.channel invokeMethod:@"shouldContinueTransaction" + arguments:[FIAObjectTranslator getMapFromSKStorefront:self.storefront + andSKPaymentTransaction:self.transaction] + result:[OCMArg any]]); + + BOOL shouldContinue = [delegate paymentQueue:OCMClassMock(SKPaymentQueue.class) + shouldContinueTransaction:self.transaction + inStorefront:self.storefront]; + + XCTAssertTrue(shouldContinue); + } +} + +#if TARGET_OS_IOS +- (void)testShouldShowPriceConsentIfNeeded { + if (@available(iOS 13.4, *)) { + FIAPPaymentQueueDelegate *delegate = + [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; + + OCMStub([self.channel + invokeMethod:@"shouldShowPriceConsent" + arguments:nil + result:([OCMArg invokeBlockWithArgs:[NSNumber numberWithBool:NO], nil])]); + + BOOL shouldShow = + [delegate paymentQueueShouldShowPriceConsent:OCMClassMock(SKPaymentQueue.class)]; + + XCTAssertFalse(shouldShow); + } +} +#endif + +#if TARGET_OS_IOS +- (void)testShouldShowPriceConsentIfNeeded_should_default_to_yes { + if (@available(iOS 13.4, *)) { + FIAPPaymentQueueDelegate *delegate = + [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; + + OCMStub([self.channel invokeMethod:@"shouldShowPriceConsent" + arguments:nil + result:[OCMArg any]]); + + BOOL shouldShow = + [delegate paymentQueueShouldShowPriceConsent:OCMClassMock(SKPaymentQueue.class)]; + + XCTAssertTrue(shouldShow); + } +} +#endif + +@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/FIATransactionCacheTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/FIATransactionCacheTests.m new file mode 100644 index 000000000000..1ba0aea76e39 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/FIATransactionCacheTests.m @@ -0,0 +1,63 @@ +// 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 + +@import in_app_purchase_storekit; + +@interface FIATransactionCacheTests : XCTestCase + +@end + +@implementation FIATransactionCacheTests + +- (void)testAddObjectsForNewKey { + NSArray *dummyArray = @[ @1, @2, @3 ]; + FIATransactionCache *cache = [[FIATransactionCache alloc] init]; + [cache addObjects:dummyArray forKey:TransactionCacheKeyUpdatedTransactions]; + + XCTAssertEqual(dummyArray, [cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); +} + +- (void)testAddObjectsForExistingKey { + NSArray *dummyArray = @[ @1, @2, @3 ]; + FIATransactionCache *cache = [[FIATransactionCache alloc] init]; + [cache addObjects:dummyArray forKey:TransactionCacheKeyUpdatedTransactions]; + + XCTAssertEqual(dummyArray, [cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); + + [cache addObjects:@[ @4, @5, @6 ] forKey:TransactionCacheKeyUpdatedTransactions]; + + NSArray *expected = @[ @1, @2, @3, @4, @5, @6 ]; + XCTAssertEqualObjects(expected, [cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); +} + +- (void)testGetObjectsForNonExistingKey { + FIATransactionCache *cache = [[FIATransactionCache alloc] init]; + XCTAssertNil([cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); +} + +- (void)testClear { + NSArray *fakeUpdatedTransactions = @[ @1, @2, @3 ]; + NSArray *fakeRemovedTransactions = @[ @"Remove 1", @"Remove 2", @"Remove 3" ]; + NSArray *fakeUpdatedDownloads = @[ @"Download 1", @"Download 2" ]; + FIATransactionCache *cache = [[FIATransactionCache alloc] init]; + [cache addObjects:fakeUpdatedTransactions forKey:TransactionCacheKeyUpdatedTransactions]; + [cache addObjects:fakeRemovedTransactions forKey:TransactionCacheKeyRemovedTransactions]; + [cache addObjects:fakeUpdatedDownloads forKey:TransactionCacheKeyUpdatedDownloads]; + + XCTAssertEqual(fakeUpdatedTransactions, + [cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); + XCTAssertEqual(fakeRemovedTransactions, + [cache getObjectsForKey:TransactionCacheKeyRemovedTransactions]); + XCTAssertEqual(fakeUpdatedDownloads, + [cache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]); + + [cache clear]; + + XCTAssertNil([cache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); + XCTAssertNil([cache getObjectsForKey:TransactionCacheKeyRemovedTransactions]); + XCTAssertNil([cache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]); +} +@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchasePluginTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchasePluginTests.m new file mode 100644 index 000000000000..f7e6dcdaab16 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchasePluginTests.m @@ -0,0 +1,541 @@ +// 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 +#import +#import "FIAPaymentQueueHandler.h" +#import "Stubs.h" + +@import in_app_purchase_storekit; + +@interface InAppPurchasePluginTest : XCTestCase + +@property(strong, nonatomic) FIAPReceiptManagerStub *receiptManagerStub; +@property(strong, nonatomic) InAppPurchasePlugin *plugin; + +@end + +@implementation InAppPurchasePluginTest + +- (void)setUp { + self.receiptManagerStub = [FIAPReceiptManagerStub new]; + self.plugin = [[InAppPurchasePluginStub alloc] initWithReceiptManager:self.receiptManagerStub]; +} + +- (void)tearDown { +} + +- (void)testInvalidMethodCall { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect result to be not implemented"]; + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"invalid" arguments:NULL]; + __block id result; + [self.plugin handleMethodCall:call + result:^(id r) { + [expectation fulfill]; + result = r; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(result, FlutterMethodNotImplemented); +} + +- (void)testCanMakePayments { + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result to be YES"]; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue canMakePayments:]" + arguments:NULL]; + __block id result; + [self.plugin handleMethodCall:call + result:^(id r) { + [expectation fulfill]; + result = r; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(result, @YES); +} + +- (void)testGetProductResponse { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect response contains 1 item"]; + FlutterMethodCall *call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin startProductRequest:result:]" + arguments:@[ @"123" ]]; + __block id result; + [self.plugin handleMethodCall:call + result:^(id r) { + [expectation fulfill]; + result = r; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssert([result isKindOfClass:[NSDictionary class]]); + NSArray *resultArray = [result objectForKey:@"products"]; + XCTAssertEqual(resultArray.count, 1); + XCTAssertTrue([resultArray.firstObject[@"productIdentifier"] isEqualToString:@"123"]); +} + +- (void)testAddPaymentShouldReturnFlutterErrorWhenArgumentsAreInvalid { + XCTestExpectation *expectation = + [self expectationWithDescription: + @"Result should contain a FlutterError when invalid parameters are passed in."]; + NSString *argument = @"Invalid argument"; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:argument]; + [self.plugin handleMethodCall:call + result:^(id _Nullable result) { + FlutterError *error = result; + XCTAssertEqualObjects(@"storekit_invalid_argument", error.code); + XCTAssertEqualObjects(@"Argument type of addPayment is not a Dictionary", + error.message); + XCTAssertEqualObjects(argument, error.details); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[ expectation ] timeout:5]; +} + +- (void)testAddPaymentShouldReturnFlutterErrorWhenPaymentFails { + NSDictionary *arguments = @{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : @YES, + }; + XCTestExpectation *expectation = + [self expectationWithDescription:@"Result should return failed state."]; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:arguments]; + + FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); + OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(NO); + self.plugin.paymentQueueHandler = mockHandler; + + [self.plugin handleMethodCall:call + result:^(id _Nullable result) { + FlutterError *error = result; + XCTAssertEqualObjects(@"storekit_duplicate_product_object", error.code); + XCTAssertEqualObjects( + @"There is a pending transaction for the same product identifier. " + @"Please either wait for it to be finished or finish it manually " + @"using `completePurchase` to avoid edge cases.", + error.message); + XCTAssertEqualObjects(arguments, error.details); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[ expectation ] timeout:5]; + OCMVerify(times(1), [mockHandler addPayment:[OCMArg any]]); +} + +- (void)testAddPaymentSuccessWithoutPaymentDiscount { + XCTestExpectation *expectation = + [self expectationWithDescription:@"Result should return success state"]; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : @YES, + }]; + FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); + OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(YES); + self.plugin.paymentQueueHandler = mockHandler; + [self.plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertNil(result); + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; +} + +- (void)testAddPaymentSuccessWithPaymentDiscount { + XCTestExpectation *expectation = + [self expectationWithDescription:@"Result should return success state"]; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : @YES, + @"paymentDiscount" : @{ + @"identifier" : @"test_identifier", + @"keyIdentifier" : @"test_key_identifier", + @"nonce" : @"4a11a9cc-3bc3-11ec-8d3d-0242ac130003", + @"signature" : @"test_signature", + @"timestamp" : @(1635847102), + } + }]; + + FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); + OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(YES); + self.plugin.paymentQueueHandler = mockHandler; + [self.plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertNil(result); + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + OCMVerify( + times(1), + [mockHandler + addPayment:[OCMArg checkWithBlock:^BOOL(id obj) { + SKPayment *payment = obj; + if (@available(iOS 12.2, *)) { + SKPaymentDiscount *discount = payment.paymentDiscount; + + return [discount.identifier isEqual:@"test_identifier"] && + [discount.keyIdentifier isEqual:@"test_key_identifier"] && + [discount.nonce + isEqual:[[NSUUID alloc] + initWithUUIDString:@"4a11a9cc-3bc3-11ec-8d3d-0242ac130003"]] && + [discount.signature isEqual:@"test_signature"] && + [discount.timestamp isEqual:@(1635847102)]; + } + + return YES; + }]]); +} + +- (void)testAddPaymentFailureWithInvalidPaymentDiscount { + // Support for payment discount is only available on iOS 12.2 and higher. + if (@available(iOS 12.2, *)) { + XCTestExpectation *expectation = + [self expectationWithDescription:@"Result should return success state"]; + NSDictionary *arguments = @{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : @YES, + @"paymentDiscount" : @{ + @"keyIdentifier" : @"test_key_identifier", + @"nonce" : @"4a11a9cc-3bc3-11ec-8d3d-0242ac130003", + @"signature" : @"test_signature", + @"timestamp" : @(1635847102), + } + }; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:arguments]; + + FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); + id translator = OCMClassMock(FIAObjectTranslator.class); + + NSString *error = @"Some error occurred"; + OCMStub(ClassMethod([translator + getSKPaymentDiscountFromMap:[OCMArg any] + withError:(NSString __autoreleasing **)[OCMArg setTo:error]])) + .andReturn(nil); + self.plugin.paymentQueueHandler = mockHandler; + [self.plugin + handleMethodCall:call + result:^(id _Nullable result) { + FlutterError *error = result; + XCTAssertEqualObjects(@"storekit_invalid_payment_discount_object", error.code); + XCTAssertEqualObjects( + @"You have requested a payment and specified a " + @"payment discount with invalid properties. Some error occurred", + error.message); + XCTAssertEqualObjects(arguments, error.details); + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + OCMVerify(never(), [mockHandler addPayment:[OCMArg any]]); + } +} + +- (void)testAddPaymentWithNullSandboxArgument { + XCTestExpectation *expectation = + [self expectationWithDescription:@"result should return success state"]; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : [NSNull null], + }]; + FIAPaymentQueueHandler *mockHandler = OCMClassMock(FIAPaymentQueueHandler.class); + OCMStub([mockHandler addPayment:[OCMArg any]]).andReturn(YES); + self.plugin.paymentQueueHandler = mockHandler; + [self.plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertNil(result); + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + OCMVerify(times(1), [mockHandler addPayment:[OCMArg checkWithBlock:^BOOL(id obj) { + SKPayment *payment = obj; + return !payment.simulatesAskToBuyInSandbox; + }]]); +} + +- (void)testRestoreTransactions { + XCTestExpectation *expectation = + [self expectationWithDescription:@"result successfully restore transactions"]; + FlutterMethodCall *call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin restoreTransactions:result:]" + arguments:nil]; + SKPaymentQueueStub *queue = [SKPaymentQueueStub new]; + queue.testState = SKPaymentTransactionStatePurchased; + __block BOOL callbackInvoked = NO; + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:^() { + callbackInvoked = YES; + [expectation fulfill]; + } + shouldAddStorePayment:nil + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; + [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue(callbackInvoked); +} + +- (void)testRetrieveReceiptDataSuccess { + XCTestExpectation *expectation = [self expectationWithDescription:@"receipt data retrieved"]; + FlutterMethodCall *call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" + arguments:nil]; + __block NSDictionary *result; + [self.plugin handleMethodCall:call + result:^(id r) { + result = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNotNil(result); + XCTAssert([result isKindOfClass:[NSString class]]); +} + +- (void)testRetrieveReceiptDataNil { + NSBundle *mockBundle = OCMPartialMock([NSBundle mainBundle]); + OCMStub(mockBundle.appStoreReceiptURL).andReturn(nil); + XCTestExpectation *expectation = [self expectationWithDescription:@"nil receipt data retrieved"]; + FlutterMethodCall *call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" + arguments:nil]; + __block NSDictionary *result; + [self.plugin handleMethodCall:call + result:^(id r) { + result = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNil(result); +} + +- (void)testRetrieveReceiptDataError { + XCTestExpectation *expectation = [self expectationWithDescription:@"receipt data retrieved"]; + FlutterMethodCall *call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" + arguments:nil]; + __block NSDictionary *result; + self.receiptManagerStub.returnError = YES; + [self.plugin handleMethodCall:call + result:^(id r) { + result = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNotNil(result); + XCTAssert([result isKindOfClass:[FlutterError class]]); + NSDictionary *details = ((FlutterError *)result).details; + XCTAssertNotNil(details[@"error"]); + NSNumber *errorCode = (NSNumber *)details[@"error"][@"code"]; + XCTAssertEqual(errorCode, [NSNumber numberWithInteger:99]); +} + +- (void)testRefreshReceiptRequest { + XCTestExpectation *expectation = [self expectationWithDescription:@"expect success"]; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin refreshReceipt:result:]" + arguments:nil]; + __block BOOL result = NO; + [self.plugin handleMethodCall:call + result:^(id r) { + result = YES; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue(result); +} + +- (void)testPresentCodeRedemptionSheet { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect successfully present Code Redemption Sheet"]; + FlutterMethodCall *call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]" + arguments:nil]; + __block BOOL callbackInvoked = NO; + [self.plugin handleMethodCall:call + result:^(id r) { + callbackInvoked = YES; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue(callbackInvoked); +} + +- (void)testGetPendingTransactions { + XCTestExpectation *expectation = [self expectationWithDescription:@"expect success"]; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue transactions]" arguments:nil]; + SKPaymentQueue *mockQueue = OCMClassMock(SKPaymentQueue.class); + NSDictionary *transactionMap = @{ + @"transactionIdentifier" : [NSNull null], + @"transactionState" : @(SKPaymentTransactionStatePurchasing), + @"payment" : [NSNull null], + @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" + code:123 + userInfo:@{}]], + @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), + @"originalTransaction" : [NSNull null], + }; + OCMStub(mockQueue.transactions).andReturn(@[ [[SKPaymentTransactionStub alloc] + initWithMap:transactionMap] ]); + + __block NSArray *resultArray; + self.plugin.paymentQueueHandler = + [[FIAPaymentQueueHandler alloc] initWithQueue:mockQueue + transactionsUpdated:nil + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:nil + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; + [self.plugin handleMethodCall:call + result:^(id r) { + resultArray = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqualObjects(resultArray, @[ transactionMap ]); +} + +- (void)testStartObservingPaymentQueue { + XCTestExpectation *expectation = + [self expectationWithDescription:@"Should return success result"]; + FlutterMethodCall *startCall = [FlutterMethodCall + methodCallWithMethodName:@"-[SKPaymentQueue startObservingTransactionQueue]" + arguments:nil]; + FIAPaymentQueueHandler *mockHandler = OCMClassMock([FIAPaymentQueueHandler class]); + self.plugin.paymentQueueHandler = mockHandler; + [self.plugin handleMethodCall:startCall + result:^(id _Nullable result) { + XCTAssertNil(result); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[ expectation ] timeout:5]; + OCMVerify(times(1), [mockHandler startObservingPaymentQueue]); +} + +- (void)testStopObservingPaymentQueue { + XCTestExpectation *expectation = + [self expectationWithDescription:@"Should return success result"]; + FlutterMethodCall *stopCall = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue stopObservingTransactionQueue]" + arguments:nil]; + FIAPaymentQueueHandler *mockHandler = OCMClassMock([FIAPaymentQueueHandler class]); + self.plugin.paymentQueueHandler = mockHandler; + [self.plugin handleMethodCall:stopCall + result:^(id _Nullable result) { + XCTAssertNil(result); + [expectation fulfill]; + }]; + + [self waitForExpectations:@[ expectation ] timeout:5]; + OCMVerify(times(1), [mockHandler stopObservingPaymentQueue]); +} + +#if TARGET_OS_IOS +- (void)testRegisterPaymentQueueDelegate { + if (@available(iOS 13, *)) { + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue registerDelegate]" + arguments:nil]; + + self.plugin.paymentQueueHandler = + [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueueStub new] + transactionsUpdated:nil + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:nil + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; + + // Verify the delegate is nil before we register one. + XCTAssertNil(self.plugin.paymentQueueHandler.delegate); + + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + + // Verify the delegate is not nil after we registered one. + XCTAssertNotNil(self.plugin.paymentQueueHandler.delegate); + } +} +#endif + +- (void)testRemovePaymentQueueDelegate { + if (@available(iOS 13, *)) { + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue removeDelegate]" + arguments:nil]; + + self.plugin.paymentQueueHandler = + [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueueStub new] + transactionsUpdated:nil + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:nil + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; + self.plugin.paymentQueueHandler.delegate = OCMProtocolMock(@protocol(SKPaymentQueueDelegate)); + + // Verify the delegate is not nil before removing it. + XCTAssertNotNil(self.plugin.paymentQueueHandler.delegate); + + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + + // Verify the delegate is nill after removing it. + XCTAssertNil(self.plugin.paymentQueueHandler.delegate); + } +} + +#if TARGET_OS_IOS +- (void)testShowPriceConsentIfNeeded { + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue showPriceConsentIfNeeded]" + arguments:nil]; + + FIAPaymentQueueHandler *mockQueueHandler = OCMClassMock(FIAPaymentQueueHandler.class); + self.plugin.paymentQueueHandler = mockQueueHandler; + + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + if (@available(iOS 13.4, *)) { + OCMVerify(times(1), [mockQueueHandler showPriceConsentIfNeeded]); + } else { + OCMVerify(never(), [mockQueueHandler showPriceConsentIfNeeded]); + } +#pragma clang diagnostic pop +} +#endif + +@end diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/RunnerTests/Info.plist b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/Info.plist similarity index 93% rename from packages/shared_preferences/shared_preferences_ios/example/ios/RunnerTests/Info.plist rename to packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/Info.plist index 64d65ca49577..6c40a6cd0c4a 100644 --- a/packages/shared_preferences/shared_preferences_ios/example/ios/RunnerTests/Info.plist +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/Info.plist @@ -13,7 +13,7 @@ CFBundleName $(PRODUCT_NAME) CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) + BNDL CFBundleShortVersionString 1.0 CFBundleVersion diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/PaymentQueueTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/PaymentQueueTests.m new file mode 100644 index 000000000000..2f8d5857c8d8 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/PaymentQueueTests.m @@ -0,0 +1,420 @@ +// 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 +#import +#import "Stubs.h" + +@import in_app_purchase_storekit; + +@interface PaymentQueueTest : XCTestCase + +@property(strong, nonatomic) NSDictionary *periodMap; +@property(strong, nonatomic) NSDictionary *discountMap; +@property(strong, nonatomic) NSDictionary *productMap; +@property(strong, nonatomic) NSDictionary *productResponseMap; + +@end + +@implementation PaymentQueueTest + +- (void)setUp { + self.periodMap = @{@"numberOfUnits" : @(0), @"unit" : @(0)}; + self.discountMap = @{ + @"price" : @1.0, + @"currencyCode" : @"USD", + @"numberOfPeriods" : @1, + @"subscriptionPeriod" : self.periodMap, + @"paymentMode" : @1 + }; + self.productMap = @{ + @"price" : @1.0, + @"currencyCode" : @"USD", + @"productIdentifier" : @"123", + @"localizedTitle" : @"title", + @"localizedDescription" : @"des", + @"subscriptionPeriod" : self.periodMap, + @"introductoryPrice" : self.discountMap, + @"subscriptionGroupIdentifier" : @"com.group" + }; + self.productResponseMap = + @{@"products" : @[ self.productMap ], @"invalidProductIdentifiers" : [NSNull null]}; +} + +- (void)testTransactionPurchased { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get purchased transcation."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStatePurchased; + __block SKPaymentTransactionStub *tran; + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + SKPaymentTransaction *transaction = transactions[0]; + tran = (SKPaymentTransactionStub *)transaction; + [expectation fulfill]; + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; + SKPayment *payment = + [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler startObservingPaymentQueue]; + [handler addPayment:payment]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(tran.transactionState, SKPaymentTransactionStatePurchased); + XCTAssertEqual(tran.transactionIdentifier, @"fakeID"); +} + +- (void)testTransactionFailed { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get failed transcation."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStateFailed; + __block SKPaymentTransactionStub *tran; + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + SKPaymentTransaction *transaction = transactions[0]; + tran = (SKPaymentTransactionStub *)transaction; + [expectation fulfill]; + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; + + SKPayment *payment = + [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler startObservingPaymentQueue]; + [handler addPayment:payment]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateFailed); + XCTAssertEqual(tran.transactionIdentifier, nil); +} + +- (void)testTransactionRestored { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get restored transcation."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStateRestored; + __block SKPaymentTransactionStub *tran; + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + SKPaymentTransaction *transaction = transactions[0]; + tran = (SKPaymentTransactionStub *)transaction; + [expectation fulfill]; + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; + + SKPayment *payment = + [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler startObservingPaymentQueue]; + [handler addPayment:payment]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateRestored); + XCTAssertEqual(tran.transactionIdentifier, @"fakeID"); +} + +- (void)testTransactionPurchasing { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get purchasing transcation."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStatePurchasing; + __block SKPaymentTransactionStub *tran; + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + SKPaymentTransaction *transaction = transactions[0]; + tran = (SKPaymentTransactionStub *)transaction; + [expectation fulfill]; + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; + + SKPayment *payment = + [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler startObservingPaymentQueue]; + [handler addPayment:payment]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(tran.transactionState, SKPaymentTransactionStatePurchasing); + XCTAssertEqual(tran.transactionIdentifier, nil); +} + +- (void)testTransactionDeferred { + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get deffered transcation."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStateDeferred; + __block SKPaymentTransactionStub *tran; + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + SKPaymentTransaction *transaction = transactions[0]; + tran = (SKPaymentTransactionStub *)transaction; + [expectation fulfill]; + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; + SKPayment *payment = + [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler startObservingPaymentQueue]; + [handler addPayment:payment]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(tran.transactionState, SKPaymentTransactionStateDeferred); + XCTAssertEqual(tran.transactionIdentifier, nil); +} + +- (void)testFinishTransaction { + XCTestExpectation *expectation = + [self expectationWithDescription:@"handler.transactions should be empty."]; + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStateDeferred; + __block FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + XCTAssertEqual(transactions.count, 1); + SKPaymentTransaction *transaction = transactions[0]; + [handler finishTransaction:transaction]; + } + transactionRemoved:^(NSArray *_Nonnull transactions) { + XCTAssertEqual(transactions.count, 1); + [expectation fulfill]; + } + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:nil + transactionCache:OCMClassMock(FIATransactionCache.class)]; + SKPayment *payment = + [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler startObservingPaymentQueue]; + [handler addPayment:payment]; + [self waitForExpectations:@[ expectation ] timeout:5]; +} + +- (void)testStartObservingPaymentQueueShouldNotProcessTransactionsWhenCacheIsEmpty { + FIATransactionCache *mockCache = OCMClassMock(FIATransactionCache.class); + FIAPaymentQueueHandler *handler = + [[FIAPaymentQueueHandler alloc] initWithQueue:[[SKPaymentQueueStub alloc] init] + transactionsUpdated:^(NSArray *_Nonnull transactions) { + XCTFail("transactionsUpdated callback should not be called when cache is empty."); + } + transactionRemoved:^(NSArray *_Nonnull transactions) { + XCTFail("transactionRemoved callback should not be called when cache is empty."); + } + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:^(NSArray *_Nonnull downloads) { + XCTFail("updatedDownloads callback should not be called when cache is empty."); + } + transactionCache:mockCache]; + + [handler startObservingPaymentQueue]; + + OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); + OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]); + OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]); +} + +- (void) + testStartObservingPaymentQueueShouldNotProcessTransactionsWhenCacheContainsEmptyTransactionArrays { + FIATransactionCache *mockCache = OCMClassMock(FIATransactionCache.class); + FIAPaymentQueueHandler *handler = + [[FIAPaymentQueueHandler alloc] initWithQueue:[[SKPaymentQueueStub alloc] init] + transactionsUpdated:^(NSArray *_Nonnull transactions) { + XCTFail("transactionsUpdated callback should not be called when cache is empty."); + } + transactionRemoved:^(NSArray *_Nonnull transactions) { + XCTFail("transactionRemoved callback should not be called when cache is empty."); + } + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:^(NSArray *_Nonnull downloads) { + XCTFail("updatedDownloads callback should not be called when cache is empty."); + } + transactionCache:mockCache]; + + OCMStub([mockCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]).andReturn(@[]); + OCMStub([mockCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]).andReturn(@[]); + OCMStub([mockCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]).andReturn(@[]); + + [handler startObservingPaymentQueue]; + + OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); + OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]); + OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]); +} + +- (void)testStartObservingPaymentQueueShouldProcessTransactionsForItemsInCache { + XCTestExpectation *updateTransactionsExpectation = + [self expectationWithDescription: + @"transactionsUpdated callback should be called with one transaction."]; + XCTestExpectation *removeTransactionsExpectation = + [self expectationWithDescription: + @"transactionsRemoved callback should be called with one transaction."]; + XCTestExpectation *updateDownloadsExpectation = + [self expectationWithDescription: + @"downloadsUpdated callback should be called with one transaction."]; + SKPaymentTransaction *mockTransaction = OCMClassMock(SKPaymentTransaction.class); + SKDownload *mockDownload = OCMClassMock(SKDownload.class); + FIATransactionCache *mockCache = OCMClassMock(FIATransactionCache.class); + FIAPaymentQueueHandler *handler = + [[FIAPaymentQueueHandler alloc] initWithQueue:[[SKPaymentQueueStub alloc] init] + transactionsUpdated:^(NSArray *_Nonnull transactions) { + XCTAssertEqualObjects(transactions, @[ mockTransaction ]); + [updateTransactionsExpectation fulfill]; + } + transactionRemoved:^(NSArray *_Nonnull transactions) { + XCTAssertEqualObjects(transactions, @[ mockTransaction ]); + [removeTransactionsExpectation fulfill]; + } + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:^(NSArray *_Nonnull downloads) { + XCTAssertEqualObjects(downloads, @[ mockDownload ]); + [updateDownloadsExpectation fulfill]; + } + transactionCache:mockCache]; + + OCMStub([mockCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]).andReturn(@[ + mockTransaction + ]); + OCMStub([mockCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]).andReturn(@[ + mockDownload + ]); + OCMStub([mockCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]).andReturn(@[ + mockTransaction + ]); + + [handler startObservingPaymentQueue]; + + [self waitForExpectations:@[ + updateTransactionsExpectation, removeTransactionsExpectation, updateDownloadsExpectation + ] + timeout:5]; + OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]); + OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]); + OCMVerify(times(1), [mockCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]); + OCMVerify(times(1), [mockCache clear]); +} + +- (void)testTransactionsShouldBeCachedWhenNotObserving { + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + FIATransactionCache *mockCache = OCMClassMock(FIATransactionCache.class); + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + XCTFail("transactionsUpdated callback should not be called when cache is empty."); + } + transactionRemoved:^(NSArray *_Nonnull transactions) { + XCTFail("transactionRemoved callback should not be called when cache is empty."); + } + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:^(NSArray *_Nonnull downloads) { + XCTFail("updatedDownloads callback should not be called when cache is empty."); + } + transactionCache:mockCache]; + + SKPayment *payment = + [SKPayment paymentWithProduct:[[SKProductStub alloc] initWithMap:self.productResponseMap]]; + [handler addPayment:payment]; + + OCMVerify(times(1), [mockCache addObjects:[OCMArg any] + forKey:TransactionCacheKeyUpdatedTransactions]); + OCMVerify(never(), [mockCache addObjects:[OCMArg any] + forKey:TransactionCacheKeyUpdatedDownloads]); + OCMVerify(never(), [mockCache addObjects:[OCMArg any] + forKey:TransactionCacheKeyRemovedTransactions]); +} + +- (void)testTransactionsShouldNotBeCachedWhenObserving { + XCTestExpectation *updateTransactionsExpectation = + [self expectationWithDescription: + @"transactionsUpdated callback should be called with one transaction."]; + XCTestExpectation *removeTransactionsExpectation = + [self expectationWithDescription: + @"transactionsRemoved callback should be called with one transaction."]; + XCTestExpectation *updateDownloadsExpectation = + [self expectationWithDescription: + @"downloadsUpdated callback should be called with one transaction."]; + SKPaymentTransaction *mockTransaction = OCMClassMock(SKPaymentTransaction.class); + SKDownload *mockDownload = OCMClassMock(SKDownload.class); + SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; + queue.testState = SKPaymentTransactionStatePurchased; + FIATransactionCache *mockCache = OCMClassMock(FIATransactionCache.class); + FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray *_Nonnull transactions) { + XCTAssertEqualObjects(transactions, @[ mockTransaction ]); + [updateTransactionsExpectation fulfill]; + } + transactionRemoved:^(NSArray *_Nonnull transactions) { + XCTAssertEqualObjects(transactions, @[ mockTransaction ]); + [removeTransactionsExpectation fulfill]; + } + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment *_Nonnull payment, SKProduct *_Nonnull product) { + return YES; + } + updatedDownloads:^(NSArray *_Nonnull downloads) { + XCTAssertEqualObjects(downloads, @[ mockDownload ]); + [updateDownloadsExpectation fulfill]; + } + transactionCache:mockCache]; + + [handler startObservingPaymentQueue]; + [handler paymentQueue:queue updatedTransactions:@[ mockTransaction ]]; + [handler paymentQueue:queue removedTransactions:@[ mockTransaction ]]; + [handler paymentQueue:queue updatedDownloads:@[ mockDownload ]]; + + [self waitForExpectations:@[ + updateTransactionsExpectation, removeTransactionsExpectation, updateDownloadsExpectation + ] + timeout:5]; + OCMVerify(never(), [mockCache addObjects:[OCMArg any] + forKey:TransactionCacheKeyUpdatedTransactions]); + OCMVerify(never(), [mockCache addObjects:[OCMArg any] + forKey:TransactionCacheKeyUpdatedDownloads]); + OCMVerify(never(), [mockCache addObjects:[OCMArg any] + forKey:TransactionCacheKeyRemovedTransactions]); +} +@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/ProductRequestHandlerTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/ProductRequestHandlerTests.m new file mode 100644 index 000000000000..ac36aae5acb5 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/ProductRequestHandlerTests.m @@ -0,0 +1,89 @@ +// 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 +#import "Stubs.h" + +@import in_app_purchase_storekit; + +#pragma tests start here + +@interface RequestHandlerTest : XCTestCase + +@end + +@implementation RequestHandlerTest + +- (void)testRequestHandlerWithProductRequestSuccess { + SKProductRequestStub *request = + [[SKProductRequestStub alloc] initWithProductIdentifiers:[NSSet setWithArray:@[ @"123" ]]]; + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get response with 1 product"]; + __block SKProductsResponse *response; + [handler + startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *error) { + response = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNotNil(response); + XCTAssertEqual(response.products.count, 1); + SKProduct *product = response.products.firstObject; + XCTAssertTrue([product.productIdentifier isEqualToString:@"123"]); +} + +- (void)testRequestHandlerWithProductRequestFailure { + SKProductRequestStub *request = [[SKProductRequestStub alloc] + initWithFailureError:[NSError errorWithDomain:@"test" code:123 userInfo:@{}]]; + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get response with 1 product"]; + __block NSError *error; + __block SKProductsResponse *response; + [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *e) { + error = e; + response = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNotNil(error); + XCTAssertEqual(error.domain, @"test"); + XCTAssertNil(response); +} + +- (void)testRequestHandlerWithRefreshReceiptSuccess { + SKReceiptRefreshRequestStub *request = + [[SKReceiptRefreshRequestStub alloc] initWithReceiptProperties:nil]; + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + XCTestExpectation *expectation = [self expectationWithDescription:@"expect no error"]; + __block NSError *e; + [handler + startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *error) { + e = error; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNil(e); +} + +- (void)testRequestHandlerWithRefreshReceiptFailure { + SKReceiptRefreshRequestStub *request = [[SKReceiptRefreshRequestStub alloc] + initWithFailureError:[NSError errorWithDomain:@"test" code:123 userInfo:@{}]]; + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + XCTestExpectation *expectation = [self expectationWithDescription:@"expect error"]; + __block NSError *error; + __block SKProductsResponse *response; + [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *e) { + error = e; + response = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNotNil(error); + XCTAssertEqual(error.domain, @"test"); + XCTAssertNil(response); +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/Stubs.h b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/Stubs.h new file mode 100644 index 000000000000..d4e8df3eba72 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/Stubs.h @@ -0,0 +1,71 @@ +// 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 +#import + +@import in_app_purchase_storekit; + +NS_ASSUME_NONNULL_BEGIN +API_AVAILABLE(ios(11.2), macos(10.13.2)) +@interface SKProductSubscriptionPeriodStub : SKProductSubscriptionPeriod +- (instancetype)initWithMap:(NSDictionary *)map; +@end + +API_AVAILABLE(ios(11.2), macos(10.13.2)) +@interface SKProductDiscountStub : SKProductDiscount +- (instancetype)initWithMap:(NSDictionary *)map; +@end + +@interface SKProductStub : SKProduct +- (instancetype)initWithMap:(NSDictionary *)map; +@end + +@interface SKProductRequestStub : SKProductsRequest +- (instancetype)initWithProductIdentifiers:(NSSet *)productIdentifiers; +- (instancetype)initWithFailureError:(NSError *)error; +@end + +@interface SKProductsResponseStub : SKProductsResponse +- (instancetype)initWithMap:(NSDictionary *)map; +@end + +@interface InAppPurchasePluginStub : InAppPurchasePlugin +@end + +@interface SKPaymentQueueStub : SKPaymentQueue +@property(assign, nonatomic) SKPaymentTransactionState testState; +@property(strong, nonatomic, nullable) id observer; +@end + +@interface SKPaymentTransactionStub : SKPaymentTransaction +- (instancetype)initWithMap:(NSDictionary *)map; +- (instancetype)initWithState:(SKPaymentTransactionState)state; +- (instancetype)initWithState:(SKPaymentTransactionState)state payment:(SKPayment *)payment; +@end + +@interface SKMutablePaymentStub : SKMutablePayment +- (instancetype)initWithMap:(NSDictionary *)map; +@end + +@interface NSErrorStub : NSError +- (instancetype)initWithMap:(NSDictionary *)map; +@end + +@interface FIAPReceiptManagerStub : FIAPReceiptManager +// Indicates whether getReceiptData of this stub is going to return an error. +// Setting this to true will let getReceiptData give a basic NSError and return nil. +@property(assign, nonatomic) BOOL returnError; +@end + +@interface SKReceiptRefreshRequestStub : SKReceiptRefreshRequest +- (instancetype)initWithFailureError:(NSError *)error; +@end + +API_AVAILABLE(ios(13.0), macos(10.15)) +@interface SKStorefrontStub : SKStorefront +- (instancetype)initWithMap:(NSDictionary *)map; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/Stubs.m b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/Stubs.m new file mode 100644 index 000000000000..f5e44d78b157 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/Stubs.m @@ -0,0 +1,330 @@ +// 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 "Stubs.h" + +@implementation SKProductSubscriptionPeriodStub + +- (instancetype)initWithMap:(NSDictionary *)map { + self = [super init]; + if (self) { + [self setValue:map[@"numberOfUnits"] ?: @(0) forKey:@"numberOfUnits"]; + [self setValue:map[@"unit"] ?: @(0) forKey:@"unit"]; + } + return self; +} + +@end + +@implementation SKProductDiscountStub + +- (instancetype)initWithMap:(NSDictionary *)map { + self = [super init]; + if (self) { + [self setValue:[[NSDecimalNumber alloc] initWithString:map[@"price"]] ?: [NSNull null] + forKey:@"price"]; + NSLocale *locale = NSLocale.systemLocale; + [self setValue:locale ?: [NSNull null] forKey:@"priceLocale"]; + [self setValue:map[@"numberOfPeriods"] ?: @(0) forKey:@"numberOfPeriods"]; + SKProductSubscriptionPeriodStub *subscriptionPeriodSub = + [[SKProductSubscriptionPeriodStub alloc] initWithMap:map[@"subscriptionPeriod"]]; + [self setValue:subscriptionPeriodSub forKey:@"subscriptionPeriod"]; + [self setValue:map[@"paymentMode"] ?: @(0) forKey:@"paymentMode"]; + if (@available(iOS 12.2, *)) { + [self setValue:map[@"identifier"] ?: [NSNull null] forKey:@"identifier"]; + [self setValue:map[@"type"] ?: @(0) forKey:@"type"]; + } + } + return self; +} + +@end + +@implementation SKProductStub + +- (instancetype)initWithMap:(NSDictionary *)map { + self = [super init]; + if (self) { + [self setValue:map[@"productIdentifier"] ?: [NSNull null] forKey:@"productIdentifier"]; + [self setValue:map[@"localizedDescription"] ?: [NSNull null] forKey:@"localizedDescription"]; + [self setValue:map[@"localizedTitle"] ?: [NSNull null] forKey:@"localizedTitle"]; + [self setValue:map[@"downloadable"] ?: @NO forKey:@"downloadable"]; + [self setValue:[[NSDecimalNumber alloc] initWithString:map[@"price"]] ?: [NSNull null] + forKey:@"price"]; + NSLocale *locale = NSLocale.systemLocale; + [self setValue:locale ?: [NSNull null] forKey:@"priceLocale"]; + [self setValue:map[@"downloadContentLengths"] ?: @(0) forKey:@"downloadContentLengths"]; + if (@available(iOS 11.2, *)) { + SKProductSubscriptionPeriodStub *period = + [[SKProductSubscriptionPeriodStub alloc] initWithMap:map[@"subscriptionPeriod"]]; + [self setValue:period ?: [NSNull null] forKey:@"subscriptionPeriod"]; + SKProductDiscountStub *discount = + [[SKProductDiscountStub alloc] initWithMap:map[@"introductoryPrice"]]; + [self setValue:discount ?: [NSNull null] forKey:@"introductoryPrice"]; + [self setValue:map[@"subscriptionGroupIdentifier"] ?: [NSNull null] + forKey:@"subscriptionGroupIdentifier"]; + } + if (@available(iOS 12.2, *)) { + NSMutableArray *discounts = [[NSMutableArray alloc] init]; + for (NSDictionary *discountMap in map[@"discounts"]) { + [discounts addObject:[[SKProductDiscountStub alloc] initWithMap:discountMap]]; + } + + [self setValue:discounts forKey:@"discounts"]; + } + } + return self; +} + +- (instancetype)initWithProductID:(NSString *)productIdentifier { + self = [super init]; + if (self) { + [self setValue:productIdentifier forKey:@"productIdentifier"]; + } + return self; +} + +@end + +@interface SKProductRequestStub () + +@property(strong, nonatomic) NSSet *identifers; +@property(strong, nonatomic) NSError *error; + +@end + +@implementation SKProductRequestStub + +- (instancetype)initWithProductIdentifiers:(NSSet *)productIdentifiers { + self = [super initWithProductIdentifiers:productIdentifiers]; + self.identifers = productIdentifiers; + return self; +} + +- (instancetype)initWithFailureError:(NSError *)error { + self = [super init]; + self.error = error; + return self; +} + +- (void)start { + NSMutableArray *productArray = [NSMutableArray new]; + for (NSString *identifier in self.identifers) { + [productArray addObject:@{@"productIdentifier" : identifier}]; + } + SKProductsResponseStub *response = + [[SKProductsResponseStub alloc] initWithMap:@{@"products" : productArray}]; + if (self.error) { + [self.delegate request:self didFailWithError:self.error]; + } else { + [self.delegate productsRequest:self didReceiveResponse:response]; + } +} + +@end + +@implementation SKProductsResponseStub + +- (instancetype)initWithMap:(NSDictionary *)map { + self = [super init]; + if (self) { + NSMutableArray *products = [NSMutableArray new]; + for (NSDictionary *productMap in map[@"products"]) { + SKProductStub *product = [[SKProductStub alloc] initWithMap:productMap]; + [products addObject:product]; + } + [self setValue:products forKey:@"products"]; + } + return self; +} + +@end + +@interface InAppPurchasePluginStub () + +@end + +@implementation InAppPurchasePluginStub + +- (SKProductRequestStub *)getProductRequestWithIdentifiers:(NSSet *)identifiers { + return [[SKProductRequestStub alloc] initWithProductIdentifiers:identifiers]; +} + +- (SKProduct *)getProduct:(NSString *)productID { + return [[SKProductStub alloc] initWithProductID:productID]; +} + +- (SKReceiptRefreshRequestStub *)getRefreshReceiptRequest:(NSDictionary *)properties { + return [[SKReceiptRefreshRequestStub alloc] initWithReceiptProperties:properties]; +} + +@end + +@interface SKPaymentQueueStub () + +@end + +@implementation SKPaymentQueueStub + +- (void)addTransactionObserver:(id)observer { + self.observer = observer; +} + +- (void)removeTransactionObserver:(id)observer { + self.observer = nil; +} + +- (void)addPayment:(SKPayment *)payment { + SKPaymentTransactionStub *transaction = + [[SKPaymentTransactionStub alloc] initWithState:self.testState payment:payment]; + [self.observer paymentQueue:self updatedTransactions:@[ transaction ]]; +} + +- (void)restoreCompletedTransactions { + if ([self.observer + respondsToSelector:@selector(paymentQueueRestoreCompletedTransactionsFinished:)]) { + [self.observer paymentQueueRestoreCompletedTransactionsFinished:self]; + } +} + +- (void)finishTransaction:(SKPaymentTransaction *)transaction { + if ([self.observer respondsToSelector:@selector(paymentQueue:removedTransactions:)]) { + [self.observer paymentQueue:self removedTransactions:@[ transaction ]]; + } +} + +@end + +@implementation SKPaymentTransactionStub { + SKPayment *_payment; +} + +- (instancetype)initWithID:(NSString *)identifier { + self = [super init]; + if (self) { + [self setValue:identifier forKey:@"transactionIdentifier"]; + } + return self; +} + +- (instancetype)initWithMap:(NSDictionary *)map { + self = [super init]; + if (self) { + [self setValue:map[@"transactionIdentifier"] forKey:@"transactionIdentifier"]; + [self setValue:map[@"transactionState"] forKey:@"transactionState"]; + if (![map[@"originalTransaction"] isKindOfClass:[NSNull class]] && + map[@"originalTransaction"]) { + [self setValue:[[SKPaymentTransactionStub alloc] initWithMap:map[@"originalTransaction"]] + forKey:@"originalTransaction"]; + } + [self setValue:map[@"error"] ? [[NSErrorStub alloc] initWithMap:map[@"error"]] : [NSNull null] + forKey:@"error"]; + [self setValue:[NSDate dateWithTimeIntervalSince1970:[map[@"transactionTimeStamp"] doubleValue]] + forKey:@"transactionDate"]; + } + return self; +} + +- (instancetype)initWithState:(SKPaymentTransactionState)state { + self = [super init]; + if (self) { + // Only purchased and restored transactions have transactionIdentifier: + // https://developer.apple.com/documentation/storekit/skpaymenttransaction/1411288-transactionidentifier?language=objc + if (state == SKPaymentTransactionStatePurchased || state == SKPaymentTransactionStateRestored) { + [self setValue:@"fakeID" forKey:@"transactionIdentifier"]; + } + [self setValue:@(state) forKey:@"transactionState"]; + } + return self; +} + +- (instancetype)initWithState:(SKPaymentTransactionState)state payment:(SKPayment *)payment { + self = [super init]; + if (self) { + // Only purchased and restored transactions have transactionIdentifier: + // https://developer.apple.com/documentation/storekit/skpaymenttransaction/1411288-transactionidentifier?language=objc + if (state == SKPaymentTransactionStatePurchased || state == SKPaymentTransactionStateRestored) { + [self setValue:@"fakeID" forKey:@"transactionIdentifier"]; + } + [self setValue:@(state) forKey:@"transactionState"]; + _payment = payment; + } + return self; +} + +- (SKPayment *)payment { + return _payment; +} + +@end + +@implementation NSErrorStub + +- (instancetype)initWithMap:(NSDictionary *)map { + return [self initWithDomain:[map objectForKey:@"domain"] + code:[[map objectForKey:@"code"] integerValue] + userInfo:[map objectForKey:@"userInfo"]]; +} + +@end + +@implementation FIAPReceiptManagerStub : FIAPReceiptManager + +- (NSData *)getReceiptData:(NSURL *)url error:(NSError **)error { + if (self.returnError) { + *error = [NSError errorWithDomain:@"test" + code:1 + userInfo:@{ + @"name" : @"test", + @"houseNr" : @5, + @"error" : [[NSError alloc] initWithDomain:@"internalTestDomain" + code:99 + userInfo:nil] + }]; + return nil; + } + NSString *originalString = [NSString stringWithFormat:@"test"]; + return [[NSData alloc] initWithBase64EncodedString:originalString options:kNilOptions]; +} + +@end + +@implementation SKReceiptRefreshRequestStub { + NSError *_error; +} + +- (instancetype)initWithReceiptProperties:(NSDictionary *)properties { + self = [super initWithReceiptProperties:properties]; + return self; +} + +- (instancetype)initWithFailureError:(NSError *)error { + self = [super init]; + _error = error; + return self; +} + +- (void)start { + if (_error) { + [self.delegate request:self didFailWithError:_error]; + } else { + [self.delegate requestDidFinish:self]; + } +} + +@end + +@implementation SKStorefrontStub + +- (instancetype)initWithMap:(NSDictionary *)map { + self = [super init]; + if (self) { + // Set stub values + [self setValue:map[@"countryCode"] forKey:@"countryCode"]; + [self setValue:map[@"identifier"] forKey:@"identifier"]; + } + return self; +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/TranslatorTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/TranslatorTests.m new file mode 100644 index 000000000000..6f77fa72a632 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/TranslatorTests.m @@ -0,0 +1,414 @@ +// 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 +#import "Stubs.h" + +@import in_app_purchase_storekit; + +@interface TranslatorTest : XCTestCase + +@property(strong, nonatomic) NSDictionary *periodMap; +@property(strong, nonatomic) NSMutableDictionary *discountMap; +@property(strong, nonatomic) NSMutableDictionary *discountMissingIdentifierMap; +@property(strong, nonatomic) NSMutableDictionary *productMap; +@property(strong, nonatomic) NSDictionary *productResponseMap; +@property(strong, nonatomic) NSDictionary *paymentMap; +@property(copy, nonatomic) NSDictionary *paymentDiscountMap; +@property(strong, nonatomic) NSDictionary *transactionMap; +@property(strong, nonatomic) NSDictionary *errorMap; +@property(strong, nonatomic) NSDictionary *localeMap; +@property(strong, nonatomic) NSDictionary *storefrontMap; +@property(strong, nonatomic) NSDictionary *storefrontAndPaymentTransactionMap; + +@end + +@implementation TranslatorTest + +- (void)setUp { + self.periodMap = @{@"numberOfUnits" : @(0), @"unit" : @(0)}; + + self.discountMap = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"price" : @"1", + @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], + @"numberOfPeriods" : @1, + @"subscriptionPeriod" : self.periodMap, + @"paymentMode" : @1, + }]; + if (@available(iOS 12.2, *)) { + self.discountMap[@"identifier"] = @"test offer id"; + self.discountMap[@"type"] = @(SKProductDiscountTypeIntroductory); + } + self.discountMissingIdentifierMap = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"price" : @"1", + @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], + @"numberOfPeriods" : @1, + @"subscriptionPeriod" : self.periodMap, + @"paymentMode" : @1, + @"identifier" : [NSNull null], + @"type" : @0, + }]; + + self.productMap = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"price" : @"1", + @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], + @"productIdentifier" : @"123", + @"localizedTitle" : @"title", + @"localizedDescription" : @"des", + }]; + if (@available(iOS 11.2, *)) { + self.productMap[@"subscriptionPeriod"] = self.periodMap; + self.productMap[@"introductoryPrice"] = self.discountMap; + } + if (@available(iOS 12.2, *)) { + self.productMap[@"discounts"] = @[ self.discountMap ]; + } + + if (@available(iOS 12.0, *)) { + self.productMap[@"subscriptionGroupIdentifier"] = @"com.group"; + } + + self.productResponseMap = + @{@"products" : @[ self.productMap ], @"invalidProductIdentifiers" : @[]}; + self.paymentMap = @{ + @"productIdentifier" : @"123", + @"requestData" : @"abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh", + @"quantity" : @(2), + @"applicationUsername" : @"app user name", + @"simulatesAskToBuyInSandbox" : @(NO) + }; + self.paymentDiscountMap = @{ + @"identifier" : @"payment_discount_identifier", + @"keyIdentifier" : @"payment_discount_key_identifier", + @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", + @"signature" : @"this is a encrypted signature", + @"timestamp" : @([NSDate date].timeIntervalSince1970), + }; + NSDictionary *originalTransactionMap = @{ + @"transactionIdentifier" : @"567", + @"transactionState" : @(SKPaymentTransactionStatePurchasing), + @"payment" : [NSNull null], + @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" + code:123 + userInfo:@{}]], + @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), + @"originalTransaction" : [NSNull null], + }; + self.transactionMap = @{ + @"transactionIdentifier" : @"567", + @"transactionState" : @(SKPaymentTransactionStatePurchasing), + @"payment" : [NSNull null], + @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" + code:123 + userInfo:@{}]], + @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), + @"originalTransaction" : originalTransactionMap, + }; + self.errorMap = @{ + @"code" : @(123), + @"domain" : @"test_domain", + @"userInfo" : @{ + @"key" : @"value", + } + }; + self.storefrontMap = @{ + @"countryCode" : @"USA", + @"identifier" : @"unique_identifier", + }; + + self.storefrontAndPaymentTransactionMap = @{ + @"storefront" : self.storefrontMap, + @"transaction" : self.transactionMap, + }; +} + +- (void)testSKProductSubscriptionPeriodStubToMap { + if (@available(iOS 11.2, *)) { + SKProductSubscriptionPeriodStub *period = + [[SKProductSubscriptionPeriodStub alloc] initWithMap:self.periodMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKProductSubscriptionPeriod:period]; + XCTAssertEqualObjects(map, self.periodMap); + } +} + +- (void)testSKProductDiscountStubToMap { + if (@available(iOS 11.2, *)) { + SKProductDiscountStub *discount = [[SKProductDiscountStub alloc] initWithMap:self.discountMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKProductDiscount:discount]; + XCTAssertEqualObjects(map, self.discountMap); + } +} + +- (void)testProductToMap { + SKProductStub *product = [[SKProductStub alloc] initWithMap:self.productMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKProduct:product]; + XCTAssertEqualObjects(map, self.productMap); +} + +- (void)testProductResponseToMap { + SKProductsResponseStub *response = + [[SKProductsResponseStub alloc] initWithMap:self.productResponseMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKProductsResponse:response]; + XCTAssertEqualObjects(map, self.productResponseMap); +} + +- (void)testPaymentToMap { + SKMutablePayment *payment = [FIAObjectTranslator getSKMutablePaymentFromMap:self.paymentMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKPayment:payment]; + XCTAssertEqualObjects(map, self.paymentMap); +} + +- (void)testPaymentTransactionToMap { + // payment is not KVC, cannot test payment field. + SKPaymentTransactionStub *paymentTransaction = + [[SKPaymentTransactionStub alloc] initWithMap:self.transactionMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKPaymentTransaction:paymentTransaction]; + XCTAssertEqualObjects(map, self.transactionMap); +} + +- (void)testError { + NSErrorStub *error = [[NSErrorStub alloc] initWithMap:self.errorMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromNSError:error]; + XCTAssertEqualObjects(map, self.errorMap); +} + +- (void)testErrorWithNSNumberAsUserInfo { + NSError *error = [NSError errorWithDomain:SKErrorDomain code:3 userInfo:@{@"key" : @42}]; + NSDictionary *expectedMap = + @{@"domain" : SKErrorDomain, @"code" : @3, @"userInfo" : @{@"key" : @42}}; + NSDictionary *map = [FIAObjectTranslator getMapFromNSError:error]; + XCTAssertEqualObjects(expectedMap, map); +} + +- (void)testErrorWithMultipleUnderlyingErrors { + NSError *underlyingErrorOne = [NSError errorWithDomain:SKErrorDomain code:2 userInfo:nil]; + NSError *underlyingErrorTwo = [NSError errorWithDomain:SKErrorDomain code:1 userInfo:nil]; + NSError *mainError = [NSError + errorWithDomain:SKErrorDomain + code:3 + userInfo:@{@"underlyingErrors" : @[ underlyingErrorOne, underlyingErrorTwo ]}]; + NSDictionary *expectedMap = @{ + @"domain" : SKErrorDomain, + @"code" : @3, + @"userInfo" : @{ + @"underlyingErrors" : @[ + @{@"domain" : SKErrorDomain, @"code" : @2, @"userInfo" : @{}}, + @{@"domain" : SKErrorDomain, @"code" : @1, @"userInfo" : @{}} + ] + } + }; + NSDictionary *map = [FIAObjectTranslator getMapFromNSError:mainError]; + XCTAssertEqualObjects(expectedMap, map); +} + +- (void)testErrorWithUnsupportedUserInfo { + NSError *error = [NSError errorWithDomain:SKErrorDomain + code:3 + userInfo:@{@"user_info" : [[NSObject alloc] init]}]; + NSDictionary *expectedMap = @{ + @"domain" : SKErrorDomain, + @"code" : @3, + @"userInfo" : @{ + @"user_info" : [NSString + stringWithFormat: + @"Unable to encode native userInfo object of type %@ to map. Please submit an " + @"issue at https://github.com/flutter/flutter/issues/new with the title " + @"\"[in_app_purchase_storekit] Unable to encode userInfo of type %@\" and add " + @"reproduction steps and the error details in the description field.", + [NSObject class], [NSObject class]] + } + }; + NSDictionary *map = [FIAObjectTranslator getMapFromNSError:error]; + XCTAssertEqualObjects(expectedMap, map); +} + +- (void)testLocaleToMap { + NSLocale *system = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; + NSDictionary *map = [FIAObjectTranslator getMapFromNSLocale:system]; + XCTAssertEqualObjects(map[@"currencySymbol"], system.currencySymbol); + XCTAssertEqualObjects(map[@"countryCode"], system.countryCode); +} + +- (void)testSKStorefrontToMap { + if (@available(iOS 13.0, *)) { + SKStorefront *storefront = [[SKStorefrontStub alloc] initWithMap:self.storefrontMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKStorefront:storefront]; + XCTAssertEqualObjects(map, self.storefrontMap); + } +} + +- (void)testSKStorefrontAndSKPaymentTransactionToMap { + if (@available(iOS 13.0, *)) { + SKStorefront *storefront = [[SKStorefrontStub alloc] initWithMap:self.storefrontMap]; + SKPaymentTransaction *transaction = + [[SKPaymentTransactionStub alloc] initWithMap:self.transactionMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKStorefront:storefront + andSKPaymentTransaction:transaction]; + XCTAssertEqualObjects(map, self.storefrontAndPaymentTransactionMap); + } +} + +- (void)testSKPaymentDiscountFromMap { + if (@available(iOS 12.2, *)) { + NSString *error = nil; + SKPaymentDiscount *paymentDiscount = + [FIAObjectTranslator getSKPaymentDiscountFromMap:self.paymentDiscountMap withError:&error]; + + XCTAssertEqual(paymentDiscount.identifier, self.paymentDiscountMap[@"identifier"]); + XCTAssertEqual(paymentDiscount.keyIdentifier, self.paymentDiscountMap[@"keyIdentifier"]); + XCTAssertEqualObjects(paymentDiscount.nonce, + [[NSUUID alloc] initWithUUIDString:self.paymentDiscountMap[@"nonce"]]); + XCTAssertEqual(paymentDiscount.signature, self.paymentDiscountMap[@"signature"]); + XCTAssertEqual(paymentDiscount.timestamp, self.paymentDiscountMap[@"timestamp"]); + } +} + +- (void)testSKPaymentDiscountFromMapMissingIdentifier { + if (@available(iOS 12.2, *)) { + NSArray *invalidValues = @[ [NSNull null], @(1), @"" ]; + + for (id value in invalidValues) { + NSDictionary *discountMap = @{ + @"identifier" : value, + @"keyIdentifier" : @"payment_discount_key_identifier", + @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", + @"signature" : @"this is a encrypted signature", + @"timestamp" : @([NSDate date].timeIntervalSince1970), + }; + + NSString *error = nil; + [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; + + XCTAssertNotNil(error); + XCTAssertEqualObjects( + error, @"When specifying a payment discount the 'identifier' field is mandatory."); + } + } +} + +- (void)testGetMapFromSKProductDiscountMissingIdentifier { + if (@available(iOS 12.2, *)) { + SKProductDiscountStub *discount = + [[SKProductDiscountStub alloc] initWithMap:self.discountMissingIdentifierMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKProductDiscount:discount]; + XCTAssertEqualObjects(map, self.discountMissingIdentifierMap); + } +} + +- (void)testSKPaymentDiscountFromMapMissingKeyIdentifier { + if (@available(iOS 12.2, *)) { + NSArray *invalidValues = @[ [NSNull null], @(1), @"" ]; + + for (id value in invalidValues) { + NSDictionary *discountMap = @{ + @"identifier" : @"payment_discount_identifier", + @"keyIdentifier" : value, + @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", + @"signature" : @"this is a encrypted signature", + @"timestamp" : @([NSDate date].timeIntervalSince1970), + }; + + NSString *error = nil; + [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; + + XCTAssertNotNil(error); + XCTAssertEqualObjects( + error, @"When specifying a payment discount the 'keyIdentifier' field is mandatory."); + } + } +} + +- (void)testSKPaymentDiscountFromMapMissingNonce { + if (@available(iOS 12.2, *)) { + NSArray *invalidValues = @[ [NSNull null], @(1), @"" ]; + + for (id value in invalidValues) { + NSDictionary *discountMap = @{ + @"identifier" : @"payment_discount_identifier", + @"keyIdentifier" : @"payment_discount_key_identifier", + @"nonce" : value, + @"signature" : @"this is a encrypted signature", + @"timestamp" : @([NSDate date].timeIntervalSince1970), + }; + + NSString *error = nil; + [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; + + XCTAssertNotNil(error); + XCTAssertEqualObjects(error, + @"When specifying a payment discount the 'nonce' field is mandatory."); + } + } +} + +- (void)testSKPaymentDiscountFromMapMissingSignature { + if (@available(iOS 12.2, *)) { + NSArray *invalidValues = @[ [NSNull null], @(1), @"" ]; + + for (id value in invalidValues) { + NSDictionary *discountMap = @{ + @"identifier" : @"payment_discount_identifier", + @"keyIdentifier" : @"payment_discount_key_identifier", + @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", + @"signature" : value, + @"timestamp" : @([NSDate date].timeIntervalSince1970), + }; + + NSString *error = nil; + [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; + + XCTAssertNotNil(error); + XCTAssertEqualObjects( + error, @"When specifying a payment discount the 'signature' field is mandatory."); + } + } +} + +- (void)testSKPaymentDiscountFromMapMissingTimestamp { + if (@available(iOS 12.2, *)) { + NSArray *invalidValues = @[ [NSNull null], @"", @(-1) ]; + + for (id value in invalidValues) { + NSDictionary *discountMap = @{ + @"identifier" : @"payment_discount_identifier", + @"keyIdentifier" : @"payment_discount_key_identifier", + @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", + @"signature" : @"this is a encrypted signature", + @"timestamp" : value, + }; + + NSString *error = nil; + [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; + + XCTAssertNotNil(error); + XCTAssertEqualObjects( + error, @"When specifying a payment discount the 'timestamp' field is mandatory."); + } + } +} + +- (void)testSKPaymentDiscountFromMapOverflowingTimestamp { + if (@available(iOS 12.2, *)) { + NSDictionary *discountMap = @{ + @"identifier" : @"payment_discount_identifier", + @"keyIdentifier" : @"payment_discount_key_identifier", + @"nonce" : @"d18981e0-9003-4365-98a2-4b90e3b62c52", + @"signature" : @"this is a encrypted signature", + @"timestamp" : @1665044583595, // timestamp 2022 Oct + }; + NSString *error = nil; + SKPaymentDiscount *paymentDiscount = + [FIAObjectTranslator getSKPaymentDiscountFromMap:discountMap withError:&error]; + XCTAssertNil(error); + XCTAssertNotNil(paymentDiscount); + XCTAssertEqual(paymentDiscount.identifier, discountMap[@"identifier"]); + XCTAssertEqual(paymentDiscount.keyIdentifier, discountMap[@"keyIdentifier"]); + XCTAssertEqualObjects(paymentDiscount.nonce, + [[NSUUID alloc] initWithUUIDString:discountMap[@"nonce"]]); + XCTAssertEqual(paymentDiscount.signature, discountMap[@"signature"]); + XCTAssertEqual(paymentDiscount.timestamp, discountMap[@"timestamp"]); + } +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Assets/.gitkeep b/packages/in_app_purchase/in_app_purchase_storekit/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.h deleted file mode 100644 index eb97ceb44754..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.h +++ /dev/null @@ -1,62 +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 -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface FIAObjectTranslator : NSObject - -// Converts an instance of SKProduct into a dictionary. -+ (NSDictionary *)getMapFromSKProduct:(SKProduct *)product; - -// Converts an instance of SKProductSubscriptionPeriod into a dictionary. -+ (NSDictionary *)getMapFromSKProductSubscriptionPeriod:(SKProductSubscriptionPeriod *)period - API_AVAILABLE(ios(11.2)); - -// Converts an instance of SKProductDiscount into a dictionary. -+ (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount - API_AVAILABLE(ios(11.2)); - -// Converts an array of SKProductDiscount instances into an array of dictionaries. -+ (nonnull NSArray *)getMapArrayFromSKProductDiscounts: - (nonnull NSArray *)productDiscounts API_AVAILABLE(ios(12.2)); - -// Converts an instance of SKProductsResponse into a dictionary. -+ (NSDictionary *)getMapFromSKProductsResponse:(SKProductsResponse *)productResponse; - -// Converts an instance of SKPayment into a dictionary. -+ (NSDictionary *)getMapFromSKPayment:(SKPayment *)payment; - -// Converts an instance of NSLocale into a dictionary. -+ (NSDictionary *)getMapFromNSLocale:(NSLocale *)locale; - -// Creates an instance of the SKMutablePayment class based on the supplied dictionary. -+ (SKMutablePayment *)getSKMutablePaymentFromMap:(NSDictionary *)map; - -// Converts an instance of SKPaymentTransaction into a dictionary. -+ (NSDictionary *)getMapFromSKPaymentTransaction:(SKPaymentTransaction *)transaction; - -// Converts an instance of NSError into a dictionary. -+ (NSDictionary *)getMapFromNSError:(NSError *)error; - -// Converts an instance of SKStorefront into a dictionary. -+ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront - API_AVAILABLE(ios(13), macos(10.15), watchos(6.2)); - -// Converts the supplied instances of SKStorefront and SKPaymentTransaction into a dictionary. -+ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront - andSKPaymentTransaction:(SKPaymentTransaction *)transaction - API_AVAILABLE(ios(13), macos(10.15), watchos(6.2)); - -// Creates an instance of the SKPaymentDiscount class based on the supplied dictionary. -+ (nullable SKPaymentDiscount *)getSKPaymentDiscountFromMap:(NSDictionary *)map - withError:(NSString *_Nullable *_Nullable)error - API_AVAILABLE(ios(12.2)); - -@end -; - -NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.h new file mode 120000 index 000000000000..6b974bc7d268 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.h @@ -0,0 +1 @@ +../../darwin/Classes/FIAObjectTranslator.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m deleted file mode 100644 index c656b58808b3..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m +++ /dev/null @@ -1,297 +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 "FIAObjectTranslator.h" - -#pragma mark - SKProduct Coders - -@implementation FIAObjectTranslator - -+ (NSDictionary *)getMapFromSKProduct:(SKProduct *)product { - if (!product) { - return nil; - } - NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ - @"localizedDescription" : product.localizedDescription ?: [NSNull null], - @"localizedTitle" : product.localizedTitle ?: [NSNull null], - @"productIdentifier" : product.productIdentifier ?: [NSNull null], - @"price" : product.price.description ?: [NSNull null] - - }]; - // TODO(cyanglaz): NSLocale is a complex object, want to see the actual need of getting this - // expanded to a map. Matching android to only get the currencySymbol for now. - // https://github.com/flutter/flutter/issues/26610 - [map setObject:[FIAObjectTranslator getMapFromNSLocale:product.priceLocale] ?: [NSNull null] - forKey:@"priceLocale"]; - if (@available(iOS 11.2, *)) { - [map setObject:[FIAObjectTranslator - getMapFromSKProductSubscriptionPeriod:product.subscriptionPeriod] - ?: [NSNull null] - forKey:@"subscriptionPeriod"]; - } - if (@available(iOS 11.2, *)) { - [map setObject:[FIAObjectTranslator getMapFromSKProductDiscount:product.introductoryPrice] - ?: [NSNull null] - forKey:@"introductoryPrice"]; - } - if (@available(iOS 12.2, *)) { - [map setObject:[FIAObjectTranslator getMapArrayFromSKProductDiscounts:product.discounts] - forKey:@"discounts"]; - } - if (@available(iOS 12.0, *)) { - [map setObject:product.subscriptionGroupIdentifier ?: [NSNull null] - forKey:@"subscriptionGroupIdentifier"]; - } - return map; -} - -+ (NSDictionary *)getMapFromSKProductSubscriptionPeriod:(SKProductSubscriptionPeriod *)period { - if (!period) { - return nil; - } - return @{@"numberOfUnits" : @(period.numberOfUnits), @"unit" : @(period.unit)}; -} - -+ (nonnull NSArray *)getMapArrayFromSKProductDiscounts: - (nonnull NSArray *)productDiscounts { - NSMutableArray *discountsMapArray = [[NSMutableArray alloc] init]; - - for (SKProductDiscount *productDiscount in productDiscounts) { - [discountsMapArray addObject:[FIAObjectTranslator getMapFromSKProductDiscount:productDiscount]]; - } - - return discountsMapArray; -} - -+ (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount { - if (!discount) { - return nil; - } - NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ - @"price" : discount.price.description ?: [NSNull null], - @"numberOfPeriods" : @(discount.numberOfPeriods), - @"subscriptionPeriod" : - [FIAObjectTranslator getMapFromSKProductSubscriptionPeriod:discount.subscriptionPeriod] - ?: [NSNull null], - @"paymentMode" : @(discount.paymentMode), - }]; - if (@available(iOS 12.2, *)) { - [map setObject:discount.identifier ?: [NSNull null] forKey:@"identifier"]; - [map setObject:@(discount.type) forKey:@"type"]; - } - - // TODO(cyanglaz): NSLocale is a complex object, want to see the actual need of getting this - // expanded to a map. Matching android to only get the currencySymbol for now. - // https://github.com/flutter/flutter/issues/26610 - [map setObject:[FIAObjectTranslator getMapFromNSLocale:discount.priceLocale] ?: [NSNull null] - forKey:@"priceLocale"]; - return map; -} - -+ (NSDictionary *)getMapFromSKProductsResponse:(SKProductsResponse *)productResponse { - if (!productResponse) { - return nil; - } - NSMutableArray *productsMapArray = [NSMutableArray new]; - for (SKProduct *product in productResponse.products) { - [productsMapArray addObject:[FIAObjectTranslator getMapFromSKProduct:product]]; - } - return @{ - @"products" : productsMapArray, - @"invalidProductIdentifiers" : productResponse.invalidProductIdentifiers ?: @[] - }; -} - -+ (NSDictionary *)getMapFromSKPayment:(SKPayment *)payment { - if (!payment) { - return nil; - } - NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ - @"productIdentifier" : payment.productIdentifier ?: [NSNull null], - @"requestData" : payment.requestData ? [[NSString alloc] initWithData:payment.requestData - encoding:NSUTF8StringEncoding] - : [NSNull null], - @"quantity" : @(payment.quantity), - @"applicationUsername" : payment.applicationUsername ?: [NSNull null] - }]; - [map setObject:@(payment.simulatesAskToBuyInSandbox) forKey:@"simulatesAskToBuyInSandbox"]; - return map; -} - -+ (NSDictionary *)getMapFromNSLocale:(NSLocale *)locale { - if (!locale) { - return nil; - } - NSMutableDictionary *map = [[NSMutableDictionary alloc] init]; - [map setObject:[locale objectForKey:NSLocaleCurrencySymbol] ?: [NSNull null] - forKey:@"currencySymbol"]; - [map setObject:[locale objectForKey:NSLocaleCurrencyCode] ?: [NSNull null] - forKey:@"currencyCode"]; - [map setObject:[locale objectForKey:NSLocaleCountryCode] ?: [NSNull null] forKey:@"countryCode"]; - return map; -} - -+ (SKMutablePayment *)getSKMutablePaymentFromMap:(NSDictionary *)map { - if (!map) { - return nil; - } - SKMutablePayment *payment = [[SKMutablePayment alloc] init]; - payment.productIdentifier = map[@"productIdentifier"]; - NSString *utf8String = map[@"requestData"]; - payment.requestData = [utf8String dataUsingEncoding:NSUTF8StringEncoding]; - payment.quantity = [map[@"quantity"] integerValue]; - payment.applicationUsername = map[@"applicationUsername"]; - payment.simulatesAskToBuyInSandbox = [map[@"simulatesAskToBuyInSandbox"] boolValue]; - return payment; -} - -+ (NSDictionary *)getMapFromSKPaymentTransaction:(SKPaymentTransaction *)transaction { - if (!transaction) { - return nil; - } - NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ - @"error" : [FIAObjectTranslator getMapFromNSError:transaction.error] ?: [NSNull null], - @"payment" : transaction.payment ? [FIAObjectTranslator getMapFromSKPayment:transaction.payment] - : [NSNull null], - @"originalTransaction" : transaction.originalTransaction - ? [FIAObjectTranslator getMapFromSKPaymentTransaction:transaction.originalTransaction] - : [NSNull null], - @"transactionTimeStamp" : transaction.transactionDate - ? @(transaction.transactionDate.timeIntervalSince1970) - : [NSNull null], - @"transactionIdentifier" : transaction.transactionIdentifier ?: [NSNull null], - @"transactionState" : @(transaction.transactionState) - }]; - - return map; -} - -+ (NSDictionary *)getMapFromNSError:(NSError *)error { - if (!error) { - return nil; - } - - NSMutableDictionary *userInfo = [NSMutableDictionary new]; - for (NSErrorUserInfoKey key in error.userInfo) { - id value = error.userInfo[key]; - userInfo[key] = [FIAObjectTranslator encodeNSErrorUserInfo:value]; - } - return @{@"code" : @(error.code), @"domain" : error.domain ?: @"", @"userInfo" : userInfo}; -} - -+ (id)encodeNSErrorUserInfo:(id)value { - if ([value isKindOfClass:[NSError class]]) { - return [FIAObjectTranslator getMapFromNSError:value]; - } else if ([value isKindOfClass:[NSURL class]]) { - return [value absoluteString]; - } else if ([value isKindOfClass:[NSNumber class]]) { - return value; - } else if ([value isKindOfClass:[NSString class]]) { - return value; - } else if ([value isKindOfClass:[NSArray class]]) { - NSMutableArray *errors = [[NSMutableArray alloc] init]; - for (id error in value) { - [errors addObject:[FIAObjectTranslator encodeNSErrorUserInfo:error]]; - } - return errors; - } else { - return [NSString - stringWithFormat: - @"Unable to encode native userInfo object of type %@ to map. Please submit an issue at " - @"https://github.com/flutter/flutter/issues/new with the title " - @"\"[in_app_purchase_storekit] " - @"Unable to encode userInfo of type %@\" and add reproduction steps and the error " - @"details in " - @"the description field.", - [value class], [value class]]; - } -} - -+ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront { - if (!storefront) { - return nil; - } - - NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ - @"countryCode" : storefront.countryCode, - @"identifier" : storefront.identifier - }]; - - return map; -} - -+ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront - andSKPaymentTransaction:(SKPaymentTransaction *)transaction { - if (!storefront || !transaction) { - return nil; - } - - NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ - @"storefront" : [FIAObjectTranslator getMapFromSKStorefront:storefront], - @"transaction" : [FIAObjectTranslator getMapFromSKPaymentTransaction:transaction] - }]; - - return map; -} - -+ (SKPaymentDiscount *)getSKPaymentDiscountFromMap:(NSDictionary *)map - withError:(NSString **)error { - if (!map || map.count <= 0) { - return nil; - } - - NSString *identifier = map[@"identifier"]; - NSString *keyIdentifier = map[@"keyIdentifier"]; - NSString *nonce = map[@"nonce"]; - NSString *signature = map[@"signature"]; - NSNumber *timestamp = map[@"timestamp"]; - - if (!identifier || ![identifier isKindOfClass:NSString.class] || - [identifier isEqualToString:@""]) { - if (error) { - *error = @"When specifying a payment discount the 'identifier' field is mandatory."; - } - return nil; - } - - if (!keyIdentifier || ![keyIdentifier isKindOfClass:NSString.class] || - [keyIdentifier isEqualToString:@""]) { - if (error) { - *error = @"When specifying a payment discount the 'keyIdentifier' field is mandatory."; - } - return nil; - } - - if (!nonce || ![nonce isKindOfClass:NSString.class] || [nonce isEqualToString:@""]) { - if (error) { - *error = @"When specifying a payment discount the 'nonce' field is mandatory."; - } - return nil; - } - - if (!signature || ![signature isKindOfClass:NSString.class] || [signature isEqualToString:@""]) { - if (error) { - *error = @"When specifying a payment discount the 'signature' field is mandatory."; - } - return nil; - } - - if (!timestamp || ![timestamp isKindOfClass:NSNumber.class] || [timestamp longLongValue] <= 0) { - if (error) { - *error = @"When specifying a payment discount the 'timestamp' field is mandatory."; - } - return nil; - } - - SKPaymentDiscount *discount = - [[SKPaymentDiscount alloc] initWithIdentifier:identifier - keyIdentifier:keyIdentifier - nonce:[[NSUUID alloc] initWithUUIDString:nonce] - signature:signature - timestamp:timestamp]; - - return discount; -} - -@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m new file mode 120000 index 000000000000..f9b4ffe6732d --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m @@ -0,0 +1 @@ +../../darwin/Classes/FIAObjectTranslator.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.h deleted file mode 100644 index a6c91fa9e6b6..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.h +++ /dev/null @@ -1,16 +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 -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -API_AVAILABLE(ios(13)) -@interface FIAPPaymentQueueDelegate : NSObject -- (id)initWithMethodChannel:(FlutterMethodChannel *)methodChannel; -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.h new file mode 120000 index 000000000000..e4b452397bc2 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.h @@ -0,0 +1 @@ +../../darwin/Classes/FIAPPaymentQueueDelegate.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.m deleted file mode 100644 index 1056086030a5..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.m +++ /dev/null @@ -1,78 +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 "FIAPPaymentQueueDelegate.h" -#import "FIAObjectTranslator.h" - -@interface FIAPPaymentQueueDelegate () - -@property(strong, nonatomic, readonly) FlutterMethodChannel *callbackChannel; - -@end - -@implementation FIAPPaymentQueueDelegate - -- (id)initWithMethodChannel:(FlutterMethodChannel *)methodChannel { - self = [super init]; - if (self) { - _callbackChannel = methodChannel; - } - - return self; -} - -- (BOOL)paymentQueue:(SKPaymentQueue *)paymentQueue - shouldContinueTransaction:(SKPaymentTransaction *)transaction - inStorefront:(SKStorefront *)newStorefront { - // Default return value for this method is true (see - // https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate/3521328-paymentqueueshouldshowpriceconse?language=objc) - __block BOOL shouldContinue = YES; - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - [self.callbackChannel invokeMethod:@"shouldContinueTransaction" - arguments:[FIAObjectTranslator getMapFromSKStorefront:newStorefront - andSKPaymentTransaction:transaction] - result:^(id _Nullable result) { - // When result is a valid instance of NSNumber use it to determine - // if the transaction should continue. Otherwise use the default - // value. - if (result && [result isKindOfClass:[NSNumber class]]) { - shouldContinue = [(NSNumber *)result boolValue]; - } - - dispatch_semaphore_signal(semaphore); - }]; - - // The client should respond within 1 second otherwise continue - // with default value. - dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)); - - return shouldContinue; -} - -- (BOOL)paymentQueueShouldShowPriceConsent:(SKPaymentQueue *)paymentQueue { - // Default return value for this method is true (see - // https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate/3521328-paymentqueueshouldshowpriceconse?language=objc) - __block BOOL shouldShowPriceConsent = YES; - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - [self.callbackChannel invokeMethod:@"shouldShowPriceConsent" - arguments:nil - result:^(id _Nullable result) { - // When result is a valid instance of NSNumber use it to determine - // if the transaction should continue. Otherwise use the default - // value. - if (result && [result isKindOfClass:[NSNumber class]]) { - shouldShowPriceConsent = [(NSNumber *)result boolValue]; - } - - dispatch_semaphore_signal(semaphore); - }]; - - // The client should respond within 1 second otherwise continue - // with default value. - dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)); - - return shouldShowPriceConsent; -} - -@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.m new file mode 120000 index 000000000000..a1b95ef97c1b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.m @@ -0,0 +1 @@ +../../darwin/Classes/FIAPPaymentQueueDelegate.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.h deleted file mode 100644 index 94020ff2348b..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.h +++ /dev/null @@ -1,17 +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 - -NS_ASSUME_NONNULL_BEGIN - -@class FlutterError; - -@interface FIAPReceiptManager : NSObject - -- (nullable NSString *)retrieveReceiptWithError:(FlutterError *_Nullable *_Nullable)error; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.h new file mode 120000 index 000000000000..88f02af0b00a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.h @@ -0,0 +1 @@ +../../darwin/Classes/FIAPReceiptManager.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.m deleted file mode 100644 index b359b415d873..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.m +++ /dev/null @@ -1,38 +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 "FIAPReceiptManager.h" -#import -#import "FIAObjectTranslator.h" - -@interface FIAPReceiptManager () -// Gets the receipt file data from the location of the url. Can be nil if -// there is an error. This interface is defined so it can be stubbed for testing. -- (NSData *)getReceiptData:(NSURL *)url error:(NSError **)error; - -@end - -@implementation FIAPReceiptManager - -- (NSString *)retrieveReceiptWithError:(FlutterError **)flutterError { - NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; - NSError *receiptError; - NSData *receipt = [self getReceiptData:receiptURL error:&receiptError]; - if (!receipt || receiptError) { - if (flutterError) { - NSDictionary *errorMap = [FIAObjectTranslator getMapFromNSError:receiptError]; - *flutterError = [FlutterError errorWithCode:errorMap[@"code"] - message:errorMap[@"domain"] - details:errorMap[@"userInfo"]]; - } - return nil; - } - return [receipt base64EncodedStringWithOptions:kNilOptions]; -} - -- (NSData *)getReceiptData:(NSURL *)url error:(NSError **)error { - return [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:error]; -} - -@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.m new file mode 120000 index 000000000000..f303c3c162a0 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.m @@ -0,0 +1 @@ +../../darwin/Classes/FIAPReceiptManager.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.h deleted file mode 100644 index cbf21d6e161f..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.h +++ /dev/null @@ -1,20 +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 -#import - -NS_ASSUME_NONNULL_BEGIN - -typedef void (^ProductRequestCompletion)(SKProductsResponse *_Nullable response, - NSError *_Nullable errror); - -@interface FIAPRequestHandler : NSObject - -- (instancetype)initWithRequest:(SKRequest *)request; -- (void)startProductRequestWithCompletionHandler:(ProductRequestCompletion)completion; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.h new file mode 120000 index 000000000000..9eb31f26b048 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.h @@ -0,0 +1 @@ +../../darwin/Classes/FIAPRequestHandler.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.m deleted file mode 100644 index 8767265d8544..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.m +++ /dev/null @@ -1,55 +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 "FIAPRequestHandler.h" -#import - -#pragma mark - Main Handler - -@interface FIAPRequestHandler () - -@property(copy, nonatomic) ProductRequestCompletion completion; -@property(strong, nonatomic) SKRequest *request; - -@end - -@implementation FIAPRequestHandler - -- (instancetype)initWithRequest:(SKRequest *)request { - self = [super init]; - if (self) { - self.request = request; - request.delegate = self; - } - return self; -} - -- (void)startProductRequestWithCompletionHandler:(ProductRequestCompletion)completion { - self.completion = completion; - [self.request start]; -} - -- (void)productsRequest:(SKProductsRequest *)request - didReceiveResponse:(SKProductsResponse *)response { - if (self.completion) { - self.completion(response, nil); - // set the completion to nil here so self.completion won't be triggered again in - // requestDidFinish for SKProductRequest. - self.completion = nil; - } -} - -- (void)requestDidFinish:(SKRequest *)request { - if (self.completion) { - self.completion(nil, nil); - } -} - -- (void)request:(SKRequest *)request didFailWithError:(NSError *)error { - if (self.completion) { - self.completion(nil, error); - } -} - -@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.m new file mode 120000 index 000000000000..d6976dc0dd26 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.m @@ -0,0 +1 @@ +../../darwin/Classes/FIAPRequestHandler.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.h deleted file mode 100644 index bb074aa6c577..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.h +++ /dev/null @@ -1,132 +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 -#import -#import "FIATransactionCache.h" - -@class SKPaymentTransaction; - -NS_ASSUME_NONNULL_BEGIN - -typedef void (^TransactionsUpdated)(NSArray *transactions); -typedef void (^TransactionsRemoved)(NSArray *transactions); -typedef void (^RestoreTransactionFailed)(NSError *error); -typedef void (^RestoreCompletedTransactionsFinished)(void); -typedef BOOL (^ShouldAddStorePayment)(SKPayment *payment, SKProduct *product); -typedef void (^UpdatedDownloads)(NSArray *downloads); - -@interface FIAPaymentQueueHandler : NSObject - -@property(NS_NONATOMIC_IOSONLY, weak, nullable) id delegate API_AVAILABLE( - ios(13.0), macos(10.15), watchos(6.2)); - -/// Creates a new FIAPaymentQueueHandler initialized with an empty -/// FIATransactionCache. -/// -/// @param queue The SKPaymentQueue instance connected to the App Store and -/// responsible for processing transactions. -/// @param transactionsUpdated Callback method that is called each time the App -/// Store indicates transactions are updated. -/// @param transactionsRemoved Callback method that is called each time the App -/// Store indicates transactions are removed. -/// @param restoreTransactionFailed Callback method that is called each time -/// the App Store indicates transactions failed -/// to restore. -/// @param restoreCompletedTransactionsFinished Callback method that is called -/// each time the App Store -/// indicates restoring of -/// transactions has finished. -/// @param shouldAddStorePayment Callback method that is called each time an -/// in-app purchase has been initiated from the -/// App Store. -/// @param updatedDownloads Callback method that is called each time the App -/// Store indicates downloads are updated. -- (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue - transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated - transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved - restoreTransactionFailed:(nullable RestoreTransactionFailed)restoreTransactionFailed - restoreCompletedTransactionsFinished: - (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished - shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment - updatedDownloads:(nullable UpdatedDownloads)updatedDownloads - DEPRECATED_MSG_ATTRIBUTE( - "Use the " - "'initWithQueue:transactionsUpdated:transactionsRemoved:restoreTransactionsFinished:" - "shouldAddStorePayment:updatedDownloads:transactionCache:' message instead."); - -/// Creates a new FIAPaymentQueueHandler. -/// -/// The "transactionsUpdated", "transactionsRemoved" and "updatedDownloads" -/// callbacks are only called while actively observing transactions. To start -/// observing transactions send the "startObservingPaymentQueue" message. -/// Sending the "stopObservingPaymentQueue" message will stop actively -/// observing transactions. When transactions are not observed they are cached -/// to the "transactionCache" and will be delivered via the -/// "transactionsUpdated", "transactionsRemoved" and "updatedDownloads" -/// callbacks as soon as the "startObservingPaymentQueue" message arrives. -/// -/// Note: cached transactions that are not processed when the application is -/// killed will be delivered again by the App Store as soon as the application -/// starts again. -/// -/// @param queue The SKPaymentQueue instance connected to the App Store and -/// responsible for processing transactions. -/// @param transactionsUpdated Callback method that is called each time the App -/// Store indicates transactions are updated. -/// @param transactionsRemoved Callback method that is called each time the App -/// Store indicates transactions are removed. -/// @param restoreTransactionFailed Callback method that is called each time -/// the App Store indicates transactions failed -/// to restore. -/// @param restoreCompletedTransactionsFinished Callback method that is called -/// each time the App Store -/// indicates restoring of -/// transactions has finished. -/// @param shouldAddStorePayment Callback method that is called each time an -/// in-app purchase has been initiated from the -/// App Store. -/// @param updatedDownloads Callback method that is called each time the App -/// Store indicates downloads are updated. -/// @param transactionCache An empty [FIATransactionCache] instance that is -/// responsible for keeping track of transactions that -/// arrive when not actively observing transactions. -- (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue - transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated - transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved - restoreTransactionFailed:(nullable RestoreTransactionFailed)restoreTransactionFailed - restoreCompletedTransactionsFinished: - (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished - shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment - updatedDownloads:(nullable UpdatedDownloads)updatedDownloads - transactionCache:(nonnull FIATransactionCache *)transactionCache; -// Can throw exceptions if the transaction type is purchasing, should always used in a @try block. -- (void)finishTransaction:(nonnull SKPaymentTransaction *)transaction; -- (void)restoreTransactions:(nullable NSString *)applicationName; -- (void)presentCodeRedemptionSheet; -- (NSArray *)getUnfinishedTransactions; - -// This method needs to be called before any other methods. -- (void)startObservingPaymentQueue; -// Call this method when the Flutter app is no longer listening -- (void)stopObservingPaymentQueue; - -// Appends a payment to the SKPaymentQueue. -// -// @param payment Payment object to be added to the payment queue. -// @return whether "addPayment" was successful. -- (BOOL)addPayment:(SKPayment *)payment; - -// Displays the price consent sheet. -// -// The price consent sheet is only displayed when the following -// it true: -// - You have increased the price of the subscription in App Store Connect. -// - The subscriber has not yet responded to a price consent query. -// Otherwise the method has no effect. -- (void)showPriceConsentIfNeeded API_AVAILABLE(ios(13.4)); - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.h new file mode 120000 index 000000000000..6bc9c2f6dc85 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.h @@ -0,0 +1 @@ +../../darwin/Classes/FIAPaymentQueueHandler.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.m deleted file mode 100644 index 59fdceded2bc..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.m +++ /dev/null @@ -1,232 +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 "FIAPaymentQueueHandler.h" -#import "FIAPPaymentQueueDelegate.h" -#import "FIATransactionCache.h" - -@interface FIAPaymentQueueHandler () - -/// The SKPaymentQueue instance connected to the App Store and responsible for processing -/// transactions. -@property(strong, nonatomic) SKPaymentQueue *queue; - -/// Callback method that is called each time the App Store indicates transactions are updated. -@property(nullable, copy, nonatomic) TransactionsUpdated transactionsUpdated; - -/// Callback method that is called each time the App Store indicates transactions are removed. -@property(nullable, copy, nonatomic) TransactionsRemoved transactionsRemoved; - -/// Callback method that is called each time the App Store indicates transactions failed to restore. -@property(nullable, copy, nonatomic) RestoreTransactionFailed restoreTransactionFailed; - -/// Callback method that is called each time the App Store indicates restoring of transactions has -/// finished. -@property(nullable, copy, nonatomic) - RestoreCompletedTransactionsFinished paymentQueueRestoreCompletedTransactionsFinished; - -/// Callback method that is called each time an in-app purchase has been initiated from the App -/// Store. -@property(nullable, copy, nonatomic) ShouldAddStorePayment shouldAddStorePayment; - -/// Callback method that is called each time the App Store indicates downloads are updated. -@property(nullable, copy, nonatomic) UpdatedDownloads updatedDownloads; - -/// The transaction cache responsible for caching transactions. -/// -/// Keeps track of transactions that arrive when the Flutter client is not -/// actively observing for transactions. -@property(strong, nonatomic, nonnull) FIATransactionCache *transactionCache; - -/// Indicates if the Flutter client is observing transactions. -/// -/// When the client is not observing, transactions are cached and send to the -/// client as soon as it starts observing. The Flutter client can start -/// observing by sending a startObservingPaymentQueue message and stop by -/// sending a stopObservingPaymentQueue message. -@property(atomic, assign, readwrite, getter=isObservingTransactions) BOOL observingTransactions; - -@end - -@implementation FIAPaymentQueueHandler - -- (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue - transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated - transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved - restoreTransactionFailed:(nullable RestoreTransactionFailed)restoreTransactionFailed - restoreCompletedTransactionsFinished: - (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished - shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment - updatedDownloads:(nullable UpdatedDownloads)updatedDownloads { - return [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:transactionsUpdated - transactionRemoved:transactionsRemoved - restoreTransactionFailed:restoreTransactionFailed - restoreCompletedTransactionsFinished:restoreCompletedTransactionsFinished - shouldAddStorePayment:shouldAddStorePayment - updatedDownloads:updatedDownloads - transactionCache:[[FIATransactionCache alloc] init]]; -} - -- (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue - transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated - transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved - restoreTransactionFailed:(nullable RestoreTransactionFailed)restoreTransactionFailed - restoreCompletedTransactionsFinished: - (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished - shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment - updatedDownloads:(nullable UpdatedDownloads)updatedDownloads - transactionCache:(nonnull FIATransactionCache *)transactionCache { - self = [super init]; - if (self) { - _queue = queue; - _transactionsUpdated = transactionsUpdated; - _transactionsRemoved = transactionsRemoved; - _restoreTransactionFailed = restoreTransactionFailed; - _paymentQueueRestoreCompletedTransactionsFinished = restoreCompletedTransactionsFinished; - _shouldAddStorePayment = shouldAddStorePayment; - _updatedDownloads = updatedDownloads; - _transactionCache = transactionCache; - - [_queue addTransactionObserver:self]; - if (@available(iOS 13.0, macOS 10.15, *)) { - queue.delegate = self.delegate; - } - } - return self; -} - -- (void)startObservingPaymentQueue { - self.observingTransactions = YES; - - [self processCachedTransactions]; -} - -- (void)stopObservingPaymentQueue { - // When the client stops observing transaction, the transaction observer is - // not removed from the SKPaymentQueue. The FIAPaymentQueueHandler will cache - // trasnactions in memory when the client is not observing, allowing the app - // to process these transactions if it starts observing again during the same - // lifetime of the app. - // - // If the app is killed, cached transactions will be removed from memory; - // however, the App Store will re-deliver the transactions as soon as the app - // is started again, since the cached transactions have not been acknowledged - // by the client (by sending the `finishTransaction` message). - self.observingTransactions = NO; -} - -- (void)processCachedTransactions { - NSArray *cachedObjects = - [self.transactionCache getObjectsForKey:TransactionCacheKeyUpdatedTransactions]; - if (cachedObjects.count != 0) { - self.transactionsUpdated(cachedObjects); - } - - cachedObjects = [self.transactionCache getObjectsForKey:TransactionCacheKeyUpdatedDownloads]; - if (cachedObjects.count != 0) { - self.updatedDownloads(cachedObjects); - } - - cachedObjects = [self.transactionCache getObjectsForKey:TransactionCacheKeyRemovedTransactions]; - if (cachedObjects.count != 0) { - self.transactionsRemoved(cachedObjects); - } - - [self.transactionCache clear]; -} - -- (BOOL)addPayment:(SKPayment *)payment { - for (SKPaymentTransaction *transaction in self.queue.transactions) { - if ([transaction.payment.productIdentifier isEqualToString:payment.productIdentifier]) { - return NO; - } - } - [self.queue addPayment:payment]; - return YES; -} - -- (void)finishTransaction:(SKPaymentTransaction *)transaction { - [self.queue finishTransaction:transaction]; -} - -- (void)restoreTransactions:(nullable NSString *)applicationName { - if (applicationName) { - [self.queue restoreCompletedTransactionsWithApplicationUsername:applicationName]; - } else { - [self.queue restoreCompletedTransactions]; - } -} - -- (void)presentCodeRedemptionSheet { - if (@available(iOS 14, *)) { - [self.queue presentCodeRedemptionSheet]; - } else { - NSLog(@"presentCodeRedemptionSheet is only available on iOS 14 or newer"); - } -} - -- (void)showPriceConsentIfNeeded { - [self.queue showPriceConsentIfNeeded]; -} - -#pragma mark - observing - -// Sent when the transaction array has changed (additions or state changes). Client should check -// state of transactions and finish as appropriate. -- (void)paymentQueue:(SKPaymentQueue *)queue - updatedTransactions:(NSArray *)transactions { - if (!self.observingTransactions) { - [_transactionCache addObjects:transactions forKey:TransactionCacheKeyUpdatedTransactions]; - return; - } - - // notify dart through callbacks. - self.transactionsUpdated(transactions); -} - -// Sent when transactions are removed from the queue (via finishTransaction:). -- (void)paymentQueue:(SKPaymentQueue *)queue - removedTransactions:(NSArray *)transactions { - if (!self.observingTransactions) { - [_transactionCache addObjects:transactions forKey:TransactionCacheKeyRemovedTransactions]; - return; - } - self.transactionsRemoved(transactions); -} - -// Sent when an error is encountered while adding transactions from the user's purchase history back -// to the queue. -- (void)paymentQueue:(SKPaymentQueue *)queue - restoreCompletedTransactionsFailedWithError:(NSError *)error { - self.restoreTransactionFailed(error); -} - -// Sent when all transactions from the user's purchase history have successfully been added back to -// the queue. -- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue { - self.paymentQueueRestoreCompletedTransactionsFinished(); -} - -// Sent when the download state has changed. -- (void)paymentQueue:(SKPaymentQueue *)queue updatedDownloads:(NSArray *)downloads { - if (!self.observingTransactions) { - [_transactionCache addObjects:downloads forKey:TransactionCacheKeyUpdatedDownloads]; - return; - } - self.updatedDownloads(downloads); -} - -// Sent when a user initiates an IAP buy from the App Store -- (BOOL)paymentQueue:(SKPaymentQueue *)queue - shouldAddStorePayment:(SKPayment *)payment - forProduct:(SKProduct *)product { - return (self.shouldAddStorePayment(payment, product)); -} - -- (NSArray *)getUnfinishedTransactions { - return self.queue.transactions; -} - -@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.m new file mode 120000 index 000000000000..8c892d29f1e6 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.m @@ -0,0 +1 @@ +../../darwin/Classes/FIAPaymentQueueHandler.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.h deleted file mode 100644 index dea3c2d85d14..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.h +++ /dev/null @@ -1,31 +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. - -NS_ASSUME_NONNULL_BEGIN - -typedef NS_ENUM(NSUInteger, TransactionCacheKey) { - TransactionCacheKeyUpdatedDownloads, - TransactionCacheKeyUpdatedTransactions, - TransactionCacheKeyRemovedTransactions -}; - -@interface FIATransactionCache : NSObject - -/// Adds objects to the transaction cache. -/// -/// If the cache already contains an array of objects on the specified key, the supplied -/// array will be appended to the existing array. -- (void)addObjects:(NSArray *)objects forKey:(TransactionCacheKey)key; - -/// Gets the array of objects stored at the given key. -/// -/// If there are no objects associated with the given key nil is returned. -- (NSArray *)getObjectsForKey:(TransactionCacheKey)key; - -/// Removes all objects from the transaction cache. -- (void)clear; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.h new file mode 120000 index 000000000000..8862d80dde39 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.h @@ -0,0 +1 @@ +../../darwin/Classes/FIATransactionCache.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.m deleted file mode 100644 index f80b9c40c7bc..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.m +++ /dev/null @@ -1,40 +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 "FIATransactionCache.h" - -@interface FIATransactionCache () - -/// A NSMutableDictionary storing the objects that are cached. -@property(nonatomic, strong, nonnull) NSMutableDictionary *cache; - -@end - -@implementation FIATransactionCache - -- (instancetype)init { - self = [super init]; - if (self) { - self.cache = [[NSMutableDictionary alloc] init]; - } - - return self; -} - -- (void)addObjects:(NSArray *)objects forKey:(TransactionCacheKey)key { - NSArray *cachedObjects = self.cache[@(key)]; - - self.cache[@(key)] = - cachedObjects ? [cachedObjects arrayByAddingObjectsFromArray:objects] : objects; -} - -- (NSArray *)getObjectsForKey:(TransactionCacheKey)key { - return self.cache[@(key)]; -} - -- (void)clear { - [self.cache removeAllObjects]; -} - -@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.m new file mode 120000 index 000000000000..8c0dd87c7e97 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.m @@ -0,0 +1 @@ +../../darwin/Classes/FIATransactionCache.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.h deleted file mode 100644 index 8cb42f3fe8c2..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.h +++ /dev/null @@ -1,17 +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 -@class FIAPaymentQueueHandler; -@class FIAPReceiptManager; - -@interface InAppPurchasePlugin : NSObject - -@property(strong, nonatomic) FIAPaymentQueueHandler *paymentQueueHandler; - -- (instancetype)initWithReceiptManager:(FIAPReceiptManager *)receiptManager - NS_DESIGNATED_INITIALIZER; -- (instancetype)init NS_UNAVAILABLE; - -@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.h new file mode 120000 index 000000000000..0ec6c66d54f8 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.h @@ -0,0 +1 @@ +../../darwin/Classes/InAppPurchasePlugin.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m deleted file mode 100644 index bfc90ea43716..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m +++ /dev/null @@ -1,438 +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 "InAppPurchasePlugin.h" -#import -#import "FIAObjectTranslator.h" -#import "FIAPPaymentQueueDelegate.h" -#import "FIAPReceiptManager.h" -#import "FIAPRequestHandler.h" -#import "FIAPaymentQueueHandler.h" - -@interface InAppPurchasePlugin () - -// Holding strong references to FIAPRequestHandlers. Remove the handlers from the set after -// the request is finished. -@property(strong, nonatomic, readonly) NSMutableSet *requestHandlers; - -// After querying the product, the available products will be saved in the map to be used -// for purchase. -@property(strong, nonatomic, readonly) NSMutableDictionary *productsCache; - -// Callback channel to dart used for when a function from the transaction observer is triggered. -@property(strong, nonatomic, readonly) FlutterMethodChannel *transactionObserverCallbackChannel; - -// Callback channel to dart used for when a function from the payment queue delegate is triggered. -@property(strong, nonatomic, readonly) FlutterMethodChannel *paymentQueueDelegateCallbackChannel; -@property(strong, nonatomic, readonly) NSObject *registrar; - -@property(strong, nonatomic, readonly) FIAPReceiptManager *receiptManager; -@property(strong, nonatomic, readonly) - FIAPPaymentQueueDelegate *paymentQueueDelegate API_AVAILABLE(ios(13)); - -@end - -@implementation InAppPurchasePlugin - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" - binaryMessenger:[registrar messenger]]; - InAppPurchasePlugin *instance = [[InAppPurchasePlugin alloc] initWithRegistrar:registrar]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (instancetype)initWithReceiptManager:(FIAPReceiptManager *)receiptManager { - self = [super init]; - _receiptManager = receiptManager; - _requestHandlers = [NSMutableSet new]; - _productsCache = [NSMutableDictionary new]; - return self; -} - -- (instancetype)initWithRegistrar:(NSObject *)registrar { - self = [self initWithReceiptManager:[FIAPReceiptManager new]]; - _registrar = registrar; - - __weak typeof(self) weakSelf = self; - _paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueue defaultQueue] - transactionsUpdated:^(NSArray *_Nonnull transactions) { - [weakSelf handleTransactionsUpdated:transactions]; - } - transactionRemoved:^(NSArray *_Nonnull transactions) { - [weakSelf handleTransactionsRemoved:transactions]; - } - restoreTransactionFailed:^(NSError *_Nonnull error) { - [weakSelf handleTransactionRestoreFailed:error]; - } - restoreCompletedTransactionsFinished:^{ - [weakSelf restoreCompletedTransactionsFinished]; - } - shouldAddStorePayment:^BOOL(SKPayment *payment, SKProduct *product) { - return [weakSelf shouldAddStorePayment:payment product:product]; - } - updatedDownloads:^void(NSArray *_Nonnull downloads) { - [weakSelf updatedDownloads:downloads]; - } - transactionCache:[[FIATransactionCache alloc] init]]; - - _transactionObserverCallbackChannel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" - binaryMessenger:[registrar messenger]]; - return self; -} - -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if ([@"-[SKPaymentQueue canMakePayments:]" isEqualToString:call.method]) { - [self canMakePayments:result]; - } else if ([@"-[SKPaymentQueue transactions]" isEqualToString:call.method]) { - [self getPendingTransactions:result]; - } else if ([@"-[InAppPurchasePlugin startProductRequest:result:]" isEqualToString:call.method]) { - [self handleProductRequestMethodCall:call result:result]; - } else if ([@"-[InAppPurchasePlugin addPayment:result:]" isEqualToString:call.method]) { - [self addPayment:call result:result]; - } else if ([@"-[InAppPurchasePlugin finishTransaction:result:]" isEqualToString:call.method]) { - [self finishTransaction:call result:result]; - } else if ([@"-[InAppPurchasePlugin restoreTransactions:result:]" isEqualToString:call.method]) { - [self restoreTransactions:call result:result]; - } else if ([@"-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]" - isEqualToString:call.method]) { - [self presentCodeRedemptionSheet:call result:result]; - } else if ([@"-[InAppPurchasePlugin retrieveReceiptData:result:]" isEqualToString:call.method]) { - [self retrieveReceiptData:call result:result]; - } else if ([@"-[InAppPurchasePlugin refreshReceipt:result:]" isEqualToString:call.method]) { - [self refreshReceipt:call result:result]; - } else if ([@"-[SKPaymentQueue startObservingTransactionQueue]" isEqualToString:call.method]) { - [self startObservingPaymentQueue:result]; - } else if ([@"-[SKPaymentQueue stopObservingTransactionQueue]" isEqualToString:call.method]) { - [self stopObservingPaymentQueue:result]; - } else if ([@"-[SKPaymentQueue registerDelegate]" isEqualToString:call.method]) { - [self registerPaymentQueueDelegate:result]; - } else if ([@"-[SKPaymentQueue removeDelegate]" isEqualToString:call.method]) { - [self removePaymentQueueDelegate:result]; - } else if ([@"-[SKPaymentQueue showPriceConsentIfNeeded]" isEqualToString:call.method]) { - [self showPriceConsentIfNeeded:result]; - } else { - result(FlutterMethodNotImplemented); - } -} - -- (void)canMakePayments:(FlutterResult)result { - result(@([SKPaymentQueue canMakePayments])); -} - -- (void)getPendingTransactions:(FlutterResult)result { - NSArray *transactions = - [self.paymentQueueHandler getUnfinishedTransactions]; - NSMutableArray *transactionMaps = [[NSMutableArray alloc] init]; - for (SKPaymentTransaction *transaction in transactions) { - [transactionMaps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; - } - result(transactionMaps); -} - -- (void)handleProductRequestMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if (![call.arguments isKindOfClass:[NSArray class]]) { - result([FlutterError errorWithCode:@"storekit_invalid_argument" - message:@"Argument type of startRequest is not array" - details:call.arguments]); - return; - } - NSArray *productIdentifiers = (NSArray *)call.arguments; - SKProductsRequest *request = - [self getProductRequestWithIdentifiers:[NSSet setWithArray:productIdentifiers]]; - FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; - [self.requestHandlers addObject:handler]; - __weak typeof(self) weakSelf = self; - [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable response, - NSError *_Nullable error) { - if (error) { - result([FlutterError errorWithCode:@"storekit_getproductrequest_platform_error" - message:error.localizedDescription - details:error.description]); - return; - } - if (!response) { - result([FlutterError errorWithCode:@"storekit_platform_no_response" - message:@"Failed to get SKProductResponse in startRequest " - @"call. Error occured on iOS platform" - details:call.arguments]); - return; - } - for (SKProduct *product in response.products) { - [self.productsCache setObject:product forKey:product.productIdentifier]; - } - result([FIAObjectTranslator getMapFromSKProductsResponse:response]); - [weakSelf.requestHandlers removeObject:handler]; - }]; -} - -- (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { - if (![call.arguments isKindOfClass:[NSDictionary class]]) { - result([FlutterError errorWithCode:@"storekit_invalid_argument" - message:@"Argument type of addPayment is not a Dictionary" - details:call.arguments]); - return; - } - NSDictionary *paymentMap = (NSDictionary *)call.arguments; - NSString *productID = [paymentMap objectForKey:@"productIdentifier"]; - // When a product is already fetched, we create a payment object with - // the product to process the payment. - SKProduct *product = [self getProduct:productID]; - if (!product) { - result([FlutterError - errorWithCode:@"storekit_invalid_payment_object" - message: - @"You have requested a payment for an invalid product. Either the " - @"`productIdentifier` of the payment is not valid or the product has not been " - @"fetched before adding the payment to the payment queue." - details:call.arguments]); - return; - } - SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product]; - payment.applicationUsername = [paymentMap objectForKey:@"applicationUsername"]; - NSNumber *quantity = [paymentMap objectForKey:@"quantity"]; - payment.quantity = (quantity != nil) ? quantity.integerValue : 1; - NSNumber *simulatesAskToBuyInSandbox = [paymentMap objectForKey:@"simulatesAskToBuyInSandbox"]; - payment.simulatesAskToBuyInSandbox = (id)simulatesAskToBuyInSandbox == (id)[NSNull null] - ? NO - : [simulatesAskToBuyInSandbox boolValue]; - - if (@available(iOS 12.2, *)) { - NSDictionary *paymentDiscountMap = [self getNonNullValueFromDictionary:paymentMap - forKey:@"paymentDiscount"]; - NSString *error = nil; - SKPaymentDiscount *paymentDiscount = - [FIAObjectTranslator getSKPaymentDiscountFromMap:paymentDiscountMap withError:&error]; - - if (error) { - result([FlutterError - errorWithCode:@"storekit_invalid_payment_discount_object" - message:[NSString stringWithFormat:@"You have requested a payment and specified a " - @"payment discount with invalid properties. %@", - error] - details:call.arguments]); - return; - } - - payment.paymentDiscount = paymentDiscount; - } - - if (![self.paymentQueueHandler addPayment:payment]) { - result([FlutterError - errorWithCode:@"storekit_duplicate_product_object" - message:@"There is a pending transaction for the same product identifier. Please " - @"either wait for it to be finished or finish it manually using " - @"`completePurchase` to avoid edge cases." - - details:call.arguments]); - return; - } - result(nil); -} - -- (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result { - if (![call.arguments isKindOfClass:[NSDictionary class]]) { - result([FlutterError errorWithCode:@"storekit_invalid_argument" - message:@"Argument type of finishTransaction is not a Dictionary" - details:call.arguments]); - return; - } - NSDictionary *paymentMap = (NSDictionary *)call.arguments; - NSString *transactionIdentifier = [paymentMap objectForKey:@"transactionIdentifier"]; - NSString *productIdentifier = [paymentMap objectForKey:@"productIdentifier"]; - - NSArray *pendingTransactions = - [self.paymentQueueHandler getUnfinishedTransactions]; - - for (SKPaymentTransaction *transaction in pendingTransactions) { - // If the user cancels the purchase dialog we won't have a transactionIdentifier. - // So if it is null AND a transaction in the pendingTransactions list has - // also a null transactionIdentifier we check for equal product identifiers. - if ([transaction.transactionIdentifier isEqualToString:transactionIdentifier] || - ([transactionIdentifier isEqual:[NSNull null]] && - transaction.transactionIdentifier == nil && - [transaction.payment.productIdentifier isEqualToString:productIdentifier])) { - @try { - [self.paymentQueueHandler finishTransaction:transaction]; - } @catch (NSException *e) { - result([FlutterError errorWithCode:@"storekit_finish_transaction_exception" - message:e.name - details:e.description]); - return; - } - } - } - - result(nil); -} - -- (void)restoreTransactions:(FlutterMethodCall *)call result:(FlutterResult)result { - if (call.arguments && ![call.arguments isKindOfClass:[NSString class]]) { - result([FlutterError - errorWithCode:@"storekit_invalid_argument" - message:@"Argument is not nil and the type of finishTransaction is not a string." - details:call.arguments]); - return; - } - [self.paymentQueueHandler restoreTransactions:call.arguments]; - result(nil); -} - -- (void)presentCodeRedemptionSheet:(FlutterMethodCall *)call result:(FlutterResult)result { - [self.paymentQueueHandler presentCodeRedemptionSheet]; - result(nil); -} - -- (void)retrieveReceiptData:(FlutterMethodCall *)call result:(FlutterResult)result { - FlutterError *error = nil; - NSString *receiptData = [self.receiptManager retrieveReceiptWithError:&error]; - if (error) { - result(error); - return; - } - result(receiptData); -} - -- (void)refreshReceipt:(FlutterMethodCall *)call result:(FlutterResult)result { - NSDictionary *arguments = call.arguments; - SKReceiptRefreshRequest *request; - if (arguments) { - if (![arguments isKindOfClass:[NSDictionary class]]) { - result([FlutterError errorWithCode:@"storekit_invalid_argument" - message:@"Argument type of startRequest is not array" - details:call.arguments]); - return; - } - NSMutableDictionary *properties = [NSMutableDictionary new]; - properties[SKReceiptPropertyIsExpired] = arguments[@"isExpired"]; - properties[SKReceiptPropertyIsRevoked] = arguments[@"isRevoked"]; - properties[SKReceiptPropertyIsVolumePurchase] = arguments[@"isVolumePurchase"]; - request = [self getRefreshReceiptRequest:properties]; - } else { - request = [self getRefreshReceiptRequest:nil]; - } - FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; - [self.requestHandlers addObject:handler]; - __weak typeof(self) weakSelf = self; - [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable response, - NSError *_Nullable error) { - if (error) { - result([FlutterError errorWithCode:@"storekit_refreshreceiptrequest_platform_error" - message:error.localizedDescription - details:error.description]); - return; - } - result(nil); - [weakSelf.requestHandlers removeObject:handler]; - }]; -} - -- (void)startObservingPaymentQueue:(FlutterResult)result { - [_paymentQueueHandler startObservingPaymentQueue]; - result(nil); -} - -- (void)stopObservingPaymentQueue:(FlutterResult)result { - [_paymentQueueHandler stopObservingPaymentQueue]; - result(nil); -} - -- (void)registerPaymentQueueDelegate:(FlutterResult)result { - if (@available(iOS 13.0, *)) { - _paymentQueueDelegateCallbackChannel = [FlutterMethodChannel - methodChannelWithName:@"plugins.flutter.io/in_app_purchase_payment_queue_delegate" - binaryMessenger:[_registrar messenger]]; - - _paymentQueueDelegate = [[FIAPPaymentQueueDelegate alloc] - initWithMethodChannel:_paymentQueueDelegateCallbackChannel]; - _paymentQueueHandler.delegate = _paymentQueueDelegate; - } - result(nil); -} - -- (void)removePaymentQueueDelegate:(FlutterResult)result { - if (@available(iOS 13.0, *)) { - _paymentQueueHandler.delegate = nil; - } - _paymentQueueDelegate = nil; - _paymentQueueDelegateCallbackChannel = nil; - result(nil); -} - -- (void)showPriceConsentIfNeeded:(FlutterResult)result { - if (@available(iOS 13.4, *)) { - [_paymentQueueHandler showPriceConsentIfNeeded]; - } - result(nil); -} - -- (id)getNonNullValueFromDictionary:(NSDictionary *)dictionary forKey:(NSString *)key { - id value = dictionary[key]; - return [value isKindOfClass:[NSNull class]] ? nil : value; -} - -#pragma mark - transaction observer: - -- (void)handleTransactionsUpdated:(NSArray *)transactions { - NSMutableArray *maps = [NSMutableArray new]; - for (SKPaymentTransaction *transaction in transactions) { - [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; - } - [self.transactionObserverCallbackChannel invokeMethod:@"updatedTransactions" arguments:maps]; -} - -- (void)handleTransactionsRemoved:(NSArray *)transactions { - NSMutableArray *maps = [NSMutableArray new]; - for (SKPaymentTransaction *transaction in transactions) { - [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; - } - [self.transactionObserverCallbackChannel invokeMethod:@"removedTransactions" arguments:maps]; -} - -- (void)handleTransactionRestoreFailed:(NSError *)error { - [self.transactionObserverCallbackChannel - invokeMethod:@"restoreCompletedTransactionsFailed" - arguments:[FIAObjectTranslator getMapFromNSError:error]]; -} - -- (void)restoreCompletedTransactionsFinished { - [self.transactionObserverCallbackChannel - invokeMethod:@"paymentQueueRestoreCompletedTransactionsFinished" - arguments:nil]; -} - -- (void)updatedDownloads:(NSArray *)downloads { - NSLog(@"Received an updatedDownloads callback, but downloads are not supported."); -} - -- (BOOL)shouldAddStorePayment:(SKPayment *)payment product:(SKProduct *)product { - // We always return NO here. And we send the message to dart to process the payment; and we will - // have a interception method that deciding if the payment should be processed (implemented by the - // programmer). - [self.productsCache setObject:product forKey:product.productIdentifier]; - [self.transactionObserverCallbackChannel - invokeMethod:@"shouldAddStorePayment" - arguments:@{ - @"payment" : [FIAObjectTranslator getMapFromSKPayment:payment], - @"product" : [FIAObjectTranslator getMapFromSKProduct:product] - }]; - return NO; -} - -#pragma mark - dependency injection (for unit testing) - -- (SKProductsRequest *)getProductRequestWithIdentifiers:(NSSet *)identifiers { - return [[SKProductsRequest alloc] initWithProductIdentifiers:identifiers]; -} - -- (SKProduct *)getProduct:(NSString *)productID { - return [self.productsCache objectForKey:productID]; -} - -- (SKReceiptRefreshRequest *)getRefreshReceiptRequest:(NSDictionary *)properties { - return [[SKReceiptRefreshRequest alloc] initWithReceiptProperties:properties]; -} - -@end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m new file mode 120000 index 000000000000..e087d55187e8 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m @@ -0,0 +1 @@ +../../darwin/Classes/InAppPurchasePlugin.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/in_app_purchase_storekit.podspec b/packages/in_app_purchase/in_app_purchase_storekit/ios/in_app_purchase_storekit.podspec deleted file mode 100644 index dd83234ac4ad..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/in_app_purchase_storekit.podspec +++ /dev/null @@ -1,24 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'in_app_purchase_storekit' - s.version = '0.0.1' - s.summary = 'Flutter In App Purchase iOS' - s.description = <<-DESC -A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/in_app_purchase/in_app_purchase_storekit' } - # TODO(mvanbeusekom): update URL when in_app_purchase_storekit package is published. - # Updating it before the package is published will cause a lint error and block the tree. - s.documentation_url = 'https://pub.dev/packages/in_app_purchase' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '9.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } -end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/in_app_purchase_storekit.podspec b/packages/in_app_purchase/in_app_purchase_storekit/ios/in_app_purchase_storekit.podspec new file mode 120000 index 000000000000..4157364db8d6 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/in_app_purchase_storekit.podspec @@ -0,0 +1 @@ +../darwin/in_app_purchase_storekit.podspec \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform_addition.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform_addition.dart index 070c138b32e6..b467b89b68a9 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform_addition.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/in_app_purchase_storekit_platform_addition.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: avoid_print + import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import '../in_app_purchase_storekit.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart index 1d98dd3f4250..5c65fb1df6de 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart @@ -10,8 +10,8 @@ import '../../store_kit_wrappers.dart'; /// The payment queue delegate can be implementated to provide information /// needed to complete transactions. /// -/// The [SKPaymentQueueDelegateWrapper] is only available on iOS 13 and higher. -/// Using the delegate on older iOS version will be ignored. +/// The [SKPaymentQueueDelegateWrapper] is available on macOS and iOS 13+. +/// Usage with versions below iOS 13 and macOS are ignored. abstract class SKPaymentQueueDelegateWrapper { /// Called by the system to check whether the transaction should continue if /// the device's App Store storefront has changed during a transaction. diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index d360a2da3fe5..859946b557bf 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -245,11 +245,13 @@ class SKPaymentQueueWrapper { } case 'shouldAddStorePayment': { + final Map arguments = + call.arguments as Map; final SKPaymentWrapper payment = SKPaymentWrapper.fromJson( - (call.arguments['payment'] as Map) + (arguments['payment']! as Map) .cast()); final SKProductWrapper product = SKProductWrapper.fromJson( - (call.arguments['product'] as Map) + (arguments['product']! as Map) .cast()); return Future(() { if (observer.shouldAddStorePayment( @@ -290,12 +292,14 @@ class SKPaymentQueueWrapper { final SKPaymentQueueDelegateWrapper delegate = _paymentQueueDelegate!; switch (call.method) { case 'shouldContinueTransaction': + final Map arguments = + call.arguments as Map; final SKPaymentTransactionWrapper transaction = SKPaymentTransactionWrapper.fromJson( - (call.arguments['transaction'] as Map) + (arguments['transaction']! as Map) .cast()); final SKStorefrontWrapper storefront = SKStorefrontWrapper.fromJson( - (call.arguments['storefront'] as Map) + (arguments['storefront']! as Map) .cast()); return delegate.shouldContinueTransaction(transaction, storefront); case 'shouldShowPriceConsent': diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_product_details.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_product_details.dart index a5d8c7287e3c..47bcf616fa40 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_product_details.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_product_details.dart @@ -12,23 +12,15 @@ class AppStoreProductDetails extends ProductDetails { /// Creates a new AppStore specific product details object with the provided /// details. AppStoreProductDetails({ - required String id, - required String title, - required String description, - required String price, - required double rawPrice, - required String currencyCode, + required super.id, + required super.title, + required super.description, + required super.price, + required super.rawPrice, + required super.currencyCode, required this.skProduct, - required String currencySymbol, - }) : super( - id: id, - title: title, - description: description, - price: price, - rawPrice: rawPrice, - currencyCode: currencyCode, - currencySymbol: currencySymbol, - ); + required super.currencySymbol, + }); /// Generate a [AppStoreProductDetails] object based on an iOS [SKProductWrapper] object. factory AppStoreProductDetails.fromSKProduct(SKProductWrapper product) { diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_details.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_details.dart index 42cb225ede0a..21a1e11116b7 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_details.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_details.dart @@ -13,19 +13,14 @@ import '../store_kit_wrappers/enum_converters.dart'; class AppStorePurchaseDetails extends PurchaseDetails { /// Creates a new AppStore specific purchase details object with the provided /// details. - AppStorePurchaseDetails( - {String? purchaseID, - required String productID, - required PurchaseVerificationData verificationData, - required String? transactionDate, - required this.skPaymentTransaction, - required PurchaseStatus status}) - : super( - productID: productID, - purchaseID: purchaseID, - transactionDate: transactionDate, - verificationData: verificationData, - status: status) { + AppStorePurchaseDetails({ + super.purchaseID, + required super.productID, + required super.verificationData, + required super.transactionDate, + required this.skPaymentTransaction, + required PurchaseStatus status, + }) : super(status: status) { this.status = status; } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart index 0e7e24166c4d..05096d3be40e 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart @@ -10,15 +10,12 @@ import '../../store_kit_wrappers.dart'; class AppStorePurchaseParam extends PurchaseParam { /// Creates a new [AppStorePurchaseParam] object with the given data. AppStorePurchaseParam({ - required ProductDetails productDetails, - String? applicationUserName, + required super.productDetails, + super.applicationUserName, this.quantity = 1, this.simulatesAskToBuyInSandbox = false, this.discount, - }) : super( - productDetails: productDetails, - applicationUserName: applicationUserName, - ); + }); /// Set it to `true` to produce an "ask to buy" flow for this payment in the /// sandbox. diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAObjectTranslator.h b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAObjectTranslator.h new file mode 120000 index 000000000000..6b974bc7d268 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAObjectTranslator.h @@ -0,0 +1 @@ +../../darwin/Classes/FIAObjectTranslator.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAObjectTranslator.m new file mode 120000 index 000000000000..f9b4ffe6732d --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAObjectTranslator.m @@ -0,0 +1 @@ +../../darwin/Classes/FIAObjectTranslator.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPPaymentQueueDelegate.h b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPPaymentQueueDelegate.h new file mode 120000 index 000000000000..e4b452397bc2 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPPaymentQueueDelegate.h @@ -0,0 +1 @@ +../../darwin/Classes/FIAPPaymentQueueDelegate.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPPaymentQueueDelegate.m b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPPaymentQueueDelegate.m new file mode 120000 index 000000000000..a1b95ef97c1b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPPaymentQueueDelegate.m @@ -0,0 +1 @@ +../../darwin/Classes/FIAPPaymentQueueDelegate.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPReceiptManager.h b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPReceiptManager.h new file mode 120000 index 000000000000..88f02af0b00a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPReceiptManager.h @@ -0,0 +1 @@ +../../darwin/Classes/FIAPReceiptManager.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPReceiptManager.m b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPReceiptManager.m new file mode 120000 index 000000000000..f303c3c162a0 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPReceiptManager.m @@ -0,0 +1 @@ +../../darwin/Classes/FIAPReceiptManager.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPRequestHandler.h b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPRequestHandler.h new file mode 120000 index 000000000000..9eb31f26b048 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPRequestHandler.h @@ -0,0 +1 @@ +../../darwin/Classes/FIAPRequestHandler.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPRequestHandler.m b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPRequestHandler.m new file mode 120000 index 000000000000..d6976dc0dd26 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPRequestHandler.m @@ -0,0 +1 @@ +../../darwin/Classes/FIAPRequestHandler.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPaymentQueueHandler.h new file mode 120000 index 000000000000..6bc9c2f6dc85 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPaymentQueueHandler.h @@ -0,0 +1 @@ +../../darwin/Classes/FIAPaymentQueueHandler.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPaymentQueueHandler.m new file mode 120000 index 000000000000..8c892d29f1e6 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPaymentQueueHandler.m @@ -0,0 +1 @@ +../../darwin/Classes/FIAPaymentQueueHandler.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIATransactionCache.h b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIATransactionCache.h new file mode 120000 index 000000000000..8862d80dde39 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIATransactionCache.h @@ -0,0 +1 @@ +../../darwin/Classes/FIATransactionCache.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIATransactionCache.m b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIATransactionCache.m new file mode 120000 index 000000000000..8c0dd87c7e97 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIATransactionCache.m @@ -0,0 +1 @@ +../../darwin/Classes/FIATransactionCache.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/InAppPurchasePlugin.h b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/InAppPurchasePlugin.h new file mode 120000 index 000000000000..0ec6c66d54f8 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/InAppPurchasePlugin.h @@ -0,0 +1 @@ +../../darwin/Classes/InAppPurchasePlugin.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/InAppPurchasePlugin.m new file mode 120000 index 000000000000..e087d55187e8 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/InAppPurchasePlugin.m @@ -0,0 +1 @@ +../../darwin/Classes/InAppPurchasePlugin.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/in_app_purchase_storekit.podspec b/packages/in_app_purchase/in_app_purchase_storekit/macos/in_app_purchase_storekit.podspec new file mode 120000 index 000000000000..4157364db8d6 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/in_app_purchase_storekit.podspec @@ -0,0 +1 @@ +../darwin/in_app_purchase_storekit.podspec \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml index 0b6e21a26978..5b734f4b630c 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml @@ -1,12 +1,12 @@ name: in_app_purchase_storekit -description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the StoreKit Framework. +description: An implementation for the iOS and macOS platforms of the Flutter `in_app_purchase` plugin. This uses the StoreKit Framework. repository: https://github.com/flutter/plugins/tree/main/packages/in_app_purchase/in_app_purchase_storekit issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.3.3 +version: 0.3.6 environment: - sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + sdk: '>=2.18.0 <3.0.0' + flutter: ">=3.3.0" flutter: plugin: @@ -14,6 +14,10 @@ flutter: platforms: ios: pluginClass: InAppPurchasePlugin + sharedDarwinSource: true + macos: + pluginClass: InAppPurchasePlugin + sharedDarwinSource: true dependencies: collection: ^1.15.0 diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart index e6b9696c8cb1..e6369161080f 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart @@ -14,7 +14,9 @@ import '../store_kit_wrappers/sk_test_stub_objects.dart'; class FakeStoreKitPlatform { FakeStoreKitPlatform() { - channel.setMockMethodCallHandler(onMethodCall); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, onMethodCall); } // pre-configured store information @@ -169,14 +171,15 @@ class FakeStoreKitPlatform { receiptData = 'refreshed receipt data'; return Future.sync(() {}); case '-[InAppPurchasePlugin addPayment:result:]': - final String id = call.arguments['productIdentifier'] as String; - final int quantity = call.arguments['quantity'] as int; + final Map arguments = _getArgumentDictionary(call); + final String id = arguments['productIdentifier']! as String; + final int quantity = arguments['quantity']! as int; // Keep the received paymentDiscount parameter when testing payment with discount. - if (call.arguments['applicationUsername'] == 'userWithDiscount') { - if (call.arguments['paymentDiscount'] != null) { - final Map discountArgument = - call.arguments['paymentDiscount'] as Map; + if (arguments['applicationUsername']! == 'userWithDiscount') { + final Map? discountArgument = + arguments['paymentDiscount'] as Map?; + if (discountArgument != null) { discountReceived = discountArgument.cast(); } else { discountReceived = {}; @@ -210,9 +213,10 @@ class FakeStoreKitPlatform { } break; case '-[InAppPurchasePlugin finishTransaction:result:]': + final Map arguments = _getArgumentDictionary(call); finishedTransactions.add(createPurchasedTransaction( - call.arguments['productIdentifier'] as String, - call.arguments['transactionIdentifier'] as String, + arguments['productIdentifier']! as String, + arguments['transactionIdentifier']! as String, quantity: transactions.first.payment.quantity)); break; case '-[SKPaymentQueue startObservingTransactionQueue]': @@ -224,4 +228,18 @@ class FakeStoreKitPlatform { } return Future.sync(() {}); } + + /// Returns the arguments of [call] as typed string-keyed Map. + /// + /// This does not do any type validation, so is only safe to call if the + /// arguments are known to be a map. + Map _getArgumentDictionary(MethodCall call) { + return (call.arguments as Map).cast(); + } } + +/// 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/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_addtion_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_addtion_test.dart index dfdff5117091..2890e7542bbe 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_addtion_test.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_addtion_test.dart @@ -15,8 +15,10 @@ void main() { final FakeStoreKitPlatform fakeStoreKitPlatform = FakeStoreKitPlatform(); setUpAll(() { - SystemChannels.platform - .setMockMethodCallHandler(fakeStoreKitPlatform.onMethodCall); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform, fakeStoreKitPlatform.onMethodCall); }); group('present code redemption sheet', () { @@ -39,3 +41,9 @@ void main() { }); }); } + +/// 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/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart index 51ff2c229483..fbb37974a208 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart @@ -21,8 +21,10 @@ void main() { late InAppPurchaseStoreKitPlatform iapStoreKitPlatform; setUpAll(() { - SystemChannels.platform - .setMockMethodCallHandler(fakeStoreKitPlatform.onMethodCall); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform, fakeStoreKitPlatform.onMethodCall); }); setUp(() { @@ -571,3 +573,9 @@ void main() { }); }); } + +/// 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/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_methodchannel_apis_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_methodchannel_apis_test.dart index ff059ffbb1fc..0cf01b0bbfd6 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_methodchannel_apis_test.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_methodchannel_apis_test.dart @@ -14,8 +14,10 @@ void main() { final FakeStoreKitPlatform fakeStoreKitPlatform = FakeStoreKitPlatform(); setUpAll(() { - SystemChannels.platform - .setMockMethodCallHandler(fakeStoreKitPlatform.onMethodCall); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform, fakeStoreKitPlatform.onMethodCall); }); setUp(() {}); @@ -185,7 +187,9 @@ void main() { class FakeStoreKitPlatform { FakeStoreKitPlatform() { - channel.setMockMethodCallHandler(onMethodCall); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, onMethodCall); } // get product request List startProductRequestParam = []; @@ -234,7 +238,7 @@ class FakeStoreKitPlatform { // receipt manager case '-[InAppPurchasePlugin retrieveReceiptData:result:]': if (getReceiptFailTest) { - throw 'some arbitrary error'; + throw Exception('some arbitrary error'); } return Future.value('receipt data'); // payment queue @@ -303,3 +307,9 @@ class TestPaymentTransactionObserver extends SKTransactionObserverWrapper { return 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/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart index df76254aabf7..3d55fe27d7b0 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart @@ -13,8 +13,10 @@ void main() { final FakeStoreKitPlatform fakeStoreKitPlatform = FakeStoreKitPlatform(); setUpAll(() { - SystemChannels.platform - .setMockMethodCallHandler(fakeStoreKitPlatform.onMethodCall); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform, fakeStoreKitPlatform.onMethodCall); }); test( @@ -148,7 +150,9 @@ class TestPaymentQueueDelegate extends SKPaymentQueueDelegateWrapper { class FakeStoreKitPlatform { FakeStoreKitPlatform() { - channel.setMockMethodCallHandler(onMethodCall); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, onMethodCall); } // indicate if the payment queue delegate is registered @@ -166,3 +170,9 @@ class FakeStoreKitPlatform { return Future.error('method not mocked'); } } + +/// 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/ios_platform_images/CHANGELOG.md b/packages/ios_platform_images/CHANGELOG.md index 1a28c9b2a550..72f1cf6d7d39 100644 --- a/packages/ios_platform_images/CHANGELOG.md +++ b/packages/ios_platform_images/CHANGELOG.md @@ -1,6 +1,15 @@ -## NEXT +## 0.2.2 -* Updates minimum Flutter version to 2.10. +* Updates minimum version to iOS 11. + +## 0.2.1+1 + +* Add lint ignore comments + +## 0.2.1 + +* Updates minimum Flutter version to 3.3.0. +* Removes usage of deprecated [ImageProvider.load]. * Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/106316). ## 0.2.0+9 diff --git a/packages/ios_platform_images/README.md b/packages/ios_platform_images/README.md index 08dfc3e40b31..9265b108595e 100644 --- a/packages/ios_platform_images/README.md +++ b/packages/ios_platform_images/README.md @@ -8,9 +8,9 @@ Flutter images. When loading images from Image.xcassets the device specific variant is chosen ([iOS documentation](https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/image-size-and-resolution/)). -| | iOS | -|-------------|------| -| **Support** | 9.0+ | +| | iOS | +|-------------|-------| +| **Support** | 11.0+ | ## Usage diff --git a/packages/ios_platform_images/example/ios/Flutter/AppFrameworkInfo.plist b/packages/ios_platform_images/example/ios/Flutter/AppFrameworkInfo.plist index f2872cf474ee..4f8d4d2456f3 100644 --- a/packages/ios_platform_images/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/ios_platform_images/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/ios_platform_images/example/ios/Podfile b/packages/ios_platform_images/example/ios/Podfile index 397864535f5d..fdcc671eb341 100644 --- a/packages/ios_platform_images/example/ios/Podfile +++ b/packages/ios_platform_images/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/ios_platform_images/example/ios/Runner.xcodeproj/project.pbxproj b/packages/ios_platform_images/example/ios/Runner.xcodeproj/project.pbxproj index 02e41bc13711..d6b4ef94bcef 100644 --- a/packages/ios_platform_images/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/ios_platform_images/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -219,7 +219,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -278,10 +278,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -332,6 +334,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -457,7 +460,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -539,7 +542,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -588,7 +591,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/packages/ios_platform_images/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/ios_platform_images/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 6de5fabfee04..7ae2cb4d4e54 100644 --- a/packages/ios_platform_images/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/ios_platform_images/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/ios_platform_images/example/lib/main.dart b/packages/ios_platform_images/example/lib/main.dart index 929814ecce00..043bc69c944d 100644 --- a/packages/ios_platform_images/example/lib/main.dart +++ b/packages/ios_platform_images/example/lib/main.dart @@ -22,6 +22,7 @@ class _MyAppState extends State { super.initState(); IosPlatformImages.resolveURL('textfile') + // ignore: avoid_print .then((String? value) => print(value)); } diff --git a/packages/ios_platform_images/example/pubspec.yaml b/packages/ios_platform_images/example/pubspec.yaml index 6045b3f67cfc..49b09bd8b637 100644 --- a/packages/ios_platform_images/example/pubspec.yaml +++ b/packages/ios_platform_images/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: cupertino_icons: ^1.0.2 diff --git a/packages/ios_platform_images/ios/ios_platform_images.podspec b/packages/ios_platform_images/ios/ios_platform_images.podspec index 3549277e9d86..02e5da149cd8 100644 --- a/packages/ios_platform_images/ios/ios_platform_images.podspec +++ b/packages/ios_platform_images/ios/ios_platform_images.podspec @@ -17,7 +17,7 @@ Downloaded by pub (not CocoaPods). s.documentation_url = 'https://pub.dev/packages/ios_platform_images' s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.platform = :ios, '9.0' + s.platform = :ios, '11.0' # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } diff --git a/packages/ios_platform_images/lib/ios_platform_images.dart b/packages/ios_platform_images/lib/ios_platform_images.dart index fa40eb08fafd..b372d362f6f7 100644 --- a/packages/ios_platform_images/lib/ios_platform_images.dart +++ b/packages/ios_platform_images/lib/ios_platform_images.dart @@ -3,9 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316) -// ignore: unnecessary_import -import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart' @@ -64,12 +61,11 @@ class _FutureMemoryImage extends ImageProvider<_FutureMemoryImage> { return SynchronousFuture<_FutureMemoryImage>(this); } - // ignore:deprecated_member_use - /// See [ImageProvider.load]. - // TODO(jmagman): Implement the new API once it lands, https://github.com/flutter/flutter/issues/103556 @override - // ignore: deprecated_member_use - ImageStreamCompleter load(_FutureMemoryImage key, DecoderCallback decode) { + ImageStreamCompleter loadBuffer( + _FutureMemoryImage key, + DecoderBufferCallback decode, // ignore: deprecated_member_use + ) { return _FutureImageStreamCompleter( codec: _loadAsync(key, decode), futureScale: _futureScale, @@ -78,13 +74,10 @@ class _FutureMemoryImage extends ImageProvider<_FutureMemoryImage> { Future _loadAsync( _FutureMemoryImage key, - // ignore: deprecated_member_use - DecoderCallback decode, - ) async { + DecoderBufferCallback decode, // ignore: deprecated_member_use + ) { assert(key == this); - return _futureBytes.then((Uint8List bytes) { - return decode(bytes); - }); + return _futureBytes.then(ui.ImmutableBuffer.fromUint8List).then(decode); } /// See [ImageProvider.operator==]. diff --git a/packages/ios_platform_images/pubspec.yaml b/packages/ios_platform_images/pubspec.yaml index 8b32b39343a7..4193e3e339bf 100644 --- a/packages/ios_platform_images/pubspec.yaml +++ b/packages/ios_platform_images/pubspec.yaml @@ -2,11 +2,11 @@ name: ios_platform_images description: A plugin to share images between Flutter and iOS in add-to-app setups. repository: https://github.com/flutter/plugins/tree/main/packages/ios_platform_images issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+ios_platform_images%22 -version: 0.2.0+9 +version: 0.2.2 environment: - sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + sdk: '>=2.18.0 <3.0.0' + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/ios_platform_images/test/ios_platform_images_test.dart b/packages/ios_platform_images/test/ios_platform_images_test.dart index 76b012002dfa..f42b78646038 100644 --- a/packages/ios_platform_images/test/ios_platform_images_test.dart +++ b/packages/ios_platform_images/test/ios_platform_images_test.dart @@ -13,16 +13,26 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return '42'; }); }); tearDown(() { - channel.setMockMethodCallHandler(null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); }); test('resolveURL', () async { expect(await IosPlatformImages.resolveURL('foobar'), '42'); }); } + +/// 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/local_auth/local_auth/CHANGELOG.md b/packages/local_auth/local_auth/CHANGELOG.md index 34e26efef238..0028704b34b1 100644 --- a/packages/local_auth/local_auth/CHANGELOG.md +++ b/packages/local_auth/local_auth/CHANGELOG.md @@ -1,6 +1,12 @@ -## NEXT +## 2.1.4 + +* Updates minimum Flutter version to 3.0. +* Updates documentation for Android version 8 and below theme compatibility. + +## 2.1.3 * Updates minimum Flutter version to 2.10. +* Removes unused `intl` dependency. ## 2.1.2 diff --git a/packages/local_auth/local_auth/README.md b/packages/local_auth/local_auth/README.md index a68692ea940a..8abf583b9dd4 100644 --- a/packages/local_auth/local_auth/README.md +++ b/packages/local_auth/local_auth/README.md @@ -252,6 +252,42 @@ types (such as face scanning) and you want to support SDKs lower than Q, _do not_ call `getAvailableBiometrics`. Simply call `authenticate` with `biometricOnly: true`. This will return an error if there was no hardware available. +#### Android theme + +Your `LaunchTheme`'s parent must be a valid `Theme.AppCompat` theme to prevent +crashes on Android 8 and below. For example, use `Theme.AppCompat.DayNight` to +enable light/dark modes for the biometric dialog. To do that go to +`android/app/src/main/res/values/styles.xml` and look for the style with name +`LaunchTheme`. Then change the parent for that style as follows: + +```xml +... + + + ... + +... +``` + +If you don't have a `styles.xml` file for your Android project you can set up +the Android theme directly in `android/app/src/main/AndroidManifest.xml`: + +```xml +... + + + +... +``` + ## Sticky Auth You can set the `stickyAuth` option on the plugin to true so that plugin does not diff --git a/packages/local_auth/local_auth/example/android/app/build.gradle b/packages/local_auth/local_auth/example/android/app/build.gradle index 3c6eca7ce8a7..0146852feb44 100644 --- a/packages/local_auth/local_auth/example/android/app/build.gradle +++ b/packages/local_auth/local_auth/example/android/app/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 31 + compileSdkVersion 33 lintOptions { disable 'InvalidPackage' diff --git a/packages/local_auth/local_auth/example/android/build.gradle b/packages/local_auth/local_auth/example/android/build.gradle index c21bff8e0a2f..3593d9636555 100644 --- a/packages/local_auth/local_auth/example/android/build.gradle +++ b/packages/local_auth/local_auth/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.3.1' } } diff --git a/packages/local_auth/local_auth/example/android/gradle.properties b/packages/local_auth/local_auth/example/android/gradle.properties index 7fe61a74cee0..e5611e4c7fa0 100644 --- a/packages/local_auth/local_auth/example/android/gradle.properties +++ b/packages/local_auth/local_auth/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1024m +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.enableR8=true diff --git a/packages/local_auth/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/local_auth/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties index 3f383641d7c3..f5c5c374a4b7 100644 --- a/packages/local_auth/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/local_auth/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/packages/local_auth/local_auth/example/lib/main.dart b/packages/local_auth/local_auth/example/lib/main.dart index 1330012421ca..146a5d92b29c 100644 --- a/packages/local_auth/local_auth/example/lib/main.dart +++ b/packages/local_auth/local_auth/example/lib/main.dart @@ -2,7 +2,7 @@ // 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 +// ignore_for_file: public_member_api_docs, avoid_print import 'dart:async'; @@ -183,6 +183,8 @@ 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( mainAxisSize: MainAxisSize.min, children: const [ @@ -196,6 +198,8 @@ 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( mainAxisSize: MainAxisSize.min, children: const [ diff --git a/packages/local_auth/local_auth/example/lib/readme_excerpts.dart b/packages/local_auth/local_auth/example/lib/readme_excerpts.dart index 340aaef28f84..ccccf5c50ae9 100644 --- a/packages/local_auth/local_auth/example/lib/readme_excerpts.dart +++ b/packages/local_auth/local_auth/example/lib/readme_excerpts.dart @@ -5,7 +5,7 @@ // This file exists solely to host compiled excerpts for README.md, and is not // intended for use as an actual example application. -// ignore_for_file: public_member_api_docs +// ignore_for_file: public_member_api_docs, avoid_print import 'package:flutter/material.dart'; // #docregion ErrorHandling diff --git a/packages/local_auth/local_auth/example/pubspec.yaml b/packages/local_auth/local_auth/example/pubspec.yaml index f7dc2fc5b9e7..e02065b6d16f 100644 --- a/packages/local_auth/local_auth/example/pubspec.yaml +++ b/packages/local_auth/local_auth/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/local_auth/local_auth/pubspec.yaml b/packages/local_auth/local_auth/pubspec.yaml index 133df06d43b0..c2d3a007d10b 100644 --- a/packages/local_auth/local_auth/pubspec.yaml +++ b/packages/local_auth/local_auth/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for Android and iOS devices to allow local authentication via fingerprint, touch ID, face ID, passcode, pin, or pattern. repository: https://github.com/flutter/plugins/tree/main/packages/local_auth/local_auth issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 2.1.2 +version: 2.1.4 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -22,7 +22,6 @@ flutter: dependencies: flutter: sdk: flutter - intl: ^0.17.0 local_auth_android: ^1.0.0 local_auth_ios: ^1.0.1 local_auth_platform_interface: ^1.0.1 diff --git a/packages/local_auth/local_auth_android/CHANGELOG.md b/packages/local_auth/local_auth_android/CHANGELOG.md index bb3235b49c88..92b671ca119f 100644 --- a/packages/local_auth/local_auth_android/CHANGELOG.md +++ b/packages/local_auth/local_auth_android/CHANGELOG.md @@ -1,3 +1,17 @@ +## 1.0.18 + +* Updates minimum Flutter version to 3.0. +* Updates androidx.core version to 1.9.0. +* Upgrades compile SDK version to 33. + +## 1.0.17 + +* Adds compatibility with `intl` 0.18.0. + +## 1.0.16 + +* Updates androidx.fragment version to 1.5.5. + ## 1.0.15 * Updates androidx.fragment version to 1.5.4. diff --git a/packages/local_auth/local_auth_android/android/build.gradle b/packages/local_auth/local_auth_android/android/build.gradle index 913ea6f33fc0..8e116709d6cc 100644 --- a/packages/local_auth/local_auth_android/android/build.gradle +++ b/packages/local_auth/local_auth_android/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.2.1' + classpath 'com.android.tools.build:gradle:7.3.1' } } @@ -22,17 +22,14 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { - compileSdkVersion 31 + compileSdkVersion 33 defaultConfig { minSdkVersion 16 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - // TODO(stuartmorgan): Enable when gradle is updated. - // disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' baseline file("lint-baseline.xml") } @@ -51,13 +48,13 @@ android { } dependencies { - api "androidx.core:core:1.8.0" + api "androidx.core:core:1.9.0" api "androidx.biometric:biometric:1.1.0" - api "androidx.fragment:fragment:1.5.4" + api "androidx.fragment:fragment:1.5.5" testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-inline:4.7.0' + testImplementation 'org.mockito:mockito-inline:5.0.0' testImplementation 'org.robolectric:robolectric:4.5' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' } diff --git a/packages/local_auth/local_auth_android/example/android/app/build.gradle b/packages/local_auth/local_auth_android/example/android/app/build.gradle index 3c6eca7ce8a7..0146852feb44 100644 --- a/packages/local_auth/local_auth_android/example/android/app/build.gradle +++ b/packages/local_auth/local_auth_android/example/android/app/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 31 + compileSdkVersion 33 lintOptions { disable 'InvalidPackage' diff --git a/packages/local_auth/local_auth_android/example/android/build.gradle b/packages/local_auth/local_auth_android/example/android/build.gradle index c21bff8e0a2f..3593d9636555 100644 --- a/packages/local_auth/local_auth_android/example/android/build.gradle +++ b/packages/local_auth/local_auth_android/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.3.1' } } diff --git a/packages/local_auth/local_auth_android/example/android/gradle.properties b/packages/local_auth/local_auth_android/example/android/gradle.properties index 7fe61a74cee0..e5611e4c7fa0 100644 --- a/packages/local_auth/local_auth_android/example/android/gradle.properties +++ b/packages/local_auth/local_auth_android/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1024m +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.enableR8=true diff --git a/packages/local_auth/local_auth_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/local_auth/local_auth_android/example/android/gradle/wrapper/gradle-wrapper.properties index 3f383641d7c3..f5c5c374a4b7 100644 --- a/packages/local_auth/local_auth_android/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/local_auth/local_auth_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 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 9909853a62af..f245af973981 100644 --- a/packages/local_auth/local_auth_android/example/lib/main.dart +++ b/packages/local_auth/local_auth_android/example/lib/main.dart @@ -2,7 +2,7 @@ // 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 +// ignore_for_file: public_member_api_docs, avoid_print import 'dart:async'; @@ -188,6 +188,8 @@ 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( mainAxisSize: MainAxisSize.min, children: const [ @@ -201,6 +203,8 @@ 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( mainAxisSize: MainAxisSize.min, children: const [ diff --git a/packages/local_auth/local_auth_android/example/pubspec.yaml b/packages/local_auth/local_auth_android/example/pubspec.yaml index c95b89ad0c2a..fddd6b50f815 100644 --- a/packages/local_auth/local_auth_android/example/pubspec.yaml +++ b/packages/local_auth/local_auth_android/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/local_auth/local_auth_android/pubspec.yaml b/packages/local_auth/local_auth_android/pubspec.yaml index 0cddc94051c3..bc81476565cb 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/plugins/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.15 +version: 1.0.18 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -21,7 +21,7 @@ dependencies: flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 - intl: ^0.17.0 + intl: ">=0.17.0 <0.19.0" local_auth_platform_interface: ^1.0.1 dev_dependencies: diff --git a/packages/local_auth/local_auth_android/test/local_auth_test.dart b/packages/local_auth/local_auth_android/test/local_auth_test.dart index 86e5713f4bd6..136613d48245 100644 --- a/packages/local_auth/local_auth_android/test/local_auth_test.dart +++ b/packages/local_auth/local_auth_android/test/local_auth_test.dart @@ -18,7 +18,9 @@ void main() { late LocalAuthAndroid localAuthentication; setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) { log.add(methodCall); switch (methodCall.method) { case 'getEnrolledBiometrics': @@ -174,3 +176,9 @@ void main() { }); }); } + +/// 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/local_auth/local_auth_ios/CHANGELOG.md b/packages/local_auth/local_auth_ios/CHANGELOG.md index e67f2a4e2ef1..eca9612fa69e 100644 --- a/packages/local_auth/local_auth_ios/CHANGELOG.md +++ b/packages/local_auth/local_auth_ios/CHANGELOG.md @@ -1,3 +1,15 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 1.0.12 + +* Adds compatibility with `intl` 0.18.0. + +## 1.0.11 + +* Fixes issue where failed authentication was failing silently + ## 1.0.10 * Updates imports for `prefer_relative_imports`. diff --git a/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m index 50dbb1a6907b..51c94ccc39e7 100644 --- a/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m +++ b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m @@ -124,7 +124,7 @@ - (void)testFailedAuthWithBiometrics { void (^reply)(BOOL, NSError *); [invocation getArgument:&reply atIndex:4]; dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ - reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); + reply(NO, [NSError errorWithDomain:@"error" code:LAErrorAuthenticationFailed userInfo:nil]); }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) @@ -140,14 +140,13 @@ - (void)testFailedAuthWithBiometrics { [plugin handleMethodCall:call result:^(id _Nullable result) { XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); - XCTAssertFalse([result boolValue]); + XCTAssertTrue([result isKindOfClass:[FlutterError class]]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } -- (void)testFailedAuthWithoutBiometrics { +- (void)testFailedWithUnknownErrorCode { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); plugin.authContextOverrides = @[ mockAuthContext ]; @@ -175,6 +174,45 @@ - (void)testFailedAuthWithoutBiometrics { @"localizedReason" : reason, }]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[FlutterError class]]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + +- (void)testSystemCancelledWithoutStickyAuth { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; + NSString *reason = @"a reason"; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + + // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not + // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on + // a background thread. + void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) { + void (^reply)(BOOL, NSError *); + [invocation getArgument:&reply atIndex:4]; + dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ + reply(NO, [NSError errorWithDomain:@"error" code:LAErrorSystemCancel userInfo:nil]); + }); + }; + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + .andDo(backgroundThreadReplyCaller); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" + arguments:@{ + @"biometricOnly" : @(NO), + @"localizedReason" : reason, + @"stickyAuth" : @(NO) + }]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin handleMethodCall:call result:^(id _Nullable result) { @@ -186,6 +224,44 @@ - (void)testFailedAuthWithoutBiometrics { [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } +- (void)testFailedAuthWithoutBiometrics { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; + NSString *reason = @"a reason"; + OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + + // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not + // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on + // a background thread. + void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) { + void (^reply)(BOOL, NSError *); + [invocation getArgument:&reply atIndex:4]; + dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ + reply(NO, [NSError errorWithDomain:@"error" code:LAErrorAuthenticationFailed userInfo:nil]); + }); + }; + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + .andDo(backgroundThreadReplyCaller); + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate" + arguments:@{ + @"biometricOnly" : @(NO), + @"localizedReason" : reason, + }]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; + [plugin handleMethodCall:call + result:^(id _Nullable result) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertTrue([result isKindOfClass:[FlutterError class]]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + - (void)testLocalizedFallbackTitle { FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); @@ -203,7 +279,7 @@ - (void)testLocalizedFallbackTitle { void (^reply)(BOOL, NSError *); [invocation getArgument:&reply atIndex:4]; dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ - reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); + reply(YES, nil); }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) @@ -220,10 +296,7 @@ - (void)testLocalizedFallbackTitle { XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin handleMethodCall:call result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); OCMVerify([mockAuthContext setLocalizedFallbackTitle:localizedFallbackTitle]); - XCTAssertFalse([result boolValue]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; @@ -245,7 +318,7 @@ - (void)testSkippedLocalizedFallbackTitle { void (^reply)(BOOL, NSError *); [invocation getArgument:&reply atIndex:4]; dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ - reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); + reply(YES, nil); }); }; OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) @@ -260,10 +333,7 @@ - (void)testSkippedLocalizedFallbackTitle { XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin handleMethodCall:call result:^(id _Nullable result) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertTrue([result isKindOfClass:[NSNumber class]]); OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]); - XCTAssertFalse([result boolValue]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; 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 fdbf11ffbcaa..63b317e54c7b 100644 --- a/packages/local_auth/local_auth_ios/example/lib/main.dart +++ b/packages/local_auth/local_auth_ios/example/lib/main.dart @@ -2,7 +2,7 @@ // 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 +// ignore_for_file: public_member_api_docs, avoid_print import 'dart:async'; @@ -187,6 +187,8 @@ 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( mainAxisSize: MainAxisSize.min, children: const [ @@ -200,6 +202,8 @@ 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( mainAxisSize: MainAxisSize.min, children: const [ diff --git a/packages/local_auth/local_auth_ios/example/pubspec.yaml b/packages/local_auth/local_auth_ios/example/pubspec.yaml index 720d5a732bd5..21b17fae7288 100644 --- a/packages/local_auth/local_auth_ios/example/pubspec.yaml +++ b/packages/local_auth/local_auth_ios/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m index 8f61fecfd814..4d982549643d 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -216,26 +216,29 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success result(@YES); } else { switch (error.code) { - case LAErrorPasscodeNotSet: #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - // TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in these constants when - // iOS 10 support is dropped. The values are the same, only the names have changed. + // TODO(stuartmorgan): Remove the pragma and s/TouchID/Biometry/ in these constants when + // iOS 10 support is dropped. The values are the same, only the names have changed. case LAErrorTouchIDNotAvailable: case LAErrorTouchIDNotEnrolled: case LAErrorTouchIDLockout: #pragma clang diagnostic pop case LAErrorUserFallback: + case LAErrorPasscodeNotSet: + case LAErrorAuthenticationFailed: [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; return; case LAErrorSystemCancel: if ([arguments[@"stickyAuth"] boolValue]) { self->_lastCallArgs = arguments; self->_lastResult = result; - return; + } else { + result(@NO); } + return; } - result(@NO); + [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; } } diff --git a/packages/local_auth/local_auth_ios/pubspec.yaml b/packages/local_auth/local_auth_ios/pubspec.yaml index 9cdeef963c34..ef2fa7fcdac7 100644 --- a/packages/local_auth/local_auth_ios/pubspec.yaml +++ b/packages/local_auth/local_auth_ios/pubspec.yaml @@ -2,11 +2,11 @@ name: local_auth_ios description: iOS implementation of the local_auth plugin. repository: https://github.com/flutter/plugins/tree/main/packages/local_auth/local_auth_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.0.10 +version: 1.0.12 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - intl: ^0.17.0 + intl: ">=0.17.0 <0.19.0" local_auth_platform_interface: ^1.0.1 dev_dependencies: diff --git a/packages/local_auth/local_auth_ios/test/local_auth_test.dart b/packages/local_auth/local_auth_ios/test/local_auth_test.dart index 0ad89e52f5ce..0d7f56d5da90 100644 --- a/packages/local_auth/local_auth_ios/test/local_auth_test.dart +++ b/packages/local_auth/local_auth_ios/test/local_auth_test.dart @@ -18,7 +18,9 @@ void main() { late LocalAuthIOS localAuthentication; setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) { log.add(methodCall); switch (methodCall.method) { case 'getEnrolledBiometrics': @@ -181,3 +183,9 @@ void main() { }); }); } + +/// 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/local_auth/local_auth_platform_interface/CHANGELOG.md b/packages/local_auth/local_auth_platform_interface/CHANGELOG.md index f0313ce99be6..be2be0ced788 100644 --- a/packages/local_auth/local_auth_platform_interface/CHANGELOG.md +++ b/packages/local_auth/local_auth_platform_interface/CHANGELOG.md @@ -1,3 +1,11 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 1.0.6 + +* Removes unused `intl` dependency. + ## 1.0.5 * Updates imports for `prefer_relative_imports`. diff --git a/packages/local_auth/local_auth_platform_interface/pubspec.yaml b/packages/local_auth/local_auth_platform_interface/pubspec.yaml index 92da218d8be5..bc54978fd3df 100644 --- a/packages/local_auth/local_auth_platform_interface/pubspec.yaml +++ b/packages/local_auth/local_auth_platform_interface/pubspec.yaml @@ -4,16 +4,15 @@ repository: https://github.com/flutter/plugins/tree/main/packages/local_auth/loc issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%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: 1.0.5 +version: 1.0.6 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: sdk: flutter - intl: ^0.17.0 plugin_platform_interface: ^2.1.2 dev_dependencies: diff --git a/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart b/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart index 824597ab2953..c513b4473574 100644 --- a/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart +++ b/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart @@ -29,7 +29,9 @@ void main() { }); test('getAvailableBiometrics', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) { log.add(methodCall); return Future.value([]); }); @@ -49,7 +51,9 @@ void main() { // existing unendorsed implementations, used 'undefined' as a special // return value from `getAvailableBiometrics` to indicate that nothing was // enrolled, but that the hardware does support biometrics. - channel.setMockMethodCallHandler((MethodCall methodCall) { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) { log.add(methodCall); return Future.value(['undefined']); }); @@ -68,7 +72,9 @@ void main() { group('Boolean returning methods', () { setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) { log.add(methodCall); return Future.value(true); }); @@ -198,3 +204,9 @@ void main() { }); }); } + +/// 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/local_auth/local_auth_windows/CHANGELOG.md b/packages/local_auth/local_auth_windows/CHANGELOG.md index b4f2061f2c27..90aa8b6b31db 100644 --- a/packages/local_auth/local_auth_windows/CHANGELOG.md +++ b/packages/local_auth/local_auth_windows/CHANGELOG.md @@ -1,3 +1,11 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 1.0.5 + +* Switches internal implementation to Pigeon. + ## 1.0.4 * Updates imports for `prefer_relative_imports`. 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 c7b3fd923891..3205cdb81bc8 100644 --- a/packages/local_auth/local_auth_windows/example/lib/main.dart +++ b/packages/local_auth/local_auth_windows/example/lib/main.dart @@ -2,7 +2,7 @@ // 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 +// ignore_for_file: public_member_api_docs, avoid_print import 'dart:async'; @@ -108,44 +108,6 @@ class _MyAppState extends State { () => _authorized = authenticated ? 'Authorized' : 'Not Authorized'); } - Future _authenticateWithBiometrics() async { - bool authenticated = false; - try { - setState(() { - _isAuthenticating = true; - _authorized = 'Authenticating'; - }); - authenticated = await LocalAuthPlatform.instance.authenticate( - localizedReason: - 'Scan your fingerprint (or face or whatever) to authenticate', - authMessages: [const WindowsAuthMessages()], - options: const AuthenticationOptions( - stickyAuth: true, - biometricOnly: true, - ), - ); - setState(() { - _isAuthenticating = false; - _authorized = 'Authenticating'; - }); - } on PlatformException catch (e) { - print(e); - setState(() { - _isAuthenticating = false; - _authorized = 'Error - ${e.message}'; - }); - return; - } - if (!mounted) { - return; - } - - final String message = authenticated ? 'Authorized' : 'Not Authorized'; - setState(() { - _authorized = message; - }); - } - Future _cancelAuthentication() async { await LocalAuthPlatform.instance.stopAuthentication(); setState(() => _isAuthenticating = false); @@ -188,6 +150,8 @@ 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( mainAxisSize: MainAxisSize.min, children: const [ @@ -201,6 +165,8 @@ 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( mainAxisSize: MainAxisSize.min, children: const [ @@ -209,18 +175,6 @@ class _MyAppState extends State { ], ), ), - ElevatedButton( - onPressed: _authenticateWithBiometrics, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(_isAuthenticating - ? 'Cancel' - : 'Authenticate: biometrics only'), - const Icon(Icons.fingerprint), - ], - ), - ), ], ), ], diff --git a/packages/local_auth/local_auth_windows/example/pubspec.yaml b/packages/local_auth/local_auth_windows/example/pubspec.yaml index 4bb2671f6826..1a1387a0875d 100644 --- a/packages/local_auth/local_auth_windows/example/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart b/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart index b373782c2187..9f918aab0585 100644 --- a/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart +++ b/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart @@ -2,20 +2,25 @@ // 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/foundation.dart'; import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; -import 'types/auth_messages_windows.dart'; + +import 'src/messages.g.dart'; export 'package:local_auth_platform_interface/types/auth_messages.dart'; export 'package:local_auth_platform_interface/types/auth_options.dart'; export 'package:local_auth_platform_interface/types/biometric_type.dart'; export 'package:local_auth_windows/types/auth_messages_windows.dart'; -const MethodChannel _channel = - MethodChannel('plugins.flutter.io/local_auth_windows'); - /// The implementation of [LocalAuthPlatform] for Windows. class LocalAuthWindows extends LocalAuthPlatform { + /// Creates a new plugin implementation instance. + LocalAuthWindows({ + @visibleForTesting LocalAuthApi? api, + }) : _api = api ?? LocalAuthApi(); + + final LocalAuthApi _api; + /// Registers this class as the default instance of [LocalAuthPlatform]. static void registerWith() { LocalAuthPlatform.instance = LocalAuthWindows(); @@ -28,55 +33,36 @@ class LocalAuthWindows extends LocalAuthPlatform { AuthenticationOptions options = const AuthenticationOptions(), }) async { assert(localizedReason.isNotEmpty); - final Map args = { - 'localizedReason': localizedReason, - 'useErrorDialogs': options.useErrorDialogs, - 'stickyAuth': options.stickyAuth, - 'sensitiveTransaction': options.sensitiveTransaction, - 'biometricOnly': options.biometricOnly, - }; - args.addAll(const WindowsAuthMessages().args); - for (final AuthMessages messages in authMessages) { - if (messages is WindowsAuthMessages) { - args.addAll(messages.args); - } + + if (options.biometricOnly) { + throw UnsupportedError( + "Windows doesn't support the biometricOnly parameter."); } - return (await _channel.invokeMethod('authenticate', args)) ?? false; + + return _api.authenticate(localizedReason); } @override Future deviceSupportsBiometrics() async { - return (await _channel.invokeMethod('deviceSupportsBiometrics')) ?? - false; + // Biometrics are supported on any supported device. + return isDeviceSupported(); } @override Future> getEnrolledBiometrics() async { - final List result = (await _channel.invokeListMethod( - 'getEnrolledBiometrics', - )) ?? - []; - final List biometrics = []; - for (final String value in result) { - switch (value) { - case 'weak': - biometrics.add(BiometricType.weak); - break; - case 'strong': - biometrics.add(BiometricType.strong); - break; - } + // Windows doesn't support querying specific biometric types. Since the + // OS considers this a strong authentication API, return weak+strong on + // any supported device. + if (await isDeviceSupported()) { + return [BiometricType.weak, BiometricType.strong]; } - return biometrics; + return []; } @override - Future isDeviceSupported() async => - (await _channel.invokeMethod('isDeviceSupported')) ?? false; + Future isDeviceSupported() async => _api.isDeviceSupported(); /// Always returns false as this method is not supported on Windows. @override - Future stopAuthentication() async { - return false; - } + Future stopAuthentication() async => false; } diff --git a/packages/local_auth/local_auth_windows/lib/src/messages.g.dart b/packages/local_auth/local_auth_windows/lib/src/messages.g.dart new file mode 100644 index 000000000000..312d1c0ba164 --- /dev/null +++ b/packages/local_auth/local_auth_windows/lib/src/messages.g.dart @@ -0,0 +1,81 @@ +// 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 (v5.0.1), 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 +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +class LocalAuthApi { + /// Constructor for [LocalAuthApi]. 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. + LocalAuthApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + /// Returns true if this device supports authentication. + Future isDeviceSupported() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LocalAuthApi.isDeviceSupported', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send(null) 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 bool?)!; + } + } + + /// Attempts to authenticate the user with the provided [localizedReason] as + /// the user-facing explanation for the authorization request. + /// + /// Returns true if authorization succeeds, false if it is attempted but is + /// not successful, and an error if authorization could not be attempted. + Future authenticate(String arg_localizedReason) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LocalAuthApi.authenticate', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_localizedReason]) 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 bool?)!; + } + } +} diff --git a/packages/local_auth/local_auth_windows/pigeons/copyright.txt b/packages/local_auth/local_auth_windows/pigeons/copyright.txt new file mode 100644 index 000000000000..1236b63caf3a --- /dev/null +++ b/packages/local_auth/local_auth_windows/pigeons/copyright.txt @@ -0,0 +1,3 @@ +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. diff --git a/packages/local_auth/local_auth_windows/pigeons/messages.dart b/packages/local_auth/local_auth_windows/pigeons/messages.dart new file mode 100644 index 000000000000..683becdd61fb --- /dev/null +++ b/packages/local_auth/local_auth_windows/pigeons/messages.dart @@ -0,0 +1,27 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + cppOptions: CppOptions(namespace: 'local_auth_windows'), + cppHeaderOut: 'windows/messages.g.h', + cppSourceOut: 'windows/messages.g.cpp', + copyrightHeader: 'pigeons/copyright.txt', +)) +@HostApi() +abstract class LocalAuthApi { + /// Returns true if this device supports authentication. + @async + bool isDeviceSupported(); + + /// Attempts to authenticate the user with the provided [localizedReason] as + /// the user-facing explanation for the authorization request. + /// + /// Returns true if authorization succeeds, false if it is attempted but is + /// not successful, and an error if authorization could not be attempted. + @async + bool authenticate(String localizedReason); +} diff --git a/packages/local_auth/local_auth_windows/pubspec.yaml b/packages/local_auth/local_auth_windows/pubspec.yaml index 9a2effed92ee..9866eef50584 100644 --- a/packages/local_auth/local_auth_windows/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/pubspec.yaml @@ -2,11 +2,11 @@ name: local_auth_windows description: Windows implementation of the local_auth plugin. repository: https://github.com/flutter/plugins/tree/main/packages/local_auth/local_auth_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.0.4 +version: 1.0.5 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -24,3 +24,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + pigeon: ^5.0.1 diff --git a/packages/local_auth/local_auth_windows/test/local_auth_test.dart b/packages/local_auth/local_auth_windows/test/local_auth_test.dart index b11c19e7b339..917e7b1784b6 100644 --- a/packages/local_auth/local_auth_windows/test/local_auth_test.dart +++ b/packages/local_auth/local_auth_windows/test/local_auth_test.dart @@ -2,78 +2,123 @@ // 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_test/flutter_test.dart'; import 'package:local_auth_windows/local_auth_windows.dart'; +import 'package:local_auth_windows/src/messages.g.dart'; void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - group('authenticate', () { - const MethodChannel channel = MethodChannel( - 'plugins.flutter.io/local_auth_windows', - ); - - final List log = []; - late LocalAuthWindows localAuthentication; + late _FakeLocalAuthApi api; + late LocalAuthWindows plugin; setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) { - log.add(methodCall); - switch (methodCall.method) { - case 'getEnrolledBiometrics': - return Future>.value(['weak', 'strong']); - default: - return Future.value(true); - } - }); - localAuthentication = LocalAuthWindows(); - log.clear(); + api = _FakeLocalAuthApi(); + plugin = LocalAuthWindows(api: api); }); - test('authenticate with no arguments passes expected defaults', () async { - await localAuthentication.authenticate( + test('authenticate handles success', () async { + api.returnValue = true; + + final bool result = await plugin.authenticate( authMessages: [const WindowsAuthMessages()], localizedReason: 'My localized reason'); - expect( - log, - [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'My localized reason', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': false, - }..addAll(const WindowsAuthMessages().args)), - ], - ); + + expect(result, true); + expect(api.passedReason, 'My localized reason'); + }); + + test('authenticate handles failure', () async { + api.returnValue = false; + + final bool result = await plugin.authenticate( + authMessages: [const WindowsAuthMessages()], + localizedReason: 'My localized reason'); + + expect(result, false); + expect(api.passedReason, 'My localized reason'); }); - test('authenticate passes all options.', () async { - await localAuthentication.authenticate( - authMessages: [const WindowsAuthMessages()], - localizedReason: 'My localized reason', - options: const AuthenticationOptions( - useErrorDialogs: false, - stickyAuth: true, - sensitiveTransaction: false, - biometricOnly: true, - ), - ); + test('authenticate throws for biometricOnly', () async { expect( - log, - [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'My localized reason', - 'useErrorDialogs': false, - 'stickyAuth': true, - 'sensitiveTransaction': false, - 'biometricOnly': true, - }..addAll(const WindowsAuthMessages().args)), - ], - ); + plugin.authenticate( + authMessages: [const WindowsAuthMessages()], + localizedReason: 'My localized reason', + options: const AuthenticationOptions(biometricOnly: true)), + throwsA(isUnsupportedError)); + }); + + test('isDeviceSupported handles supported', () async { + api.returnValue = true; + + final bool result = await plugin.isDeviceSupported(); + + expect(result, true); + }); + + test('isDeviceSupported handles unsupported', () async { + api.returnValue = false; + + final bool result = await plugin.isDeviceSupported(); + + expect(result, false); + }); + + test('deviceSupportsBiometrics handles supported', () async { + api.returnValue = true; + + final bool result = await plugin.deviceSupportsBiometrics(); + + expect(result, true); + }); + + test('deviceSupportsBiometrics handles unsupported', () async { + api.returnValue = false; + + final bool result = await plugin.deviceSupportsBiometrics(); + + expect(result, false); + }); + + test('getEnrolledBiometrics returns expected values when supported', + () async { + api.returnValue = true; + + final List result = await plugin.getEnrolledBiometrics(); + + expect(result, [BiometricType.weak, BiometricType.strong]); + }); + + test('getEnrolledBiometrics returns nothing when unsupported', () async { + api.returnValue = false; + + final List result = await plugin.getEnrolledBiometrics(); + + expect(result, isEmpty); + }); + + test('stopAuthentication returns false', () async { + final bool result = await plugin.stopAuthentication(); + + expect(result, false); }); }); } + +class _FakeLocalAuthApi implements LocalAuthApi { + /// The return value for [isDeviceSupported] and [authenticate]. + bool returnValue = false; + + /// The argument that was passed to [authenticate]. + String? passedReason; + + @override + Future authenticate(String localizedReason) async { + passedReason = localizedReason; + return returnValue; + } + + @override + Future isDeviceSupported() async { + return returnValue; + } +} diff --git a/packages/local_auth/local_auth_windows/windows/CMakeLists.txt b/packages/local_auth/local_auth_windows/windows/CMakeLists.txt index bcf59bb827c7..9784aa5badd9 100644 --- a/packages/local_auth/local_auth_windows/windows/CMakeLists.txt +++ b/packages/local_auth/local_auth_windows/windows/CMakeLists.txt @@ -49,12 +49,14 @@ include_directories(BEFORE SYSTEM ${CMAKE_BINARY_DIR}/include) list(APPEND PLUGIN_SOURCES "local_auth_plugin.cpp" + "local_auth.h" + "messages.g.cpp" + "messages.g.h" ) add_library(${PLUGIN_NAME} SHARED "include/local_auth_windows/local_auth_plugin.h" "local_auth_windows.cpp" - "local_auth.h" ${PLUGIN_SOURCES} ) apply_standard_settings(${PLUGIN_NAME}) diff --git a/packages/local_auth/local_auth_windows/windows/local_auth.h b/packages/local_auth/local_auth_windows/windows/local_auth.h index 94b91f88345a..9cdc6efbcd15 100644 --- a/packages/local_auth/local_auth_windows/windows/local_auth.h +++ b/packages/local_auth/local_auth_windows/windows/local_auth.h @@ -10,8 +10,6 @@ #include #include -#include "include/local_auth_windows/local_auth_plugin.h" - // Include prior to C++/WinRT Headers #include #include @@ -23,6 +21,8 @@ #include #include +#include "messages.g.h" + namespace local_auth_windows { // Abstract class that is used to determine whether a user @@ -50,7 +50,7 @@ class UserConsentVerifier { UserConsentVerifier& operator=(const UserConsentVerifier&) = delete; }; -class LocalAuthPlugin : public flutter::Plugin { +class LocalAuthPlugin : public flutter::Plugin, public LocalAuthApi { public: static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); @@ -62,28 +62,25 @@ class LocalAuthPlugin : public flutter::Plugin { // Exists for unit testing with mock implementations. LocalAuthPlugin(std::unique_ptr user_consent_verifier); - // Handles method calls from Dart on this plugin's channel. - void HandleMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result); - virtual ~LocalAuthPlugin(); + // LocalAuthApi: + void IsDeviceSupported( + std::function reply)> result) override; + void Authenticate(const std::string& localized_reason, + std::function reply)> result) override; + private: std::unique_ptr user_consent_verifier_; // Starts authentication process. - winrt::fire_and_forget Authenticate( - const flutter::MethodCall& method_call, - std::unique_ptr> result); - - // Returns enrolled biometric types available on device. - winrt::fire_and_forget GetEnrolledBiometrics( - std::unique_ptr> result); + winrt::fire_and_forget AuthenticateCoroutine( + const std::string& localized_reason, + std::function reply)> result); // Returns whether the system supports Windows Hello. - winrt::fire_and_forget IsDeviceSupported( - std::unique_ptr> result); + winrt::fire_and_forget IsDeviceSupportedCoroutine( + std::function reply)> result); }; -} // namespace local_auth_windows \ No newline at end of file +} // namespace local_auth_windows diff --git a/packages/local_auth/local_auth_windows/windows/local_auth_plugin.cpp b/packages/local_auth/local_auth_windows/windows/local_auth_plugin.cpp index 7a25abb53010..80fab37ee50d 100644 --- a/packages/local_auth/local_auth_windows/windows/local_auth_plugin.cpp +++ b/packages/local_auth/local_auth_windows/windows/local_auth_plugin.cpp @@ -4,24 +4,10 @@ #include #include "local_auth.h" +#include "messages.g.h" namespace { -template -// Helper method for getting an argument from an EncodableValue. -T GetArgument(const std::string arg, const flutter::EncodableValue* args, - T fallback) { - T result{fallback}; - const auto* arguments = std::get_if(args); - if (arguments) { - auto result_it = arguments->find(flutter::EncodableValue(arg)); - if (result_it != arguments->end()) { - result = std::get(result_it->second); - } - } - return result; -} - // Returns the window's HWND for a given FlutterView. HWND GetRootWindow(flutter::FlutterView* view) { return ::GetAncestor(view->GetNativeWindow(), GA_ROOT); @@ -110,19 +96,9 @@ class UserConsentVerifierImpl : public UserConsentVerifier { // static void LocalAuthPlugin::RegisterWithRegistrar( flutter::PluginRegistrarWindows* registrar) { - auto channel = - std::make_unique>( - registrar->messenger(), "plugins.flutter.io/local_auth_windows", - &flutter::StandardMethodCodec::GetInstance()); - auto plugin = std::make_unique( [registrar]() { return GetRootWindow(registrar->GetView()); }); - - channel->SetMethodCallHandler( - [plugin_pointer = plugin.get()](const auto& call, auto result) { - plugin_pointer->HandleMethodCall(call, std::move(result)); - }); - + LocalAuthApi::SetUp(registrar->messenger(), plugin.get()); registrar->AddPlugin(std::move(plugin)); } @@ -137,36 +113,22 @@ LocalAuthPlugin::LocalAuthPlugin( LocalAuthPlugin::~LocalAuthPlugin() {} -void LocalAuthPlugin::HandleMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result) { - if (method_call.method_name().compare("authenticate") == 0) { - Authenticate(method_call, std::move(result)); - } else if (method_call.method_name().compare("getEnrolledBiometrics") == 0) { - GetEnrolledBiometrics(std::move(result)); - } else if (method_call.method_name().compare("isDeviceSupported") == 0 || - method_call.method_name().compare("deviceSupportsBiometrics") == - 0) { - IsDeviceSupported(std::move(result)); - } else { - result->NotImplemented(); - } +void LocalAuthPlugin::IsDeviceSupported( + std::function reply)> result) { + IsDeviceSupportedCoroutine(std::move(result)); +} + +void LocalAuthPlugin::Authenticate( + const std::string& localized_reason, + std::function reply)> result) { + AuthenticateCoroutine(localized_reason, std::move(result)); } // Starts authentication process. -winrt::fire_and_forget LocalAuthPlugin::Authenticate( - const flutter::MethodCall& method_call, - std::unique_ptr> result) { - std::wstring reason = Utf16FromUtf8(GetArgument( - "localizedReason", method_call.arguments(), std::string())); - - bool biometric_only = - GetArgument("biometricOnly", method_call.arguments(), false); - if (biometric_only) { - result->Error("biometricOnlyNotSupported", - "Windows doesn't support the biometricOnly parameter."); - co_return; - } +winrt::fire_and_forget LocalAuthPlugin::AuthenticateCoroutine( + const std::string& localized_reason, + std::function reply)> result) { + std::wstring reason = Utf16FromUtf8(localized_reason); winrt::Windows::Security::Credentials::UI::UserConsentVerifierAvailability ucv_availability = @@ -175,17 +137,19 @@ winrt::fire_and_forget LocalAuthPlugin::Authenticate( if (ucv_availability == winrt::Windows::Security::Credentials::UI:: UserConsentVerifierAvailability::DeviceNotPresent) { - result->Error("NoHardware", "No biometric hardware found"); + result(FlutterError("NoHardware", "No biometric hardware found")); co_return; } else if (ucv_availability == winrt::Windows::Security::Credentials::UI:: UserConsentVerifierAvailability::NotConfiguredForUser) { - result->Error("NotEnrolled", "No biometrics enrolled on this device."); + result( + FlutterError("NotEnrolled", "No biometrics enrolled on this device.")); co_return; } else if (ucv_availability != winrt::Windows::Security::Credentials::UI:: UserConsentVerifierAvailability::Available) { - result->Error("NotAvailable", "Required security features not enabled"); + result( + FlutterError("NotAvailable", "Required security features not enabled")); co_return; } @@ -195,42 +159,21 @@ winrt::fire_and_forget LocalAuthPlugin::Authenticate( co_await user_consent_verifier_->RequestVerificationForWindowAsync( reason); - result->Success(flutter::EncodableValue( - consent_result == winrt::Windows::Security::Credentials::UI:: - UserConsentVerificationResult::Verified)); + result(consent_result == winrt::Windows::Security::Credentials::UI:: + UserConsentVerificationResult::Verified); } catch (...) { - result->Success(flutter::EncodableValue(false)); - } -} - -// Returns biometric types available on device. -winrt::fire_and_forget LocalAuthPlugin::GetEnrolledBiometrics( - std::unique_ptr> result) { - try { - flutter::EncodableList biometrics; - winrt::Windows::Security::Credentials::UI::UserConsentVerifierAvailability - ucv_availability = - co_await user_consent_verifier_->CheckAvailabilityAsync(); - if (ucv_availability == winrt::Windows::Security::Credentials::UI:: - UserConsentVerifierAvailability::Available) { - biometrics.push_back(flutter::EncodableValue("weak")); - biometrics.push_back(flutter::EncodableValue("strong")); - } - result->Success(biometrics); - } catch (const std::exception& e) { - result->Error("no_biometrics_available", e.what()); + result(false); } } // Returns whether the device supports Windows Hello or not. -winrt::fire_and_forget LocalAuthPlugin::IsDeviceSupported( - std::unique_ptr> result) { +winrt::fire_and_forget LocalAuthPlugin::IsDeviceSupportedCoroutine( + std::function reply)> result) { winrt::Windows::Security::Credentials::UI::UserConsentVerifierAvailability ucv_availability = co_await user_consent_verifier_->CheckAvailabilityAsync(); - result->Success(flutter::EncodableValue( - ucv_availability == winrt::Windows::Security::Credentials::UI:: - UserConsentVerifierAvailability::Available)); + result(ucv_availability == winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability::Available); } } // namespace local_auth_windows diff --git a/packages/local_auth/local_auth_windows/windows/messages.g.cpp b/packages/local_auth/local_auth_windows/windows/messages.g.cpp new file mode 100644 index 000000000000..e44b17c6a38d --- /dev/null +++ b/packages/local_auth/local_auth_windows/windows/messages.g.cpp @@ -0,0 +1,110 @@ +// 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 (v5.0.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#undef _HAS_EXCEPTIONS + +#include "messages.g.h" + +#include +#include +#include +#include + +#include +#include +#include + +namespace local_auth_windows { +/// The codec used by LocalAuthApi. +const flutter::StandardMessageCodec& LocalAuthApi::GetCodec() { + return flutter::StandardMessageCodec::GetInstance( + &flutter::StandardCodecSerializer::GetInstance()); +} + +// Sets up an instance of `LocalAuthApi` to handle messages through the +// `binary_messenger`. +void LocalAuthApi::SetUp(flutter::BinaryMessenger* binary_messenger, + LocalAuthApi* api) { + { + auto channel = + std::make_unique>( + binary_messenger, + "dev.flutter.pigeon.LocalAuthApi.isDeviceSupported", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::EncodableValue& message, + const flutter::MessageReply& reply) { + try { + api->IsDeviceSupported([reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + flutter::EncodableList wrapped; + wrapped.push_back( + flutter::EncodableValue(std::move(output).TakeValue())); + reply(flutter::EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = + std::make_unique>( + binary_messenger, "dev.flutter.pigeon.LocalAuthApi.authenticate", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_localized_reason_arg = args.at(0); + if (encodable_localized_reason_arg.IsNull()) { + reply(WrapError("localized_reason_arg unexpectedly null.")); + return; + } + const auto& localized_reason_arg = + std::get(encodable_localized_reason_arg); + api->Authenticate( + localized_reason_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + flutter::EncodableList wrapped; + wrapped.push_back( + flutter::EncodableValue(std::move(output).TakeValue())); + reply(flutter::EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } +} + +flutter::EncodableValue LocalAuthApi::WrapError( + std::string_view error_message) { + return flutter::EncodableValue(flutter::EncodableList{ + flutter::EncodableValue(std::string(error_message)), + flutter::EncodableValue("Error"), flutter::EncodableValue()}); +} +flutter::EncodableValue LocalAuthApi::WrapError(const FlutterError& error) { + return flutter::EncodableValue(flutter::EncodableList{ + flutter::EncodableValue(error.message()), + flutter::EncodableValue(error.code()), error.details()}); +} + +} // namespace local_auth_windows diff --git a/packages/local_auth/local_auth_windows/windows/messages.g.h b/packages/local_auth/local_auth_windows/windows/messages.g.h new file mode 100644 index 000000000000..2ceff7732c90 --- /dev/null +++ b/packages/local_auth/local_auth_windows/windows/messages.g.h @@ -0,0 +1,93 @@ +// 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 (v5.0.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#ifndef PIGEON_LOCAL_AUTH_WINDOWS_H_ +#define PIGEON_LOCAL_AUTH_WINDOWS_H_ +#include +#include +#include +#include + +#include +#include +#include + +namespace local_auth_windows { + +// Generated class from Pigeon. + +class FlutterError { + public: + explicit FlutterError(const std::string& code) : code_(code) {} + explicit FlutterError(const std::string& code, const std::string& message) + : code_(code), message_(message) {} + explicit FlutterError(const std::string& code, const std::string& message, + const flutter::EncodableValue& details) + : code_(code), message_(message), details_(details) {} + + const std::string& code() const { return code_; } + const std::string& message() const { return message_; } + const flutter::EncodableValue& details() const { return details_; } + + private: + std::string code_; + std::string message_; + flutter::EncodableValue details_; +}; + +template +class ErrorOr { + public: + ErrorOr(const T& rhs) { new (&v_) T(rhs); } + ErrorOr(const T&& rhs) { v_ = std::move(rhs); } + ErrorOr(const FlutterError& rhs) { new (&v_) FlutterError(rhs); } + ErrorOr(const FlutterError&& rhs) { v_ = std::move(rhs); } + + bool has_error() const { return std::holds_alternative(v_); } + const T& value() const { return std::get(v_); }; + const FlutterError& error() const { return std::get(v_); }; + + private: + friend class LocalAuthApi; + ErrorOr() = default; + T TakeValue() && { return std::get(std::move(v_)); } + + std::variant v_; +}; + +// Generated interface from Pigeon that represents a handler of messages from +// Flutter. +class LocalAuthApi { + public: + LocalAuthApi(const LocalAuthApi&) = delete; + LocalAuthApi& operator=(const LocalAuthApi&) = delete; + virtual ~LocalAuthApi(){}; + // Returns true if this device supports authentication. + virtual void IsDeviceSupported( + std::function reply)> result) = 0; + // Attempts to authenticate the user with the provided [localizedReason] as + // the user-facing explanation for the authorization request. + // + // Returns true if authorization succeeds, false if it is attempted but is + // not successful, and an error if authorization could not be attempted. + virtual void Authenticate( + const std::string& localized_reason, + std::function reply)> result) = 0; + + // The codec used by LocalAuthApi. + static const flutter::StandardMessageCodec& GetCodec(); + // Sets up an instance of `LocalAuthApi` to handle messages through the + // `binary_messenger`. + static void SetUp(flutter::BinaryMessenger* binary_messenger, + LocalAuthApi* api); + static flutter::EncodableValue WrapError(std::string_view error_message); + static flutter::EncodableValue WrapError(const FlutterError& error); + + protected: + LocalAuthApi() = default; +}; +} // namespace local_auth_windows +#endif // PIGEON_LOCAL_AUTH_WINDOWS_H_ diff --git a/packages/local_auth/local_auth_windows/windows/test/local_auth_plugin_test.cpp b/packages/local_auth/local_auth_windows/windows/test/local_auth_plugin_test.cpp index 3828b05eef07..6b1b0ed79c3f 100644 --- a/packages/local_auth/local_auth_windows/windows/test/local_auth_plugin_test.cpp +++ b/packages/local_auth/local_auth_windows/windows/test/local_auth_plugin_test.cpp @@ -4,10 +4,6 @@ #include "include/local_auth_windows/local_auth_plugin.h" -#include -#include -#include -#include #include #include #include @@ -32,9 +28,6 @@ using ::testing::Pointee; using ::testing::Return; TEST(LocalAuthPlugin, IsDeviceSupportedHandlerSuccessIfVerifierAvailable) { - std::unique_ptr result = - std::make_unique(); - std::unique_ptr mockConsentVerifier = std::make_unique(); @@ -48,48 +41,14 @@ TEST(LocalAuthPlugin, IsDeviceSupportedHandlerSuccessIfVerifierAvailable) { }); LocalAuthPlugin plugin(std::move(mockConsentVerifier)); + ErrorOr result(false); + plugin.IsDeviceSupported([&result](ErrorOr reply) { result = reply; }); - EXPECT_CALL(*result, ErrorInternal).Times(0); - EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(true)))); - - plugin.HandleMethodCall( - flutter::MethodCall("isDeviceSupported", - std::make_unique()), - std::move(result)); + EXPECT_FALSE(result.has_error()); + EXPECT_TRUE(result.value()); } TEST(LocalAuthPlugin, IsDeviceSupportedHandlerSuccessIfVerifierNotAvailable) { - std::unique_ptr result = - std::make_unique(); - - std::unique_ptr mockConsentVerifier = - std::make_unique(); - - EXPECT_CALL(*mockConsentVerifier, CheckAvailabilityAsync) - .Times(1) - .WillOnce([]() -> winrt::Windows::Foundation::IAsyncOperation< - winrt::Windows::Security::Credentials::UI:: - UserConsentVerifierAvailability> { - co_return winrt::Windows::Security::Credentials::UI:: - UserConsentVerifierAvailability::DeviceNotPresent; - }); - - LocalAuthPlugin plugin(std::move(mockConsentVerifier)); - - EXPECT_CALL(*result, ErrorInternal).Times(0); - EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(false)))); - - plugin.HandleMethodCall( - flutter::MethodCall("isDeviceSupported", - std::make_unique()), - std::move(result)); -} - -TEST(LocalAuthPlugin, - GetEnrolledBiometricsHandlerReturnEmptyListIfVerifierNotAvailable) { - std::unique_ptr result = - std::make_unique(); - std::unique_ptr mockConsentVerifier = std::make_unique(); @@ -103,72 +62,14 @@ TEST(LocalAuthPlugin, }); LocalAuthPlugin plugin(std::move(mockConsentVerifier)); + ErrorOr result(true); + plugin.IsDeviceSupported([&result](ErrorOr reply) { result = reply; }); - EXPECT_CALL(*result, ErrorInternal).Times(0); - EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableList()))); - - plugin.HandleMethodCall( - flutter::MethodCall("getEnrolledBiometrics", - std::make_unique()), - std::move(result)); -} - -TEST(LocalAuthPlugin, - GetEnrolledBiometricsHandlerReturnNonEmptyListIfVerifierAvailable) { - std::unique_ptr result = - std::make_unique(); - - std::unique_ptr mockConsentVerifier = - std::make_unique(); - - EXPECT_CALL(*mockConsentVerifier, CheckAvailabilityAsync) - .Times(1) - .WillOnce([]() -> winrt::Windows::Foundation::IAsyncOperation< - winrt::Windows::Security::Credentials::UI:: - UserConsentVerifierAvailability> { - co_return winrt::Windows::Security::Credentials::UI:: - UserConsentVerifierAvailability::Available; - }); - - LocalAuthPlugin plugin(std::move(mockConsentVerifier)); - - EXPECT_CALL(*result, ErrorInternal).Times(0); - EXPECT_CALL(*result, - SuccessInternal(Pointee(EncodableList( - {EncodableValue("weak"), EncodableValue("strong")})))); - - plugin.HandleMethodCall( - flutter::MethodCall("getEnrolledBiometrics", - std::make_unique()), - std::move(result)); -} - -TEST(LocalAuthPlugin, AuthenticateHandlerDoesNotSupportBiometricOnly) { - std::unique_ptr result = - std::make_unique(); - - std::unique_ptr mockConsentVerifier = - std::make_unique(); - - LocalAuthPlugin plugin(std::move(mockConsentVerifier)); - - EXPECT_CALL(*result, ErrorInternal).Times(1); - EXPECT_CALL(*result, SuccessInternal).Times(0); - - std::unique_ptr args = - std::make_unique(EncodableMap({ - {"localizedReason", EncodableValue("My Reason")}, - {"biometricOnly", EncodableValue(true)}, - })); - - plugin.HandleMethodCall(flutter::MethodCall("authenticate", std::move(args)), - std::move(result)); + EXPECT_FALSE(result.has_error()); + EXPECT_FALSE(result.value()); } TEST(LocalAuthPlugin, AuthenticateHandlerWorksWhenAuthorized) { - std::unique_ptr result = - std::make_unique(); - std::unique_ptr mockConsentVerifier = std::make_unique(); @@ -193,24 +94,15 @@ TEST(LocalAuthPlugin, AuthenticateHandlerWorksWhenAuthorized) { }); LocalAuthPlugin plugin(std::move(mockConsentVerifier)); + ErrorOr result(false); + plugin.Authenticate("My Reason", + [&result](ErrorOr reply) { result = reply; }); - EXPECT_CALL(*result, ErrorInternal).Times(0); - EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(true)))); - - std::unique_ptr args = - std::make_unique(EncodableMap({ - {"localizedReason", EncodableValue("My Reason")}, - {"biometricOnly", EncodableValue(false)}, - })); - - plugin.HandleMethodCall(flutter::MethodCall("authenticate", std::move(args)), - std::move(result)); + EXPECT_FALSE(result.has_error()); + EXPECT_TRUE(result.value()); } TEST(LocalAuthPlugin, AuthenticateHandlerWorksWhenNotAuthorized) { - std::unique_ptr result = - std::make_unique(); - std::unique_ptr mockConsentVerifier = std::make_unique(); @@ -235,18 +127,12 @@ TEST(LocalAuthPlugin, AuthenticateHandlerWorksWhenNotAuthorized) { }); LocalAuthPlugin plugin(std::move(mockConsentVerifier)); + ErrorOr result(true); + plugin.Authenticate("My Reason", + [&result](ErrorOr reply) { result = reply; }); - EXPECT_CALL(*result, ErrorInternal).Times(0); - EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(false)))); - - std::unique_ptr args = - std::make_unique(EncodableMap({ - {"localizedReason", EncodableValue("My Reason")}, - {"biometricOnly", EncodableValue(false)}, - })); - - plugin.HandleMethodCall(flutter::MethodCall("authenticate", std::move(args)), - std::move(result)); + EXPECT_FALSE(result.has_error()); + EXPECT_FALSE(result.value()); } } // namespace test diff --git a/packages/local_auth/local_auth_windows/windows/test/mocks.h b/packages/local_auth/local_auth_windows/windows/test/mocks.h index d82ae801b4b9..a31eb98aa7ef 100644 --- a/packages/local_auth/local_auth_windows/windows/test/mocks.h +++ b/packages/local_auth/local_auth_windows/windows/test/mocks.h @@ -5,10 +5,6 @@ #ifndef PACKAGES_LOCAL_AUTH_LOCAL_AUTH_WINDOWS_WINDOWS_TEST_MOCKS_H_ #define PACKAGES_LOCAL_AUTH_LOCAL_AUTH_WINDOWS_WINDOWS_TEST_MOCKS_H_ -#include -#include -#include -#include #include #include @@ -19,23 +15,8 @@ namespace test { namespace { -using flutter::EncodableMap; -using flutter::EncodableValue; using ::testing::_; -class MockMethodResult : public flutter::MethodResult<> { - public: - ~MockMethodResult() = default; - - MOCK_METHOD(void, SuccessInternal, (const EncodableValue* result), - (override)); - MOCK_METHOD(void, ErrorInternal, - (const std::string& error_code, const std::string& error_message, - const EncodableValue* details), - (override)); - MOCK_METHOD(void, NotImplementedInternal, (), (override)); -}; - class MockUserConsentVerifier : public UserConsentVerifier { public: explicit MockUserConsentVerifier(){}; diff --git a/packages/path_provider/path_provider/CHANGELOG.md b/packages/path_provider/path_provider/CHANGELOG.md index 436523551924..0f5e8e6d7225 100644 --- a/packages/path_provider/path_provider/CHANGELOG.md +++ b/packages/path_provider/path_provider/CHANGELOG.md @@ -1,5 +1,11 @@ ## NEXT +* Updates minimum Flutter version to 3.0. + +## 2.0.12 + +* Switches to the new `path_provider_foundation` implementation package + for iOS and macOS. * Updates code for `no_leading_underscores_for_local_identifiers` lint. * Updates minimum Flutter version to 2.10. * Fixes avoid_redundant_argument_values lint warnings and minor typos. diff --git a/packages/path_provider/path_provider/README.md b/packages/path_provider/path_provider/README.md index 3a52e3e72050..6a954d2ece61 100644 --- a/packages/path_provider/path_provider/README.md +++ b/packages/path_provider/path_provider/README.md @@ -36,7 +36,7 @@ Directories support by platform: | External Storage | ✔️ | ❌ | ❌ | ❌️ | ❌️ | | External Cache Directories | ✔️ | ❌ | ❌ | ❌️ | ❌️ | | External Storage Directories | ✔️ | ❌ | ❌ | ❌️ | ❌️ | -| Downloads | ❌ | ❌ | ✔️ | ✔️ | ✔️ | +| Downloads | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | ## Testing diff --git a/packages/path_provider/path_provider/example/android/gradle.properties b/packages/path_provider/path_provider/example/android/gradle.properties index 38c8d4544ff1..2f3603c9ff62 100644 --- a/packages/path_provider/path_provider/example/android/gradle.properties +++ b/packages/path_provider/path_provider/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/packages/path_provider/path_provider/example/integration_test/path_provider_test.dart b/packages/path_provider/path_provider/example/integration_test/path_provider_test.dart index bf150f66f49b..f59a8faf31e0 100644 --- a/packages/path_provider/path_provider/example/integration_test/path_provider_test.dart +++ b/packages/path_provider/path_provider/example/integration_test/path_provider_test.dart @@ -87,19 +87,16 @@ void main() { } testWidgets('getDownloadsDirectory', (WidgetTester tester) async { - if (Platform.isIOS || Platform.isAndroid) { + if (Platform.isAndroid) { final Future result = getDownloadsDirectory(); expect(result, throwsA(isInstanceOf())); } else { final Directory? result = await getDownloadsDirectory(); - if (Platform.isMacOS) { - // On recent versions of macOS, actually using the downloads directory - // requires a user prompt, so will fail on CI. Instead, just check that - // it returned a path with the expected directory name. - expect(result?.path, endsWith('Downloads')); - } else { - _verifySampleFile(result, 'downloads'); - } + // On recent versions of macOS, actually using the downloads directory + // requires a user prompt (so will fail on CI), and on some platforms the + // directory may not exist. Instead of verifying that it exists, just + // check that it returned a path. + expect(result?.path, isNotEmpty); } }); } diff --git a/packages/path_provider/path_provider/example/pubspec.yaml b/packages/path_provider/path_provider/example/pubspec.yaml index 5964a267f96d..ffb878bcf146 100644 --- a/packages/path_provider/path_provider/example/pubspec.yaml +++ b/packages/path_provider/path_provider/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/path_provider/path_provider/lib/path_provider.dart b/packages/path_provider/path_provider/lib/path_provider.dart index e89d29dc0036..b58a7ff6cc7b 100644 --- a/packages/path_provider/path_provider/lib/path_provider.dart +++ b/packages/path_provider/path_provider/lib/path_provider.dart @@ -45,11 +45,11 @@ PathProviderPlatform get _platform => PathProviderPlatform.instance; /// (and cleaning up) files or directories within this directory. This /// directory is scoped to the calling application. /// -/// On iOS, this uses the `NSCachesDirectory` API. +/// Example implementations: +/// - `NSCachesDirectory` on iOS and macOS. +/// - `Context.getCacheDir` on Android. /// -/// On Android, this uses the `getCacheDir` API on the context. -/// -/// Throws a `MissingPlatformDirectoryException` if the system is unable to +/// Throws a [MissingPlatformDirectoryException] if the system is unable to /// provide the directory. Future getTemporaryDirectory() async { final String? path = await _platform.getTemporaryPath(); @@ -63,15 +63,16 @@ Future getTemporaryDirectory() async { /// Path to a directory where the application may place application support /// files. /// +/// If this directory does not exist, it is created automatically. +/// /// Use this for files you don’t want exposed to the user. Your app should not /// use this directory for user data files. /// -/// On iOS, this uses the `NSApplicationSupportDirectory` API. -/// If this directory does not exist, it is created automatically. -/// -/// On Android, this function uses the `getFilesDir` API on the context. +/// Example implementations: +/// - `NSApplicationSupportDirectory` on iOS and macOS. +/// - The Flutter engine's `PathUtils.getFilesDir` API on Android. /// -/// Throws a `MissingPlatformDirectoryException` if the system is unable to +/// Throws a [MissingPlatformDirectoryException] if the system is unable to /// provide the directory. Future getApplicationSupportDirectory() async { final String? path = await _platform.getApplicationSupportPath(); @@ -86,10 +87,14 @@ Future getApplicationSupportDirectory() async { /// Path to the directory where application can store files that are persistent, /// backed up, and not visible to the user, such as sqlite.db. /// -/// On Android, this function throws an [UnsupportedError] as no equivalent -/// path exists. +/// Example implementations: +/// - `NSApplicationSupportDirectory` on iOS and macOS. +/// +/// Throws an [UnsupportedError] if this is not supported on the current +/// platform. For example, this is unlikely to ever be supported on Android, +/// as no equivalent path exists. /// -/// Throws a `MissingPlatformDirectoryException` if the system is unable to +/// Throws a [MissingPlatformDirectoryException] if the system is unable to /// provide the directory on a supported platform. Future getLibraryDirectory() async { final String? path = await _platform.getLibraryPath(); @@ -102,14 +107,14 @@ Future getLibraryDirectory() async { /// Path to a directory where the application may place data that is /// user-generated, or that cannot otherwise be recreated by your application. /// -/// On iOS, this uses the `NSDocumentDirectory` API. Consider using -/// [getApplicationSupportDirectory] instead if the data is not user-generated. +/// Consider using another path, such as [getApplicationSupportDirectory] or +/// [getExternalStorageDirectory], if the data is not user-generated. /// -/// On Android, this uses the `getDataDirectory` API on the context. Consider -/// using [getExternalStorageDirectory] instead if data is intended to be visible -/// to the user. +/// Example implementations: +/// - `NSDocumentDirectory` on iOS and macOS. +/// - The Flutter engine's `PathUtils.getDataDirectory` API on Android. /// -/// Throws a `MissingPlatformDirectoryException` if the system is unable to +/// Throws a [MissingPlatformDirectoryException] if the system is unable to /// provide the directory. Future getApplicationDocumentsDirectory() async { final String? path = await _platform.getApplicationDocumentsPath(); @@ -121,13 +126,13 @@ Future getApplicationDocumentsDirectory() async { } /// Path to a directory where the application may access top level storage. -/// The current operating system should be determined before issuing this -/// function call, as this functionality is only available on Android. /// -/// On iOS, this function throws an [UnsupportedError] as it is not possible -/// to access outside the app's sandbox. +/// Example implementation: +/// - `getExternalFilesDir(null)` on Android. /// -/// On Android this uses the `getExternalFilesDir(null)`. +/// Throws an [UnsupportedError] if this is not supported on the current +/// platform (for example, on iOS where it is not possible to access outside +/// the app's sandbox). Future getExternalStorageDirectory() async { final String? path = await _platform.getExternalStoragePath(); if (path == null) { @@ -136,19 +141,19 @@ Future getExternalStorageDirectory() async { return Directory(path); } -/// Paths to directories where application specific external cache data can be -/// stored. These paths typically reside on external storage like separate -/// partitions or SD cards. Phones may have multiple storage directories -/// available. +/// Paths to directories where application specific cache data can be stored +/// externally. /// -/// The current operating system should be determined before issuing this -/// function call, as this functionality is only available on Android. +/// These paths typically reside on external storage like separate partitions +/// or SD cards. Phones may have multiple storage directories available. /// -/// On iOS, this function throws an UnsupportedError as it is not possible -/// to access outside the app's sandbox. +/// Example implementation: +/// - Context.getExternalCacheDirs() on Android (or +/// Context.getExternalCacheDir() on API levels below 19). /// -/// On Android this returns Context.getExternalCacheDirs() or -/// Context.getExternalCacheDir() on API levels below 19. +/// Throws an [UnsupportedError] if this is not supported on the current +/// platform. This is unlikely to ever be supported on any platform other than +/// Android. Future?> getExternalCacheDirectories() async { final List? paths = await _platform.getExternalCachePaths(); if (paths == null) { @@ -158,18 +163,19 @@ Future?> getExternalCacheDirectories() async { return paths.map((String path) => Directory(path)).toList(); } -/// Paths to directories where application specific data can be stored. +/// Paths to directories where application specific data can be stored +/// externally. +/// /// These paths typically reside on external storage like separate partitions /// or SD cards. Phones may have multiple storage directories available. /// -/// The current operating system should be determined before issuing this -/// function call, as this functionality is only available on Android. +/// Example implementation: +/// - Context.getExternalFilesDirs(type) on Android (or +/// Context.getExternalFilesDir(type) on API levels below 19). /// -/// On iOS, this function throws an UnsupportedError as it is not possible -/// to access outside the app's sandbox. -/// -/// On Android this returns Context.getExternalFilesDirs(String type) or -/// Context.getExternalFilesDir(String type) on API levels below 19. +/// Throws an [UnsupportedError] if this is not supported on the current +/// platform. This is unlikely to ever be supported on any platform other than +/// Android. Future?> getExternalStorageDirectories({ /// Optional parameter. See [StorageDirectory] for more informations on /// how this type translates to Android storage directories. @@ -185,10 +191,12 @@ Future?> getExternalStorageDirectories({ } /// Path to the directory where downloaded files can be stored. -/// This is typically only relevant on desktop operating systems. /// -/// On Android and on iOS, this function throws an [UnsupportedError] as no equivalent -/// path exists. +/// The returned directory is not guaranteed to exist, so clients should verify +/// that it does before using it, and potentially create it if necessary. +/// +/// Throws an [UnsupportedError] if this is not supported on the current +/// platform. Future getDownloadsDirectory() async { final String? path = await _platform.getDownloadsPath(); if (path == null) { diff --git a/packages/path_provider/path_provider/pubspec.yaml b/packages/path_provider/path_provider/pubspec.yaml index 8b68e1264fe7..8c139ccbb87b 100644 --- a/packages/path_provider/path_provider/pubspec.yaml +++ b/packages/path_provider/path_provider/pubspec.yaml @@ -2,11 +2,11 @@ name: path_provider description: Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. repository: https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.11 +version: 2.0.12 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -14,11 +14,11 @@ flutter: android: default_package: path_provider_android ios: - default_package: path_provider_ios - macos: - default_package: path_provider_macos + default_package: path_provider_foundation linux: default_package: path_provider_linux + macos: + default_package: path_provider_foundation windows: default_package: path_provider_windows @@ -26,9 +26,8 @@ dependencies: flutter: sdk: flutter path_provider_android: ^2.0.6 - path_provider_ios: ^2.0.6 + path_provider_foundation: ^2.1.0 path_provider_linux: ^2.0.1 - path_provider_macos: ^2.0.0 path_provider_platform_interface: ^2.0.0 path_provider_windows: ^2.0.2 diff --git a/packages/path_provider/path_provider_android/CHANGELOG.md b/packages/path_provider/path_provider_android/CHANGELOG.md index ebd656816ead..acf99b7a5e25 100644 --- a/packages/path_provider/path_provider_android/CHANGELOG.md +++ b/packages/path_provider/path_provider_android/CHANGELOG.md @@ -1,3 +1,11 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 2.0.22 + +* Removes unused Guava dependency. + ## 2.0.21 * Updates code for `no_leading_underscores_for_local_identifiers` lint. diff --git a/packages/path_provider/path_provider_android/android/build.gradle b/packages/path_provider/path_provider_android/android/build.gradle index 9661390dbc80..926142e5eaf8 100644 --- a/packages/path_provider/path_provider_android/android/build.gradle +++ b/packages/path_provider/path_provider_android/android/build.gradle @@ -29,10 +29,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - // TODO(stuartmorgan): Enable when gradle is updated. - // disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -55,6 +52,5 @@ android { dependencies { implementation 'androidx.annotation:annotation:1.5.0' - implementation 'com.google.guava:guava:28.1-android' testImplementation 'junit:junit:4.13.2' } diff --git a/packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java b/packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java index 7ef82198b22c..285d62ec68fd 100644 --- a/packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java +++ b/packages/path_provider/path_provider_android/android/src/main/java/io/flutter/plugins/pathprovider/PathProviderPlugin.java @@ -9,6 +9,7 @@ import android.os.Build.VERSION_CODES; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.BinaryMessenger.TaskQueue; @@ -17,7 +18,6 @@ import java.io.File; import java.util.ArrayList; import java.util.List; -import javax.annotation.Nullable; public class PathProviderPlugin implements FlutterPlugin, PathProviderApi { static final String TAG = "PathProviderPlugin"; diff --git a/packages/path_provider/path_provider_android/example/android/gradle.properties b/packages/path_provider/path_provider_android/example/android/gradle.properties index 38c8d4544ff1..2f3603c9ff62 100644 --- a/packages/path_provider/path_provider_android/example/android/gradle.properties +++ b/packages/path_provider/path_provider_android/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/packages/path_provider/path_provider_android/example/pubspec.yaml b/packages/path_provider/path_provider_android/example/pubspec.yaml index b460d6ba49ce..e53c44ffda68 100644 --- a/packages/path_provider/path_provider_android/example/pubspec.yaml +++ b/packages/path_provider/path_provider_android/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/path_provider/path_provider_android/pubspec.yaml b/packages/path_provider/path_provider_android/pubspec.yaml index 5d1e6c7b2db6..dcdf938feee5 100644 --- a/packages/path_provider/path_provider_android/pubspec.yaml +++ b/packages/path_provider/path_provider_android/pubspec.yaml @@ -2,11 +2,11 @@ name: path_provider_android description: Android implementation of the path_provider plugin. repository: https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.21 +version: 2.0.22 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/path_provider/path_provider_macos/.gitignore b/packages/path_provider/path_provider_foundation/.gitignore similarity index 100% rename from packages/path_provider/path_provider_macos/.gitignore rename to packages/path_provider/path_provider_foundation/.gitignore diff --git a/packages/path_provider/path_provider_ios/AUTHORS b/packages/path_provider/path_provider_foundation/AUTHORS similarity index 100% rename from packages/path_provider/path_provider_ios/AUTHORS rename to packages/path_provider/path_provider_foundation/AUTHORS diff --git a/packages/path_provider/path_provider_foundation/CHANGELOG.md b/packages/path_provider/path_provider_foundation/CHANGELOG.md new file mode 100644 index 000000000000..7adb04f4c984 --- /dev/null +++ b/packages/path_provider/path_provider_foundation/CHANGELOG.md @@ -0,0 +1,13 @@ +## NEXT + +* Updates minimum supported Flutter version to 3.0. + +## 2.1.1 + +* Fixes a regression in the path retured by `getApplicationSupportDirectory` on iOS. + +## 2.1.0 + +* Renames the package previously published as + [`path_provider_macos`](https://pub.dev/packages/path_provider_macos) +* Adds iOS support. diff --git a/packages/path_provider/path_provider_ios/LICENSE b/packages/path_provider/path_provider_foundation/LICENSE similarity index 100% rename from packages/path_provider/path_provider_ios/LICENSE rename to packages/path_provider/path_provider_foundation/LICENSE diff --git a/packages/path_provider/path_provider_macos/README.md b/packages/path_provider/path_provider_foundation/README.md similarity index 78% rename from packages/path_provider/path_provider_macos/README.md rename to packages/path_provider/path_provider_foundation/README.md index 6641134aefd9..474244b27aea 100644 --- a/packages/path_provider/path_provider_macos/README.md +++ b/packages/path_provider/path_provider_foundation/README.md @@ -1,6 +1,6 @@ -# path\_provider\_macos +# path\_provider\_foundation -The macos implementation of [`path_provider`][1]. +The iOS and macOS implementation of [`path_provider`][1]. ## Usage diff --git a/packages/path_provider/path_provider_foundation/darwin/Classes/PathProviderPlugin.swift b/packages/path_provider/path_provider_foundation/darwin/Classes/PathProviderPlugin.swift new file mode 100644 index 000000000000..af043090f545 --- /dev/null +++ b/packages/path_provider/path_provider_foundation/darwin/Classes/PathProviderPlugin.swift @@ -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. + +import Foundation + +#if os(iOS) +import Flutter +#elseif os(macOS) +import FlutterMacOS +#endif + +public class PathProviderPlugin: NSObject, FlutterPlugin, PathProviderApi { + public static func register(with registrar: FlutterPluginRegistrar) { + let instance = PathProviderPlugin() + // Workaround for https://github.com/flutter/flutter/issues/118103. +#if os(iOS) + let messenger = registrar.messenger() +#else + let messenger = registrar.messenger +#endif + PathProviderApiSetup.setUp(binaryMessenger: messenger, api: instance) + } + + func getDirectoryPath(type: DirectoryType) -> String? { + var path = getDirectory(ofType: fileManagerDirectoryForType(type)) + #if os(macOS) + // In a non-sandboxed app, this is a shared directory where applications are + // expected to use its bundle ID as a subdirectory. (For non-sandboxed apps, + // adding the extra path is harmless). + // This is not done for iOS, for compatibility with older versions of the + // plugin. + if type == .applicationSupport { + if let basePath = path { + let basePathURL = URL.init(fileURLWithPath: basePath) + path = basePathURL.appendingPathComponent(Bundle.main.bundleIdentifier!).path + } + } + #endif + return path + } +} + +/// Returns the FileManager constant corresponding to the given type. +private func fileManagerDirectoryForType(_ type: DirectoryType) -> FileManager.SearchPathDirectory { + switch type { + case .applicationDocuments: + return FileManager.SearchPathDirectory.documentDirectory + case .applicationSupport: + return FileManager.SearchPathDirectory.applicationSupportDirectory + case .downloads: + return FileManager.SearchPathDirectory.downloadsDirectory + case .library: + return FileManager.SearchPathDirectory.libraryDirectory + case .temp: + return FileManager.SearchPathDirectory.cachesDirectory + } +} + +/// Returns the user-domain directory of the given type. +private func getDirectory(ofType directory: FileManager.SearchPathDirectory) -> String? { + let paths = NSSearchPathForDirectoriesInDomains( + directory, + FileManager.SearchPathDomainMask.userDomainMask, + true) + return paths.first +} diff --git a/packages/path_provider/path_provider_foundation/darwin/Classes/messages.g.swift b/packages/path_provider/path_provider_foundation/darwin/Classes/messages.g.swift new file mode 100644 index 000000000000..08ab62aafdb7 --- /dev/null +++ b/packages/path_provider/path_provider_foundation/darwin/Classes/messages.g.swift @@ -0,0 +1,60 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v5.0.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation +#if os(iOS) +import Flutter +#elseif os(macOS) +import FlutterMacOS +#else +#error("Unsupported platform.") +#endif + + +/// Generated class from Pigeon. + +enum DirectoryType: Int { + case applicationDocuments = 0 + case applicationSupport = 1 + case downloads = 2 + case library = 3 + case temp = 4 +} +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol PathProviderApi { + func getDirectoryPath(type: DirectoryType) -> String? +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class PathProviderApiSetup { + /// The codec used by PathProviderApi. + /// Sets up an instance of `PathProviderApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: PathProviderApi?) { + let getDirectoryPathChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.PathProviderApi.getDirectoryPath", binaryMessenger: binaryMessenger) + if let api = api { + getDirectoryPathChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let typeArg = DirectoryType(rawValue: args[0] as! Int)! + let result = api.getDirectoryPath(type: typeArg) + reply(wrapResult(result)) + } + } else { + getDirectoryPathChannel.setMessageHandler(nil) + } + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: FlutterError) -> [Any?] { + return [ + error.code, + error.message, + error.details + ] +} diff --git a/packages/path_provider/path_provider_macos/example/macos/RunnerTests/RunnerTests.swift b/packages/path_provider/path_provider_foundation/darwin/RunnerTests/RunnerTests.swift similarity index 60% rename from packages/path_provider/path_provider_macos/example/macos/RunnerTests/RunnerTests.swift rename to packages/path_provider/path_provider_foundation/darwin/RunnerTests/RunnerTests.swift index 35704cdb06d8..99a56f2bfebf 100644 --- a/packages/path_provider/path_provider_macos/example/macos/RunnerTests/RunnerTests.swift +++ b/packages/path_provider/path_provider_foundation/darwin/RunnerTests/RunnerTests.swift @@ -2,20 +2,19 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import FlutterMacOS import XCTest -import path_provider_macos +@testable import path_provider_foundation + +#if os(iOS) +import Flutter +#elseif os(macOS) +import FlutterMacOS +#endif class RunnerTests: XCTestCase { func testGetTemporaryDirectory() throws { let plugin = PathProviderPlugin() - var path: String? - plugin.handle( - FlutterMethodCall(methodName: "getTemporaryDirectory", arguments: nil), - result: { (result: Any?) -> Void in - path = result as? String - - }) + let path = plugin.getDirectoryPath(type: .temp) XCTAssertEqual( path, NSSearchPathForDirectoriesInDomains( @@ -27,13 +26,7 @@ class RunnerTests: XCTestCase { func testGetApplicationDocumentsDirectory() throws { let plugin = PathProviderPlugin() - var path: String? - plugin.handle( - FlutterMethodCall(methodName: "getApplicationDocumentsDirectory", arguments: nil), - result: { (result: Any?) -> Void in - path = result as? String - - }) + let path = plugin.getDirectoryPath(type: .applicationDocuments) XCTAssertEqual( path, NSSearchPathForDirectoriesInDomains( @@ -45,15 +38,20 @@ class RunnerTests: XCTestCase { func testGetApplicationSupportDirectory() throws { let plugin = PathProviderPlugin() - var path: String? - plugin.handle( - FlutterMethodCall(methodName: "getApplicationSupportDirectory", arguments: nil), - result: { (result: Any?) -> Void in - path = result as? String - - }) - // The application support directory path should be the system application support - // path with an added subdirectory based on the app name. + let path = plugin.getDirectoryPath(type: .applicationSupport) +#if os(iOS) + // On iOS, the application support directory path should be just the system application + // support path. + XCTAssertEqual( + path, + NSSearchPathForDirectoriesInDomains( + FileManager.SearchPathDirectory.applicationSupportDirectory, + FileManager.SearchPathDomainMask.userDomainMask, + true + ).first) +#else + // On macOS, the application support directory path should be the system application + // support path with an added subdirectory based on the app name. XCTAssert( path!.hasPrefix( NSSearchPathForDirectoriesInDomains( @@ -62,17 +60,12 @@ class RunnerTests: XCTestCase { true ).first!)) XCTAssert(path!.hasSuffix("Example")) +#endif } func testGetLibraryDirectory() throws { let plugin = PathProviderPlugin() - var path: String? - plugin.handle( - FlutterMethodCall(methodName: "getLibraryDirectory", arguments: nil), - result: { (result: Any?) -> Void in - path = result as? String - - }) + let path = plugin.getDirectoryPath(type: .library) XCTAssertEqual( path, NSSearchPathForDirectoriesInDomains( @@ -84,13 +77,7 @@ class RunnerTests: XCTestCase { func testGetDownloadsDirectory() throws { let plugin = PathProviderPlugin() - var path: String? - plugin.handle( - FlutterMethodCall(methodName: "getDownloadsDirectory", arguments: nil), - result: { (result: Any?) -> Void in - path = result as? String - - }) + let path = plugin.getDirectoryPath(type: .downloads) XCTAssertEqual( path, NSSearchPathForDirectoriesInDomains( diff --git a/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation.podspec b/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation.podspec new file mode 100644 index 000000000000..36093b567fb9 --- /dev/null +++ b/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation.podspec @@ -0,0 +1,25 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'path_provider_foundation' + s.version = '0.0.1' + s.summary = 'An iOS and macOS implementation of the path_provider plugin.' + s.description = <<-DESC + An iOS and macOS implementation of the Flutter plugin for getting commonly used locations on the filesystem. + DESC + s.homepage = 'https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_foundation' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_foundation' } + s.source_files = 'Classes/**/*' + s.ios.dependency 'Flutter' + s.osx.dependency 'FlutterMacOS' + s.ios.deployment_target = '9.0' + s.osx.deployment_target = '10.11' + s.ios.xcconfig = { + 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', + 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', + } + s.swift_version = '5.0' +end diff --git a/packages/path_provider/path_provider_ios/example/README.md b/packages/path_provider/path_provider_foundation/example/README.md similarity index 100% rename from packages/path_provider/path_provider_ios/example/README.md rename to packages/path_provider/path_provider_foundation/example/README.md diff --git a/packages/path_provider/path_provider_macos/example/integration_test/path_provider_test.dart b/packages/path_provider/path_provider_foundation/example/integration_test/path_provider_test.dart similarity index 100% rename from packages/path_provider/path_provider_macos/example/integration_test/path_provider_test.dart rename to packages/path_provider/path_provider_foundation/example/integration_test/path_provider_test.dart diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/.gitignore b/packages/path_provider/path_provider_foundation/example/ios/.gitignore similarity index 95% rename from packages/shared_preferences/shared_preferences_ios/example/ios/.gitignore rename to packages/path_provider/path_provider_foundation/example/ios/.gitignore index e96ef602b8d1..7a7f9873ad7d 100644 --- a/packages/shared_preferences/shared_preferences_ios/example/ios/.gitignore +++ b/packages/path_provider/path_provider_foundation/example/ios/.gitignore @@ -1,3 +1,4 @@ +**/dgph *.mode1v3 *.mode2v3 *.moved-aside @@ -18,6 +19,7 @@ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig +Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ diff --git a/packages/path_provider/path_provider_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/path_provider/path_provider_foundation/example/ios/Flutter/AppFrameworkInfo.plist similarity index 86% rename from packages/path_provider/path_provider_ios/example/ios/Flutter/AppFrameworkInfo.plist rename to packages/path_provider/path_provider_foundation/example/ios/Flutter/AppFrameworkInfo.plist index 3a9c234f96d4..9625e105df39 100644 --- a/packages/path_provider/path_provider_ios/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/path_provider/path_provider_foundation/example/ios/Flutter/AppFrameworkInfo.plist @@ -20,11 +20,7 @@ ???? CFBundleVersion 1.0 - UIRequiredDeviceCapabilities - - arm64 - MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Flutter/Debug.xcconfig b/packages/path_provider/path_provider_foundation/example/ios/Flutter/Debug.xcconfig similarity index 58% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Flutter/Debug.xcconfig rename to packages/path_provider/path_provider_foundation/example/ios/Flutter/Debug.xcconfig index d0eccdcaf401..ec97fc6f3021 100644 --- a/packages/shared_preferences/shared_preferences_ios/example/ios/Flutter/Debug.xcconfig +++ b/packages/path_provider/path_provider_foundation/example/ios/Flutter/Debug.xcconfig @@ -1,3 +1,2 @@ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Flutter/Release.xcconfig b/packages/path_provider/path_provider_foundation/example/ios/Flutter/Release.xcconfig similarity index 58% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Flutter/Release.xcconfig rename to packages/path_provider/path_provider_foundation/example/ios/Flutter/Release.xcconfig index c751c1d022fa..c4855bfe2000 100644 --- a/packages/shared_preferences/shared_preferences_ios/example/ios/Flutter/Release.xcconfig +++ b/packages/path_provider/path_provider_foundation/example/ios/Flutter/Release.xcconfig @@ -1,3 +1,2 @@ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" diff --git a/packages/path_provider/path_provider_ios/example/ios/Podfile b/packages/path_provider/path_provider_foundation/example/ios/Podfile similarity index 95% rename from packages/path_provider/path_provider_ios/example/ios/Podfile rename to packages/path_provider/path_provider_foundation/example/ios/Podfile index 3924e59aa0f9..211fcba3d000 100644 --- a/packages/path_provider/path_provider_ios/example/ios/Podfile +++ b/packages/path_provider/path_provider_foundation/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -28,7 +28,11 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe flutter_ios_podfile_setup target 'Runner' do + use_frameworks! + use_modular_headers! + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do inherit! :search_paths end diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/project.pbxproj similarity index 60% rename from packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/project.pbxproj rename to packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/project.pbxproj index 601985b46ae6..70cdc7657d6d 100644 --- a/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,24 +3,23 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 51; objects = { /* Begin PBXBuildFile section */ - 2D9222481EC32A19007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D9222471EC32A19007564B0 /* GeneratedPluginRegistrant.m */; }; + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 33258D7929818305006BAA98 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33258D7729818302006BAA98 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 60774162343BF6F19B3D65CE /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E2EF24BBF807F7F7B95F2B9 /* libPods-RunnerTests.a */; }; - 85DDFCF6BBDEE02B9D9F8138 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C0EE60090AA5F3AAAF2175B6 /* libPods-Runner.a */; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 569E86265D93B926F433B2DF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 479D5DD53D431F6BBABA2E43 /* Pods_Runner.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - F76AC1DF26671E960040C8BC /* PathProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F76AC1DE26671E960040C8BC /* PathProviderTests.m */; }; + D18DAAE2A3406D4789C8DAB2 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 988A82A3033B36B9EAF2782B /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - F76AC1E126671E960040C8BC /* PBXContainerItemProxy */ = { + 3380327829784D96002D32AE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; proxyType = 1; @@ -43,60 +42,57 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0E2EF24BBF807F7F7B95F2B9 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 29F2567B3AE74A9113ED3394 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - 2D9222461EC32A19007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 2D9222471EC32A19007564B0 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1E28C831B7D8EA9408BFB69A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 33258D7729818302006BAA98 /* RunnerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RunnerTests.swift; path = ../../darwin/RunnerTests/RunnerTests.swift; sourceTree = ""; }; + 3380327429784D96002D32AE /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 694A199F61914F41AAFD0B7F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 6EB685EA3DDA2EED39600D11 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 479D5DD53D431F6BBABA2E43 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5DB8EF5A2759054360D79B8D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 86F7986E9DC17432CC8AE464 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 91DA83C3D33EB641BAEA3087 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C0EE60090AA5F3AAAF2175B6 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - D317CA1E83064E01753D8BB5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - F76AC1DC26671E960040C8BC /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F76AC1DE26671E960040C8BC /* PathProviderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PathProviderTests.m; sourceTree = ""; }; - F76AC1E026671E960040C8BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 988A82A3033B36B9EAF2782B /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B0CB6DC5569DDEB858FBEB22 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + C1E50EBAA845915BAF5591C9 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { + 3380327129784D96002D32AE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 85DDFCF6BBDEE02B9D9F8138 /* libPods-Runner.a in Frameworks */, + D18DAAE2A3406D4789C8DAB2 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - F76AC1D926671E960040C8BC /* Frameworks */ = { + 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 60774162343BF6F19B3D65CE /* libPods-RunnerTests.a in Frameworks */, + 569E86265D93B926F433B2DF /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { + 33258D76298182CC006BAA98 /* RunnerTests */ = { isa = PBXGroup; children = ( - 694A199F61914F41AAFD0B7F /* Pods-Runner.debug.xcconfig */, - D317CA1E83064E01753D8BB5 /* Pods-Runner.release.xcconfig */, - 6EB685EA3DDA2EED39600D11 /* Pods-RunnerTests.debug.xcconfig */, - 29F2567B3AE74A9113ED3394 /* Pods-RunnerTests.release.xcconfig */, + 33258D7729818302006BAA98 /* RunnerTests.swift */, ); - name = Pods; + name = RunnerTests; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { @@ -115,10 +111,10 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, - F76AC1DD26671E960040C8BC /* RunnerTests */, + 33258D76298182CC006BAA98 /* RunnerTests */, 97C146EF1CF9000F007C117D /* Products */, - 840012C8B5EDBCF56B0E4AC1 /* Pods */, - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, + E1C876D20454FC3A1ED7F7E5 /* Pods */, + C72F144CE69E83C4574EB334 /* Frameworks */, ); sourceTree = ""; }; @@ -126,7 +122,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, - F76AC1DC26671E960040C8BC /* RunnerTests.xctest */, + 3380327429784D96002D32AE /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -134,59 +130,74 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( - 2D9222461EC32A19007564B0 /* GeneratedPluginRegistrant.h */, - 2D9222471EC32A19007564B0 /* GeneratedPluginRegistrant.m */, - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = { + C72F144CE69E83C4574EB334 /* Frameworks */ = { isa = PBXGroup; children = ( - C0EE60090AA5F3AAAF2175B6 /* libPods-Runner.a */, - 0E2EF24BBF807F7F7B95F2B9 /* libPods-RunnerTests.a */, + 479D5DD53D431F6BBABA2E43 /* Pods_Runner.framework */, + 988A82A3033B36B9EAF2782B /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; }; - F76AC1DD26671E960040C8BC /* RunnerTests */ = { + E1C876D20454FC3A1ED7F7E5 /* Pods */ = { isa = PBXGroup; children = ( - F76AC1DE26671E960040C8BC /* PathProviderTests.m */, - F76AC1E026671E960040C8BC /* Info.plist */, - ); - path = RunnerTests; + 5DB8EF5A2759054360D79B8D /* Pods-Runner.debug.xcconfig */, + B0CB6DC5569DDEB858FBEB22 /* Pods-Runner.release.xcconfig */, + 91DA83C3D33EB641BAEA3087 /* Pods-Runner.profile.xcconfig */, + 1E28C831B7D8EA9408BFB69A /* Pods-RunnerTests.debug.xcconfig */, + C1E50EBAA845915BAF5591C9 /* Pods-RunnerTests.release.xcconfig */, + 86F7986E9DC17432CC8AE464 /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 3380327329784D96002D32AE /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3380327D29784D96002D32AE /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 9144B1C9B36C0B00C1DF8FBB /* [CP] Check Pods Manifest.lock */, + 3380327029784D96002D32AE /* Sources */, + 3380327129784D96002D32AE /* Frameworks */, + 3380327229784D96002D32AE /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3380327929784D96002D32AE /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 3380327429784D96002D32AE /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */, + 45F307B61DA47FC553C87CA6 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 246FA3B3BBF06301555F5A51 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -197,46 +208,28 @@ productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; - F76AC1DB26671E960040C8BC /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = F76AC1E526671E960040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 31566AD39C1C7EF9EB261E6F /* [CP] Check Pods Manifest.lock */, - F76AC1D826671E960040C8BC /* Sources */, - F76AC1D926671E960040C8BC /* Frameworks */, - F76AC1DA26671E960040C8BC /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - F76AC1E226671E960040C8BC /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = F76AC1DC26671E960040C8BC /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 1400; LastUpgradeCheck = 1300; - ORGANIZATIONNAME = "The Flutter Authors"; + ORGANIZATIONNAME = ""; TargetAttributes = { + 3380327329784D96002D32AE = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - }; - F76AC1DB26671E960040C8BC = { - CreatedOnToolsVersion = 12.5; - ProvisioningStyle = Automatic; - TestTargetID = 97C146ED1CF9000F007C117D; + LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; + compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -249,53 +242,48 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, - F76AC1DB26671E960040C8BC /* RunnerTests */, + 3380327329784D96002D32AE /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { + 3380327229784D96002D32AE /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - F76AC1DA26671E960040C8BC /* Resources */ = { + 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 31566AD39C1C7EF9EB261E6F /* [CP] Check Pods Manifest.lock */ = { + 246FA3B3BBF06301555F5A51 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { @@ -312,66 +300,91 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { + 45F307B61DA47FC553C87CA6 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "Run Script"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; - AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = { + 9144B1C9B36C0B00C1DF8FBB /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { + 3380327029784D96002D32AE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 2D9222481EC32A19007564B0 /* GeneratedPluginRegistrant.m in Sources */, + 33258D7929818305006BAA98 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - F76AC1D826671E960040C8BC /* Sources */ = { + 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - F76AC1DF26671E960040C8BC /* PathProviderTests.m in Sources */, + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - F76AC1E226671E960040C8BC /* PBXTargetDependency */ = { + 3380327929784D96002D32AE /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = F76AC1E126671E960040C8BC /* PBXContainerItemProxy */; + targetProxy = 3380327829784D96002D32AE /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -395,11 +408,126 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.pathProviderFoundationExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 3380327A29784D96002D32AE /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1E28C831B7D8EA9408BFB69A /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 3380327B29784D96002D32AE /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C1E50EBAA845915BAF5591C9 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 3380327C29784D96002D32AE /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 86F7986E9DC17432CC8AE464 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -443,7 +571,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -455,7 +583,6 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -493,9 +620,12 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -506,20 +636,20 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.pathProviderExample; + PRODUCT_BUNDLE_IDENTIFIER = com.example.pathProviderFoundationExample; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; @@ -528,77 +658,51 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.pathProviderExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; - F76AC1E326671E960040C8BC /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 6EB685EA3DDA2EED39600D11 /* Pods-RunnerTests.debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = RunnerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; - }; - name = Debug; - }; - F76AC1E426671E960040C8BC /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 29F2567B3AE74A9113ED3394 /* Pods-RunnerTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = RunnerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.example.pathProviderFoundationExample; PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + 3380327D29784D96002D32AE /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, + 3380327A29784D96002D32AE /* Debug */, + 3380327B29784D96002D32AE /* Release */, + 3380327C29784D96002D32AE /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - F76AC1E526671E960040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( - F76AC1E326671E960040C8BC /* Debug */, - F76AC1E426671E960040C8BC /* Release */, + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 95% rename from packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index ec3713b95db5..b5d62ddeb711 100644 --- a/packages/path_provider/path_provider_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -38,10 +38,11 @@ + skipped = "NO" + parallelizable = "YES"> @@ -71,7 +72,7 @@ + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/path_provider/path_provider_foundation/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to packages/path_provider/path_provider_foundation/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/AppDelegate.swift b/packages/path_provider/path_provider_foundation/example/ios/Runner/AppDelegate.swift similarity index 100% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Runner/AppDelegate.swift rename to packages/path_provider/path_provider_foundation/example/ios/Runner/AppDelegate.swift diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 94% rename from packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index d22f10b2ab63..d36b1fab2d9d 100644 --- a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -107,6 +107,12 @@ "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" } ], "info" : { diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from packages/path_provider/path_provider_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/path_provider/path_provider_foundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 51% rename from packages/path_provider/path_provider_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard index ebf48f603974..f2e259c7c939 100644 --- a/packages/path_provider/path_provider_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ b/packages/path_provider/path_provider_foundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -1,8 +1,8 @@ - + - + @@ -10,13 +10,20 @@ - - + + - - + + + + + + + + + @@ -24,4 +31,7 @@ + + + diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Base.lproj/Main.storyboard b/packages/path_provider/path_provider_foundation/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/path_provider/path_provider_ios/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/Info.plist b/packages/path_provider/path_provider_foundation/example/ios/Runner/Info.plist similarity index 78% rename from packages/path_provider/path_provider_ios/example/ios/Runner/Info.plist rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Info.plist index 342db6a5dcaf..5bdb9bcc0635 100644 --- a/packages/path_provider/path_provider_ios/example/ios/Runner/Info.plist +++ b/packages/path_provider/path_provider_foundation/example/ios/Runner/Info.plist @@ -3,7 +3,9 @@ CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Path Provider Foundation CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -11,25 +13,21 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - path_provider_example + path_provider_foundation_example CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion - 1 + $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main - UIRequiredDeviceCapabilities - - arm64 - UISupportedInterfaceOrientations UIInterfaceOrientationPortrait @@ -45,5 +43,9 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Runner-Bridging-Header.h b/packages/path_provider/path_provider_foundation/example/ios/Runner/Runner-Bridging-Header.h similarity index 100% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Runner-Bridging-Header.h rename to packages/path_provider/path_provider_foundation/example/ios/Runner/Runner-Bridging-Header.h diff --git a/packages/path_provider/path_provider_macos/example/lib/main.dart b/packages/path_provider/path_provider_foundation/example/lib/main.dart similarity index 88% rename from packages/path_provider/path_provider_macos/example/lib/main.dart rename to packages/path_provider/path_provider_foundation/example/lib/main.dart index 13a6fada5fef..cc3fc13de89f 100644 --- a/packages/path_provider/path_provider_macos/example/lib/main.dart +++ b/packages/path_provider/path_provider_foundation/example/lib/main.dart @@ -22,6 +22,7 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { String? _tempDirectory = 'Unknown'; String? _downloadsDirectory = 'Unknown'; + String? _libraryDirectory = 'Unknown'; String? _appSupportDirectory = 'Unknown'; String? _documentsDirectory = 'Unknown'; @@ -36,6 +37,7 @@ class _MyAppState extends State { String? tempDirectory; String? downloadsDirectory; String? appSupportDirectory; + String? libraryDirectory; String? documentsDirectory; final PathProviderPlatform provider = PathProviderPlatform.instance; @@ -56,6 +58,12 @@ class _MyAppState extends State { documentsDirectory = 'Failed to get documents directory: $exception'; } + try { + libraryDirectory = await provider.getLibraryPath(); + } catch (exception) { + libraryDirectory = 'Failed to get library directory: $exception'; + } + try { appSupportDirectory = await provider.getApplicationSupportPath(); } catch (exception) { @@ -65,6 +73,7 @@ class _MyAppState extends State { setState(() { _tempDirectory = tempDirectory; _downloadsDirectory = downloadsDirectory; + _libraryDirectory = libraryDirectory; _appSupportDirectory = appSupportDirectory; _documentsDirectory = documentsDirectory; }); @@ -83,6 +92,7 @@ class _MyAppState extends State { Text('Temp Directory: $_tempDirectory\n'), Text('Documents Directory: $_documentsDirectory\n'), Text('Downloads Directory: $_downloadsDirectory\n'), + Text('Library Directory: $_libraryDirectory\n'), Text('Application Support Directory: $_appSupportDirectory\n'), ], ), diff --git a/packages/path_provider/path_provider_macos/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/path_provider/path_provider_foundation/example/macos/Flutter/Flutter-Debug.xcconfig similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Flutter/Flutter-Debug.xcconfig rename to packages/path_provider/path_provider_foundation/example/macos/Flutter/Flutter-Debug.xcconfig diff --git a/packages/path_provider/path_provider_macos/example/macos/Flutter/Flutter-Release.xcconfig b/packages/path_provider/path_provider_foundation/example/macos/Flutter/Flutter-Release.xcconfig similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Flutter/Flutter-Release.xcconfig rename to packages/path_provider/path_provider_foundation/example/macos/Flutter/Flutter-Release.xcconfig diff --git a/packages/path_provider/path_provider_macos/example/macos/Podfile b/packages/path_provider/path_provider_foundation/example/macos/Podfile similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Podfile rename to packages/path_provider/path_provider_foundation/example/macos/Podfile diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner.xcodeproj/project.pbxproj b/packages/path_provider/path_provider_foundation/example/macos/Runner.xcodeproj/project.pbxproj similarity index 99% rename from packages/path_provider/path_provider_macos/example/macos/Runner.xcodeproj/project.pbxproj rename to packages/path_provider/path_provider_foundation/example/macos/Runner.xcodeproj/project.pbxproj index a63463993c6e..5abc18a86297 100644 --- a/packages/path_provider/path_provider_macos/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/path_provider/path_provider_foundation/example/macos/Runner.xcodeproj/project.pbxproj @@ -82,7 +82,7 @@ 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 = ""; }; 33EBD3A726728EA70013E557 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 33EBD3A926728EA70013E557 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 33EBD3A926728EA70013E557 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../../../darwin/RunnerTests/RunnerTests.swift; sourceTree = ""; }; 33EBD3AB26728EA70013E557 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 46139048DB9F59D473B61B5E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; @@ -367,11 +367,11 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/path_provider_macos/path_provider_macos.framework", + "${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_macos.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/path_provider/path_provider_foundation/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/path_provider/path_provider_foundation/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/path_provider/path_provider_foundation/example/macos/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata rename to packages/path_provider/path_provider_foundation/example/macos/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/path_provider/path_provider_foundation/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/path_provider/path_provider_foundation/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/path_provider/path_provider_foundation/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/path_provider/path_provider_foundation/example/macos/Runner/AppDelegate.swift b/packages/path_provider/path_provider_foundation/example/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000000..5cec4c48f620 --- /dev/null +++ b/packages/path_provider/path_provider_foundation/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/path_provider/path_provider_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/path_provider/path_provider_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/path_provider/path_provider_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/path_provider/path_provider_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png rename to packages/path_provider/path_provider_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/path_provider/path_provider_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png rename to packages/path_provider/path_provider_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/path_provider/path_provider_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png rename to packages/path_provider/path_provider_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/path_provider/path_provider_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png rename to packages/path_provider/path_provider_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/path_provider/path_provider_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png rename to packages/path_provider/path_provider_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/path_provider/path_provider_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png rename to packages/path_provider/path_provider_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/path_provider/path_provider_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png rename to packages/path_provider/path_provider_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/path_provider/path_provider_foundation/example/macos/Runner/Base.lproj/MainMenu.xib similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner/Base.lproj/MainMenu.xib rename to packages/path_provider/path_provider_foundation/example/macos/Runner/Base.lproj/MainMenu.xib diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/path_provider/path_provider_foundation/example/macos/Runner/Configs/AppInfo.xcconfig similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner/Configs/AppInfo.xcconfig rename to packages/path_provider/path_provider_foundation/example/macos/Runner/Configs/AppInfo.xcconfig diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Configs/Debug.xcconfig b/packages/path_provider/path_provider_foundation/example/macos/Runner/Configs/Debug.xcconfig similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Configs/Debug.xcconfig rename to packages/path_provider/path_provider_foundation/example/macos/Runner/Configs/Debug.xcconfig diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Configs/Release.xcconfig b/packages/path_provider/path_provider_foundation/example/macos/Runner/Configs/Release.xcconfig similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Configs/Release.xcconfig rename to packages/path_provider/path_provider_foundation/example/macos/Runner/Configs/Release.xcconfig diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Configs/Warnings.xcconfig b/packages/path_provider/path_provider_foundation/example/macos/Runner/Configs/Warnings.xcconfig similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Configs/Warnings.xcconfig rename to packages/path_provider/path_provider_foundation/example/macos/Runner/Configs/Warnings.xcconfig diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner/DebugProfile.entitlements b/packages/path_provider/path_provider_foundation/example/macos/Runner/DebugProfile.entitlements similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner/DebugProfile.entitlements rename to packages/path_provider/path_provider_foundation/example/macos/Runner/DebugProfile.entitlements diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/RunnerTests/Info.plist b/packages/path_provider/path_provider_foundation/example/macos/Runner/Info.plist similarity index 60% rename from packages/shared_preferences/shared_preferences_macos/example/macos/RunnerTests/Info.plist rename to packages/path_provider/path_provider_foundation/example/macos/Runner/Info.plist index 64d65ca49577..4789daa6a443 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/macos/RunnerTests/Info.plist +++ b/packages/path_provider/path_provider_foundation/example/macos/Runner/Info.plist @@ -6,6 +6,8 @@ $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) + CFBundleIconFile + CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion @@ -13,10 +15,18 @@ CFBundleName $(PRODUCT_NAME) CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) + APPL CFBundleShortVersionString - 1.0 + $(FLUTTER_BUILD_NAME) CFBundleVersion - 1 + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication diff --git a/packages/path_provider/path_provider_foundation/example/macos/Runner/MainFlutterWindow.swift b/packages/path_provider/path_provider_foundation/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000000..32aaeedceb1f --- /dev/null +++ b/packages/path_provider/path_provider_foundation/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/path_provider/path_provider_macos/example/macos/Runner/Release.entitlements b/packages/path_provider/path_provider_foundation/example/macos/Runner/Release.entitlements similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/Runner/Release.entitlements rename to packages/path_provider/path_provider_foundation/example/macos/Runner/Release.entitlements diff --git a/packages/path_provider/path_provider_ios/example/ios/RunnerTests/Info.plist b/packages/path_provider/path_provider_foundation/example/macos/RunnerTests/Info.plist similarity index 100% rename from packages/path_provider/path_provider_ios/example/ios/RunnerTests/Info.plist rename to packages/path_provider/path_provider_foundation/example/macos/RunnerTests/Info.plist diff --git a/packages/path_provider/path_provider_macos/example/pubspec.yaml b/packages/path_provider/path_provider_foundation/example/pubspec.yaml similarity index 88% rename from packages/path_provider/path_provider_macos/example/pubspec.yaml rename to packages/path_provider/path_provider_foundation/example/pubspec.yaml index 8c69e69ce122..fcf599564659 100644 --- a/packages/path_provider/path_provider_macos/example/pubspec.yaml +++ b/packages/path_provider/path_provider_foundation/example/pubspec.yaml @@ -4,14 +4,14 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: sdk: flutter - path_provider_macos: + path_provider_foundation: # When depending on this package from a real application you should use: - # path_provider_macos: ^x.y.z + # path_provider_foundation: ^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. diff --git a/packages/path_provider/path_provider_ios/example/test_driver/integration_test.dart b/packages/path_provider/path_provider_foundation/example/test_driver/integration_test.dart similarity index 100% rename from packages/path_provider/path_provider_ios/example/test_driver/integration_test.dart rename to packages/path_provider/path_provider_foundation/example/test_driver/integration_test.dart diff --git a/packages/path_provider/path_provider_foundation/ios/Classes/PathProviderPlugin.swift b/packages/path_provider/path_provider_foundation/ios/Classes/PathProviderPlugin.swift new file mode 120000 index 000000000000..47ec1bfb28ca --- /dev/null +++ b/packages/path_provider/path_provider_foundation/ios/Classes/PathProviderPlugin.swift @@ -0,0 +1 @@ +../../darwin/Classes/PathProviderPlugin.swift \ No newline at end of file diff --git a/packages/path_provider/path_provider_foundation/ios/Classes/messages.g.swift b/packages/path_provider/path_provider_foundation/ios/Classes/messages.g.swift new file mode 120000 index 000000000000..11bcf06e96a8 --- /dev/null +++ b/packages/path_provider/path_provider_foundation/ios/Classes/messages.g.swift @@ -0,0 +1 @@ +../../darwin/Classes/messages.g.swift \ No newline at end of file diff --git a/packages/path_provider/path_provider_foundation/ios/README.md b/packages/path_provider/path_provider_foundation/ios/README.md new file mode 100644 index 000000000000..fd7261950f35 --- /dev/null +++ b/packages/path_provider/path_provider_foundation/ios/README.md @@ -0,0 +1,4 @@ +This only contains symlinks to ../darwin, to support versions of Flutter +prior that don't include https://github.com/flutter/flutter/pull/115337. +Once the minimum Flutter version supported by this implementation is one that +includes that functionality, this directory should be removed. diff --git a/packages/path_provider/path_provider_foundation/ios/path_provider_foundation.podspec b/packages/path_provider/path_provider_foundation/ios/path_provider_foundation.podspec new file mode 120000 index 000000000000..feae183dd621 --- /dev/null +++ b/packages/path_provider/path_provider_foundation/ios/path_provider_foundation.podspec @@ -0,0 +1 @@ +../darwin/path_provider_foundation.podspec \ No newline at end of file diff --git a/packages/path_provider/path_provider_foundation/lib/messages.g.dart b/packages/path_provider/path_provider_foundation/lib/messages.g.dart new file mode 100644 index 000000000000..81a9cd5cc525 --- /dev/null +++ b/packages/path_provider/path_provider_foundation/lib/messages.g.dart @@ -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. +// Autogenerated from Pigeon (v5.0.0), 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 +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +enum DirectoryType { + applicationDocuments, + applicationSupport, + downloads, + library, + temp, +} + +class PathProviderApi { + /// Constructor for [PathProviderApi]. 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. + PathProviderApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + Future getDirectoryPath(DirectoryType arg_type) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getDirectoryPath', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_type.index]) 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 (replyList[0] as String?); + } + } +} diff --git a/packages/path_provider/path_provider_ios/lib/path_provider_ios.dart b/packages/path_provider/path_provider_foundation/lib/path_provider_foundation.dart similarity index 51% rename from packages/path_provider/path_provider_ios/lib/path_provider_ios.dart rename to packages/path_provider/path_provider_foundation/lib/path_provider_foundation.dart index 05be0534764a..9fdda6935245 100644 --- a/packages/path_provider/path_provider_ios/lib/path_provider_ios.dart +++ b/packages/path_provider/path_provider_foundation/lib/path_provider_foundation.dart @@ -5,26 +5,27 @@ import 'dart:io'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; + import 'messages.g.dart'; -/// The iOS implementation of [PathProviderPlatform]. -class PathProviderIOS extends PathProviderPlatform { - /// The method channel used to interact with the native platform. +/// The iOS and macOS implementation of [PathProviderPlatform]. +class PathProviderFoundation extends PathProviderPlatform { final PathProviderApi _pathProvider = PathProviderApi(); /// Registers this class as the default instance of [PathProviderPlatform] static void registerWith() { - PathProviderPlatform.instance = PathProviderIOS(); + PathProviderPlatform.instance = PathProviderFoundation(); } @override - Future getTemporaryPath() async { - return _pathProvider.getTemporaryPath(); + Future getTemporaryPath() { + return _pathProvider.getDirectoryPath(DirectoryType.temp); } @override Future getApplicationSupportPath() async { - final String? path = await _pathProvider.getApplicationSupportPath(); + final String? path = + await _pathProvider.getDirectoryPath(DirectoryType.applicationSupport); if (path != null) { // Ensure the directory exists before returning it, for consistency with // other platforms. @@ -34,34 +35,37 @@ class PathProviderIOS extends PathProviderPlatform { } @override - Future getLibraryPath() async { - return _pathProvider.getLibraryPath(); + Future getLibraryPath() { + return _pathProvider.getDirectoryPath(DirectoryType.library); } @override - Future getApplicationDocumentsPath() async { - return _pathProvider.getApplicationDocumentsPath(); + Future getApplicationDocumentsPath() { + return _pathProvider.getDirectoryPath(DirectoryType.applicationDocuments); } @override Future getExternalStoragePath() async { - throw UnsupportedError('getExternalStoragePath is not supported on iOS'); + throw UnsupportedError( + 'getExternalStoragePath is not supported on this platform'); } @override Future?> getExternalCachePaths() async { - throw UnsupportedError('getExternalCachePaths is not supported on iOS'); + throw UnsupportedError( + 'getExternalCachePaths is not supported on this platform'); } @override Future?> getExternalStoragePaths({ StorageDirectory? type, }) async { - throw UnsupportedError('getExternalStoragePaths is not supported on iOS'); + throw UnsupportedError( + 'getExternalStoragePaths is not supported on this platform'); } @override - Future getDownloadsPath() async { - throw UnsupportedError('getDownloadsPath is not supported on iOS'); + Future getDownloadsPath() { + return _pathProvider.getDirectoryPath(DirectoryType.downloads); } } diff --git a/packages/path_provider/path_provider_foundation/macos/Classes/PathProviderPlugin.swift b/packages/path_provider/path_provider_foundation/macos/Classes/PathProviderPlugin.swift new file mode 120000 index 000000000000..47ec1bfb28ca --- /dev/null +++ b/packages/path_provider/path_provider_foundation/macos/Classes/PathProviderPlugin.swift @@ -0,0 +1 @@ +../../darwin/Classes/PathProviderPlugin.swift \ No newline at end of file diff --git a/packages/path_provider/path_provider_foundation/macos/Classes/messages.g.swift b/packages/path_provider/path_provider_foundation/macos/Classes/messages.g.swift new file mode 120000 index 000000000000..11bcf06e96a8 --- /dev/null +++ b/packages/path_provider/path_provider_foundation/macos/Classes/messages.g.swift @@ -0,0 +1 @@ +../../darwin/Classes/messages.g.swift \ No newline at end of file diff --git a/packages/path_provider/path_provider_foundation/macos/README.md b/packages/path_provider/path_provider_foundation/macos/README.md new file mode 100644 index 000000000000..fd7261950f35 --- /dev/null +++ b/packages/path_provider/path_provider_foundation/macos/README.md @@ -0,0 +1,4 @@ +This only contains symlinks to ../darwin, to support versions of Flutter +prior that don't include https://github.com/flutter/flutter/pull/115337. +Once the minimum Flutter version supported by this implementation is one that +includes that functionality, this directory should be removed. diff --git a/packages/path_provider/path_provider_foundation/macos/path_provider_foundation.podspec b/packages/path_provider/path_provider_foundation/macos/path_provider_foundation.podspec new file mode 120000 index 000000000000..feae183dd621 --- /dev/null +++ b/packages/path_provider/path_provider_foundation/macos/path_provider_foundation.podspec @@ -0,0 +1 @@ +../darwin/path_provider_foundation.podspec \ No newline at end of file diff --git a/packages/path_provider/path_provider_foundation/pigeons/copyright.txt b/packages/path_provider/path_provider_foundation/pigeons/copyright.txt new file mode 100644 index 000000000000..1236b63caf3a --- /dev/null +++ b/packages/path_provider/path_provider_foundation/pigeons/copyright.txt @@ -0,0 +1,3 @@ +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. diff --git a/packages/path_provider/path_provider_ios/pigeons/messages.dart b/packages/path_provider/path_provider_foundation/pigeons/messages.dart similarity index 63% rename from packages/path_provider/path_provider_ios/pigeons/messages.dart rename to packages/path_provider/path_provider_foundation/pigeons/messages.dart index 2ed79914e821..8c82ab4fcf14 100644 --- a/packages/path_provider/path_provider_ios/pigeons/messages.dart +++ b/packages/path_provider/path_provider_foundation/pigeons/messages.dart @@ -5,17 +5,20 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions( input: 'pigeons/messages.dart', - objcOptions: ObjcOptions(prefix: 'FLT'), - objcHeaderOut: 'ios/Classes/messages.g.h', - objcSourceOut: 'ios/Classes/messages.g.m', + swiftOut: 'macos/Classes/messages.g.swift', dartOut: 'lib/messages.g.dart', dartTestOut: 'test/messages_test.g.dart', copyrightHeader: 'pigeons/copyright.txt', )) +enum DirectoryType { + applicationDocuments, + applicationSupport, + downloads, + library, + temp, +} + @HostApi(dartHostTestHandler: 'TestPathProviderApi') abstract class PathProviderApi { - String? getTemporaryPath(); - String? getApplicationSupportPath(); - String? getLibraryPath(); - String? getApplicationDocumentsPath(); + String? getDirectoryPath(DirectoryType type); } diff --git a/packages/path_provider/path_provider_macos/pubspec.yaml b/packages/path_provider/path_provider_foundation/pubspec.yaml similarity index 52% rename from packages/path_provider/path_provider_macos/pubspec.yaml rename to packages/path_provider/path_provider_foundation/pubspec.yaml index 289e13103ddd..30dd655acc00 100644 --- a/packages/path_provider/path_provider_macos/pubspec.yaml +++ b/packages/path_provider/path_provider_foundation/pubspec.yaml @@ -1,20 +1,25 @@ -name: path_provider_macos -description: macOS implementation of the path_provider plugin -repository: https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_macos +name: path_provider_foundation +description: iOS and macOS implementation of the path_provider plugin +repository: https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_foundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.6 +version: 2.1.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: implements: path_provider platforms: + ios: + pluginClass: PathProviderPlugin + dartPluginClass: PathProviderFoundation + sharedDarwinSource: true macos: pluginClass: PathProviderPlugin - dartPluginClass: PathProviderMacOS + dartPluginClass: PathProviderFoundation + sharedDarwinSource: true dependencies: flutter: @@ -22,6 +27,9 @@ dependencies: path_provider_platform_interface: ^2.0.1 dev_dependencies: + build_runner: ^2.3.2 flutter_test: sdk: flutter + mockito: ^5.3.2 path: ^1.8.0 + pigeon: ^5.0.0 diff --git a/packages/path_provider/path_provider_foundation/test/messages_test.g.dart b/packages/path_provider/path_provider_foundation/test/messages_test.g.dart new file mode 100644 index 000000000000..9fb9b954cede --- /dev/null +++ b/packages/path_provider/path_provider_foundation/test/messages_test.g.dart @@ -0,0 +1,44 @@ +// 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 (v5.0.0), 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 +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:path_provider_foundation/messages.g.dart'; + +abstract class TestPathProviderApi { + static const MessageCodec codec = StandardMessageCodec(); + + String? getDirectoryPath(DirectoryType type); + + static void setup(TestPathProviderApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PathProviderApi.getDirectoryPath', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PathProviderApi.getDirectoryPath was null.'); + final List args = (message as List?)!; + final DirectoryType? arg_type = + args[0] == null ? null : DirectoryType.values[args[0] as int]; + assert(arg_type != null, + 'Argument for dev.flutter.pigeon.PathProviderApi.getDirectoryPath was null, expected non-null DirectoryType.'); + final String? output = api.getDirectoryPath(arg_type!); + return [output]; + }); + } + } + } +} diff --git a/packages/path_provider/path_provider_macos/test/path_provider_macos_test.dart b/packages/path_provider/path_provider_foundation/test/path_provider_foundation_test.dart similarity index 50% rename from packages/path_provider/path_provider_macos/test/path_provider_macos_test.dart rename to packages/path_provider/path_provider_foundation/test/path_provider_foundation_test.dart index 7e783aad24e9..e291e3bf25d8 100644 --- a/packages/path_provider/path_provider_macos/test/path_provider_macos_test.dart +++ b/packages/path_provider/path_provider_foundation/test/path_provider_foundation_test.dart @@ -4,61 +4,33 @@ import 'dart:io'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; import 'package:path/path.dart' as p; -import 'package:path_provider_macos/path_provider_macos.dart'; +import 'package:path_provider_foundation/messages.g.dart'; +import 'package:path_provider_foundation/path_provider_foundation.dart'; +import 'messages_test.g.dart'; +import 'path_provider_foundation_test.mocks.dart'; + +@GenerateMocks([TestPathProviderApi]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('PathProviderMacOS', () { - late PathProviderMacOS pathProvider; - late List log; + group('PathProviderFoundation', () { + late PathProviderFoundation pathProvider; + late MockTestPathProviderApi mockApi; // These unit tests use the actual filesystem, since an injectable // filesystem would add a runtime dependency to the package, so everything // is contained to a temporary directory. late Directory testRoot; - late String temporaryPath; - late String applicationSupportPath; - late String libraryPath; - late String applicationDocumentsPath; - late String downloadsPath; - setUp(() async { - pathProvider = PathProviderMacOS(); - testRoot = Directory.systemTemp.createTempSync(); - final String basePath = testRoot.path; - temporaryPath = p.join(basePath, 'temporary', 'path'); - applicationSupportPath = - p.join(basePath, 'application', 'support', 'path'); - libraryPath = p.join(basePath, 'library', 'path'); - applicationDocumentsPath = - p.join(basePath, 'application', 'documents', 'path'); - downloadsPath = p.join(basePath, 'downloads', 'path'); - - log = []; - TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger - .setMockMethodCallHandler(pathProvider.methodChannel, - (MethodCall methodCall) async { - log.add(methodCall); - switch (methodCall.method) { - case 'getTemporaryDirectory': - return temporaryPath; - case 'getApplicationSupportDirectory': - return applicationSupportPath; - case 'getLibraryDirectory': - return libraryPath; - case 'getApplicationDocumentsDirectory': - return applicationDocumentsPath; - case 'getDownloadsDirectory': - return downloadsPath; - default: - return null; - } - }); + pathProvider = PathProviderFoundation(); + mockApi = MockTestPathProviderApi(); + TestPathProviderApi.setup(mockApi); }); tearDown(() { @@ -66,57 +38,71 @@ void main() { }); test('getTemporaryPath', () async { + final String temporaryPath = p.join(testRoot.path, 'temporary', 'path'); + when(mockApi.getDirectoryPath(DirectoryType.temp)) + .thenReturn(temporaryPath); + final String? path = await pathProvider.getTemporaryPath(); - expect( - log, - [isMethodCall('getTemporaryDirectory', arguments: null)], - ); + + verify(mockApi.getDirectoryPath(DirectoryType.temp)); expect(path, temporaryPath); }); test('getApplicationSupportPath', () async { + final String applicationSupportPath = + p.join(testRoot.path, 'application', 'support', 'path'); + when(mockApi.getDirectoryPath(DirectoryType.applicationSupport)) + .thenReturn(applicationSupportPath); + final String? path = await pathProvider.getApplicationSupportPath(); - expect( - log, - [ - isMethodCall('getApplicationSupportDirectory', arguments: null) - ], - ); + + verify(mockApi.getDirectoryPath(DirectoryType.applicationSupport)); expect(path, applicationSupportPath); }); test('getApplicationSupportPath creates the directory if necessary', () async { + final String applicationSupportPath = + p.join(testRoot.path, 'application', 'support', 'path'); + when(mockApi.getDirectoryPath(DirectoryType.applicationSupport)) + .thenReturn(applicationSupportPath); + final String? path = await pathProvider.getApplicationSupportPath(); + expect(Directory(path!).existsSync(), isTrue); }); test('getLibraryPath', () async { + final String libraryPath = p.join(testRoot.path, 'library', 'path'); + when(mockApi.getDirectoryPath(DirectoryType.library)) + .thenReturn(libraryPath); + final String? path = await pathProvider.getLibraryPath(); - expect( - log, - [isMethodCall('getLibraryDirectory', arguments: null)], - ); + + verify(mockApi.getDirectoryPath(DirectoryType.library)); expect(path, libraryPath); }); test('getApplicationDocumentsPath', () async { + final String applicationDocumentsPath = + p.join(testRoot.path, 'application', 'documents', 'path'); + when(mockApi.getDirectoryPath(DirectoryType.applicationDocuments)) + .thenReturn(applicationDocumentsPath); + final String? path = await pathProvider.getApplicationDocumentsPath(); - expect( - log, - [ - isMethodCall('getApplicationDocumentsDirectory', arguments: null) - ], - ); + + verify(mockApi.getDirectoryPath(DirectoryType.applicationDocuments)); expect(path, applicationDocumentsPath); }); test('getDownloadsPath', () async { + final String downloadsPath = p.join(testRoot.path, 'downloads', 'path'); + when(mockApi.getDirectoryPath(DirectoryType.downloads)) + .thenReturn(downloadsPath); + final String? result = await pathProvider.getDownloadsPath(); - expect( - log, - [isMethodCall('getDownloadsDirectory', arguments: null)], - ); + + verify(mockApi.getDirectoryPath(DirectoryType.downloads)); expect(result, downloadsPath); }); diff --git a/packages/path_provider/path_provider_foundation/test/path_provider_foundation_test.mocks.dart b/packages/path_provider/path_provider_foundation/test/path_provider_foundation_test.mocks.dart new file mode 100644 index 000000000000..cd3a1c7e8416 --- /dev/null +++ b/packages/path_provider/path_provider_foundation/test/path_provider_foundation_test.mocks.dart @@ -0,0 +1,37 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in path_provider_foundation/test/path_provider_foundation_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:mockito/mockito.dart' as _i1; +import 'package:path_provider_foundation/messages.g.dart' as _i3; + +import 'messages_test.g.dart' as _i2; + +// 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 [TestPathProviderApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestPathProviderApi extends _i1.Mock + implements _i2.TestPathProviderApi { + MockTestPathProviderApi() { + _i1.throwOnMissingStub(this); + } + + @override + String? getDirectoryPath(_i3.DirectoryType? type) => + (super.noSuchMethod(Invocation.method( + #getDirectoryPath, + [type], + )) as String?); +} diff --git a/packages/path_provider/path_provider_ios/CHANGELOG.md b/packages/path_provider/path_provider_ios/CHANGELOG.md deleted file mode 100644 index ffc158c19156..000000000000 --- a/packages/path_provider/path_provider_ios/CHANGELOG.md +++ /dev/null @@ -1,28 +0,0 @@ -## NEXT - -* Updates minimum Flutter version to 2.10. - -## 2.0.11 - -* Lower minimim version back to 2.8. - -## 2.0.10 - -* Switches backend to pigeon. - -## 2.0.9 - -* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors - lint warnings. - -## 2.0.8 - -* Switches to a package-internal implementation of the platform interface. - -## 2.0.7 - -* Fixes link in README. - -## 2.0.6 - -* Split from `path_provider` as a federated implementation. diff --git a/packages/path_provider/path_provider_ios/README.md b/packages/path_provider/path_provider_ios/README.md deleted file mode 100644 index dcfa06dcdfb0..000000000000 --- a/packages/path_provider/path_provider_ios/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# path\_provider\_ios - -The iOS implementation of [`path_provider`][1]. - -## Usage - -This package is [endorsed][2], which means you can simply use `path_provider` -normally. This package will be automatically included in your app when you do. - -[1]: https://pub.dev/packages/path_provider -[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/path_provider/path_provider_ios/example/integration_test/path_provider_test.dart b/packages/path_provider/path_provider_ios/example/integration_test/path_provider_test.dart deleted file mode 100644 index 5c6cf5a63579..000000000000 --- a/packages/path_provider/path_provider_ios/example/integration_test/path_provider_test.dart +++ /dev/null @@ -1,69 +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:io'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('getTemporaryDirectory', (WidgetTester tester) async { - final PathProviderPlatform provider = PathProviderPlatform.instance; - final String? result = await provider.getTemporaryPath(); - _verifySampleFile(result, 'temporaryDirectory'); - }); - - testWidgets('getApplicationDocumentsDirectory', (WidgetTester tester) async { - final PathProviderPlatform provider = PathProviderPlatform.instance; - final String? result = await provider.getApplicationDocumentsPath(); - _verifySampleFile(result, 'applicationDocuments'); - }); - - testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async { - final PathProviderPlatform provider = PathProviderPlatform.instance; - final String? result = await provider.getApplicationSupportPath(); - _verifySampleFile(result, 'applicationSupport'); - }); - - testWidgets('getLibraryDirectory', (WidgetTester tester) async { - final PathProviderPlatform provider = PathProviderPlatform.instance; - final String? result = await provider.getLibraryPath(); - _verifySampleFile(result, 'library'); - }); - - testWidgets('getExternalStorageDirectory', (WidgetTester tester) async { - final PathProviderPlatform provider = PathProviderPlatform.instance; - expect(() => provider.getExternalStoragePath(), - throwsA(isInstanceOf())); - }); - - testWidgets('getExternalCacheDirectories', (WidgetTester tester) async { - final PathProviderPlatform provider = PathProviderPlatform.instance; - expect(() => provider.getExternalCachePaths(), - throwsA(isInstanceOf())); - }); -} - -/// Verify a file called [name] in [directoryPath] by recreating it with test -/// contents when necessary. -void _verifySampleFile(String? directoryPath, String name) { - expect(directoryPath, isNotNull); - if (directoryPath == null) { - return; - } - final Directory directory = Directory(directoryPath); - final File file = File('${directory.path}${Platform.pathSeparator}$name'); - - if (file.existsSync()) { - file.deleteSync(); - expect(file.existsSync(), isFalse); - } - - file.writeAsStringSync('Hello world!'); - expect(file.readAsStringSync(), 'Hello world!'); - expect(directory.listSync(), isNotEmpty); - file.deleteSync(); -} diff --git a/packages/path_provider/path_provider_ios/example/ios/Flutter/Debug.xcconfig b/packages/path_provider/path_provider_ios/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index 9803018ca79d..000000000000 --- a/packages/path_provider/path_provider_ios/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Generated.xcconfig" -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" diff --git a/packages/path_provider/path_provider_ios/example/ios/Flutter/Release.xcconfig b/packages/path_provider/path_provider_ios/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index a4a8c604e13d..000000000000 --- a/packages/path_provider/path_provider_ios/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Generated.xcconfig" -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/AppDelegate.m b/packages/path_provider/path_provider_ios/example/ios/Runner/AppDelegate.m deleted file mode 100644 index b790a0a52635..000000000000 --- a/packages/path_provider/path_provider_ios/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,16 +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. - -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/main.m b/packages/path_provider/path_provider_ios/example/ios/Runner/main.m deleted file mode 100644 index f143297b30d6..000000000000 --- a/packages/path_provider/path_provider_ios/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +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 -#import -#import "AppDelegate.h" - -int main(int argc, char *argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/path_provider/path_provider_ios/example/ios/RunnerTests/PathProviderTests.m b/packages/path_provider/path_provider_ios/example/ios/RunnerTests/PathProviderTests.m deleted file mode 100644 index 87d227795614..000000000000 --- a/packages/path_provider/path_provider_ios/example/ios/RunnerTests/PathProviderTests.m +++ /dev/null @@ -1,18 +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 path_provider_ios; -@import XCTest; - -@interface PathProviderTests : XCTestCase -@end - -@implementation PathProviderTests - -- (void)testPlugin { - FLTPathProviderPlugin *plugin = [[FLTPathProviderPlugin alloc] init]; - XCTAssertNotNil(plugin); -} - -@end diff --git a/packages/path_provider/path_provider_ios/example/lib/main.dart b/packages/path_provider/path_provider_ios/example/lib/main.dart deleted file mode 100644 index d7140b76a06b..000000000000 --- a/packages/path_provider/path_provider_ios/example/lib/main.dart +++ /dev/null @@ -1,133 +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. - -// ignore_for_file: public_member_api_docs - -import 'package:flutter/material.dart'; -import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; - -void main() { - runApp(const MyApp()); -} - -class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Path Provider', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: const MyHomePage(title: 'Path Provider'), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({Key? key, required this.title}) : super(key: key); - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - final PathProviderPlatform provider = PathProviderPlatform.instance; - Future? _tempDirectory; - Future? _appSupportDirectory; - Future? _appLibraryDirectory; - Future? _appDocumentsDirectory; - - void _requestTempDirectory() { - setState(() { - _tempDirectory = provider.getTemporaryPath(); - }); - } - - Widget _buildDirectory( - BuildContext context, AsyncSnapshot snapshot) { - Text text = const Text(''); - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.hasError) { - text = Text('Error: ${snapshot.error}'); - } else if (snapshot.hasData) { - text = Text('path: ${snapshot.data}'); - } else { - text = const Text('path unavailable'); - } - } - return Padding(padding: const EdgeInsets.all(16.0), child: text); - } - - void _requestAppDocumentsDirectory() { - setState(() { - _appDocumentsDirectory = provider.getApplicationDocumentsPath(); - }); - } - - void _requestAppSupportDirectory() { - setState(() { - _appSupportDirectory = provider.getApplicationSupportPath(); - }); - } - - void _requestAppLibraryDirectory() { - setState(() { - _appLibraryDirectory = provider.getLibraryPath(); - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(widget.title), - ), - body: Center( - child: ListView( - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton( - onPressed: _requestTempDirectory, - child: const Text('Get Temporary Directory'), - ), - ), - FutureBuilder( - future: _tempDirectory, builder: _buildDirectory), - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton( - onPressed: _requestAppDocumentsDirectory, - child: const Text('Get Application Documents Directory'), - ), - ), - FutureBuilder( - future: _appDocumentsDirectory, builder: _buildDirectory), - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton( - onPressed: _requestAppSupportDirectory, - child: const Text('Get Application Support Directory'), - ), - ), - FutureBuilder( - future: _appSupportDirectory, builder: _buildDirectory), - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton( - onPressed: _requestAppLibraryDirectory, - child: const Text('Get Application Library Directory'), - ), - ), - FutureBuilder( - future: _appLibraryDirectory, builder: _buildDirectory), - ], - ), - ), - ); - } -} diff --git a/packages/path_provider/path_provider_ios/example/pubspec.yaml b/packages/path_provider/path_provider_ios/example/pubspec.yaml deleted file mode 100644 index f1d885513948..000000000000 --- a/packages/path_provider/path_provider_ios/example/pubspec.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: path_provider_example -description: Demonstrates how to use the path_provider plugin. -publish_to: none - -environment: - sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" - -dependencies: - flutter: - sdk: flutter - path_provider_ios: - # When depending on this package from a real application you should use: - # path_provider_ios: ^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: ../ - path_provider_platform_interface: ^2.0.0 - -dev_dependencies: - flutter_driver: - sdk: flutter - flutter_test: - sdk: flutter - integration_test: - sdk: flutter - -flutter: - uses-material-design: true diff --git a/packages/path_provider/path_provider_ios/ios/Assets/.gitkeep b/packages/path_provider/path_provider_ios/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.m b/packages/path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.m deleted file mode 100644 index 82b8df5382fb..000000000000 --- a/packages/path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.m +++ /dev/null @@ -1,43 +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 "FLTPathProviderPlugin.h" -#import "messages.g.h" - -static NSString *GetDirectoryOfType(NSSearchPathDirectory dir) { - NSArray *paths = NSSearchPathForDirectoriesInDomains(dir, NSUserDomainMask, YES); - return paths.firstObject; -} - -@interface FLTPathProviderPlugin () -@end - -@implementation FLTPathProviderPlugin - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FLTPathProviderPlugin *plugin = [[FLTPathProviderPlugin alloc] init]; - FLTPathProviderApiSetup(registrar.messenger, plugin); -} - -- (nullable NSString *)getApplicationDocumentsPathWithError: - (FlutterError *_Nullable __autoreleasing *_Nonnull)error { - return GetDirectoryOfType(NSDocumentDirectory); -} - -- (nullable NSString *)getApplicationSupportPathWithError: - (FlutterError *_Nullable __autoreleasing *_Nonnull)error { - return GetDirectoryOfType(NSApplicationSupportDirectory); -} - -- (nullable NSString *)getLibraryPathWithError: - (FlutterError *_Nullable __autoreleasing *_Nonnull)error { - return GetDirectoryOfType(NSLibraryDirectory); -} - -- (nullable NSString *)getTemporaryPathWithError: - (FlutterError *_Nullable __autoreleasing *_Nonnull)error { - return GetDirectoryOfType(NSCachesDirectory); -} - -@end diff --git a/packages/path_provider/path_provider_ios/ios/Classes/messages.g.h b/packages/path_provider/path_provider_ios/ios/Classes/messages.g.h deleted file mode 100644 index b6c1d4d92dd4..000000000000 --- a/packages/path_provider/path_provider_ios/ios/Classes/messages.g.h +++ /dev/null @@ -1,28 +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. -// Autogenerated from Pigeon (v3.1.5), do not edit directly. -// See also: https://pub.dev/packages/pigeon -#import -@protocol FlutterBinaryMessenger; -@protocol FlutterMessageCodec; -@class FlutterError; -@class FlutterStandardTypedData; - -NS_ASSUME_NONNULL_BEGIN - -/// The codec used by FLTPathProviderApi. -NSObject *FLTPathProviderApiGetCodec(void); - -@protocol FLTPathProviderApi -- (nullable NSString *)getTemporaryPathWithError:(FlutterError *_Nullable *_Nonnull)error; -- (nullable NSString *)getApplicationSupportPathWithError:(FlutterError *_Nullable *_Nonnull)error; -- (nullable NSString *)getLibraryPathWithError:(FlutterError *_Nullable *_Nonnull)error; -- (nullable NSString *)getApplicationDocumentsPathWithError: - (FlutterError *_Nullable *_Nonnull)error; -@end - -extern void FLTPathProviderApiSetup(id binaryMessenger, - NSObject *_Nullable api); - -NS_ASSUME_NONNULL_END diff --git a/packages/path_provider/path_provider_ios/ios/Classes/messages.g.m b/packages/path_provider/path_provider_ios/ios/Classes/messages.g.m deleted file mode 100644 index 2589df1837e7..000000000000 --- a/packages/path_provider/path_provider_ios/ios/Classes/messages.g.m +++ /dev/null @@ -1,138 +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. -// Autogenerated from Pigeon (v3.1.5), do not edit directly. -// See also: https://pub.dev/packages/pigeon -#import "messages.g.h" -#import - -#if !__has_feature(objc_arc) -#error File requires ARC to be enabled. -#endif - -static NSDictionary *wrapResult(id result, FlutterError *error) { - NSDictionary *errorDict = (NSDictionary *)[NSNull null]; - if (error) { - errorDict = @{ - @"code" : (error.code ?: [NSNull null]), - @"message" : (error.message ?: [NSNull null]), - @"details" : (error.details ?: [NSNull null]), - }; - } - return @{ - @"result" : (result ?: [NSNull null]), - @"error" : errorDict, - }; -} - -@interface FLTPathProviderApiCodecReader : FlutterStandardReader -@end -@implementation FLTPathProviderApiCodecReader -@end - -@interface FLTPathProviderApiCodecWriter : FlutterStandardWriter -@end -@implementation FLTPathProviderApiCodecWriter -@end - -@interface FLTPathProviderApiCodecReaderWriter : FlutterStandardReaderWriter -@end -@implementation FLTPathProviderApiCodecReaderWriter -- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { - return [[FLTPathProviderApiCodecWriter alloc] initWithData:data]; -} -- (FlutterStandardReader *)readerWithData:(NSData *)data { - return [[FLTPathProviderApiCodecReader alloc] initWithData:data]; -} -@end - -NSObject *FLTPathProviderApiGetCodec() { - static dispatch_once_t sPred = 0; - static FlutterStandardMessageCodec *sSharedObject = nil; - dispatch_once(&sPred, ^{ - FLTPathProviderApiCodecReaderWriter *readerWriter = - [[FLTPathProviderApiCodecReaderWriter alloc] init]; - sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; - }); - return sSharedObject; -} - -void FLTPathProviderApiSetup(id binaryMessenger, - NSObject *api) { - { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.PathProviderApi.getTemporaryPath" - binaryMessenger:binaryMessenger - codec:FLTPathProviderApiGetCodec()]; - if (api) { - NSCAssert( - [api respondsToSelector:@selector(getTemporaryPathWithError:)], - @"FLTPathProviderApi api (%@) doesn't respond to @selector(getTemporaryPathWithError:)", - api); - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FlutterError *error; - NSString *output = [api getTemporaryPathWithError:&error]; - callback(wrapResult(output, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } - { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.PathProviderApi.getApplicationSupportPath" - binaryMessenger:binaryMessenger - codec:FLTPathProviderApiGetCodec()]; - if (api) { - NSCAssert([api respondsToSelector:@selector(getApplicationSupportPathWithError:)], - @"FLTPathProviderApi api (%@) doesn't respond to " - @"@selector(getApplicationSupportPathWithError:)", - api); - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FlutterError *error; - NSString *output = [api getApplicationSupportPathWithError:&error]; - callback(wrapResult(output, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } - { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.PathProviderApi.getLibraryPath" - binaryMessenger:binaryMessenger - codec:FLTPathProviderApiGetCodec()]; - if (api) { - NSCAssert( - [api respondsToSelector:@selector(getLibraryPathWithError:)], - @"FLTPathProviderApi api (%@) doesn't respond to @selector(getLibraryPathWithError:)", - api); - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FlutterError *error; - NSString *output = [api getLibraryPathWithError:&error]; - callback(wrapResult(output, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } - { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.PathProviderApi.getApplicationDocumentsPath" - binaryMessenger:binaryMessenger - codec:FLTPathProviderApiGetCodec()]; - if (api) { - NSCAssert([api respondsToSelector:@selector(getApplicationDocumentsPathWithError:)], - @"FLTPathProviderApi api (%@) doesn't respond to " - @"@selector(getApplicationDocumentsPathWithError:)", - api); - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FlutterError *error; - NSString *output = [api getApplicationDocumentsPathWithError:&error]; - callback(wrapResult(output, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } -} diff --git a/packages/path_provider/path_provider_ios/ios/path_provider_ios.podspec b/packages/path_provider/path_provider_ios/ios/path_provider_ios.podspec deleted file mode 100644 index f1f94e996093..000000000000 --- a/packages/path_provider/path_provider_ios/ios/path_provider_ios.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'path_provider_ios' - s.version = '0.0.1' - s.summary = 'Flutter Path Provider' - s.description = <<-DESC -A Flutter plugin for getting commonly used locations on the filesystem. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_ios' } - s.documentation_url = 'https://pub.dev/packages/path_provider' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '9.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } -end diff --git a/packages/path_provider/path_provider_ios/lib/messages.g.dart b/packages/path_provider/path_provider_ios/lib/messages.g.dart deleted file mode 100644 index 1914119b8bd8..000000000000 --- a/packages/path_provider/path_provider_ios/lib/messages.g.dart +++ /dev/null @@ -1,124 +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. -// Autogenerated from Pigeon (v3.1.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 -// @dart = 2.12 -import 'dart:async'; -import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; - -import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; -import 'package:flutter/services.dart'; - -class _PathProviderApiCodec extends StandardMessageCodec { - const _PathProviderApiCodec(); -} - -class PathProviderApi { - /// Constructor for [PathProviderApi]. 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. - PathProviderApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - - final BinaryMessenger? _binaryMessenger; - - static const MessageCodec codec = _PathProviderApiCodec(); - - Future getTemporaryPath() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PathProviderApi.getTemporaryPath', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return (replyMap['result'] as String?); - } - } - - Future getApplicationSupportPath() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PathProviderApi.getApplicationSupportPath', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return (replyMap['result'] as String?); - } - } - - Future getLibraryPath() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PathProviderApi.getLibraryPath', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return (replyMap['result'] as String?); - } - } - - Future getApplicationDocumentsPath() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PathProviderApi.getApplicationDocumentsPath', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return (replyMap['result'] as String?); - } - } -} diff --git a/packages/path_provider/path_provider_ios/pubspec.yaml b/packages/path_provider/path_provider_ios/pubspec.yaml deleted file mode 100644 index 2f6171cd70bd..000000000000 --- a/packages/path_provider/path_provider_ios/pubspec.yaml +++ /dev/null @@ -1,34 +0,0 @@ -name: path_provider_ios -description: iOS implementation of the path_provider plugin. -repository: https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_ios -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.11 - -environment: - sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" - -flutter: - plugin: - implements: path_provider - platforms: - ios: - pluginClass: FLTPathProviderPlugin - dartPluginClass: PathProviderIOS - -dependencies: - flutter: - sdk: flutter - path_provider_platform_interface: ^2.0.0 - -dev_dependencies: - flutter_driver: - sdk: flutter - flutter_test: - sdk: flutter - integration_test: - sdk: flutter - path: ^1.8.0 - pigeon: ^3.1.5 - plugin_platform_interface: ^2.0.0 - test: ^1.16.0 diff --git a/packages/path_provider/path_provider_ios/test/messages_test.g.dart b/packages/path_provider/path_provider_ios/test/messages_test.g.dart deleted file mode 100644 index d1c9ff88dca1..000000000000 --- a/packages/path_provider/path_provider_ios/test/messages_test.g.dart +++ /dev/null @@ -1,88 +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. -// Autogenerated from Pigeon (v3.1.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 -// ignore_for_file: avoid_relative_lib_imports -// @dart = 2.12 -import 'dart:async'; -import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; -import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../lib/messages.g.dart'; - -class _TestPathProviderApiCodec extends StandardMessageCodec { - const _TestPathProviderApiCodec(); -} - -abstract class TestPathProviderApi { - static const MessageCodec codec = _TestPathProviderApiCodec(); - - String? getTemporaryPath(); - String? getApplicationSupportPath(); - String? getLibraryPath(); - String? getApplicationDocumentsPath(); - static void setup(TestPathProviderApi? api, - {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PathProviderApi.getTemporaryPath', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - // ignore message - final String? output = api.getTemporaryPath(); - return {'result': output}; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PathProviderApi.getApplicationSupportPath', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - // ignore message - final String? output = api.getApplicationSupportPath(); - return {'result': output}; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PathProviderApi.getLibraryPath', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - // ignore message - final String? output = api.getLibraryPath(); - return {'result': output}; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PathProviderApi.getApplicationDocumentsPath', - codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - // ignore message - final String? output = api.getApplicationDocumentsPath(); - return {'result': output}; - }); - } - } - } -} diff --git a/packages/path_provider/path_provider_ios/test/path_provider_ios_test.dart b/packages/path_provider/path_provider_ios/test/path_provider_ios_test.dart deleted file mode 100644 index 16a7cd8d71a2..000000000000 --- a/packages/path_provider/path_provider_ios/test/path_provider_ios_test.dart +++ /dev/null @@ -1,115 +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:io'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:path/path.dart' as p; -import 'package:path_provider_ios/path_provider_ios.dart'; -import 'messages_test.g.dart'; - -class _Api implements TestPathProviderApi { - String? applicationDocumentsPath; - String? applicationSupportPath; - String? libraryPath; - String? temporaryPath; - - @override - String? getApplicationDocumentsPath() => applicationDocumentsPath; - - @override - String? getApplicationSupportPath() => applicationSupportPath; - - @override - String? getLibraryPath() => libraryPath; - - @override - String? getTemporaryPath() => temporaryPath; -} - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('PathProviderIOS', () { - late PathProviderIOS pathProvider; - // These unit tests use the actual filesystem, since an injectable - // filesystem would add a runtime dependency to the package, so everything - // is contained to a temporary directory. - late Directory testRoot; - - late String temporaryPath; - late String applicationSupportPath; - late String libraryPath; - late String applicationDocumentsPath; - late _Api api; - - setUp(() async { - pathProvider = PathProviderIOS(); - - testRoot = Directory.systemTemp.createTempSync(); - final String basePath = testRoot.path; - temporaryPath = p.join(basePath, 'temporary', 'path'); - applicationSupportPath = - p.join(basePath, 'application', 'support', 'path'); - libraryPath = p.join(basePath, 'library', 'path'); - applicationDocumentsPath = - p.join(basePath, 'application', 'documents', 'path'); - - api = _Api(); - api.applicationDocumentsPath = applicationDocumentsPath; - api.applicationSupportPath = applicationSupportPath; - api.libraryPath = libraryPath; - api.temporaryPath = temporaryPath; - TestPathProviderApi.setup(api); - }); - - tearDown(() { - testRoot.deleteSync(recursive: true); - }); - - test('getTemporaryPath', () async { - final String? path = await pathProvider.getTemporaryPath(); - expect(path, temporaryPath); - }); - - test('getApplicationSupportPath', () async { - final String? path = await pathProvider.getApplicationSupportPath(); - expect(path, applicationSupportPath); - }); - - test('getApplicationSupportPath creates the directory if necessary', - () async { - final String? path = await pathProvider.getApplicationSupportPath(); - expect(Directory(path!).existsSync(), isTrue); - }); - - test('getLibraryPath', () async { - final String? path = await pathProvider.getLibraryPath(); - expect(path, libraryPath); - }); - - test('getApplicationDocumentsPath', () async { - final String? path = await pathProvider.getApplicationDocumentsPath(); - expect(path, applicationDocumentsPath); - }); - - test('getDownloadsPath throws', () async { - expect(pathProvider.getDownloadsPath(), throwsA(isUnsupportedError)); - }); - - test('getExternalCachePaths throws', () async { - expect(pathProvider.getExternalCachePaths(), throwsA(isUnsupportedError)); - }); - - test('getExternalStoragePath throws', () async { - expect( - pathProvider.getExternalStoragePath(), throwsA(isUnsupportedError)); - }); - - test('getExternalStoragePaths throws', () async { - expect( - pathProvider.getExternalStoragePaths(), throwsA(isUnsupportedError)); - }); - }); -} diff --git a/packages/path_provider/path_provider_linux/CHANGELOG.md b/packages/path_provider/path_provider_linux/CHANGELOG.md index baf3283348de..fa37eec3013b 100644 --- a/packages/path_provider/path_provider_linux/CHANGELOG.md +++ b/packages/path_provider/path_provider_linux/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 2.1.8 -* Updates minimum Flutter version to 2.10. +* Adds compatibility with `xdg_directories` 1.0. +* Updates minimum Flutter version to 3.0. ## 2.1.7 diff --git a/packages/path_provider/path_provider_linux/example/lib/main.dart b/packages/path_provider/path_provider_linux/example/lib/main.dart index 1c7c7e87397a..d7201468f6c1 100644 --- a/packages/path_provider/path_provider_linux/example/lib/main.dart +++ b/packages/path_provider/path_provider_linux/example/lib/main.dart @@ -41,29 +41,25 @@ class _MyAppState extends State { // Platform messages may fail, so we use a try/catch PlatformException. try { tempDirectory = await _provider.getTemporaryPath(); - } on PlatformException catch (e, stackTrace) { + } on PlatformException { tempDirectory = 'Failed to get temp directory.'; - print('$tempDirectory $e $stackTrace'); } try { downloadsDirectory = await _provider.getDownloadsPath(); - } on PlatformException catch (e, stackTrace) { + } on PlatformException { downloadsDirectory = 'Failed to get downloads directory.'; - print('$downloadsDirectory $e $stackTrace'); } try { documentsDirectory = await _provider.getApplicationDocumentsPath(); - } on PlatformException catch (e, stackTrace) { + } on PlatformException { documentsDirectory = 'Failed to get documents directory.'; - print('$documentsDirectory $e $stackTrace'); } try { appSupportDirectory = await _provider.getApplicationSupportPath(); - } on PlatformException catch (e, stackTrace) { + } on PlatformException { appSupportDirectory = 'Failed to get documents directory.'; - print('$appSupportDirectory $e $stackTrace'); } // If the widget was removed from the tree while the asynchronous platform // message was in flight, we want to discard the reply rather than calling diff --git a/packages/path_provider/path_provider_linux/example/pubspec.yaml b/packages/path_provider/path_provider_linux/example/pubspec.yaml index 8d8940ba2f05..a305575bb13b 100644 --- a/packages/path_provider/path_provider_linux/example/pubspec.yaml +++ b/packages/path_provider/path_provider_linux/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: "none" environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/path_provider/path_provider_linux/pubspec.yaml b/packages/path_provider/path_provider_linux/pubspec.yaml index 41d587360b5e..ecb9ea67525e 100644 --- a/packages/path_provider/path_provider_linux/pubspec.yaml +++ b/packages/path_provider/path_provider_linux/pubspec.yaml @@ -2,11 +2,11 @@ name: path_provider_linux description: Linux implementation of the path_provider plugin repository: https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.1.7 +version: 2.1.8 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -21,7 +21,7 @@ dependencies: sdk: flutter path: ^1.8.0 path_provider_platform_interface: ^2.0.0 - xdg_directories: ^0.2.0 + xdg_directories: ">=0.2.0 <2.0.0" dev_dependencies: flutter_test: diff --git a/packages/path_provider/path_provider_macos/CHANGELOG.md b/packages/path_provider/path_provider_macos/CHANGELOG.md deleted file mode 100644 index 61727e4d364b..000000000000 --- a/packages/path_provider/path_provider_macos/CHANGELOG.md +++ /dev/null @@ -1,91 +0,0 @@ -## NEXT - -* Updates minimum Flutter version to 2.10. - -## 2.0.6 - -* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors - lint warnings. - -## 2.0.5 - -* Removes dependency on `meta`. - -## 2.0.4 - -* Switches to a package-internal implementation of the platform interface. - -## 2.0.3 - -* Fixes link in README. - -## 2.0.2 - -* Add Swift language version to podspec. -* Add native unit tests. -* Updated installation instructions in README. - -## 2.0.1 - -* Add `implements` to pubspec.yaml. - -## 2.0.0 - -* Update Dart SDK constraint for null safety compatibility. - -## 0.0.4+9 - -* Remove placeholder Dart file. - -## 0.0.4+8 - -* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. - -## 0.0.4+7 - -* Update Flutter SDK constraint. - -## 0.0.4+6 - -* Remove unused `test` dependency. -* Update Dart SDK constraint in example. - -## 0.0.4+5 - -* Update license header. - -## 0.0.4+4 - -* Remove no-op android folder in the example app. - -## 0.0.4+3 - -* Remove Android folder from `path_provider_macos`. - -## 0.0.4+2 - -* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). - -## 0.0.4+1 - -* Fix CocoaPods podspec lint warnings. - -## 0.0.4 - -* Adds an example app to run integration tests. - -## 0.0.3+1 - -* Make the pedantic `dev_dependency` explicit. - -## 0.0.3 - -* Added support for user's downloads directory. - -## 0.0.2+1 - -* Update README. - -## 0.0.1 - -* Initial open source release. diff --git a/packages/path_provider/path_provider_macos/lib/path_provider_macos.dart b/packages/path_provider/path_provider_macos/lib/path_provider_macos.dart deleted file mode 100644 index 5dc3176e9b89..000000000000 --- a/packages/path_provider/path_provider_macos/lib/path_provider_macos.dart +++ /dev/null @@ -1,72 +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:io'; - -import 'package:flutter/foundation.dart' show visibleForTesting; -import 'package:flutter/services.dart'; -import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; - -/// The macOS implementation of [PathProviderPlatform]. -class PathProviderMacOS extends PathProviderPlatform { - /// The method channel used to interact with the native platform. - @visibleForTesting - MethodChannel methodChannel = - const MethodChannel('plugins.flutter.io/path_provider_macos'); - - /// Registers this class as the default instance of [PathProviderPlatform] - static void registerWith() { - PathProviderPlatform.instance = PathProviderMacOS(); - } - - @override - Future getTemporaryPath() { - return methodChannel.invokeMethod('getTemporaryDirectory'); - } - - @override - Future getApplicationSupportPath() async { - final String? path = await methodChannel - .invokeMethod('getApplicationSupportDirectory'); - if (path != null) { - // Ensure the directory exists before returning it, for consistency with - // other platforms. - await Directory(path).create(recursive: true); - } - return path; - } - - @override - Future getLibraryPath() { - return methodChannel.invokeMethod('getLibraryDirectory'); - } - - @override - Future getApplicationDocumentsPath() { - return methodChannel - .invokeMethod('getApplicationDocumentsDirectory'); - } - - @override - Future getExternalStoragePath() async { - throw UnsupportedError('getExternalStoragePath is not supported on macOS'); - } - - @override - Future?> getExternalCachePaths() async { - throw UnsupportedError('getExternalCachePaths is not supported on macOS'); - } - - @override - Future?> getExternalStoragePaths({ - StorageDirectory? type, - }) async { - throw UnsupportedError('getExternalStoragePaths is not supported on macOS'); - } - - @override - Future getDownloadsPath() { - return methodChannel.invokeMethod('getDownloadsDirectory'); - } -} diff --git a/packages/path_provider/path_provider_macos/macos/Classes/PathProviderPlugin.swift b/packages/path_provider/path_provider_macos/macos/Classes/PathProviderPlugin.swift deleted file mode 100644 index e138eee759ac..000000000000 --- a/packages/path_provider/path_provider_macos/macos/Classes/PathProviderPlugin.swift +++ /dev/null @@ -1,47 +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 FlutterMacOS -import Foundation - -public class PathProviderPlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel( - name: "plugins.flutter.io/path_provider_macos", - binaryMessenger: registrar.messenger) - let instance = PathProviderPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "getTemporaryDirectory": - result(getDirectory(ofType: FileManager.SearchPathDirectory.cachesDirectory)) - case "getApplicationDocumentsDirectory": - result(getDirectory(ofType: FileManager.SearchPathDirectory.documentDirectory)) - case "getApplicationSupportDirectory": - var path = getDirectory(ofType: FileManager.SearchPathDirectory.applicationSupportDirectory) - if let basePath = path { - let basePathURL = URL.init(fileURLWithPath: basePath) - path = basePathURL.appendingPathComponent(Bundle.main.bundleIdentifier!).path - } - result(path) - case "getLibraryDirectory": - result(getDirectory(ofType: FileManager.SearchPathDirectory.libraryDirectory)) - case "getDownloadsDirectory": - result(getDirectory(ofType: FileManager.SearchPathDirectory.downloadsDirectory)) - default: - result(FlutterMethodNotImplemented) - } - } -} - -/// Returns the user-domain director of the given type. -private func getDirectory(ofType directory: FileManager.SearchPathDirectory) -> String? { - let paths = NSSearchPathForDirectoriesInDomains( - directory, - FileManager.SearchPathDomainMask.userDomainMask, - true) - return paths.first -} diff --git a/packages/path_provider/path_provider_macos/macos/path_provider_macos.podspec b/packages/path_provider/path_provider_macos/macos/path_provider_macos.podspec deleted file mode 100644 index 14c468231f8c..000000000000 --- a/packages/path_provider/path_provider_macos/macos/path_provider_macos.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'path_provider_macos' - s.version = '0.0.1' - s.summary = 'A macOS implementation of the path_provider plugin.' - s.description = <<-DESC - A macOS implementation of the Flutter plugin for getting commonly used locations on the filesystem. - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_macos' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_macos' } - s.source_files = 'Classes/**/*' - s.dependency 'FlutterMacOS' - - s.platform = :osx - s.osx.deployment_target = '10.11' - s.swift_version = '5.0' -end - diff --git a/packages/path_provider/path_provider_platform_interface/CHANGELOG.md b/packages/path_provider/path_provider_platform_interface/CHANGELOG.md index f12e1ec53ade..e3470dc36844 100644 --- a/packages/path_provider/path_provider_platform_interface/CHANGELOG.md +++ b/packages/path_provider/path_provider_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 2.0.5 * Updates imports for `prefer_relative_imports`. diff --git a/packages/path_provider/path_provider_platform_interface/pubspec.yaml b/packages/path_provider/path_provider_platform_interface/pubspec.yaml index 6ce7ec662b33..3ce20f6f85db 100644 --- a/packages/path_provider/path_provider_platform_interface/pubspec.yaml +++ b/packages/path_provider/path_provider_platform_interface/pubspec.yaml @@ -8,7 +8,7 @@ version: 2.0.5 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/path_provider/path_provider_platform_interface/test/method_channel_path_provider_test.dart b/packages/path_provider/path_provider_platform_interface/test/method_channel_path_provider_test.dart index 69c9b2b01f19..035e7becb9ff 100644 --- a/packages/path_provider/path_provider_platform_interface/test/method_channel_path_provider_test.dart +++ b/packages/path_provider/path_provider_platform_interface/test/method_channel_path_provider_test.dart @@ -25,8 +25,10 @@ void main() { setUp(() async { methodChannelPathProvider = MethodChannelPathProvider(); - methodChannelPathProvider.methodChannel - .setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(methodChannelPathProvider.methodChannel, + (MethodCall methodCall) async { log.add(methodCall); switch (methodCall.method) { case 'getTemporaryDirectory': @@ -204,3 +206,9 @@ void main() { }); }); } + +/// 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/path_provider/path_provider_windows/CHANGELOG.md b/packages/path_provider/path_provider_windows/CHANGELOG.md index 757f13dbb533..08920a9569e8 100644 --- a/packages/path_provider/path_provider_windows/CHANGELOG.md +++ b/packages/path_provider/path_provider_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 2.1.3 * Updates minimum Flutter version to 2.10. diff --git a/packages/path_provider/path_provider_windows/example/pubspec.yaml b/packages/path_provider/path_provider_windows/example/pubspec.yaml index d70a4a84f504..306f20c354df 100644 --- a/packages/path_provider/path_provider_windows/example/pubspec.yaml +++ b/packages/path_provider/path_provider_windows/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/plugin_platform_interface/CHANGELOG.md b/packages/plugin_platform_interface/CHANGELOG.md index 0b5a6b63a52f..93e45c814668 100644 --- a/packages/plugin_platform_interface/CHANGELOG.md +++ b/packages/plugin_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported Dart version. + ## 2.1.3 * Minor fixes for new analysis options. diff --git a/packages/plugin_platform_interface/pubspec.yaml b/packages/plugin_platform_interface/pubspec.yaml index 6a4bc488693b..25189d942f84 100644 --- a/packages/plugin_platform_interface/pubspec.yaml +++ b/packages/plugin_platform_interface/pubspec.yaml @@ -18,7 +18,7 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 2.1.3 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" dependencies: meta: ^1.3.0 diff --git a/packages/quick_actions/quick_actions/CHANGELOG.md b/packages/quick_actions/quick_actions/CHANGELOG.md index 7d1881596255..0787c27014f1 100644 --- a/packages/quick_actions/quick_actions/CHANGELOG.md +++ b/packages/quick_actions/quick_actions/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 1.0.1 * Updates implementaion package versions to current versions. diff --git a/packages/quick_actions/quick_actions/example/android/gradle.properties b/packages/quick_actions/quick_actions/example/android/gradle.properties index 38c8d4544ff1..2f3603c9ff62 100644 --- a/packages/quick_actions/quick_actions/example/android/gradle.properties +++ b/packages/quick_actions/quick_actions/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/packages/quick_actions/quick_actions/example/pubspec.yaml b/packages/quick_actions/quick_actions/example/pubspec.yaml index 1a10a653db06..c629384ee5e2 100644 --- a/packages/quick_actions/quick_actions/example/pubspec.yaml +++ b/packages/quick_actions/quick_actions/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/quick_actions/quick_actions/pubspec.yaml b/packages/quick_actions/quick_actions/pubspec.yaml index 08b486fe50e3..3f1bf57a70f0 100644 --- a/packages/quick_actions/quick_actions/pubspec.yaml +++ b/packages/quick_actions/quick_actions/pubspec.yaml @@ -7,7 +7,7 @@ version: 1.0.1 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/quick_actions/quick_actions_android/CHANGELOG.md b/packages/quick_actions/quick_actions_android/CHANGELOG.md index bc809a4dc477..6587627b2145 100644 --- a/packages/quick_actions/quick_actions_android/CHANGELOG.md +++ b/packages/quick_actions/quick_actions_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 1.0.0 * Updates version to 1.0 to reflect current status. diff --git a/packages/quick_actions/quick_actions_android/android/build.gradle b/packages/quick_actions/quick_actions_android/android/build.gradle index e4cdec819ec9..4291fa020ef9 100644 --- a/packages/quick_actions/quick_actions_android/android/build.gradle +++ b/packages/quick_actions/quick_actions_android/android/build.gradle @@ -29,15 +29,12 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - // TODO(stuartmorgan): Enable when gradle is updated. - // disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' } dependencies { testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:4.7.0' + testImplementation 'org.mockito:mockito-core:5.0.0' } compileOptions { diff --git a/packages/quick_actions/quick_actions_android/example/android/app/build.gradle b/packages/quick_actions/quick_actions_android/example/android/app/build.gradle index 666194bc11b0..c9cbddb9ffeb 100644 --- a/packages/quick_actions/quick_actions_android/example/android/app/build.gradle +++ b/packages/quick_actions/quick_actions_android/example/android/app/build.gradle @@ -62,6 +62,6 @@ dependencies { androidTestImplementation "androidx.test:runner:$androidXTestVersion" androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' androidTestImplementation 'androidx.test.ext:junit:1.0.0' - androidTestImplementation 'org.mockito:mockito-core:4.7.0' - androidTestImplementation 'org.mockito:mockito-android:4.7.0' + androidTestImplementation 'org.mockito:mockito-core:5.0.0' + androidTestImplementation 'org.mockito:mockito-android:5.0.0' } diff --git a/packages/quick_actions/quick_actions_android/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java b/packages/quick_actions/quick_actions_android/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java index cfcef3e1c76f..f401f6f73975 100644 --- a/packages/quick_actions/quick_actions_android/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java +++ b/packages/quick_actions/quick_actions_android/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java @@ -28,7 +28,6 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -86,9 +85,6 @@ public void appShortcutsAreCreated() { } } - // TODO(bparrishMines): The test is ignored because it fails when ran on Firebase Test Lab. See - // https://github.com/flutter/flutter/issues/114246. - @Ignore @Test public void appShortcutLaunchActivityAfterStarting() { // Arrange @@ -119,7 +115,7 @@ public void appShortcutLaunchActivityAfterStarting() { "AppShortcut:" + firstShortcut.getId() + " does not launch the correct activity", // We can only find the shortcut type in content description while inspecting it in Ui // Automator Viewer. - device.hasObject(By.desc(firstShortcut.getId() + appReadySentinel))); + device.hasObject(By.descContains(firstShortcut.getId() + appReadySentinel))); // This is Android SingleTop behavior in which Android does not destroy the initial activity and // launch a new activity. Assert.assertEquals(initialActivity.get(), currentActivity.get()); diff --git a/packages/quick_actions/quick_actions_android/example/android/gradle.properties b/packages/quick_actions/quick_actions_android/example/android/gradle.properties index 38c8d4544ff1..2f3603c9ff62 100644 --- a/packages/quick_actions/quick_actions_android/example/android/gradle.properties +++ b/packages/quick_actions/quick_actions_android/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/packages/quick_actions/quick_actions_android/example/pubspec.yaml b/packages/quick_actions/quick_actions_android/example/pubspec.yaml index c560d4dd5f1e..48a6fe9fd1a5 100644 --- a/packages/quick_actions/quick_actions_android/example/pubspec.yaml +++ b/packages/quick_actions/quick_actions_android/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.15.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/quick_actions/quick_actions_android/pubspec.yaml b/packages/quick_actions/quick_actions_android/pubspec.yaml index e47a1fdc13e9..038c8631287f 100644 --- a/packages/quick_actions/quick_actions_android/pubspec.yaml +++ b/packages/quick_actions/quick_actions_android/pubspec.yaml @@ -6,7 +6,7 @@ version: 1.0.0 environment: sdk: ">=2.15.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/quick_actions/quick_actions_android/test/quick_actions_android_test.dart b/packages/quick_actions/quick_actions_android/test/quick_actions_android_test.dart index 40cfe458615d..0a98f5d4e55b 100644 --- a/packages/quick_actions/quick_actions_android/test/quick_actions_android_test.dart +++ b/packages/quick_actions/quick_actions_android/test/quick_actions_android_test.dart @@ -21,8 +21,10 @@ void main() { QuickActionsAndroid buildQuickActionsPlugin() { final QuickActionsAndroid quickActions = QuickActionsAndroid(); - quickActions.channel - .setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(quickActions.channel, + (MethodCall methodCall) async { log.add(methodCall); return ''; }); @@ -162,3 +164,9 @@ void main() { }); }); } + +/// 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/quick_actions/quick_actions_ios/CHANGELOG.md b/packages/quick_actions/quick_actions_ios/CHANGELOG.md index 31fe43832d2f..e135fa4c9b69 100644 --- a/packages/quick_actions/quick_actions_ios/CHANGELOG.md +++ b/packages/quick_actions/quick_actions_ios/CHANGELOG.md @@ -1,5 +1,10 @@ ## NEXT +* Updates minimum Flutter version to 3.0. + +## 1.0.2 + +* Migrates remaining components to Swift and removes all Objective-C settings. * Migrates `RunnerUITests` to Swift. ## 1.0.1 diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Podfile b/packages/quick_actions/quick_actions_ios/example/ios/Podfile index b52805241c18..3924e59aa0f9 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/Podfile +++ b/packages/quick_actions/quick_actions_ios/example/ios/Podfile @@ -31,7 +31,6 @@ target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths - pod 'OCMock', '~> 3.9.1' end end diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/project.pbxproj index c853a197ca9b..f5b708bbb54b 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/quick_actions/quick_actions_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -16,9 +16,12 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + E092A7ED28D10802005C7F67 /* MockMethodChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E092A7EA28D10801005C7F67 /* MockMethodChannel.swift */; }; + E092A7EE28D10802005C7F67 /* QuickActionsPluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E092A7EB28D10802005C7F67 /* QuickActionsPluginTests.swift */; }; + E092A7F128D10890005C7F67 /* MockShortcutItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E092A7F028D10890005C7F67 /* MockShortcutItemProvider.swift */; }; + E092A7F428D110B3005C7F67 /* DefaultShortcutItemParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E092A7F328D110B3005C7F67 /* DefaultShortcutItemParserTests.swift */; }; E092A7F628D128EB005C7F67 /* RunnerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E092A7F528D128EB005C7F67 /* RunnerUITests.swift */; }; - E0C09C29289C729D00E6977E /* FLTQuickActionsPluginTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C09C28289C729D00E6977E /* FLTQuickActionsPluginTests.m */; }; - E0C09C32289DBFCA00E6977E /* FLTShortcutStateManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C09C31289DBFCA00E6977E /* FLTShortcutStateManagerTests.m */; }; + E0A075D529147FE200329BAE /* MockShortcutItemParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0A075D429147FE200329BAE /* MockShortcutItemParser.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -75,9 +78,12 @@ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9D27FE1F0F21D4D47DDA16DE /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; C35AD3650AB6BF850E016715 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + E092A7EA28D10801005C7F67 /* MockMethodChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockMethodChannel.swift; sourceTree = ""; }; + E092A7EB28D10802005C7F67 /* QuickActionsPluginTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuickActionsPluginTests.swift; sourceTree = ""; }; + E092A7F028D10890005C7F67 /* MockShortcutItemProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockShortcutItemProvider.swift; sourceTree = ""; }; + E092A7F328D110B3005C7F67 /* DefaultShortcutItemParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultShortcutItemParserTests.swift; sourceTree = ""; }; E092A7F528D128EB005C7F67 /* RunnerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerUITests.swift; sourceTree = ""; }; - E0C09C28289C729D00E6977E /* FLTQuickActionsPluginTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTQuickActionsPluginTests.m; sourceTree = ""; }; - E0C09C31289DBFCA00E6977E /* FLTShortcutStateManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTShortcutStateManagerTests.m; sourceTree = ""; }; + E0A075D429147FE200329BAE /* MockShortcutItemParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockShortcutItemParser.swift; sourceTree = ""; }; F0609304FBCAEC2289164BD5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -111,9 +117,10 @@ 33E20B3326EFCDFC00A4A191 /* RunnerTests */ = { isa = PBXGroup; children = ( + E092A7F228D10908005C7F67 /* Mocks */, 33E20B3626EFCDFC00A4A191 /* Info.plist */, - E0C09C31289DBFCA00E6977E /* FLTShortcutStateManagerTests.m */, - E0C09C28289C729D00E6977E /* FLTQuickActionsPluginTests.m */, + E092A7EB28D10802005C7F67 /* QuickActionsPluginTests.swift */, + E092A7F328D110B3005C7F67 /* DefaultShortcutItemParserTests.swift */, ); path = RunnerTests; sourceTree = ""; @@ -205,6 +212,16 @@ name = Pods; sourceTree = ""; }; + E092A7F228D10908005C7F67 /* Mocks */ = { + isa = PBXGroup; + children = ( + E092A7EA28D10801005C7F67 /* MockMethodChannel.swift */, + E092A7F028D10890005C7F67 /* MockShortcutItemProvider.swift */, + E0A075D429147FE200329BAE /* MockShortcutItemParser.swift */, + ); + path = Mocks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -277,6 +294,7 @@ TargetAttributes = { 33E20B3126EFCDFC00A4A191 = { CreatedOnToolsVersion = 12.5; + LastSwiftMigration = 1330; TestTargetID = 97C146ED1CF9000F007C117D; }; 686BE82C25E58CCF00862533 = { @@ -416,8 +434,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - E0C09C32289DBFCA00E6977E /* FLTShortcutStateManagerTests.m in Sources */, - E0C09C29289C729D00E6977E /* FLTQuickActionsPluginTests.m in Sources */, + E092A7EE28D10802005C7F67 /* QuickActionsPluginTests.swift in Sources */, + E092A7ED28D10802005C7F67 /* MockMethodChannel.swift in Sources */, + E092A7F128D10890005C7F67 /* MockShortcutItemProvider.swift in Sources */, + E0A075D529147FE200329BAE /* MockShortcutItemParser.swift in Sources */, + E092A7F428D110B3005C7F67 /* DefaultShortcutItemParserTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -479,6 +500,7 @@ baseConfigurationReference = 9D27FE1F0F21D4D47DDA16DE /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; INFOPLIST_FILE = RunnerTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -487,6 +509,8 @@ ); PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; }; name = Debug; @@ -496,6 +520,7 @@ baseConfigurationReference = 96F949A6B78E2DC62B93C4F8 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; INFOPLIST_FILE = RunnerTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -504,6 +529,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; }; name = Release; diff --git a/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/DefaultShortcutItemParserTests.swift b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/DefaultShortcutItemParserTests.swift new file mode 100644 index 000000000000..739f88e90454 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/DefaultShortcutItemParserTests.swift @@ -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. + +import Flutter +import XCTest + +@testable import quick_actions_ios + +class DefaultShortcutItemParserTests: XCTestCase { + + func testParseShortcutItems() { + let rawItem = [ + "type": "SearchTheThing", + "localizedTitle": "Search the thing", + "icon": "search_the_thing.png", + ] + + let expectedItem = UIApplicationShortcutItem( + type: "SearchTheThing", + localizedTitle: "Search the thing", + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(templateImageName: "search_the_thing.png"), + userInfo: nil) + + let parser = DefaultShortcutItemParser() + XCTAssertEqual(parser.parseShortcutItems([rawItem]), [expectedItem]) + } + + func testParseShortcutItems_noIcon() { + let rawItem: [String: Any] = [ + "type": "SearchTheThing", + "localizedTitle": "Search the thing", + "icon": NSNull(), + ] + + let expectedItem = UIApplicationShortcutItem( + type: "SearchTheThing", + localizedTitle: "Search the thing", + localizedSubtitle: nil, + icon: nil, + userInfo: nil) + + let parser = DefaultShortcutItemParser() + XCTAssertEqual(parser.parseShortcutItems([rawItem]), [expectedItem]) + } + + func testParseShortcutItems_noType() { + let rawItem = [ + "localizedTitle": "Search the thing", + "icon": "search_the_thing.png", + ] + + let parser = DefaultShortcutItemParser() + XCTAssertEqual(parser.parseShortcutItems([rawItem]), []) + } + + func testParseShortcutItems_noLocalizedTitle() { + let rawItem = [ + "type": "SearchTheThing", + "icon": "search_the_thing.png", + ] + + let parser = DefaultShortcutItemParser() + XCTAssertEqual(parser.parseShortcutItems([rawItem]), []) + } +} diff --git a/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/FLTQuickActionsPluginTests.m b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/FLTQuickActionsPluginTests.m deleted file mode 100644 index 89651b573822..000000000000 --- a/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/FLTQuickActionsPluginTests.m +++ /dev/null @@ -1,210 +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 Flutter; -@import quick_actions_ios; -@import XCTest; -#import - -@interface FLTQuickActionsPluginTests : XCTestCase - -@end - -@implementation FLTQuickActionsPluginTests - -- (void)testHandleMethodCall_setShortcutItems { - NSDictionary *rawItem = @{ - @"type" : @"SearchTheThing", - @"localizedTitle" : @"Search the thing", - @"icon" : @"search_the_thing.png", - }; - - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"setShortcutItems" - arguments:@[ rawItem ]]; - - FLTShortcutStateManager *mockShortcutStateManager = OCMClassMock([FLTShortcutStateManager class]); - - QuickActionsPlugin *plugin = - [[QuickActionsPlugin alloc] initWithChannel:OCMClassMock([FlutterMethodChannel class]) - shortcutStateManager:mockShortcutStateManager]; - XCTestExpectation *resultExpectation = - [self expectationWithDescription:@"result block must be called."]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertNil(result, @"result block must be called with nil."); - [resultExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:1 handler:nil]; - - OCMVerify([mockShortcutStateManager setShortcutItems:@[ rawItem ]]); -} - -- (void)testHandleMethodCall_clearShortcutItems { - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"clearShortcutItems" - arguments:nil]; - FLTShortcutStateManager *mockShortcutStateManager = OCMClassMock([FLTShortcutStateManager class]); - QuickActionsPlugin *plugin = - [[QuickActionsPlugin alloc] initWithChannel:OCMClassMock([FlutterMethodChannel class]) - shortcutStateManager:mockShortcutStateManager]; - XCTestExpectation *resultExpectation = - [self expectationWithDescription:@"result block must be called."]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertNil(result, @"result block must be called with nil."); - [resultExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:1 handler:nil]; - OCMVerify([mockShortcutStateManager setShortcutItems:@[]]); -} - -- (void)testHandleMethodCall_getLaunchAction { - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getLaunchAction" - arguments:nil]; - - QuickActionsPlugin *plugin = - [[QuickActionsPlugin alloc] initWithChannel:OCMClassMock([FlutterMethodChannel class]) - shortcutStateManager:OCMClassMock([FLTShortcutStateManager class])]; - XCTestExpectation *resultExpectation = - [self expectationWithDescription:@"result block must be called."]; - [plugin handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertNil(result, @"result block must be called with nil."); - [resultExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:1 handler:nil]; -} - -- (void)testHandleMethodCall_nonExistMethods { - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"nonExist" arguments:nil]; - - QuickActionsPlugin *plugin = - [[QuickActionsPlugin alloc] initWithChannel:OCMClassMock([FlutterMethodChannel class]) - shortcutStateManager:OCMClassMock([FLTShortcutStateManager class])]; - XCTestExpectation *resultExpectation = - [self expectationWithDescription:@"result must be called."]; - [plugin - handleMethodCall:call - result:^(id _Nullable result) { - XCTAssertEqual(result, FlutterMethodNotImplemented, - @"result block must be called with FlutterMethodNotImplemented"); - [resultExpectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:1 handler:nil]; -} - -- (void)testApplicationPerformActionForShortcutItem { - id mockChannel = OCMClassMock([FlutterMethodChannel class]); - QuickActionsPlugin *plugin = - [[QuickActionsPlugin alloc] initWithChannel:mockChannel - shortcutStateManager:OCMClassMock([FLTShortcutStateManager class])]; - - UIApplicationShortcutItem *item = [[UIApplicationShortcutItem alloc] - initWithType:@"SearchTheThing" - localizedTitle:@"Search the thing" - localizedSubtitle:nil - icon:[UIApplicationShortcutIcon - iconWithTemplateImageName:@"search_the_thing.png"] - userInfo:nil]; - - BOOL actionResult = [plugin application:[UIApplication sharedApplication] - performActionForShortcutItem:item - completionHandler:^(BOOL succeeded){/* no-op */}]; - XCTAssert(actionResult, @"performActionForShortcutItem must return true."); - OCMVerify([mockChannel invokeMethod:@"launch" arguments:item.type]); -} - -- (void)testApplicationDidFinishLaunchingWithOptions_launchWithShortcut { - id mockShortcutStateManager = OCMClassMock([FLTShortcutStateManager class]); - QuickActionsPlugin *plugin = - [[QuickActionsPlugin alloc] initWithChannel:OCMClassMock([FlutterMethodChannel class]) - shortcutStateManager:mockShortcutStateManager]; - - UIApplicationShortcutItem *item = [[UIApplicationShortcutItem alloc] - initWithType:@"SearchTheThing" - localizedTitle:@"Search the thing" - localizedSubtitle:nil - icon:[UIApplicationShortcutIcon - iconWithTemplateImageName:@"search_the_thing.png"] - userInfo:nil]; - - BOOL launchResult = [plugin application:[UIApplication sharedApplication] - didFinishLaunchingWithOptions:@{UIApplicationLaunchOptionsShortcutItemKey : item}]; - - XCTAssertFalse(launchResult, - @"didFinishLaunchingWithOptions must return false if launched from shortcut."); -} - -- (void)testApplicationDidFinishLaunchingWithOptions_launchWithoutShortcut { - QuickActionsPlugin *plugin = - [[QuickActionsPlugin alloc] initWithChannel:OCMClassMock([FlutterMethodChannel class]) - shortcutStateManager:OCMClassMock([FLTShortcutStateManager class])]; - BOOL launchResult = [plugin application:[UIApplication sharedApplication] - didFinishLaunchingWithOptions:@{}]; - XCTAssertTrue(launchResult, - @"didFinishLaunchingWithOptions must return true if not launched from shortcut."); -} - -- (void)testApplicationDidBecomeActive_launchWithoutShortcut { - id mockChannel = OCMClassMock([FlutterMethodChannel class]); - id mockShortcutStateManager = OCMClassMock([FLTShortcutStateManager class]); - QuickActionsPlugin *plugin = - [[QuickActionsPlugin alloc] initWithChannel:mockChannel - shortcutStateManager:mockShortcutStateManager]; - - BOOL launchResult = [plugin application:[UIApplication sharedApplication] - didFinishLaunchingWithOptions:@{}]; - XCTAssertTrue(launchResult, - @"didFinishLaunchingWithOptions must return true if not launched from shortcut."); - [plugin applicationDidBecomeActive:[UIApplication sharedApplication]]; - OCMVerify(never(), [mockChannel invokeMethod:OCMOCK_ANY arguments:OCMOCK_ANY]); -} - -- (void)testApplicationDidBecomeActive_launchWithShortcut { - id mockChannel = OCMClassMock([FlutterMethodChannel class]); - id mockShortcutStateManager = OCMClassMock([FLTShortcutStateManager class]); - QuickActionsPlugin *plugin = - [[QuickActionsPlugin alloc] initWithChannel:mockChannel - shortcutStateManager:mockShortcutStateManager]; - - UIApplicationShortcutItem *item = [[UIApplicationShortcutItem alloc] - initWithType:@"SearchTheThing" - localizedTitle:@"Search the thing" - localizedSubtitle:nil - icon:[UIApplicationShortcutIcon - iconWithTemplateImageName:@"search_the_thing.png"] - userInfo:nil]; - BOOL launchResult = [plugin application:[UIApplication sharedApplication] - didFinishLaunchingWithOptions:@{UIApplicationLaunchOptionsShortcutItemKey : item}]; - XCTAssertFalse(launchResult, - @"didFinishLaunchingWithOptions must return false if launched from shortcut."); - [plugin applicationDidBecomeActive:[UIApplication sharedApplication]]; - OCMVerify([mockChannel invokeMethod:@"launch" arguments:item.type]); -} - -- (void)testApplicationDidBecomeActive_launchWithShortcut_becomeActiveTwice { - id mockChannel = OCMClassMock([FlutterMethodChannel class]); - id mockShortcutStateManager = OCMClassMock([FLTShortcutStateManager class]); - QuickActionsPlugin *plugin = - [[QuickActionsPlugin alloc] initWithChannel:mockChannel - shortcutStateManager:mockShortcutStateManager]; - - UIApplicationShortcutItem *item = [[UIApplicationShortcutItem alloc] - initWithType:@"SearchTheThing" - localizedTitle:@"Search the thing" - localizedSubtitle:nil - icon:[UIApplicationShortcutIcon - iconWithTemplateImageName:@"search_the_thing.png"] - userInfo:nil]; - BOOL launchResult = [plugin application:[UIApplication sharedApplication] - didFinishLaunchingWithOptions:@{UIApplicationLaunchOptionsShortcutItemKey : item}]; - XCTAssertFalse(launchResult, - @"didFinishLaunchingWithOptions must return false if launched from shortcut."); - [plugin applicationDidBecomeActive:[UIApplication sharedApplication]]; - [plugin applicationDidBecomeActive:[UIApplication sharedApplication]]; - // shortcut should only be handled once per launch. - OCMVerify(times(1), [mockChannel invokeMethod:@"launch" arguments:item.type]); -} - -@end diff --git a/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/FLTShortcutStateManagerTests.m b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/FLTShortcutStateManagerTests.m deleted file mode 100644 index 96fbf229e566..000000000000 --- a/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/FLTShortcutStateManagerTests.m +++ /dev/null @@ -1,62 +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 quick_actions_ios; -@import XCTest; -#import - -@interface FLTShortcutStateManagerTests : XCTestCase -@end - -@implementation FLTShortcutStateManagerTests - -- (void)testSetShortcutItems_shouldSetItem { - id mockApplication = OCMPartialMock([UIApplication sharedApplication]); - OCMStub([mockApplication sharedApplication]).andReturn(mockApplication); - - FLTShortcutStateManager *shortcutStateManager = [[FLTShortcutStateManager alloc] init]; - - NSDictionary *rawItem = @{ - @"type" : @"SearchTheThing", - @"localizedTitle" : @"Search the thing", - @"icon" : @"search_the_thing.png", - }; - - [shortcutStateManager setShortcutItems:@[ rawItem ]]; - - UIApplicationShortcutItem *expectedItem = [[UIApplicationShortcutItem alloc] - initWithType:@"SearchTheThing" - localizedTitle:@"Search the thing" - localizedSubtitle:nil - icon:[UIApplicationShortcutIcon - iconWithTemplateImageName:@"search_the_thing.png"] - userInfo:nil]; - - OCMVerify([mockApplication setShortcutItems:@[ expectedItem ]]); -} - -- (void)testSetShortcutItems_shouldSetItemWithoutIcon { - id mockApplication = OCMPartialMock([UIApplication sharedApplication]); - OCMStub([mockApplication sharedApplication]).andReturn(mockApplication); - - NSDictionary *rawItem = @{ - @"type" : @"SearchTheThing", - @"localizedTitle" : @"Search the thing", - // Dart's null value is passed to iOS as `NSNull`. - // The key value pair is still present in the dictionary. - @"icon" : [NSNull null], - }; - FLTShortcutStateManager *shortcutStateManager = [[FLTShortcutStateManager alloc] init]; - [shortcutStateManager setShortcutItems:@[ rawItem ]]; - - UIApplicationShortcutItem *expectedItem = - [[UIApplicationShortcutItem alloc] initWithType:@"SearchTheThing" - localizedTitle:@"Search the thing" - localizedSubtitle:nil - icon:nil - userInfo:nil]; - OCMVerify([mockApplication setShortcutItems:@[ expectedItem ]]); -} - -@end diff --git a/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/Mocks/MockMethodChannel.swift b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/Mocks/MockMethodChannel.swift new file mode 100644 index 000000000000..b52fa1daec95 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/Mocks/MockMethodChannel.swift @@ -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 Foundation + +@testable import quick_actions_ios + +final class MockMethodChannel: MethodChannel { + var invokeMethodStub: ((_ methods: String, _ arguments: Any?) -> Void)? = nil + func invokeMethod(_ method: String, arguments: Any?) { + invokeMethodStub?(method, arguments) + } +} diff --git a/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/Mocks/MockShortcutItemParser.swift b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/Mocks/MockShortcutItemParser.swift new file mode 100644 index 000000000000..3b5a09653958 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/Mocks/MockShortcutItemParser.swift @@ -0,0 +1,16 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Foundation + +@testable import quick_actions_ios + +final class MockShortcutItemParser: ShortcutItemParser { + + var parseShortcutItemsStub: ((_ items: [[String: Any]]) -> [UIApplicationShortcutItem])? = nil + + func parseShortcutItems(_ items: [[String: Any]]) -> [UIApplicationShortcutItem] { + return parseShortcutItemsStub?(items) ?? [] + } +} diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/AppDelegate.h b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/Mocks/MockShortcutItemProvider.swift similarity index 51% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Runner/AppDelegate.h rename to packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/Mocks/MockShortcutItemProvider.swift index 0681d288bb70..85477415667e 100644 --- a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/AppDelegate.h +++ b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/Mocks/MockShortcutItemProvider.swift @@ -2,9 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#import -#import +@testable import quick_actions_ios -@interface AppDelegate : FlutterAppDelegate - -@end +final class MockShortcutItemProvider: ShortcutItemProviding { + var shortcutItems: [UIApplicationShortcutItem]? = nil +} diff --git a/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/QuickActionsPluginTests.swift b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/QuickActionsPluginTests.swift new file mode 100644 index 000000000000..268a89ba5a5b --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/QuickActionsPluginTests.swift @@ -0,0 +1,294 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Flutter +import XCTest + +@testable import quick_actions_ios + +class QuickActionsPluginTests: XCTestCase { + + func testHandleMethodCall_setShortcutItems() { + let rawItem = [ + "type": "SearchTheThing", + "localizedTitle": "Search the thing", + "icon": "search_the_thing.png", + ] + let item = UIApplicationShortcutItem( + type: "SearchTheThing", + localizedTitle: "Search the thing", + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(templateImageName: "search_the_thing.png"), + userInfo: nil) + + let call = FlutterMethodCall(methodName: "setShortcutItems", arguments: [rawItem]) + + let mockChannel = MockMethodChannel() + let mockShortcutItemProvider = MockShortcutItemProvider() + let mockShortcutItemParser = MockShortcutItemParser() + + let plugin = QuickActionsPlugin( + channel: mockChannel, + shortcutItemProvider: mockShortcutItemProvider, + shortcutItemParser: mockShortcutItemParser) + + let parseShortcutItemsExpectation = expectation( + description: "parseShortcutItems must be called.") + mockShortcutItemParser.parseShortcutItemsStub = { items in + XCTAssertEqual(items as? [[String: String]], [rawItem]) + parseShortcutItemsExpectation.fulfill() + return [item] + } + + let resultExpectation = expectation(description: "result block must be called.") + plugin.handle(call) { result in + XCTAssertNil(result, "result block must be called with nil.") + resultExpectation.fulfill() + } + XCTAssertEqual(mockShortcutItemProvider.shortcutItems, [item], "Must set shortcut items.") + waitForExpectations(timeout: 1) + } + + func testHandleMethodCall_clearShortcutItems() { + let item = UIApplicationShortcutItem( + type: "SearchTheThing", + localizedTitle: "Search the thing", + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(templateImageName: "search_the_thing.png"), + userInfo: nil) + + let call = FlutterMethodCall(methodName: "clearShortcutItems", arguments: nil) + let mockChannel = MockMethodChannel() + let mockShortcutItemProvider = MockShortcutItemProvider() + let mockShortcutItemParser = MockShortcutItemParser() + + mockShortcutItemProvider.shortcutItems = [item] + + let plugin = QuickActionsPlugin( + channel: mockChannel, + shortcutItemProvider: mockShortcutItemProvider, + shortcutItemParser: mockShortcutItemParser) + + let resultExpectation = expectation(description: "result block must be called.") + plugin.handle(call) { result in + XCTAssertNil(result, "result block must be called with nil.") + resultExpectation.fulfill() + } + + XCTAssertEqual(mockShortcutItemProvider.shortcutItems, [], "Must clear shortcut items.") + waitForExpectations(timeout: 1) + } + + func testHandleMethodCall_getLaunchAction() { + let call = FlutterMethodCall(methodName: "getLaunchAction", arguments: nil) + + let mockChannel = MockMethodChannel() + let mockShortcutItemProvider = MockShortcutItemProvider() + let mockShortcutItemParser = MockShortcutItemParser() + + let plugin = QuickActionsPlugin( + channel: mockChannel, + shortcutItemProvider: mockShortcutItemProvider, + shortcutItemParser: mockShortcutItemParser) + + let resultExpectation = expectation(description: "result block must be called.") + plugin.handle(call) { result in + XCTAssertNil(result, "result block must be called with nil.") + resultExpectation.fulfill() + } + + waitForExpectations(timeout: 1) + } + + func testHandleMethodCall_nonExistMethods() { + let call = FlutterMethodCall(methodName: "nonExist", arguments: nil) + + let mockChannel = MockMethodChannel() + let mockShortcutItemProvider = MockShortcutItemProvider() + let mockShortcutItemParser = MockShortcutItemParser() + + let plugin = QuickActionsPlugin( + channel: mockChannel, + shortcutItemProvider: mockShortcutItemProvider, + shortcutItemParser: mockShortcutItemParser) + + let resultExpectation = expectation(description: "result block must be called.") + + plugin.handle(call) { result in + XCTAssertEqual( + result as? NSObject, FlutterMethodNotImplemented, + "result block must be called with FlutterMethodNotImplemented") + resultExpectation.fulfill() + } + + waitForExpectations(timeout: 1) + } + + func testApplicationPerformActionForShortcutItem() { + let mockChannel = MockMethodChannel() + let mockShortcutItemProvider = MockShortcutItemProvider() + let mockShortcutItemParser = MockShortcutItemParser() + + let plugin = QuickActionsPlugin( + channel: mockChannel, + shortcutItemProvider: mockShortcutItemProvider, + shortcutItemParser: mockShortcutItemParser) + + let item = UIApplicationShortcutItem( + type: "SearchTheThing", + localizedTitle: "Search the thing", + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(templateImageName: "search_the_thing.png"), + userInfo: nil) + + let invokeMethodExpectation = expectation(description: "invokeMethod must be called.") + mockChannel.invokeMethodStub = { method, arguments in + XCTAssertEqual(method, "launch") + XCTAssertEqual(arguments as? String, item.type) + invokeMethodExpectation.fulfill() + } + + let actionResult = plugin.application( + UIApplication.shared, + performActionFor: item + ) { success in /* no-op */ } + + XCTAssert(actionResult, "performActionForShortcutItem must return true.") + waitForExpectations(timeout: 1) + } + + func testApplicationDidFinishLaunchingWithOptions_launchWithShortcut() { + let mockChannel = MockMethodChannel() + let mockShortcutItemProvider = MockShortcutItemProvider() + let mockShortcutItemParser = MockShortcutItemParser() + + let plugin = QuickActionsPlugin( + channel: mockChannel, + shortcutItemProvider: mockShortcutItemProvider, + shortcutItemParser: mockShortcutItemParser) + + let item = UIApplicationShortcutItem( + type: "SearchTheThing", + localizedTitle: "Search the thing", + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(templateImageName: "search_the_thing.png"), + userInfo: nil) + + let launchResult = plugin.application( + UIApplication.shared, + didFinishLaunchingWithOptions: [UIApplication.LaunchOptionsKey.shortcutItem: item]) + XCTAssertFalse( + launchResult, "didFinishLaunchingWithOptions must return false if launched from shortcut.") + } + + func testApplicationDidFinishLaunchingWithOptions_launchWithoutShortcut() { + let mockChannel = MockMethodChannel() + let mockShortcutItemProvider = MockShortcutItemProvider() + let mockShortcutItemParser = MockShortcutItemParser() + + let plugin = QuickActionsPlugin( + channel: mockChannel, + shortcutItemProvider: mockShortcutItemProvider, + shortcutItemParser: mockShortcutItemParser) + + let launchResult = plugin.application(UIApplication.shared, didFinishLaunchingWithOptions: [:]) + XCTAssert( + launchResult, "didFinishLaunchingWithOptions must return true if not launched from shortcut.") + } + + func testApplicationDidBecomeActive_launchWithoutShortcut() { + let mockChannel = MockMethodChannel() + let mockShortcutItemProvider = MockShortcutItemProvider() + let mockShortcutItemParser = MockShortcutItemParser() + + let plugin = QuickActionsPlugin( + channel: mockChannel, + shortcutItemProvider: mockShortcutItemProvider, + shortcutItemParser: mockShortcutItemParser) + + mockChannel.invokeMethodStub = { _, _ in + XCTFail("invokeMethod should not be called if launch without shortcut.") + } + + let launchResult = plugin.application(UIApplication.shared, didFinishLaunchingWithOptions: [:]) + XCTAssert( + launchResult, "didFinishLaunchingWithOptions must return true if not launched from shortcut.") + + plugin.applicationDidBecomeActive(UIApplication.shared) + } + + func testApplicationDidBecomeActive_launchWithShortcut() { + let item = UIApplicationShortcutItem( + type: "SearchTheThing", + localizedTitle: "Search the thing", + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(templateImageName: "search_the_thing.png"), + userInfo: nil) + + let mockChannel = MockMethodChannel() + let mockShortcutItemProvider = MockShortcutItemProvider() + let mockShortcutItemParser = MockShortcutItemParser() + + let plugin = QuickActionsPlugin( + channel: mockChannel, + shortcutItemProvider: mockShortcutItemProvider, + shortcutItemParser: mockShortcutItemParser) + + let invokeMethodExpectation = expectation(description: "invokeMethod must be called.") + mockChannel.invokeMethodStub = { method, arguments in + XCTAssertEqual(method, "launch") + XCTAssertEqual(arguments as? String, item.type) + invokeMethodExpectation.fulfill() + } + + let launchResult = plugin.application( + UIApplication.shared, + didFinishLaunchingWithOptions: [UIApplication.LaunchOptionsKey.shortcutItem: item]) + + XCTAssertFalse( + launchResult, "didFinishLaunchingWithOptions must return false if launched from shortcut.") + + plugin.applicationDidBecomeActive(UIApplication.shared) + waitForExpectations(timeout: 1) + } + + func testApplicationDidBecomeActive_launchWithShortcut_becomeActiveTwice() { + let item = UIApplicationShortcutItem( + type: "SearchTheThing", + localizedTitle: "Search the thing", + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(templateImageName: "search_the_thing.png"), + userInfo: nil) + + let mockChannel = MockMethodChannel() + let mockShortcutItemProvider = MockShortcutItemProvider() + let mockShortcutItemParser = MockShortcutItemParser() + + let plugin = QuickActionsPlugin( + channel: mockChannel, + shortcutItemProvider: mockShortcutItemProvider, + shortcutItemParser: mockShortcutItemParser) + + let invokeMethodExpectation = expectation(description: "invokeMethod must be called.") + + var invokeMehtodCount = 0 + mockChannel.invokeMethodStub = { method, arguments in + invokeMehtodCount += 1 + invokeMethodExpectation.fulfill() + } + + let launchResult = plugin.application( + UIApplication.shared, + didFinishLaunchingWithOptions: [UIApplication.LaunchOptionsKey.shortcutItem: item]) + + XCTAssertFalse( + launchResult, "didFinishLaunchingWithOptions must return false if launched from shortcut.") + + plugin.applicationDidBecomeActive(UIApplication.shared) + waitForExpectations(timeout: 1) + + XCTAssertEqual(invokeMehtodCount, 1, "shortcut should only be handled once per launch.") + } + +} diff --git a/packages/quick_actions/quick_actions_ios/example/pubspec.yaml b/packages/quick_actions/quick_actions_ios/example/pubspec.yaml index ecac371720d6..af0697022ea3 100644 --- a/packages/quick_actions/quick_actions_ios/example/pubspec.yaml +++ b/packages/quick_actions/quick_actions_ios/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.15.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/quick_actions/quick_actions_ios/ios/Classes/FLTShortcutStateManager.h b/packages/quick_actions/quick_actions_ios/ios/Classes/FLTShortcutStateManager.h deleted file mode 100644 index 05d0433db6e0..000000000000 --- a/packages/quick_actions/quick_actions_ios/ios/Classes/FLTShortcutStateManager.h +++ /dev/null @@ -1,18 +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 - -NS_ASSUME_NONNULL_BEGIN - -/// Manages the shortcut related states. -@interface FLTShortcutStateManager : NSObject - -/// Sets the list of shortcut items. -/// -/// @param items the list of shortcut items to be parsed and set. -- (void)setShortcutItems:(NSArray *)items API_AVAILABLE(ios(9.0)); -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/quick_actions/quick_actions_ios/ios/Classes/FLTShortcutStateManager.m b/packages/quick_actions/quick_actions_ios/ios/Classes/FLTShortcutStateManager.m deleted file mode 100644 index e39edd2b0f37..000000000000 --- a/packages/quick_actions/quick_actions_ios/ios/Classes/FLTShortcutStateManager.m +++ /dev/null @@ -1,32 +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 "FLTShortcutStateManager.h" - -@implementation FLTShortcutStateManager - -- (void)setShortcutItems:(NSArray *)items { - NSMutableArray *newShortcuts = [[NSMutableArray alloc] init]; - - for (id item in items) { - UIApplicationShortcutItem *shortcut = [self deserializeShortcutItem:item]; - [newShortcuts addObject:shortcut]; - } - - [UIApplication sharedApplication].shortcutItems = newShortcuts; -} - -- (UIApplicationShortcutItem *)deserializeShortcutItem:(NSDictionary *)serialized { - UIApplicationShortcutIcon *icon = - [serialized[@"icon"] isKindOfClass:[NSNull class]] - ? nil - : [UIApplicationShortcutIcon iconWithTemplateImageName:serialized[@"icon"]]; - return [[UIApplicationShortcutItem alloc] initWithType:serialized[@"type"] - localizedTitle:serialized[@"localizedTitle"] - localizedSubtitle:nil - icon:icon - userInfo:nil]; -} - -@end diff --git a/packages/quick_actions/quick_actions_ios/ios/Classes/MethodChannel.swift b/packages/quick_actions/quick_actions_ios/ios/Classes/MethodChannel.swift new file mode 100644 index 000000000000..5d52790dd4b2 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/ios/Classes/MethodChannel.swift @@ -0,0 +1,16 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Flutter + +/// A channel for platform code to communicate with the Dart code. +protocol MethodChannel { + /// Invokes a method in Dart code. + /// - Parameter method the method name. + /// - Parameter arguments the method arguments. + func invokeMethod(_ method: String, arguments: Any?) +} + +/// A default implementation of the `MethodChannel` protocol. +extension FlutterMethodChannel: MethodChannel {} diff --git a/packages/quick_actions/quick_actions_ios/ios/Classes/QuickActionsPlugin.swift b/packages/quick_actions/quick_actions_ios/ios/Classes/QuickActionsPlugin.swift index 26d6d20f8c02..8522c5ff5288 100644 --- a/packages/quick_actions/quick_actions_ios/ios/Classes/QuickActionsPlugin.swift +++ b/packages/quick_actions/quick_actions_ios/ios/Classes/QuickActionsPlugin.swift @@ -15,19 +15,20 @@ public final class QuickActionsPlugin: NSObject, FlutterPlugin { registrar.addApplicationDelegate(instance) } - private let channel: FlutterMethodChannel - private let shortcutStateManager: FLTShortcutStateManager + private let channel: MethodChannel + private let shortcutItemProvider: ShortcutItemProviding + private let shortcutItemParser: ShortcutItemParser /// The type of the shortcut item selected when launching the app. private var launchingShortcutType: String? = nil - // TODO: (hellohuanlin) remove `@objc` attribute and make it non-public after migrating tests to Swift. - @objc - public init( - channel: FlutterMethodChannel, - shortcutStateManager: FLTShortcutStateManager = FLTShortcutStateManager() + init( + channel: MethodChannel, + shortcutItemProvider: ShortcutItemProviding = UIApplication.shared, + shortcutItemParser: ShortcutItemParser = DefaultShortcutItemParser() ) { self.channel = channel - self.shortcutStateManager = shortcutStateManager + self.shortcutItemProvider = shortcutItemProvider + self.shortcutItemParser = shortcutItemParser } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { @@ -35,10 +36,10 @@ public final class QuickActionsPlugin: NSObject, FlutterPlugin { case "setShortcutItems": // `arguments` must be an array of dictionaries let items = call.arguments as! [[String: Any]] - shortcutStateManager.setShortcutItems(items) + shortcutItemProvider.shortcutItems = shortcutItemParser.parseShortcutItems(items) result(nil) case "clearShortcutItems": - shortcutStateManager.setShortcutItems([]) + shortcutItemProvider.shortcutItems = [] result(nil) case "getLaunchAction": result(nil) diff --git a/packages/quick_actions/quick_actions_ios/ios/Classes/ShortcutItemParser.swift b/packages/quick_actions/quick_actions_ios/ios/Classes/ShortcutItemParser.swift new file mode 100644 index 000000000000..0945b4a386f8 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/ios/Classes/ShortcutItemParser.swift @@ -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. + +import UIKit + +/// A parser that parses an array of raw shortcut items. +protocol ShortcutItemParser { + + /// Parses an array of raw shortcut items into an array of UIApplicationShortcutItems + /// + /// - Parameter items an array of raw shortcut items to be parsed. + /// - Returns an array of parsed shortcut items to be set. + /// + func parseShortcutItems(_ items: [[String: Any]]) -> [UIApplicationShortcutItem] +} + +/// A default implementation of the `ShortcutItemParser` protocol. +final class DefaultShortcutItemParser: ShortcutItemParser { + + func parseShortcutItems(_ items: [[String: Any]]) -> [UIApplicationShortcutItem] { + return items.compactMap { deserializeShortcutItem(with: $0) } + } + + private func deserializeShortcutItem(with serialized: [String: Any]) -> UIApplicationShortcutItem? + { + guard + let type = serialized["type"] as? String, + let localizedTitle = serialized["localizedTitle"] as? String + else { + return nil + } + + let icon = (serialized["icon"] as? String).map { + UIApplicationShortcutIcon(templateImageName: $0) + } + + // type and localizedTitle are required. + return UIApplicationShortcutItem( + type: type, + localizedTitle: localizedTitle, + localizedSubtitle: nil, + icon: icon, + userInfo: nil) + } +} diff --git a/packages/quick_actions/quick_actions_ios/ios/Classes/ShortcutItemProviding.swift b/packages/quick_actions/quick_actions_ios/ios/Classes/ShortcutItemProviding.swift new file mode 100644 index 000000000000..e8854863bf95 --- /dev/null +++ b/packages/quick_actions/quick_actions_ios/ios/Classes/ShortcutItemProviding.swift @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import UIKit + +/// Provides the capability to get and set the app's home screen shortcut items. +protocol ShortcutItemProviding: AnyObject { + + /// An array of shortcut items for home screen. + var shortcutItems: [UIApplicationShortcutItem]? { get set } +} + +/// A default implementation of the `ShortcutItemProviding` protocol. +extension UIApplication: ShortcutItemProviding {} diff --git a/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios.podspec b/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios.podspec index d8090caa8ef6..a6fff92025b2 100644 --- a/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios.podspec +++ b/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios.podspec @@ -15,12 +15,11 @@ Downloaded by pub (not CocoaPods). s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/quick_actions' } s.documentation_url = 'https://pub.dev/packages/quick_actions' s.swift_version = '5.0' - s.source_files = 'Classes/**/*.{h,m,swift}' + s.source_files = 'Classes/**/*.swift' s.xcconfig = { 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', } - s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' s.platform = :ios, '9.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } diff --git a/packages/quick_actions/quick_actions_ios/pubspec.yaml b/packages/quick_actions/quick_actions_ios/pubspec.yaml index f01ae4aed9c3..2b7572368773 100644 --- a/packages/quick_actions/quick_actions_ios/pubspec.yaml +++ b/packages/quick_actions/quick_actions_ios/pubspec.yaml @@ -2,11 +2,11 @@ name: quick_actions_ios description: An implementation for the iOS platform of the Flutter `quick_actions` plugin. repository: https://github.com/flutter/plugins/tree/main/packages/quick_actions/quick_actions_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 1.0.1 +version: 1.0.2 environment: sdk: ">=2.15.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/quick_actions/quick_actions_ios/test/quick_actions_ios_test.dart b/packages/quick_actions/quick_actions_ios/test/quick_actions_ios_test.dart index 36827a5c6d8c..d2b062fff223 100644 --- a/packages/quick_actions/quick_actions_ios/test/quick_actions_ios_test.dart +++ b/packages/quick_actions/quick_actions_ios/test/quick_actions_ios_test.dart @@ -21,8 +21,10 @@ void main() { QuickActionsIos buildQuickActionsPlugin() { final QuickActionsIos quickActions = QuickActionsIos(); - quickActions.channel - .setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(quickActions.channel, + (MethodCall methodCall) async { log.add(methodCall); return ''; }); @@ -162,3 +164,9 @@ void main() { }); }); } + +/// 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/quick_actions/quick_actions_platform_interface/CHANGELOG.md b/packages/quick_actions/quick_actions_platform_interface/CHANGELOG.md index 950864f96653..6bbfd5a35f67 100644 --- a/packages/quick_actions/quick_actions_platform_interface/CHANGELOG.md +++ b/packages/quick_actions/quick_actions_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 1.0.3 * Updates imports for `prefer_relative_imports`. diff --git a/packages/quick_actions/quick_actions_platform_interface/pubspec.yaml b/packages/quick_actions/quick_actions_platform_interface/pubspec.yaml index 2990da603c14..cfde0a76f5b2 100644 --- a/packages/quick_actions/quick_actions_platform_interface/pubspec.yaml +++ b/packages/quick_actions/quick_actions_platform_interface/pubspec.yaml @@ -8,7 +8,7 @@ version: 1.0.3 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/quick_actions/quick_actions_platform_interface/test/method_channel_quick_actions_test.dart b/packages/quick_actions/quick_actions_platform_interface/test/method_channel_quick_actions_test.dart index 240f11bd8037..c1a508fbfb92 100644 --- a/packages/quick_actions/quick_actions_platform_interface/test/method_channel_quick_actions_test.dart +++ b/packages/quick_actions/quick_actions_platform_interface/test/method_channel_quick_actions_test.dart @@ -18,8 +18,10 @@ void main() { final List log = []; setUp(() { - quickActions.channel - .setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(quickActions.channel, + (MethodCall methodCall) async { log.add(methodCall); return ''; }); @@ -148,3 +150,9 @@ void main() { }); }); } + +/// 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/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index b719ff158ff4..ed44436dfe1e 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,5 +1,15 @@ ## NEXT +* Updates minimum Flutter version to 3.0. + +## 2.0.17 + +* Updates code for stricter lint checks. + +## 2.0.16 + +* Switches to the new `shared_preferences_foundation` implementation package + for iOS and macOS. * Updates code for `no_leading_underscores_for_local_identifiers` lint. * Updates minimum Flutter version to 2.10. diff --git a/packages/shared_preferences/shared_preferences/example/android/gradle.properties b/packages/shared_preferences/shared_preferences/example/android/gradle.properties index 94adc3a3f97a..598d13fee446 100644 --- a/packages/shared_preferences/shared_preferences/example/android/gradle.properties +++ b/packages/shared_preferences/shared_preferences/example/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/packages/shared_preferences/shared_preferences/example/lib/main.dart b/packages/shared_preferences/shared_preferences/example/lib/main.dart index a2e72b446925..f9690395f10d 100644 --- a/packages/shared_preferences/shared_preferences/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences/example/lib/main.dart @@ -66,9 +66,11 @@ class SharedPreferencesDemoState extends State { future: _counter, builder: (BuildContext context, AsyncSnapshot snapshot) { switch (snapshot.connectionState) { + case ConnectionState.none: case ConnectionState.waiting: return const CircularProgressIndicator(); - default: + case ConnectionState.active: + case ConnectionState.done: if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } else { diff --git a/packages/shared_preferences/shared_preferences/example/pubspec.yaml b/packages/shared_preferences/shared_preferences/example/pubspec.yaml index 6964656d16ef..944538da0d0c 100644 --- a/packages/shared_preferences/shared_preferences/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences/pubspec.yaml b/packages/shared_preferences/shared_preferences/pubspec.yaml index 7a310d9a584a..30ee569c3ad3 100644 --- a/packages/shared_preferences/shared_preferences/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. repository: https://github.com/flutter/plugins/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.0.15 +version: 2.0.17 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -15,11 +15,11 @@ flutter: android: default_package: shared_preferences_android ios: - default_package: shared_preferences_ios + default_package: shared_preferences_foundation linux: default_package: shared_preferences_linux macos: - default_package: shared_preferences_macos + default_package: shared_preferences_foundation web: default_package: shared_preferences_web windows: @@ -29,9 +29,8 @@ dependencies: flutter: sdk: flutter shared_preferences_android: ^2.0.8 - shared_preferences_ios: ^2.0.8 + shared_preferences_foundation: ^2.1.0 shared_preferences_linux: ^2.0.1 - shared_preferences_macos: ^2.0.0 shared_preferences_platform_interface: ^2.0.0 shared_preferences_web: ^2.0.0 shared_preferences_windows: ^2.0.1 diff --git a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md index d9d7bbd82f46..727f2b626d81 100644 --- a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md @@ -1,3 +1,11 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 2.0.15 + +* Updates code for stricter lint checks. + ## 2.0.14 * Fixes typo in `SharedPreferencesAndroid` docs. diff --git a/packages/shared_preferences/shared_preferences_android/android/build.gradle b/packages/shared_preferences/shared_preferences_android/android/build.gradle index feae770a475d..29f946bf7f77 100644 --- a/packages/shared_preferences/shared_preferences_android/android/build.gradle +++ b/packages/shared_preferences/shared_preferences_android/android/build.gradle @@ -37,14 +37,12 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' baseline file("lint-baseline.xml") } dependencies { testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-inline:4.7.0' + testImplementation 'org.mockito:mockito-inline:5.0.0' } diff --git a/packages/shared_preferences/shared_preferences_android/example/android/gradle.properties b/packages/shared_preferences/shared_preferences_android/example/android/gradle.properties index 94adc3a3f97a..598d13fee446 100644 --- a/packages/shared_preferences/shared_preferences_android/example/android/gradle.properties +++ b/packages/shared_preferences/shared_preferences_android/example/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/packages/shared_preferences/shared_preferences_android/example/lib/main.dart b/packages/shared_preferences/shared_preferences_android/example/lib/main.dart index bb513b09f6d5..cbcad6391beb 100644 --- a/packages/shared_preferences/shared_preferences_android/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_android/example/lib/main.dart @@ -70,9 +70,11 @@ class SharedPreferencesDemoState extends State { future: _counter, builder: (BuildContext context, AsyncSnapshot snapshot) { switch (snapshot.connectionState) { + case ConnectionState.none: case ConnectionState.waiting: return const CircularProgressIndicator(); - default: + case ConnectionState.active: + case ConnectionState.done: if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } else { diff --git a/packages/shared_preferences/shared_preferences_android/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_android/example/pubspec.yaml index bd1272c71d80..c0bc6668e3dd 100644 --- a/packages/shared_preferences/shared_preferences_android/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_android/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences_android/pubspec.yaml b/packages/shared_preferences/shared_preferences_android/pubspec.yaml index 7692c114bfce..d968dcbce55b 100644 --- a/packages/shared_preferences/shared_preferences_android/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_android/pubspec.yaml @@ -2,11 +2,11 @@ name: shared_preferences_android description: Android implementation of the shared_preferences plugin repository: https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.14 +version: 2.0.15 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/shared_preferences/shared_preferences_android/test/shared_preferences_android_test.dart b/packages/shared_preferences/shared_preferences_android/test/shared_preferences_android_test.dart index 92d9e8f53be1..f1043daac1a4 100644 --- a/packages/shared_preferences/shared_preferences_android/test/shared_preferences_android_test.dart +++ b/packages/shared_preferences/shared_preferences_android/test/shared_preferences_android_test.dart @@ -33,25 +33,36 @@ void main() { setUp(() async { testData = InMemorySharedPreferencesStore.empty(); - channel.setMockMethodCallHandler((MethodCall methodCall) async { + Map getArgumentDictionary(MethodCall call) { + return (call.arguments as Map) + .cast(); + } + + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); if (methodCall.method == 'getAll') { - return await testData.getAll(); + return testData.getAll(); } if (methodCall.method == 'remove') { - final String key = methodCall.arguments['key']! as String; - return await testData.remove(key); + final Map arguments = + getArgumentDictionary(methodCall); + final String key = arguments['key']! as String; + return testData.remove(key); } if (methodCall.method == 'clear') { - return await testData.clear(); + return testData.clear(); } final RegExp setterRegExp = RegExp(r'set(.*)'); final Match? match = setterRegExp.matchAsPrefix(methodCall.method); if (match?.groupCount == 1) { final String valueType = match!.group(1)!; - final String key = methodCall.arguments['key'] as String; - final Object value = methodCall.arguments['value'] as Object; - return await testData.setValue(valueType, key, value); + final Map arguments = + getArgumentDictionary(methodCall); + final String key = arguments['key']! as String; + final Object value = arguments['value']!; + return testData.setValue(valueType, key, value); } fail('Unexpected method call: ${methodCall.method}'); }); @@ -115,3 +126,9 @@ void main() { }); }); } + +/// 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/path_provider/path_provider_macos/AUTHORS b/packages/shared_preferences/shared_preferences_foundation/AUTHORS similarity index 100% rename from packages/path_provider/path_provider_macos/AUTHORS rename to packages/shared_preferences/shared_preferences_foundation/AUTHORS diff --git a/packages/shared_preferences/shared_preferences_foundation/CHANGELOG.md b/packages/shared_preferences/shared_preferences_foundation/CHANGELOG.md new file mode 100644 index 000000000000..b178143ca0b8 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/CHANGELOG.md @@ -0,0 +1,19 @@ +## 2.1.3 + +* Uses the new `sharedDarwinSource` flag when available. +* Updates minimum Flutter version to 3.0. + +## 2.1.2 + +* Updates code for stricter lint checks. + +## 2.1.1 + +* Adds Swift runtime search paths in podspec to avoid crash in Objective-C apps. + Convert example app to Objective-C to catch future Swift runtime issues. + +## 2.1.0 + +* Renames the package previously published as + [`shared_preferences_macos`](https://pub.dev/packages/shared_preferences_macos) +* Adds iOS support. diff --git a/packages/path_provider/path_provider_macos/LICENSE b/packages/shared_preferences/shared_preferences_foundation/LICENSE similarity index 100% rename from packages/path_provider/path_provider_macos/LICENSE rename to packages/shared_preferences/shared_preferences_foundation/LICENSE diff --git a/packages/shared_preferences/shared_preferences_ios/README.md b/packages/shared_preferences/shared_preferences_foundation/README.md similarity index 77% rename from packages/shared_preferences/shared_preferences_ios/README.md rename to packages/shared_preferences/shared_preferences_foundation/README.md index 5c9ced3b2096..1aaa9253399b 100644 --- a/packages/shared_preferences/shared_preferences_ios/README.md +++ b/packages/shared_preferences/shared_preferences_foundation/README.md @@ -1,6 +1,6 @@ -# shared\_preferences\_ios +# shared\_preferences\_foundation -The iOS implementation of [`shared_preferences`][1]. +The iOS and macOS implementation of [`shared_preferences`][1]. ## Usage diff --git a/packages/shared_preferences/shared_preferences_foundation/darwin/Classes/SharedPreferencesPlugin.swift b/packages/shared_preferences/shared_preferences_foundation/darwin/Classes/SharedPreferencesPlugin.swift new file mode 100644 index 000000000000..c97698ce0f7c --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/darwin/Classes/SharedPreferencesPlugin.swift @@ -0,0 +1,64 @@ +// 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 Foundation + +#if os(iOS) +import Flutter +#elseif os(macOS) +import FlutterMacOS +#endif + +public class SharedPreferencesPlugin: NSObject, FlutterPlugin, UserDefaultsApi { + public static func register(with registrar: FlutterPluginRegistrar) { + let instance = SharedPreferencesPlugin() + // Workaround for https://github.com/flutter/flutter/issues/118103. +#if os(iOS) + let messenger = registrar.messenger() +#else + let messenger = registrar.messenger +#endif + UserDefaultsApiSetup.setUp(binaryMessenger: messenger, api: instance) + } + + func getAll() -> [String? : Any?] { + return getAllPrefs(); + } + + func setBool(key: String, value: Bool) { + UserDefaults.standard.set(value, forKey: key) + } + + func setDouble(key: String, value: Double) { + UserDefaults.standard.set(value, forKey: key) + } + + func setValue(key: String, value: Any) { + UserDefaults.standard.set(value, forKey: key) + } + + func remove(key: String) { + UserDefaults.standard.removeObject(forKey: key) + } + + func clear() { + let defaults = UserDefaults.standard + for (key, _) in getAllPrefs() { + defaults.removeObject(forKey: key) + } + } +} + +/// Returns all preferences stored by this plugin. +private func getAllPrefs() -> [String: Any] { + var filteredPrefs: [String: Any] = [:] + if let appDomain = Bundle.main.bundleIdentifier, + let prefs = UserDefaults.standard.persistentDomain(forName: appDomain) + { + for (key, value) in prefs where key.hasPrefix("flutter.") { + filteredPrefs[key] = value + } + } + return filteredPrefs +} diff --git a/packages/shared_preferences/shared_preferences_foundation/darwin/Classes/messages.g.swift b/packages/shared_preferences/shared_preferences_foundation/darwin/Classes/messages.g.swift new file mode 100644 index 000000000000..933217b7bf96 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/darwin/Classes/messages.g.swift @@ -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. +// Autogenerated from Pigeon (v5.0.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation +#if os(iOS) +import Flutter +#elseif os(macOS) +import FlutterMacOS +#else +#error("Unsupported platform.") +#endif + + +/// Generated class from Pigeon. +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol UserDefaultsApi { + func remove(key: String) + func setBool(key: String, value: Bool) + func setDouble(key: String, value: Double) + func setValue(key: String, value: Any) + func getAll() -> [String?: Any?] + func clear() +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class UserDefaultsApiSetup { + /// The codec used by UserDefaultsApi. + /// Sets up an instance of `UserDefaultsApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: UserDefaultsApi?) { + let removeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UserDefaultsApi.remove", binaryMessenger: binaryMessenger) + if let api = api { + removeChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let keyArg = args[0] as! String + api.remove(key: keyArg) + reply(wrapResult(nil)) + } + } else { + removeChannel.setMessageHandler(nil) + } + let setBoolChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UserDefaultsApi.setBool", binaryMessenger: binaryMessenger) + if let api = api { + setBoolChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let keyArg = args[0] as! String + let valueArg = args[1] as! Bool + api.setBool(key: keyArg, value: valueArg) + reply(wrapResult(nil)) + } + } else { + setBoolChannel.setMessageHandler(nil) + } + let setDoubleChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UserDefaultsApi.setDouble", binaryMessenger: binaryMessenger) + if let api = api { + setDoubleChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let keyArg = args[0] as! String + let valueArg = args[1] as! Double + api.setDouble(key: keyArg, value: valueArg) + reply(wrapResult(nil)) + } + } else { + setDoubleChannel.setMessageHandler(nil) + } + let setValueChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UserDefaultsApi.setValue", binaryMessenger: binaryMessenger) + if let api = api { + setValueChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let keyArg = args[0] as! String + let valueArg = args[1]! + api.setValue(key: keyArg, value: valueArg) + reply(wrapResult(nil)) + } + } else { + setValueChannel.setMessageHandler(nil) + } + let getAllChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UserDefaultsApi.getAll", binaryMessenger: binaryMessenger) + if let api = api { + getAllChannel.setMessageHandler { _, reply in + let result = api.getAll() + reply(wrapResult(result)) + } + } else { + getAllChannel.setMessageHandler(nil) + } + let clearChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UserDefaultsApi.clear", binaryMessenger: binaryMessenger) + if let api = api { + clearChannel.setMessageHandler { _, reply in + api.clear() + reply(wrapResult(nil)) + } + } else { + clearChannel.setMessageHandler(nil) + } + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: FlutterError) -> [Any?] { + return [ + error.code, + error.message, + error.details + ] +} diff --git a/packages/shared_preferences/shared_preferences_foundation/darwin/Tests/RunnerTests.swift b/packages/shared_preferences/shared_preferences_foundation/darwin/Tests/RunnerTests.swift new file mode 100644 index 000000000000..a4dd4b58f923 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/darwin/Tests/RunnerTests.swift @@ -0,0 +1,64 @@ +// 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 XCTest + +#if os(iOS) +import Flutter +#elseif os(macOS) +import FlutterMacOS +#endif + +@testable import shared_preferences_foundation + +class RunnerTests: XCTestCase { + func testSetAndGet() throws { + let plugin = SharedPreferencesPlugin() + + plugin.setBool(key: "flutter.aBool", value: true) + plugin.setDouble(key: "flutter.aDouble", value: 3.14) + plugin.setValue(key: "flutter.anInt", value: 42) + plugin.setValue(key: "flutter.aString", value: "hello world") + plugin.setValue(key: "flutter.aStringList", value: ["hello", "world"]) + + let storedValues = plugin.getAll() + XCTAssertEqual(storedValues["flutter.aBool"] as? Bool, true) + XCTAssertEqual(storedValues["flutter.aDouble"] as! Double, 3.14, accuracy: 0.0001) + XCTAssertEqual(storedValues["flutter.anInt"] as? Int, 42) + XCTAssertEqual(storedValues["flutter.aString"] as? String, "hello world") + XCTAssertEqual(storedValues["flutter.aStringList"] as? Array, ["hello", "world"]) + } + + func testRemove() throws { + let plugin = SharedPreferencesPlugin() + let testKey = "flutter.foo" + plugin.setValue(key: testKey, value: 42) + + // Make sure there is something to remove, so the test can't pass due to a set failure. + let preRemovalValues = plugin.getAll() + XCTAssertEqual(preRemovalValues[testKey] as? Int, 42) + + // Then verify that removing it works. + plugin.remove(key: testKey) + + let finalValues = plugin.getAll() + XCTAssertNil(finalValues[testKey] as Any?) + } + + func testClear() throws { + let plugin = SharedPreferencesPlugin() + let testKey = "flutter.foo" + plugin.setValue(key: testKey, value: 42) + + // Make sure there is something to clear, so the test can't pass due to a set failure. + let preRemovalValues = plugin.getAll() + XCTAssertEqual(preRemovalValues[testKey] as? Int, 42) + + // Then verify that clearing works. + plugin.clear() + + let finalValues = plugin.getAll() + XCTAssertNil(finalValues[testKey] as Any?) + } +} diff --git a/packages/shared_preferences/shared_preferences_macos/macos/shared_preferences_macos.podspec b/packages/shared_preferences/shared_preferences_foundation/darwin/shared_preferences_foundation.podspec similarity index 52% rename from packages/shared_preferences/shared_preferences_macos/macos/shared_preferences_macos.podspec rename to packages/shared_preferences/shared_preferences_foundation/darwin/shared_preferences_foundation.podspec index 590b0c34adcf..b645bb520bab 100644 --- a/packages/shared_preferences/shared_preferences_macos/macos/shared_preferences_macos.podspec +++ b/packages/shared_preferences/shared_preferences_foundation/darwin/shared_preferences_foundation.podspec @@ -2,22 +2,26 @@ # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html # Pod::Spec.new do |s| - s.name = 'shared_preferences_macos' + s.name = 'shared_preferences_foundation' s.version = '0.0.1' - s.summary = 'macOS implementation of the shared_preferences plugin.' + s.summary = 'iOS and macOS implementation of the shared_preferences plugin.' s.description = <<-DESC Wraps NSUserDefaults, providing a persistent store for simple key-value pairs. DESC - s.homepage = 'https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_macos' + s.homepage = 'https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_foundation' s.license = { :type => 'BSD', :file => '../LICENSE' } s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_macos' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_foundation' } s.source_files = 'Classes/**/*' - s.dependency 'FlutterMacOS' - - s.platform = :osx, '10.11' + s.ios.dependency 'Flutter' + s.osx.dependency 'FlutterMacOS' + s.ios.deployment_target = '9.0' + s.osx.deployment_target = '10.11' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.xcconfig = { + 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', + 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', + } s.swift_version = '5.0' end - diff --git a/packages/shared_preferences/shared_preferences_ios/example/.gitignore b/packages/shared_preferences/shared_preferences_foundation/example/.gitignore similarity index 92% rename from packages/shared_preferences/shared_preferences_ios/example/.gitignore rename to packages/shared_preferences/shared_preferences_foundation/example/.gitignore index 0fa6b675c0a5..24476c5d1eb5 100644 --- a/packages/shared_preferences/shared_preferences_ios/example/.gitignore +++ b/packages/shared_preferences/shared_preferences_foundation/example/.gitignore @@ -8,6 +8,7 @@ .buildlog/ .history .svn/ +migrate_working_dir/ # IntelliJ related *.iml @@ -31,9 +32,6 @@ .pub/ /build/ -# Web related -lib/generated_plugin_registrant.dart - # Symbolication related app.*.symbols diff --git a/packages/path_provider/path_provider_macos/example/README.md b/packages/shared_preferences/shared_preferences_foundation/example/README.md similarity index 100% rename from packages/path_provider/path_provider_macos/example/README.md rename to packages/shared_preferences/shared_preferences_foundation/example/README.md diff --git a/packages/shared_preferences/shared_preferences_macos/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_foundation/example/integration_test/shared_preferences_test.dart similarity index 98% rename from packages/shared_preferences/shared_preferences_macos/example/integration_test/shared_preferences_test.dart rename to packages/shared_preferences/shared_preferences_foundation/example/integration_test/shared_preferences_test.dart index a980eab26679..b3c1973c2cd5 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_foundation/example/integration_test/shared_preferences_test.dart @@ -9,7 +9,7 @@ import 'package:shared_preferences_platform_interface/shared_preferences_platfor void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('SharedPreferencesMacOS', () { + group('SharedPreferencesFoundation', () { const Map kTestValues = { 'flutter.String': 'hello world', 'flutter.bool': true, diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/.gitignore b/packages/shared_preferences/shared_preferences_foundation/example/ios/.gitignore new file mode 100644 index 000000000000..7a7f9873ad7d --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/shared_preferences/shared_preferences_foundation/example/ios/Flutter/AppFrameworkInfo.plist similarity index 86% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Flutter/AppFrameworkInfo.plist rename to packages/shared_preferences/shared_preferences_foundation/example/ios/Flutter/AppFrameworkInfo.plist index 3a9c234f96d4..9625e105df39 100644 --- a/packages/shared_preferences/shared_preferences_ios/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/shared_preferences/shared_preferences_foundation/example/ios/Flutter/AppFrameworkInfo.plist @@ -20,11 +20,7 @@ ???? CFBundleVersion 1.0 - UIRequiredDeviceCapabilities - - arm64 - MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Flutter/Debug.xcconfig b/packages/shared_preferences/shared_preferences_foundation/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..ec97fc6f3021 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Flutter/Release.xcconfig b/packages/shared_preferences/shared_preferences_foundation/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..c4855bfe2000 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Podfile b/packages/shared_preferences/shared_preferences_foundation/example/ios/Podfile similarity index 95% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Podfile rename to packages/shared_preferences/shared_preferences_foundation/example/ios/Podfile index 3924e59aa0f9..fdcc671eb341 100644 --- a/packages/shared_preferences/shared_preferences_ios/example/ios/Podfile +++ b/packages/shared_preferences/shared_preferences_foundation/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -28,6 +28,9 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe flutter_ios_podfile_setup target 'Runner' do + use_frameworks! + use_modular_headers! + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcodeproj/project.pbxproj similarity index 59% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/project.pbxproj rename to packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcodeproj/project.pbxproj index c7567b312596..920741d8f335 100644 --- a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,24 +3,23 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ - 2D92224B1EC342E7007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D92224A1EC342E7007564B0 /* GeneratedPluginRegistrant.m */; }; + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 4E8BDD90E81668641A750C18 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 556D8EFC85341B7D1FDF536D /* libPods-RunnerTests.a */; }; - 66F8BCECCEFF62F4071D2DFC /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 12E03CD14DABAA3AD3923183 /* libPods-Runner.a */; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - F76AC2092669B6AE0040C8BC /* SharedPreferencesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F76AC2082669B6AE0040C8BC /* SharedPreferencesTests.m */; }; + 9AF3A5CAB88B27F2BC36D686 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D4C443E7E16FB2AD978AC1D6 /* Pods_RunnerTests.framework */; }; + B81650923B266CE1F32B75E4 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F3702C135032CE4599D8327B /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - F76AC20B2669B6AE0040C8BC /* PBXContainerItemProxy */ = { + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; proxyType = 1; @@ -43,60 +42,66 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 081A3238A89B77A99B096D83 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 12E03CD14DABAA3AD3923183 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 2D9222491EC342E7007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 2D92224A1EC342E7007564B0 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../../../darwin/Tests/RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3A8F2565F3AF472E2E0A219E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 556D8EFC85341B7D1FDF536D /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 6F1615DD96BB2B955423149B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 942E815CEF30E101E045B849 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 87C77C652D5BC0B23F81E01F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A2FC4F1DC78D7C01312F877F /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - D896CE48B6CC2EB7D42CB6B6 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - F76AC2062669B6AE0040C8BC /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F76AC2082669B6AE0040C8BC /* SharedPreferencesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SharedPreferencesTests.m; sourceTree = ""; }; - F76AC20A2669B6AE0040C8BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B5E9D2BAFD0E9BF6494E5389 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + D4C443E7E16FB2AD978AC1D6 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D9DC9227831D288079E5C887 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + F1B6CB00204D3430428972D5 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + F3702C135032CE4599D8327B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { + 3F94D8484CE6A0609BCE7680 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 66F8BCECCEFF62F4071D2DFC /* libPods-Runner.a in Frameworks */, + 9AF3A5CAB88B27F2BC36D686 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; - F76AC2032669B6AE0040C8BC /* Frameworks */ = { + 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4E8BDD90E81668641A750C18 /* libPods-RunnerTests.a in Frameworks */, + B81650923B266CE1F32B75E4 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { + 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( - 942E815CEF30E101E045B849 /* Pods-Runner.debug.xcconfig */, - 081A3238A89B77A99B096D83 /* Pods-Runner.release.xcconfig */, - A2FC4F1DC78D7C01312F877F /* Pods-RunnerTests.debug.xcconfig */, - D896CE48B6CC2EB7D42CB6B6 /* Pods-RunnerTests.release.xcconfig */, + 331C807B294A618700263BE5 /* RunnerTests.swift */, ); - name = Pods; + path = RunnerTests; + sourceTree = ""; + }; + 4E1DD4374F34EBDF7F4214F0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + F3702C135032CE4599D8327B /* Pods_Runner.framework */, + D4C443E7E16FB2AD978AC1D6 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { @@ -115,10 +120,10 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, - F76AC2072669B6AE0040C8BC /* RunnerTests */, 97C146EF1CF9000F007C117D /* Products */, - 840012C8B5EDBCF56B0E4AC1 /* Pods */, - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, + 331C8082294A63A400263BE5 /* RunnerTests */, + DA43E4FDD6392A0D5FBF1611 /* Pods */, + 4E1DD4374F34EBDF7F4214F0 /* Frameworks */, ); sourceTree = ""; }; @@ -126,7 +131,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, - F76AC2062669B6AE0040C8BC /* RunnerTests.xctest */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -134,59 +139,66 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( - 2D9222491EC342E7007564B0 /* GeneratedPluginRegistrant.h */, - 2D92224A1EC342E7007564B0 /* GeneratedPluginRegistrant.m */, - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = { + DA43E4FDD6392A0D5FBF1611 /* Pods */ = { isa = PBXGroup; children = ( - 12E03CD14DABAA3AD3923183 /* libPods-Runner.a */, - 556D8EFC85341B7D1FDF536D /* libPods-RunnerTests.a */, + 3A8F2565F3AF472E2E0A219E /* Pods-Runner.debug.xcconfig */, + 87C77C652D5BC0B23F81E01F /* Pods-Runner.release.xcconfig */, + 6F1615DD96BB2B955423149B /* Pods-Runner.profile.xcconfig */, + F1B6CB00204D3430428972D5 /* Pods-RunnerTests.debug.xcconfig */, + D9DC9227831D288079E5C887 /* Pods-RunnerTests.release.xcconfig */, + B5E9D2BAFD0E9BF6494E5389 /* Pods-RunnerTests.profile.xcconfig */, ); - name = Frameworks; - sourceTree = ""; - }; - F76AC2072669B6AE0040C8BC /* RunnerTests */ = { - isa = PBXGroup; - children = ( - F76AC2082669B6AE0040C8BC /* SharedPreferencesTests.m */, - F76AC20A2669B6AE0040C8BC /* Info.plist */, - ); - path = RunnerTests; + name = Pods; + path = Pods; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 9DEF57700431B717ADF93FFA /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 3F94D8484CE6A0609BCE7680 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */, + 4B0B07E2CB0088D1DE03E09A /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 3AC0A86331B4FD70A0EF91D9 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -197,46 +209,27 @@ productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; - F76AC2052669B6AE0040C8BC /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = F76AC20F2669B6AE0040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - DB9B98025BDEFED85B1B62A7 /* [CP] Check Pods Manifest.lock */, - F76AC2022669B6AE0040C8BC /* Sources */, - F76AC2032669B6AE0040C8BC /* Frameworks */, - F76AC2042669B6AE0040C8BC /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - F76AC20C2669B6AE0040C8BC /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = F76AC2062669B6AE0040C8BC /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Flutter Authors"; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - }; - F76AC2052669B6AE0040C8BC = { - CreatedOnToolsVersion = 12.5; - ProvisioningStyle = Automatic; - TestTargetID = 97C146ED1CF9000F007C117D; + LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; + compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -249,71 +242,79 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, - F76AC2052669B6AE0040C8BC /* RunnerTests */, + 331C8080294A63A400263BE5 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { + 331C807F294A63A400263BE5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - F76AC2042669B6AE0040C8BC /* Resources */ = { + 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 3AC0A86331B4FD70A0EF91D9 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - inputPaths = ( + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "Thin Binary"; - outputPaths = ( + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Run Script"; + name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = { + 4B0B07E2CB0088D1DE03E09A /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); @@ -322,7 +323,22 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - DB9B98025BDEFED85B1B62A7 /* [CP] Check Pods Manifest.lock */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + 9DEF57700431B717ADF93FFA /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -347,31 +363,30 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { + 331C807D294A63A400263BE5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 2D92224B1EC342E7007564B0 /* GeneratedPluginRegistrant.m in Sources */, + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - F76AC2022669B6AE0040C8BC /* Sources */ = { + 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - F76AC2092669B6AE0040C8BC /* SharedPreferencesTests.m in Sources */, + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - F76AC20C2669B6AE0040C8BC /* PBXTargetDependency */ = { + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = F76AC20B2669B6AE0040C8BC /* PBXContainerItemProxy */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -395,11 +410,131 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.sharedPreferencesFoundationExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F1B6CB00204D3430428972D5 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.sharedPreferencesFoundationExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D9DC9227831D288079E5C887 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.sharedPreferencesFoundationExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B5E9D2BAFD0E9BF6494E5389 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.sharedPreferencesFoundationExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -443,7 +578,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -455,7 +590,6 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -493,9 +627,12 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -506,19 +643,20 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.sharedPreferencesExample; + PRODUCT_BUNDLE_IDENTIFIER = com.example.sharedPreferencesFoundationExample; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; @@ -527,76 +665,51 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.sharedPreferencesExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; - F76AC20D2669B6AE0040C8BC /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = A2FC4F1DC78D7C01312F877F /* Pods-RunnerTests.debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = RunnerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; - }; - name = Debug; - }; - F76AC20E2669B6AE0040C8BC /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = D896CE48B6CC2EB7D42CB6B6 /* Pods-RunnerTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = RunnerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.example.sharedPreferencesFoundationExample; PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - F76AC20F2669B6AE0040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( - F76AC20D2669B6AE0040C8BC /* Debug */, - F76AC20E2669B6AE0040C8BC /* Release */, + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 94% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5e29b432c48c..e42adcb34c2d 100644 --- a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ + skipped = "NO" + parallelizable = "YES"> @@ -71,7 +72,7 @@ + + + + + + diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/AppDelegate.swift b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/AppDelegate.swift new file mode 100644 index 000000000000..caf998393333 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 94% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index d22f10b2ab63..d36b1fab2d9d 100644 --- a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -107,6 +107,12 @@ "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" } ], "info" : { diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 zcmeHN2~<R zh`|8`A_PQ1nSu(UMFx?8j8PC!!VDphaL#`F42fd#7Vlc`zIE4n%Y~eiz4y1j|NDpi z?<@|pSJ-HM`qifhf@m%MamgwK83`XpBA<+azdF#2QsT{X@z0A9Bq>~TVErigKH1~P zRX-!h-f0NJ4Mh++{D}J+K>~~rq}d%o%+4dogzXp7RxX4C>Km5XEI|PAFDmo;DFm6G zzjVoB`@qW98Yl0Kvc-9w09^PrsobmG*Eju^=3f?0o-t$U)TL1B3;sZ^!++3&bGZ!o-*6w?;oOhf z=A+Qb$scV5!RbG+&2S}BQ6YH!FKb0``VVX~T$dzzeSZ$&9=X$3)_7Z{SspSYJ!lGE z7yig_41zpQ)%5dr4ff0rh$@ky3-JLRk&DK)NEIHecf9c*?Z1bUB4%pZjQ7hD!A0r-@NF(^WKdr(LXj|=UE7?gBYGgGQV zidf2`ZT@pzXf7}!NH4q(0IMcxsUGDih(0{kRSez&z?CFA0RVXsVFw3^u=^KMtt95q z43q$b*6#uQDLoiCAF_{RFc{!H^moH_cmll#Fc^KXi{9GDl{>%+3qyfOE5;Zq|6#Hb zp^#1G+z^AXfRKaa9HK;%b3Ux~U@q?xg<2DXP%6k!3E)PA<#4$ui8eDy5|9hA5&{?v z(-;*1%(1~-NTQ`Is1_MGdQ{+i*ccd96ab$R$T3=% zw_KuNF@vI!A>>Y_2pl9L{9h1-C6H8<)J4gKI6{WzGBi<@u3P6hNsXG=bRq5c+z;Gc3VUCe;LIIFDmQAGy+=mRyF++u=drBWV8-^>0yE9N&*05XHZpPlE zxu@?8(ZNy7rm?|<+UNe0Vs6&o?l`Pt>P&WaL~M&#Eh%`rg@Mbb)J&@DA-wheQ>hRV z<(XhigZAT z>=M;URcdCaiO3d^?H<^EiEMDV+7HsTiOhoaMX%P65E<(5xMPJKxf!0u>U~uVqnPN7T!X!o@_gs3Ct1 zlZ_$5QXP4{Aj645wG_SNT&6m|O6~Tsl$q?nK*)(`{J4b=(yb^nOATtF1_aS978$x3 zx>Q@s4i3~IT*+l{@dx~Hst21fR*+5}S1@cf>&8*uLw-0^zK(+OpW?cS-YG1QBZ5q! zgTAgivzoF#`cSz&HL>Ti!!v#?36I1*l^mkrx7Y|K6L#n!-~5=d3;K<;Zqi|gpNUn_ z_^GaQDEQ*jfzh;`j&KXb66fWEk1K7vxQIMQ_#Wu_%3 z4Oeb7FJ`8I>Px;^S?)}2+4D_83gHEq>8qSQY0PVP?o)zAv3K~;R$fnwTmI-=ZLK`= zTm+0h*e+Yfr(IlH3i7gUclNH^!MU>id$Jw>O?2i0Cila#v|twub21@e{S2v}8Z13( zNDrTXZVgris|qYm<0NU(tAPouG!QF4ZNpZPkX~{tVf8xY690JqY1NVdiTtW+NqyRP zZ&;T0ikb8V{wxmFhlLTQ&?OP7 z;(z*<+?J2~z*6asSe7h`$8~Se(@t(#%?BGLVs$p``;CyvcT?7Y!{tIPva$LxCQ&4W z6v#F*);|RXvI%qnoOY&i4S*EL&h%hP3O zLsrFZhv&Hu5tF$Lx!8(hs&?!Kx5&L(fdu}UI5d*wn~A`nPUhG&Rv z2#ixiJdhSF-K2tpVL=)5UkXRuPAFrEW}7mW=uAmtVQ&pGE-&az6@#-(Te^n*lrH^m@X-ftVcwO_#7{WI)5v(?>uC9GG{lcGXYJ~Q8q zbMFl7;t+kV;|;KkBW2!P_o%Czhw&Q(nXlxK9ak&6r5t_KH8#1Mr-*0}2h8R9XNkr zto5-b7P_auqTJb(TJlmJ9xreA=6d=d)CVbYP-r4$hDn5|TIhB>SReMfh&OVLkMk-T zYf%$taLF0OqYF?V{+6Xkn>iX@TuqQ?&cN6UjC9YF&%q{Ut3zv{U2)~$>-3;Dp)*(? zg*$mu8^i=-e#acaj*T$pNowo{xiGEk$%DusaQiS!KjJH96XZ-hXv+jk%ard#fu=@Q z$AM)YWvE^{%tDfK%nD49=PI|wYu}lYVbB#a7wtN^Nml@CE@{Gv7+jo{_V?I*jkdLD zJE|jfdrmVbkfS>rN*+`#l%ZUi5_bMS<>=MBDNlpiSb_tAF|Zy`K7kcp@|d?yaTmB^ zo?(vg;B$vxS|SszusORgDg-*Uitzdi{dUV+glA~R8V(?`3GZIl^egW{a919!j#>f` znL1o_^-b`}xnU0+~KIFLQ)$Q6#ym%)(GYC`^XM*{g zv3AM5$+TtDRs%`2TyR^$(hqE7Y1b&`Jd6dS6B#hDVbJlUXcG3y*439D8MrK!2D~6gn>UD4Imctb z+IvAt0iaW73Iq$K?4}H`7wq6YkTMm`tcktXgK0lKPmh=>h+l}Y+pDtvHnG>uqBA)l zAH6BV4F}v$(o$8Gfo*PB>IuaY1*^*`OTx4|hM8jZ?B6HY;F6p4{`OcZZ(us-RVwDx zUzJrCQlp@mz1ZFiSZ*$yX3c_#h9J;yBE$2g%xjmGF4ca z&yL`nGVs!Zxsh^j6i%$a*I3ZD2SoNT`{D%mU=LKaEwbN(_J5%i-6Va?@*>=3(dQy` zOv%$_9lcy9+(t>qohkuU4r_P=R^6ME+wFu&LA9tw9RA?azGhjrVJKy&8=*qZT5Dr8g--d+S8zAyJ$1HlW3Olryt`yE zFIph~Z6oF&o64rw{>lgZISC6p^CBer9C5G6yq%?8tC+)7*d+ib^?fU!JRFxynRLEZ zj;?PwtS}Ao#9whV@KEmwQgM0TVP{hs>dg(1*DiMUOKHdQGIqa0`yZnHk9mtbPfoLx zo;^V6pKUJ!5#n`w2D&381#5#_t}AlTGEgDz$^;u;-vxDN?^#5!zN9ngytY@oTv!nc zp1Xn8uR$1Z;7vY`-<*?DfPHB;x|GUi_fI9@I9SVRv1)qETbNU_8{5U|(>Du84qP#7 z*l9Y$SgA&wGbj>R1YeT9vYjZuC@|{rajTL0f%N@>3$DFU=`lSPl=Iv;EjuGjBa$Gw zHD-;%YOE@<-!7-Mn`0WuO3oWuL6tB2cpPw~Nvuj|KM@))ixuDK`9;jGMe2d)7gHin zS<>k@!x;!TJEc#HdL#RF(`|4W+H88d4V%zlh(7#{q2d0OQX9*FW^`^_<3r$kabWAB z$9BONo5}*(%kx zOXi-yM_cmB3>inPpI~)duvZykJ@^^aWzQ=eQ&STUa}2uT@lV&WoRzkUoE`rR0)`=l zFT%f|LA9fCw>`enm$p7W^E@U7RNBtsh{_-7vVz3DtB*y#*~(L9+x9*wn8VjWw|Q~q zKFsj1Yl>;}%MG3=PY`$g$_mnyhuV&~O~u~)968$0b2!Jkd;2MtAP#ZDYw9hmK_+M$ zb3pxyYC&|CuAbtiG8HZjj?MZJBFbt`ryf+c1dXFuC z0*ZQhBzNBd*}s6K_G}(|Z_9NDV162#y%WSNe|FTDDhx)K!c(mMJh@h87@8(^YdK$&d*^WQe8Z53 z(|@MRJ$Lk-&ii74MPIs80WsOFZ(NX23oR-?As+*aq6b?~62@fSVmM-_*cb1RzZ)`5$agEiL`-E9s7{GM2?(KNPgK1(+c*|-FKoy}X(D_b#etO|YR z(BGZ)0Ntfv-7R4GHoXp?l5g#*={S1{u-QzxCGng*oWr~@X-5f~RA14b8~B+pLKvr4 zfgL|7I>jlak9>D4=(i(cqYf7#318!OSR=^`xxvI!bBlS??`xxWeg?+|>MxaIdH1U~#1tHu zB{QMR?EGRmQ_l4p6YXJ{o(hh-7Tdm>TAX380TZZZyVkqHNzjUn*_|cb?T? zt;d2s-?B#Mc>T-gvBmQZx(y_cfkXZO~{N zT6rP7SD6g~n9QJ)8F*8uHxTLCAZ{l1Y&?6v)BOJZ)=R-pY=Y=&1}jE7fQ>USS}xP#exo57uND0i*rEk@$;nLvRB@u~s^dwRf?G?_enN@$t* zbL%JO=rV(3Ju8#GqUpeE3l_Wu1lN9Y{D4uaUe`g>zlj$1ER$6S6@{m1!~V|bYkhZA z%CvrDRTkHuajMU8;&RZ&itnC~iYLW4DVkP<$}>#&(`UO>!n)Po;Mt(SY8Yb`AS9lt znbX^i?Oe9r_o=?})IHKHoQGKXsps_SE{hwrg?6dMI|^+$CeC&z@*LuF+P`7LfZ*yr+KN8B4{Nzv<`A(wyR@!|gw{zB6Ha ziwPAYh)oJ(nlqSknu(8g9N&1hu0$vFK$W#mp%>X~AU1ay+EKWcFdif{% z#4!4aoVVJ;ULmkQf!ke2}3hqxLK>eq|-d7Ly7-J9zMpT`?dxo6HdfJA|t)?qPEVBDv z{y_b?4^|YA4%WW0VZd8C(ZgQzRI5(I^)=Ub`Y#MHc@nv0w-DaJAqsbEHDWG8Ia6ju zo-iyr*sq((gEwCC&^TYBWt4_@|81?=B-?#P6NMff(*^re zYqvDuO`K@`mjm_Jd;mW_tP`3$cS?R$jR1ZN09$YO%_iBqh5ftzSpMQQtxKFU=FYmP zeY^jph+g<4>YO;U^O>-NFLn~-RqlHvnZl2yd2A{Yc1G@Ga$d+Q&(f^tnPf+Z7serIU};17+2DU_f4Z z@GaPFut27d?!YiD+QP@)T=77cR9~MK@bd~pY%X(h%L={{OIb8IQmf-!xmZkm8A0Ga zQSWONI17_ru5wpHg3jI@i9D+_Y|pCqVuHJNdHUauTD=R$JcD2K_liQisqG$(sm=k9;L* z!L?*4B~ql7uioSX$zWJ?;q-SWXRFhz2Jt4%fOHA=Bwf|RzhwqdXGr78y$J)LR7&3T zE1WWz*>GPWKZ0%|@%6=fyx)5rzUpI;bCj>3RKzNG_1w$fIFCZ&UR0(7S?g}`&Pg$M zf`SLsz8wK82Vyj7;RyKmY{a8G{2BHG%w!^T|Njr!h9TO2LaP^_f22Q1=l$QiU84ao zHe_#{S6;qrC6w~7{y(hs-?-j?lbOfgH^E=XcSgnwW*eEz{_Z<_xN#0001NP)t-s|Ns9~ z#rXRE|M&d=0au&!`~QyF`q}dRnBDt}*!qXo`c{v z{Djr|@Adh0(D_%#_&mM$D6{kE_x{oE{l@J5@%H*?%=t~i_`ufYOPkAEn!pfkr2$fs z652Tz0001XNklqeeKN4RM4i{jKqmiC$?+xN>3Apn^ z0QfuZLym_5b<*QdmkHjHlj811{If)dl(Z2K0A+ekGtrFJb?g|wt#k#pV-#A~bK=OT ts8>{%cPtyC${m|1#B1A6#u!Q;umknL1chzTM$P~L002ovPDHLkV1lTfnu!1a literal 0 HcmV?d00001 diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..797d452e458972bab9d994556c8305db4c827017 GIT binary patch literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed2d933e1120817fe9182483a228007b18ab6ae GIT binary patch literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 literal 0 HcmV?d00001 diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd7b0099ca80c806f8fe495613e8d6c69460d76 GIT binary patch literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX literal 0 HcmV?d00001 diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fe730945a01f64a61e2235dbe3f45b08f7729182 GIT binary patch literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..502f463a9bc882b461c96aadf492d1729e49e725 GIT binary patch literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec303439225b78712f49115768196d8d76f6790 GIT binary patch literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f5fea27c705180eb716271f41b582e76dcbd90 GIT binary patch literal 1674 zcmV;526g#~P){YQnis^a@{&-nmRmq)<&%Mztj67_#M}W?l>kYSliK<%xAp;0j{!}J0!o7b zE>q9${Lb$D&h7k=+4=!ek^n+`0zq>LL1O?lVyea53S5x`Nqqo2YyeuIrQrJj9XjOp z{;T5qbj3}&1vg1VK~#9!?b~^C5-}JC@Pyrv-6dSEqJqT}#j9#dJ@GzT@B8}x zU&J@bBI>f6w6en+CeI)3^kC*U?}X%OD8$Fd$H&LV$H&LV$H&LV#|K5~mLYf|VqzOc zkc7qL~0sOYuM{tG`rYEDV{DWY`Z8&)kW*hc2VkBuY+^Yx&92j&StN}Wp=LD zxoGxXw6f&8sB^u})h@b@z0RBeD`K7RMR9deyL(ZJu#39Z>rT)^>v}Khq8U-IbIvT> z?4pV9qGj=2)TNH3d)=De<+^w;>S7m_eFKTvzeaBeir45xY!^m!FmxnljbSS_3o=g( z->^wC9%qkR{kbGnW8MfFew_o9h3(r55Is`L$8KI@d+*%{=Nx+FXJ98L0PjFIu;rGnnfY zn1R5Qnp<{Jq0M1vX=X&F8gtLmcWv$1*M@4ZfF^9``()#hGTeKeP`1!iED ztNE(TN}M5}3Bbc*d=FIv`DNv&@|C6yYj{sSqUj5oo$#*0$7pu|Dd2TLI>t5%I zIa4Dvr(iayb+5x=j*Vum9&irk)xV1`t509lnPO0%skL8_1c#Xbamh(2@f?4yUI zhhuT5<#8RJhGz4%b$`PJwKPAudsm|at?u;*hGgnA zU1;9gnxVBC)wA(BsB`AW54N{|qmikJR*%x0c`{LGsSfa|NK61pYH(r-UQ4_JXd!Rsz)=k zL{GMc5{h138)fF5CzHEDM>+FqY)$pdN3}Ml+riTgJOLN0F*Vh?{9ESR{SVVg>*>=# zix;VJHPtvFFCRY$Ks*F;VX~%*r9F)W`PmPE9F!(&s#x07n2<}?S{(ygpXgX-&B&OM zONY&BRQ(#%0%jeQs?oJ4P!p*R98>qCy5p8w>_gpuh39NcOlp)(wOoz0sY-Qz55eB~ z7OC-fKBaD1sE3$l-6QgBJO!n?QOTza`!S_YK z_v-lm^7{VO^8Q@M_^8F)09Ki6%=s?2_5eupee(w1FB%aqSweusQ-T+CH0Xt{` zFjMvW{@C&TB)k25()nh~_yJ9coBRL(0oO@HK~z}7?bm5j;y@69;bvlHb2tf!$ReA~x{22wTq550 z?f?Hnw(;m3ip30;QzdV~7pi!wyMYhDtXW#cO7T>|f=bdFhu+F!zMZ2UFj;GUKX7tI z;hv3{q~!*pMj75WP_c}>6)IWvg5_yyg<9Op()eD1hWC19M@?_9_MHec{Z8n3FaF{8 z;u`Mw0ly(uE>*CgQYv{be6ab2LWhlaH1^iLIM{olnag$78^Fd}%dR7;JECQ+hmk|o z!u2&!3MqPfP5ChDSkFSH8F2WVOEf0(E_M(JL17G}Y+fg0_IuW%WQ zG(mG&u?|->YSdk0;8rc{yw2@2Z&GA}z{Wb91Ooz9VhA{b2DYE7RmG zjL}?eq#iX%3#k;JWMx_{^2nNax`xPhByFiDX+a7uTGU|otOvIAUy|dEKkXOm-`aWS z27pUzD{a)Ct<6p{{3)+lq@i`t@%>-wT4r?*S}k)58e09WZYP0{{R3FC5Sl00039P)t-s|Ns9~ z#rP?<_5oL$Q^olD{r_0T`27C={r>*`|Nj71npVa5OTzc(_WfbW_({R{p56NV{r*M2 z_xt?)2V0#0NsfV0u>{42ctGP(8vQj-Btk1n|O0ZD=YLwd&R{Ko41Gr9H= zY@z@@bOAMB5Ltl$E>bJJ{>JP30ZxkmI%?eW{k`b?Wy<&gOo;dS`~CR$Vwb@XWtR|N zi~t=w02?-0&j0TD{>bb6sNwsK*!p?V`RMQUl(*DVjk-9Cx+-z1KXab|Ka2oXhX5f% z`$|e!000AhNklrxs)5QTeTVRiEmz~MKK1WAjCw(c-JK6eox;2O)?`? zTG`AHia671e^vgmp!llKp|=5sVHk#C7=~epA~VAf-~%aPC=%Qw01h8mnSZ|p?hz91 z7p83F3%LVu9;S$tSI$C^%^yud1dfTM_6p2|+5Ejp$bd`GDvbR|xit>i!ZD&F>@CJrPmu*UjD&?DfZs=$@e3FQA(vNiU+$A*%a} z?`XcG2jDxJ_ZQ#Md`H{4Lpf6QBDp81_KWZ6Tk#yCy1)32zO#3<7>b`eT7UyYH1eGz z;O(rH$=QR*L%%ZcBpc=eGua?N55nD^K(8<#gl2+pN_j~b2MHs4#mcLmv%DkspS-3< zpI1F=^9siI0s-;IN_IrA;5xm~3?3!StX}pUv0vkxMaqm+zxrg7X7(I&*N~&dEd0kD z-FRV|g=|QuUsuh>-xCI}vD2imzYIOIdcCVV=$Bz@*u0+Bs<|L^)32nN*=wu3n%Ynw z@1|eLG>!8ruU1pFXUfb`j>(=Gy~?Rn4QJ-c3%3T|(Frd!bI`9u&zAnyFYTqlG#&J7 zAkD(jpw|oZLNiA>;>hgp1KX7-wxC~31II47gc zHcehD6Uxlf%+M^^uN5Wc*G%^;>D5qT{>=uxUhX%WJu^Z*(_Wq9y}npFO{Hhb>s6<9 zNi0pHXWFaVZnb)1+RS&F)xOv6&aeILcI)`k#0YE+?e)5&#r7J#c`3Z7x!LpTc01dx zrdC3{Z;joZ^KN&))zB_i)I9fWedoN>Zl-6_Iz+^G&*ak2jpF07*qoM6N<$f;w%0(f|Me literal 0 HcmV?d00001 diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0467bf12aa4d28f374bb26596605a46dcbb3e7c8 GIT binary patch literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H literal 0 HcmV?d00001 diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000000..0bedcf2fd467 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000000..89c2725b70f1 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 51% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard index ebf48f603974..f2e259c7c939 100644 --- a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -1,8 +1,8 @@ - + - + @@ -10,13 +10,20 @@ - - + + - - + + + + + + + + + @@ -24,4 +31,7 @@ + + + diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Base.lproj/Main.storyboard b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Info.plist b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Info.plist similarity index 78% rename from packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Info.plist rename to packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Info.plist index 22fc4c23715d..30d5f4b0e845 100644 --- a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Info.plist +++ b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Info.plist @@ -3,7 +3,9 @@ CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Shared Preferences Foundation CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -11,25 +13,21 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - shared_preferences_example + shared_preferences_foundation_example CFBundlePackageType APPL CFBundleShortVersionString - 1.0 + $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion - 1 + $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main - UIRequiredDeviceCapabilities - - arm64 - UISupportedInterfaceOrientations UIInterfaceOrientationPortrait @@ -45,5 +43,9 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.h b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Runner-Bridging-Header.h similarity index 63% rename from packages/path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.h rename to packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Runner-Bridging-Header.h index 8f9fd4597f9d..eb7e8ba8052f 100644 --- a/packages/path_provider/path_provider_ios/ios/Classes/FLTPathProviderPlugin.h +++ b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Runner-Bridging-Header.h @@ -2,7 +2,4 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#import - -@interface FLTPathProviderPlugin : NSObject -@end +#import "GeneratedPluginRegistrant.h" diff --git a/packages/shared_preferences/shared_preferences_macos/example/lib/main.dart b/packages/shared_preferences/shared_preferences_foundation/example/lib/main.dart similarity index 95% rename from packages/shared_preferences/shared_preferences_macos/example/lib/main.dart rename to packages/shared_preferences/shared_preferences_foundation/example/lib/main.dart index e6bbe5931471..a5aedd54ab6f 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_foundation/example/lib/main.dart @@ -72,9 +72,11 @@ class SharedPreferencesDemoState extends State { future: _counter, builder: (BuildContext context, AsyncSnapshot snapshot) { switch (snapshot.connectionState) { + case ConnectionState.none: case ConnectionState.waiting: return const CircularProgressIndicator(); - default: + case ConnectionState.active: + case ConnectionState.done: if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } else { diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/shared_preferences/shared_preferences_foundation/example/macos/Flutter/Flutter-Debug.xcconfig similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Flutter/Flutter-Debug.xcconfig rename to packages/shared_preferences/shared_preferences_foundation/example/macos/Flutter/Flutter-Debug.xcconfig diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Flutter/Flutter-Release.xcconfig b/packages/shared_preferences/shared_preferences_foundation/example/macos/Flutter/Flutter-Release.xcconfig similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Flutter/Flutter-Release.xcconfig rename to packages/shared_preferences/shared_preferences_foundation/example/macos/Flutter/Flutter-Release.xcconfig diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Podfile b/packages/shared_preferences/shared_preferences_foundation/example/macos/Podfile similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Podfile rename to packages/shared_preferences/shared_preferences_foundation/example/macos/Podfile diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcodeproj/project.pbxproj b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner.xcodeproj/project.pbxproj similarity index 99% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcodeproj/project.pbxproj rename to packages/shared_preferences/shared_preferences_foundation/example/macos/Runner.xcodeproj/project.pbxproj index 96f46f062f91..0bfa5f0a93d7 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -78,7 +78,7 @@ 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 = ""; }; 33EBD39826727BD10013E557 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 33EBD39A26727BD10013E557 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 33EBD39A26727BD10013E557 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../../../darwin/Tests/RunnerTests.swift; sourceTree = ""; }; 33EBD39C26727BD10013E557 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 5067D74CB28D28AE3B3DD05B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 53F020549CA1E801ACA3428F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; @@ -260,7 +260,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1250; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -347,6 +347,7 @@ }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -389,11 +390,11 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/shared_preferences_macos/shared_preferences_macos.framework", + "${BUILT_PRODUCTS_DIR}/shared_preferences_foundation/shared_preferences_foundation.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_macos.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_foundation.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 99% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/shared_preferences/shared_preferences_foundation/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 208a9bafa77a..6700d7ba4c05 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ + + + + + + diff --git a/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/AppDelegate.swift b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000000..5cec4c48f620 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/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/shared_preferences/shared_preferences_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png rename to packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png rename to packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png rename to packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png rename to packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png rename to packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png rename to packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png rename to packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Base.lproj/MainMenu.xib similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Base.lproj/MainMenu.xib rename to packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Base.lproj/MainMenu.xib diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Configs/AppInfo.xcconfig similarity index 100% rename from packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Configs/AppInfo.xcconfig rename to packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Configs/AppInfo.xcconfig diff --git a/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Configs/Debug.xcconfig b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000000..36b0fd9464f4 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Configs/Release.xcconfig b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000000..dff4f49561c8 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Configs/Warnings.xcconfig b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000000..42bcbf4780b1 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/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/shared_preferences/shared_preferences_foundation/example/macos/Runner/DebugProfile.entitlements b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000000..dddb8a30c851 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/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/shared_preferences/shared_preferences_foundation/example/macos/Runner/Info.plist b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Info.plist new file mode 100644 index 000000000000..4789daa6a443 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/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/shared_preferences/shared_preferences_foundation/example/macos/Runner/MainFlutterWindow.swift b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000000..32aaeedceb1f --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/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/shared_preferences/shared_preferences_foundation/example/macos/Runner/Release.entitlements b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Release.entitlements new file mode 100644 index 000000000000..852fa1a4728a --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/packages/path_provider/path_provider_macos/example/macos/RunnerTests/Info.plist b/packages/shared_preferences/shared_preferences_foundation/example/macos/RunnerTests/Info.plist similarity index 100% rename from packages/path_provider/path_provider_macos/example/macos/RunnerTests/Info.plist rename to packages/shared_preferences/shared_preferences_foundation/example/macos/RunnerTests/Info.plist diff --git a/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_foundation/example/pubspec.yaml similarity index 78% rename from packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml rename to packages/shared_preferences/shared_preferences_foundation/example/pubspec.yaml index f650fb7a6268..ef67f234e7c5 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_foundation/example/pubspec.yaml @@ -1,17 +1,17 @@ name: shared_preferences_example -description: Demonstrates how to use the shared_preferences plugin. +description: Testbed for the shared_preferences_foundation implementation. publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: sdk: flutter - shared_preferences_macos: + shared_preferences_foundation: # When depending on this package from a real application you should use: - # shared_preferences_macos: ^x.y.z + # shared_preferences_foundation: ^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. diff --git a/packages/path_provider/path_provider_macos/example/test_driver/integration_test.dart b/packages/shared_preferences/shared_preferences_foundation/example/test_driver/integration_test.dart similarity index 100% rename from packages/path_provider/path_provider_macos/example/test_driver/integration_test.dart rename to packages/shared_preferences/shared_preferences_foundation/example/test_driver/integration_test.dart diff --git a/packages/shared_preferences/shared_preferences_foundation/ios/Classes/SharedPreferencesPlugin.swift b/packages/shared_preferences/shared_preferences_foundation/ios/Classes/SharedPreferencesPlugin.swift new file mode 120000 index 000000000000..7b5941d0dc67 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/ios/Classes/SharedPreferencesPlugin.swift @@ -0,0 +1 @@ +../../darwin/Classes/SharedPreferencesPlugin.swift \ No newline at end of file diff --git a/packages/shared_preferences/shared_preferences_foundation/ios/Classes/messages.g.swift b/packages/shared_preferences/shared_preferences_foundation/ios/Classes/messages.g.swift new file mode 120000 index 000000000000..11bcf06e96a8 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/ios/Classes/messages.g.swift @@ -0,0 +1 @@ +../../darwin/Classes/messages.g.swift \ No newline at end of file diff --git a/packages/shared_preferences/shared_preferences_foundation/ios/README.md b/packages/shared_preferences/shared_preferences_foundation/ios/README.md new file mode 100644 index 000000000000..fd7261950f35 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/ios/README.md @@ -0,0 +1,4 @@ +This only contains symlinks to ../darwin, to support versions of Flutter +prior that don't include https://github.com/flutter/flutter/pull/115337. +Once the minimum Flutter version supported by this implementation is one that +includes that functionality, this directory should be removed. diff --git a/packages/shared_preferences/shared_preferences_foundation/ios/shared_preferences_foundation.podspec b/packages/shared_preferences/shared_preferences_foundation/ios/shared_preferences_foundation.podspec new file mode 120000 index 000000000000..59dcc19e0bf9 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/ios/shared_preferences_foundation.podspec @@ -0,0 +1 @@ +../darwin/shared_preferences_foundation.podspec \ No newline at end of file diff --git a/packages/shared_preferences/shared_preferences_ios/lib/messages.g.dart b/packages/shared_preferences/shared_preferences_foundation/lib/messages.g.dart similarity index 53% rename from packages/shared_preferences/shared_preferences_ios/lib/messages.g.dart rename to packages/shared_preferences/shared_preferences_foundation/lib/messages.g.dart index 0e76291fb655..f7c6c21567d2 100644 --- a/packages/shared_preferences/shared_preferences_ios/lib/messages.g.dart +++ b/packages/shared_preferences/shared_preferences_foundation/lib/messages.g.dart @@ -1,50 +1,41 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v1.0.16), do not edit directly. +// Autogenerated from Pigeon (v5.0.0), 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 -// @dart = 2.12 +// 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 import 'dart:async'; -import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; -import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; -class _UserDefaultsApiCodec extends StandardMessageCodec { - const _UserDefaultsApiCodec(); -} - class UserDefaultsApi { /// Constructor for [UserDefaultsApi]. 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. UserDefaultsApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _UserDefaultsApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future remove(String arg_key) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UserDefaultsApi.remove', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_key]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_key]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -55,21 +46,18 @@ class UserDefaultsApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UserDefaultsApi.setBool', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_key, arg_value]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_key, arg_value]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -80,21 +68,18 @@ class UserDefaultsApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UserDefaultsApi.setDouble', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_key, arg_value]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_key, arg_value]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -105,21 +90,18 @@ class UserDefaultsApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UserDefaultsApi.setValue', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_key, arg_value]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_key, arg_value]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -130,25 +112,25 @@ class UserDefaultsApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UserDefaultsApi.getAll', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + final List? replyList = await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } 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: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as Map?)! - .cast(); + return (replyList[0] as Map?)!.cast(); } } @@ -156,21 +138,17 @@ class UserDefaultsApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UserDefaultsApi.clear', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + final List? replyList = await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', - details: null, ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; diff --git a/packages/shared_preferences/shared_preferences_ios/lib/shared_preferences_ios.dart b/packages/shared_preferences/shared_preferences_foundation/lib/shared_preferences_foundation.dart similarity index 85% rename from packages/shared_preferences/shared_preferences_ios/lib/shared_preferences_ios.dart rename to packages/shared_preferences/shared_preferences_foundation/lib/shared_preferences_foundation.dart index 10638840804e..46b0ec41ea80 100644 --- a/packages/shared_preferences/shared_preferences_ios/lib/shared_preferences_ios.dart +++ b/packages/shared_preferences/shared_preferences_foundation/lib/shared_preferences_foundation.dart @@ -8,8 +8,8 @@ import 'messages.g.dart'; typedef _Setter = Future Function(String key, Object value); -/// iOS implementation of shared_preferences. -class SharedPreferencesIOS extends SharedPreferencesStorePlatform { +/// iOS and macOS implementation of shared_preferences. +class SharedPreferencesFoundation extends SharedPreferencesStorePlatform { final UserDefaultsApi _api = UserDefaultsApi(); late final Map _setters = { 'Bool': (String key, Object value) { @@ -29,9 +29,10 @@ class SharedPreferencesIOS extends SharedPreferencesStorePlatform { }, }; - /// Registers this class as the default instance of [PathProviderPlatform]. + /// Registers this class as the default instance of + /// [SharedPreferencesStorePlatform]. static void registerWith() { - SharedPreferencesStorePlatform.instance = SharedPreferencesIOS(); + SharedPreferencesStorePlatform.instance = SharedPreferencesFoundation(); } @override diff --git a/packages/shared_preferences/shared_preferences_foundation/macos/Classes/SharedPreferencesPlugin.swift b/packages/shared_preferences/shared_preferences_foundation/macos/Classes/SharedPreferencesPlugin.swift new file mode 120000 index 000000000000..7b5941d0dc67 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/macos/Classes/SharedPreferencesPlugin.swift @@ -0,0 +1 @@ +../../darwin/Classes/SharedPreferencesPlugin.swift \ No newline at end of file diff --git a/packages/shared_preferences/shared_preferences_foundation/macos/Classes/messages.g.swift b/packages/shared_preferences/shared_preferences_foundation/macos/Classes/messages.g.swift new file mode 120000 index 000000000000..11bcf06e96a8 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/macos/Classes/messages.g.swift @@ -0,0 +1 @@ +../../darwin/Classes/messages.g.swift \ No newline at end of file diff --git a/packages/shared_preferences/shared_preferences_foundation/macos/README.md b/packages/shared_preferences/shared_preferences_foundation/macos/README.md new file mode 100644 index 000000000000..fd7261950f35 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/macos/README.md @@ -0,0 +1,4 @@ +This only contains symlinks to ../darwin, to support versions of Flutter +prior that don't include https://github.com/flutter/flutter/pull/115337. +Once the minimum Flutter version supported by this implementation is one that +includes that functionality, this directory should be removed. diff --git a/packages/shared_preferences/shared_preferences_foundation/macos/shared_preferences_foundation.podspec b/packages/shared_preferences/shared_preferences_foundation/macos/shared_preferences_foundation.podspec new file mode 120000 index 000000000000..59dcc19e0bf9 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_foundation/macos/shared_preferences_foundation.podspec @@ -0,0 +1 @@ +../darwin/shared_preferences_foundation.podspec \ No newline at end of file diff --git a/packages/shared_preferences/shared_preferences_ios/pigeons/copyright_header.txt b/packages/shared_preferences/shared_preferences_foundation/pigeons/copyright_header.txt similarity index 100% rename from packages/shared_preferences/shared_preferences_ios/pigeons/copyright_header.txt rename to packages/shared_preferences/shared_preferences_foundation/pigeons/copyright_header.txt diff --git a/packages/shared_preferences/shared_preferences_ios/pigeons/messages.dart b/packages/shared_preferences/shared_preferences_foundation/pigeons/messages.dart similarity index 63% rename from packages/shared_preferences/shared_preferences_ios/pigeons/messages.dart rename to packages/shared_preferences/shared_preferences_foundation/pigeons/messages.dart index 6b5648f9e2f0..81848ed53279 100644 --- a/packages/shared_preferences/shared_preferences_ios/pigeons/messages.dart +++ b/packages/shared_preferences/shared_preferences_foundation/pigeons/messages.dart @@ -6,17 +6,20 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions( dartOut: 'lib/messages.g.dart', - dartTestOut: 'test/messages.g.dart', - objcHeaderOut: 'ios/Classes/messages.g.h', - objcSourceOut: 'ios/Classes/messages.g.m', + dartTestOut: 'test/test_api.g.dart', + swiftOut: 'darwin/Classes/messages.g.swift', copyrightHeader: 'pigeons/copyright_header.txt', )) @HostApi(dartHostTestHandler: 'TestUserDefaultsApi') abstract class UserDefaultsApi { void remove(String key); + // TODO(stuartmorgan): Give these setters better Swift signatures (_,forKey:) + // once https://github.com/flutter/flutter/issues/105932 is fixed. void setBool(String key, bool value); void setDouble(String key, double value); void setValue(String key, Object value); + // TODO(stuartmorgan): Make these non-nullable once + // https://github.com/flutter/flutter/issues/97848 is fixed. Map getAll(); void clear(); } diff --git a/packages/shared_preferences/shared_preferences_macos/pubspec.yaml b/packages/shared_preferences/shared_preferences_foundation/pubspec.yaml similarity index 52% rename from packages/shared_preferences/shared_preferences_macos/pubspec.yaml rename to packages/shared_preferences/shared_preferences_foundation/pubspec.yaml index 77f5f11f0525..3deb07fc5960 100644 --- a/packages/shared_preferences/shared_preferences_macos/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_foundation/pubspec.yaml @@ -1,20 +1,25 @@ -name: shared_preferences_macos -description: macOS implementation of the shared_preferences plugin. -repository: https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_macos +name: shared_preferences_foundation +description: iOS and macOS implementation of the shared_preferences plugin. +repository: https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_foundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.4 +version: 2.1.3 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: implements: shared_preferences platforms: + ios: + pluginClass: SharedPreferencesPlugin + dartPluginClass: SharedPreferencesFoundation + sharedDarwinSource: true macos: pluginClass: SharedPreferencesPlugin - dartPluginClass: SharedPreferencesMacOS + dartPluginClass: SharedPreferencesFoundation + sharedDarwinSource: true dependencies: flutter: @@ -24,3 +29,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + pigeon: ^5.0.0 diff --git a/packages/shared_preferences/shared_preferences_ios/test/shared_preferences_ios_test.dart b/packages/shared_preferences/shared_preferences_foundation/test/shared_preferences_foundation_test.dart similarity index 65% rename from packages/shared_preferences/shared_preferences_ios/test/shared_preferences_ios_test.dart rename to packages/shared_preferences/shared_preferences_foundation/test/shared_preferences_foundation_test.dart index efafb230d9de..6c0635a3342f 100644 --- a/packages/shared_preferences/shared_preferences_ios/test/shared_preferences_ios_test.dart +++ b/packages/shared_preferences/shared_preferences_foundation/test/shared_preferences_foundation_test.dart @@ -4,10 +4,10 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences_ios/shared_preferences_ios.dart'; +import 'package:shared_preferences_foundation/shared_preferences_foundation.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; -import 'messages.g.dart'; +import 'test_api.g.dart'; class _MockSharedPreferencesApi implements TestUserDefaultsApi { final Map items = {}; @@ -45,43 +45,52 @@ class _MockSharedPreferencesApi implements TestUserDefaultsApi { void main() { TestWidgetsFlutterBinding.ensureInitialized(); - _MockSharedPreferencesApi api = _MockSharedPreferencesApi(); - SharedPreferencesIOS plugin = SharedPreferencesIOS(); + late _MockSharedPreferencesApi api; setUp(() { api = _MockSharedPreferencesApi(); TestUserDefaultsApi.setup(api); - plugin = SharedPreferencesIOS(); }); test('registerWith', () { - SharedPreferencesIOS.registerWith(); - expect( - SharedPreferencesStorePlatform.instance, isA()); + SharedPreferencesFoundation.registerWith(); + expect(SharedPreferencesStorePlatform.instance, + isA()); }); test('remove', () async { + final SharedPreferencesFoundation plugin = SharedPreferencesFoundation(); api.items['flutter.hi'] = 'world'; expect(await plugin.remove('flutter.hi'), isTrue); expect(api.items.containsKey('flutter.hi'), isFalse); }); test('clear', () async { + final SharedPreferencesFoundation plugin = SharedPreferencesFoundation(); api.items['flutter.hi'] = 'world'; expect(await plugin.clear(), isTrue); expect(api.items.containsKey('flutter.hi'), isFalse); }); test('getAll', () async { - api.items['flutter.hi'] = 'world'; - api.items['flutter.bye'] = 'dust'; + final SharedPreferencesFoundation plugin = SharedPreferencesFoundation(); + api.items['flutter.aBool'] = true; + api.items['flutter.aDouble'] = 3.14; + api.items['flutter.anInt'] = 42; + api.items['flutter.aString'] = 'hello world'; + api.items['flutter.aStringList'] = ['hello', 'world']; final Map all = await plugin.getAll(); - expect(all.length, 2); - expect(all['flutter.hi'], api.items['flutter.hi']); - expect(all['flutter.bye'], api.items['flutter.bye']); + expect(all.length, 5); + expect(all['flutter.aBool'], api.items['flutter.aBool']); + expect(all['flutter.aDouble'], + closeTo(api.items['flutter.aDouble']! as num, 0.0001)); + expect(all['flutter.anInt'], api.items['flutter.anInt']); + expect(all['flutter.aString'], api.items['flutter.aString']); + expect(all['flutter.aStringList'], api.items['flutter.aStringList']); }); test('setValue', () async { + final SharedPreferencesFoundation plugin = SharedPreferencesFoundation(); expect(await plugin.setValue('Bool', 'flutter.Bool', true), isTrue); expect(api.items['flutter.Bool'], true); expect(await plugin.setValue('Double', 'flutter.Double', 1.5), isTrue); @@ -98,6 +107,7 @@ void main() { }); test('setValue with unsupported type', () { + final SharedPreferencesFoundation plugin = SharedPreferencesFoundation(); expect(() async { await plugin.setValue('Map', 'flutter.key', {}); }, throwsA(isA())); diff --git a/packages/shared_preferences/shared_preferences_ios/test/messages.g.dart b/packages/shared_preferences/shared_preferences_foundation/test/test_api.g.dart similarity index 88% rename from packages/shared_preferences/shared_preferences_ios/test/messages.g.dart rename to packages/shared_preferences/shared_preferences_foundation/test/test_api.g.dart index 12fbc0635784..12f97bd2b794 100644 --- a/packages/shared_preferences/shared_preferences_ios/test/messages.g.dart +++ b/packages/shared_preferences/shared_preferences_foundation/test/test_api.g.dart @@ -1,31 +1,33 @@ // 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 (v1.0.16), do not edit directly. +// Autogenerated from Pigeon (v5.0.0), 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 -// @dart = 2.12 +// 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 import 'dart:async'; -import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; -import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences_ios/messages.g.dart'; - -class _TestUserDefaultsApiCodec extends StandardMessageCodec { - const _TestUserDefaultsApiCodec(); -} +import 'package:shared_preferences_foundation/messages.g.dart'; abstract class TestUserDefaultsApi { - static const MessageCodec codec = _TestUserDefaultsApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void remove(String key); + void setBool(String key, bool value); + void setDouble(String key, double value); + void setValue(String key, Object value); + Map getAll(); + void clear(); + static void setup(TestUserDefaultsApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -43,7 +45,7 @@ abstract class TestUserDefaultsApi { assert(arg_key != null, 'Argument for dev.flutter.pigeon.UserDefaultsApi.remove was null, expected non-null String.'); api.remove(arg_key!); - return {}; + return []; }); } } @@ -65,7 +67,7 @@ abstract class TestUserDefaultsApi { assert(arg_value != null, 'Argument for dev.flutter.pigeon.UserDefaultsApi.setBool was null, expected non-null bool.'); api.setBool(arg_key!, arg_value!); - return {}; + return []; }); } } @@ -87,7 +89,7 @@ abstract class TestUserDefaultsApi { assert(arg_value != null, 'Argument for dev.flutter.pigeon.UserDefaultsApi.setDouble was null, expected non-null double.'); api.setDouble(arg_key!, arg_value!); - return {}; + return []; }); } } @@ -109,7 +111,7 @@ abstract class TestUserDefaultsApi { assert(arg_value != null, 'Argument for dev.flutter.pigeon.UserDefaultsApi.setValue was null, expected non-null Object.'); api.setValue(arg_key!, arg_value!); - return {}; + return []; }); } } @@ -123,7 +125,7 @@ abstract class TestUserDefaultsApi { channel.setMockMessageHandler((Object? message) async { // ignore message final Map output = api.getAll(); - return {'result': output}; + return [output]; }); } } @@ -137,7 +139,7 @@ abstract class TestUserDefaultsApi { channel.setMockMessageHandler((Object? message) async { // ignore message api.clear(); - return {}; + return []; }); } } diff --git a/packages/shared_preferences/shared_preferences_ios/AUTHORS b/packages/shared_preferences/shared_preferences_ios/AUTHORS deleted file mode 100644 index 493a0b4ef9c2..000000000000 --- a/packages/shared_preferences/shared_preferences_ios/AUTHORS +++ /dev/null @@ -1,66 +0,0 @@ -# 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. -The Chromium Authors -German Saprykin -Benjamin Sauer -larsenthomasj@gmail.com -Ali Bitek -Pol Batlló -Anatoly Pulyaevskiy -Hayden Flinner -Stefano Rodriguez -Salvatore Giordano -Brian Armstrong -Paul DeMarco -Fabricio Nogueira -Simon Lightfoot -Ashton Thomas -Thomas Danner -Diego Velásquez -Hajime Nakamura -Tuyển Vũ Xuân -Miguel Ruivo -Sarthak Verma -Mike Diarmid -Invertase -Elliot Hesp -Vince Varga -Aawaz Gyawali -EUI Limited -Katarina Sheremet -Thomas Stockx -Sarbagya Dhaubanjar -Ozkan Eksi -Rishab Nayak -ko2ic -Jonathan Younger -Jose Sanchez -Debkanchan Samadder -Audrius Karosevicius -Lukasz Piliszczuk -SoundReply Solutions GmbH -Rafal Wachol -Pau Picas -Christian Weder -Alexandru Tuca -Christian Weder -Rhodes Davis Jr. -Luigi Agosti -Quentin Le Guennec -Koushik Ravikumar -Nissim Dsilva -Giancarlo Rocha -Ryo Miyake -Théo Champion -Kazuki Yamaguchi -Eitan Schwartz -Chris Rutkowski -Juan Alvarez -Aleksandr Yurkovskiy -Anton Borries -Alex Li -Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/shared_preferences/shared_preferences_ios/CHANGELOG.md b/packages/shared_preferences/shared_preferences_ios/CHANGELOG.md deleted file mode 100644 index d2101e0784cf..000000000000 --- a/packages/shared_preferences/shared_preferences_ios/CHANGELOG.md +++ /dev/null @@ -1,25 +0,0 @@ -## NEXT - -* Updates code for `no_leading_underscores_for_local_identifiers` lint. -* Updates minimum Flutter version to 2.10. - -## 2.1.1 - -* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors - lint warnings. - -## 2.1.0 - -* Upgrades to using Pigeon. - -## 2.0.10 - -* Switches to an in-package method channel implementation. - -## 2.0.9 - -* Removes dependency on `meta`. - -## 2.0.8 - -* Split from `shared_preferences` as a federated implementation. diff --git a/packages/shared_preferences/shared_preferences_ios/LICENSE b/packages/shared_preferences/shared_preferences_ios/LICENSE deleted file mode 100644 index c6823b81eb84..000000000000 --- a/packages/shared_preferences/shared_preferences_ios/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -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/shared_preferences/shared_preferences_ios/example/.metadata b/packages/shared_preferences/shared_preferences_ios/example/.metadata deleted file mode 100644 index e0e9530fccc9..000000000000 --- a/packages/shared_preferences/shared_preferences_ios/example/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: 79b49b9e1057f90ebf797725233c6b311722de69 - channel: dev - -project_type: app diff --git a/packages/shared_preferences/shared_preferences_ios/example/README.md b/packages/shared_preferences/shared_preferences_ios/example/README.md deleted file mode 100644 index 96b8bb17dbff..000000000000 --- a/packages/shared_preferences/shared_preferences_ios/example/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# 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/shared_preferences/shared_preferences_ios/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_ios/example/integration_test/shared_preferences_test.dart deleted file mode 100644 index b4b21871701c..000000000000 --- a/packages/shared_preferences/shared_preferences_ios/example/integration_test/shared_preferences_test.dart +++ /dev/null @@ -1,106 +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 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('SharedPreferencesIos', () { - const Map kTestValues = { - 'flutter.String': 'hello world', - 'flutter.bool': true, - 'flutter.int': 42, - 'flutter.double': 3.14159, - 'flutter.List': ['foo', 'bar'], - }; - - const Map kTestValues2 = { - 'flutter.String': 'goodbye world', - 'flutter.bool': false, - 'flutter.int': 1337, - 'flutter.double': 2.71828, - 'flutter.List': ['baz', 'quox'], - }; - - late SharedPreferencesStorePlatform preferences; - - setUp(() async { - preferences = SharedPreferencesStorePlatform.instance; - }); - - tearDown(() { - preferences.clear(); - }); - - // Normally the app-facing package adds the prefix, but since this test - // bypasses the app-facing package it needs to be manually added. - String prefixedKey(String key) { - return 'flutter.$key'; - } - - testWidgets('reading', (WidgetTester _) async { - final Map values = await preferences.getAll(); - expect(values[prefixedKey('String')], isNull); - expect(values[prefixedKey('bool')], isNull); - expect(values[prefixedKey('int')], isNull); - expect(values[prefixedKey('double')], isNull); - expect(values[prefixedKey('List')], isNull); - }); - - testWidgets('writing', (WidgetTester _) async { - await Future.wait(>[ - preferences.setValue( - 'String', prefixedKey('String'), kTestValues2['flutter.String']!), - preferences.setValue( - 'Bool', prefixedKey('bool'), kTestValues2['flutter.bool']!), - preferences.setValue( - 'Int', prefixedKey('int'), kTestValues2['flutter.int']!), - preferences.setValue( - 'Double', prefixedKey('double'), kTestValues2['flutter.double']!), - preferences.setValue( - 'StringList', prefixedKey('List'), kTestValues2['flutter.List']!) - ]); - final Map values = await preferences.getAll(); - expect(values[prefixedKey('String')], kTestValues2['flutter.String']); - expect(values[prefixedKey('bool')], kTestValues2['flutter.bool']); - expect(values[prefixedKey('int')], kTestValues2['flutter.int']); - expect(values[prefixedKey('double')], kTestValues2['flutter.double']); - expect(values[prefixedKey('List')], kTestValues2['flutter.List']); - }); - - testWidgets('removing', (WidgetTester _) async { - final String key = prefixedKey('testKey'); - await preferences.setValue('String', key, kTestValues['flutter.String']!); - await preferences.setValue('Bool', key, kTestValues['flutter.bool']!); - await preferences.setValue('Int', key, kTestValues['flutter.int']!); - await preferences.setValue('Double', key, kTestValues['flutter.double']!); - await preferences.setValue( - 'StringList', key, kTestValues['flutter.List']!); - await preferences.remove(key); - final Map values = await preferences.getAll(); - expect(values[key], isNull); - }); - - testWidgets('clearing', (WidgetTester _) async { - await preferences.setValue( - 'String', 'String', kTestValues['flutter.String']!); - await preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']!); - await preferences.setValue('Int', 'int', kTestValues['flutter.int']!); - await preferences.setValue( - 'Double', 'double', kTestValues['flutter.double']!); - await preferences.setValue( - 'StringList', 'List', kTestValues['flutter.List']!); - await preferences.clear(); - final Map values = await preferences.getAll(); - expect(values['String'], null); - expect(values['bool'], null); - expect(values['int'], null); - expect(values['double'], null); - expect(values['List'], null); - }); - }); -} diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/AppDelegate.m b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/AppDelegate.m deleted file mode 100644 index b790a0a52635..000000000000 --- a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,16 +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. - -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03016f6c994b70f38d1b7346e5831b531f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 564 zcmV-40?Yl0P)Px$?ny*JR5%f>l)FnDQ543{x%ZCiu33$Wg!pQFfT_}?5Q|_VSlIbLC`dpoMXL}9 zHfd9&47Mo(7D231gb+kjFxZHS4-m~7WurTH&doVX2KI5sU4v(sJ1@T9eCIKPjsqSr z)C01LsCxk=72-vXmX}CQD#BD;Cthymh&~=f$Q8nn0J<}ZrusBy4PvRNE}+1ceuj8u z0mW5k8fmgeLnTbWHGwfKA3@PdZxhn|PypR&^p?weGftrtCbjF#+zk_5BJh7;0`#Wr zgDpM_;Ax{jO##IrT`Oz;MvfwGfV$zD#c2xckpcXC6oou4ML~ezCc2EtnsQTB4tWNg z?4bkf;hG7IMfhgNI(FV5Gs4|*GyMTIY0$B=_*mso9Ityq$m^S>15>-?0(zQ<8Qy<_TjHE33(?_M8oaM zyc;NxzRVK@DL6RJnX%U^xW0Gpg(lXp(!uK1v0YgHjs^ZXSQ|m#lV7ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca859a3f474b03065bef75ba58a9e4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb86cdfe0d15b4b0d98334a86163658..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1716 zcmds$`#;kQ7{|XelZftyR5~xW7?MLxS4^|Hw3&P7^y)@A9Fj{Xm1~_CIV^XZ%SLBn zA;!r`GqGHg=7>xrB{?psZQs88ZaedDoagm^KF{a*>G|dJWRSe^I$DNW008I^+;Kjt z>9p3GNR^I;v>5_`+91i(*G;u5|L+Bu6M=(afLjtkya#yZ175|z$pU~>2#^Z_pCZ7o z1c6UNcv2B3?; zX%qdxCXQpdKRz=#b*q0P%b&o)5ZrNZt7$fiETSK_VaY=mb4GK`#~0K#~9^ zcY!`#Af+4h?UMR-gMKOmpuYeN5P*RKF!(tb`)oe0j2BH1l?=>y#S5pMqkx6i{*=V9JF%>N8`ewGhRE(|WohnD59R^$_36{4>S zDFlPC5|k?;SPsDo87!B{6*7eqmMdU|QZ84>6)Kd9wNfh90=y=TFQay-0__>=<4pk& zYDjgIhL-jQ9o>z32K)BgAH+HxamL{ZL~ozu)Qqe@a`FpH=oQRA8=L-m-1dam(Ix2V z?du;LdMO+ooBelr^_y4{|44tmgH^2hSzPFd;U^!1p>6d|o)(-01z{i&Kj@)z-yfWQ)V#3Uo!_U}q3u`(fOs`_f^ueFii1xBNUB z6MecwJN$CqV&vhc+)b(p4NzGGEgwWNs z@*lUV6LaduZH)4_g!cE<2G6#+hJrWd5(|p1Z;YJ7ifVHv+n49btR}dq?HHDjl{m$T z!jLZcGkb&XS2OG~u%&R$(X+Z`CWec%QKt>NGYvd5g20)PU(dOn^7%@6kQb}C(%=vr z{?RP(z~C9DPnL{q^@pVw@|Vx~@3v!9dCaBtbh2EdtoNHm4kGxp>i#ct)7p|$QJs+U z-a3qtcPvhihub?wnJqEt>zC@)2suY?%-96cYCm$Q8R%-8$PZYsx3~QOLMDf(piXMm zB=<63yQk1AdOz#-qsEDX>>c)EES%$owHKue;?B3)8aRd}m~_)>SL3h2(9X;|+2#7X z+#2)NpD%qJvCQ0a-uzZLmz*ms+l*N}w)3LRQ*6>|Ub-fyptY(keUxw+)jfwF5K{L9 z|Cl_w=`!l_o><384d&?)$6Nh(GAm=4p_;{qVn#hI8lqewW7~wUlyBM-4Z|)cZr?Rh z=xZ&Ol>4(CU85ea(CZ^aO@2N18K>ftl8>2MqetAR53_JA>Fal`^)1Y--Am~UDa4th zKfCYpcXky$XSFDWBMIl(q=Mxj$iMBX=|j9P)^fDmF(5(5$|?Cx}DKEJa&XZP%OyE`*GvvYQ4PV&!g2|L^Q z?YG}tx;sY@GzMmsY`7r$P+F_YLz)(e}% zyakqFB<6|x9R#TdoP{R$>o7y(-`$$p0NxJ6?2B8tH)4^yF(WhqGZlM3=9Ibs$%U1w zWzcss*_c0=v_+^bfb`kBFsI`d;ElwiU%frgRB%qBjn@!0U2zZehBn|{%uNIKBA7n= zzE`nnwTP85{g;8AkYxA68>#muXa!G>xH22D1I*SiD~7C?7Za+9y7j1SHiuSkKK*^O zsZ==KO(Ua#?YUpXl{ViynyT#Hzk=}5X$e04O@fsMQjb}EMuPWFO0e&8(2N(29$@Vd zn1h8Yd>6z(*p^E{c(L0Lg=wVdupg!z@WG;E0k|4a%s7Up5C0c)55XVK*|x9RQeZ1J@1v9MX;>n34(i>=YE@Iur`0Vah(inE3VUFZNqf~tSz{1fz3Fsn_x4F>o(Yo;kpqvBe-sbwH(*Y zu$JOl0b83zu$JMvy<#oH^Wl>aWL*?aDwnS0iEAwC?DK@aT)GHRLhnz2WCvf3Ba;o=aY7 z2{Asu5MEjGOY4O#Ggz@@J;q*0`kd2n8I3BeNuMmYZf{}pg=jTdTCrIIYuW~luKecn z+E-pHY%ohj@uS0%^ z&(OxwPFPD$+#~`H?fMvi9geVLci(`K?Kj|w{rZ9JgthFHV+=6vMbK~0)Ea<&WY-NC zy-PnZft_k2tfeQ*SuC=nUj4H%SQ&Y$gbH4#2sT0cU0SdFs=*W*4hKGpuR1{)mV;Qf5pw4? zfiQgy0w3fC*w&Bj#{&=7033qFR*<*61B4f9K%CQvxEn&bsWJ{&winp;FP!KBj=(P6 z4Z_n4L7cS;ao2)ax?Tm|I1pH|uLpDSRVghkA_UtFFuZ0b2#>!8;>-_0ELjQSD-DRd z4im;599VHDZYtnWZGAB25W-e(2VrzEh|etsv2YoP#VbIZ{aFkwPrzJ#JvCvA*mXS& z`}Q^v9(W4GiSs}#s7BaN!WA2bniM$0J(#;MR>uIJ^uvgD3GS^%*ikdW6-!VFUU?JV zZc2)4cMsX@j z5HQ^e3BUzOdm}yC-xA%SY``k$rbfk z;CHqifhU*jfGM@DkYCecD9vl*qr58l6x<8URB=&%{!Cu3RO*MrKZ4VO}V6R0a zZw3Eg^0iKWM1dcTYZ0>N899=r6?+adUiBKPciJw}L$=1f4cs^bio&cr9baLF>6#BM z(F}EXe-`F=f_@`A7+Q&|QaZ??Txp_dB#lg!NH=t3$G8&06MFhwR=Iu*Im0s_b2B@| znW>X}sy~m#EW)&6E&!*0%}8UAS)wjt+A(io#wGI@Z2S+Ms1Cxl%YVE800007ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee1c98386d13b17e89f719e83555b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a98e212cca15ea7bf2ab5de5108680..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3831 zcmVjJBgitF5mAp-i>4+KS_oR{|13AP->1TD4=w)g|)JHOx|a2Wk1Va z!k)vP$UcQ#mdj%wNQoaJ!w>jv_6&JPyutpQps?s5dmDQ>`%?Bvj>o<%kYG!YW6H-z zu`g$@mp`;qDR!51QaS}|ZToSuAGcJ7$2HF0z`ln4t!#Yg46>;vGG9N9{V@9z#}6v* zfP?}r6b{*-C*)(S>NECI_E~{QYzN5SXRmVnP<=gzP+_Sp(Aza_hKlZ{C1D&l*(7IKXxQC1Z9#6wx}YrGcn~g%;icdw>T0Rf^w0{ z$_wn1J+C0@!jCV<%Go5LA45e{5gY9PvZp8uM$=1}XDI+9m7!A95L>q>>oe0$nC->i zeexUIvq%Uk<-$>DiDb?!In)lAmtuMWxvWlk`2>4lNuhSsjAf2*2tjT`y;@d}($o)S zn(+W&hJ1p0xy@oxP%AM15->wPLp{H!k)BdBD$toBpJh+crWdsNV)qsHaqLg2_s|Ih z`8E9z{E3sA!}5aKu?T!#enD(wLw?IT?k-yWVHZ8Akz4k5(TZJN^zZgm&zM28sfTD2BYJ|Fde3Xzh;;S` z=GXTnY4Xc)8nYoz6&vF;P7{xRF-{|2Xs5>a5)@BrnQ}I(_x7Cgpx#5&Td^4Q9_FnQ zX5so*;#8-J8#c$OlA&JyPp$LKUhC~-e~Ij!L%uSMu!-VZG7Hx-L{m2DVR2i=GR(_% zCVD!4N`I)&Q5S`?P&fQZ=4#Dgt_v2-DzkT}K(9gF0L(owe-Id$Rc2qZVLqI_M_DyO z9@LC#U28_LU{;wGZ&))}0R2P4MhajKCd^K#D+JJ&JIXZ_p#@+7J9A&P<0kdRujtQ_ zOy>3=C$kgi6$0pW06KaLz!21oOryKM3ZUOWqppndxfH}QpgjEJ`j7Tzn5bk6K&@RA?vl##y z$?V~1E(!wB5rH`>3nc&@)|#<1dN2cMzzm=PGhQ|Yppne(C-Vlt450IXc`J4R0W@I7 zd1e5uW6juvO%ni(WX7BsKx3MLngO7rHO;^R5I~0^nE^9^E_eYLgiR9&KnJ)pBbfno zSVnW$0R+&6jOOsZ82}nJ126+c|%svPo;TeUku<2G7%?$oft zyaO;tVo}(W)VsTUhq^XmFi#2z%-W9a{7mXn{uzivYQ_d6b7VJG{77naW(vHt-uhnY zVN#d!JTqVh(7r-lhtXVU6o})aZbDt_;&wJVGl2FKYFBFpU-#9U)z#(A%=IVnqytR$SY-sO( z($oNE09{D^@OuYPz&w~?9>Fl5`g9u&ecFGhqX=^#fmR=we0CJw+5xna*@oHnkahk+ z9aWeE3v|An+O5%?4fA&$Fgu~H_YmqR!yIU!bFCk4!#pAj%(lI(A5n)n@Id#M)O9Yx zJU9oKy{sRAIV3=5>(s8n{8ryJ!;ho}%pn6hZKTKbqk=&m=f*UnK$zW3YQP*)pw$O* zIfLA^!-bmBl6%d_n$#tP8Zd_(XdA*z*WH|E_yILwjtI~;jK#v-6jMl^?<%Y%`gvpwv&cFb$||^v4D&V=aNy?NGo620jL3VZnA%s zH~I|qPzB~e(;p;b^gJr7Ure#7?8%F0m4vzzPy^^(q4q1OdthF}Fi*RmVZN1OwTsAP zn9CZP`FazX3^kG(KodIZ=Kty8DLTy--UKfa1$6XugS zk%6v$Kmxt6U!YMx0JQ)0qX*{CXwZZk$vEROidEc7=J-1;peNat!vS<3P-FT5po>iE z!l3R+<`#x|+_hw!HjQGV=8!q|76y8L7N8gP3$%0kfush|u0uU^?dKBaeRSBUpOZ0c z62;D&Mdn2}N}xHRFTRI?zRv=>=AjHgH}`2k4WK=#AHB)UFrR-J87GgX*x5fL^W2#d z=(%K8-oZfMO=i{aWRDg=FX}UubM4eotRDcn;OR#{3q=*?3mE3_oJ-~prjhxh%PgQT zyn)Qozaq0@o&|LEgS{Ind4Swsr;b`u185hZPOBLL<`d2%^Yp1?oL)=jnLi;Zo0ZDliTtQ^b5SmfIMe{T==zZkbvn$KTQGlbG8w}s@M3TZnde;1Am46P3juKb zl9GU&3F=q`>j!`?SyH#r@O59%@aMX^rx}Nxe<>NqpUp5=lX1ojGDIR*-D^SDuvCKF z?3$xG(gVUsBERef_YjPFl^rU9EtD{pt z0CXwpN7BN3!8>hajGaTVk-wl=9rxmfWtIhC{mheHgStLi^+Nz12a?4r(fz)?3A%at zMlvQmL<2-R)-@G1wJ0^zQK%mR=r4d{Y3fHp){nWXUL#|CqXl(+v+qDh>FkF9`eWrW zfr^D%LNfOcTNvtx0JXR35J0~Jpi2#P3Q&80w+nqNfc}&G0A~*)lGHKv=^FE+b(37|)zL;KLF>oiGfb(?&1 zV3XRu!Sw>@quKiab%g6jun#oZ%!>V#A%+lNc?q>6+VvyAn=kf_6z^(TZUa4Eelh{{ zqFX-#dY(EV@7l$NE&kv9u9BR8&Ojd#ZGJ6l8_BW}^r?DIS_rU2(XaGOK z225E@kH5Opf+CgD^{y29jD4gHbGf{1MD6ggQ&%>UG4WyPh5q_tb`{@_34B?xfSO*| zZv8!)q;^o-bz`MuxXk*G^}(6)ACb@=Lfs`Hxoh>`Y0NE8QRQ!*p|SH@{r8=%RKd4p z+#Ty^-0kb=-H-O`nAA3_6>2z(D=~Tbs(n8LHxD0`R0_ATFqp-SdY3(bZ3;VUM?J=O zKCNsxsgt@|&nKMC=*+ZqmLHhX1KHbAJs{nGVMs6~TiF%Q)P@>!koa$%oS zjXa=!5>P`vC-a}ln!uH1ooeI&v?=?v7?1n~P(wZ~0>xWxd_Aw;+}9#eULM7M8&E?Y zC-ZLhi3RoM92SXUb-5i-Lmt5_rfjE{6y^+24`y$1lywLyHO!)Boa7438K4#iLe?rh z2O~YGSgFUBH?og*6=r9rme=peP~ah`(8Zt7V)j5!V0KPFf_mebo3z95U8(up$-+EA^9dTRLq>Yl)YMBuch9%=e5B`Vnb>o zt03=kq;k2TgGe4|lGne&zJa~h(UGutjP_zr?a7~#b)@15XNA>Dj(m=gg2Q5V4-$)D|Q9}R#002ovPDHLkV1o7DH3k3x diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/shared_preferences/shared_preferences_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7941ef3f6dcb7f06a192d8dcb308d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1888 zcmV-m2cP(fP)x~L`~4d)Rspd&<9kFh{hn*KP1LP0~$;u(LfAu zp%fx&qLBcRHx$G|3q(bv@+b;o0*D|jwD-Q9uQR(l*ST}s+uPgQ-MeFwZ#GS?b332? z&Tk$&_miXn3IGq)AmQ)3sisq{raD4(k*bHvpCe-TdWq^NRTEVM)i9xbgQ&ccnUVx* zEY%vS%gDcSg=!tuIK8$Th2_((_h^+7;R|G{n06&O2#6%LK`a}n?h_fL18btz<@lFG za}xS}u?#DBMB> zw^b($1Z)`9G?eP95EKi&$eOy@K%h;ryrR3la%;>|o*>CgB(s>dDcNOXg}CK9SPmD? zmr-s{0wRmxUnbDrYfRvnZ@d z6johZ2sMX{YkGSKWd}m|@V7`Degt-43=2M?+jR%8{(H$&MLLmS;-|JxnX2pnz;el1jsvqQz}pGSF<`mqEXRQ5sC4#BbwnB_4` zc5bFE-Gb#JV3tox9fp-vVEN{(tOCpRse`S+@)?%pz+zVJXSooTrNCUg`R6`hxwb{) zC@{O6MKY8tfZ5@!yy=p5Y|#+myRL=^{tc(6YgAnkg3I(Cd!r5l;|;l-MQ8B`;*SCE z{u)uP^C$lOPM z5d~UhKhRRmvv{LIa^|oavk1$QiEApSrP@~Jjbg`<*dW4TO?4qG%a%sTPUFz(QtW5( zM)lA+5)0TvH~aBaOAs|}?u2FO;yc-CZ1gNM1dAxJ?%m?YsGR`}-xk2*dxC}r5j$d* zE!#Vtbo69h>V4V`BL%_&$} z+oJAo@jQ^Tk`;%xw-4G>hhb&)B?##U+(6Fi7nno`C<|#PVA%$Y{}N-?(Gc$1%tr4Pc}}hm~yY#fTOe!@v9s-ik$dX~|ygArPhByaXn8 zpI^FUjNWMsTFKTP3X7m?UK)3m zp6rI^_zxRYrx6_QmhoWoDR`fp4R7gu6;gdO)!KexaoO2D88F9x#TM1(9Bn7g;|?|o z)~$n&Lh#hCP6_LOPD>a)NmhW})LADx2kq=X7}7wYRj-0?dXr&bHaRWCfSqvzFa=sn z-8^gSyn-RmH=BZ{AJZ~!8n5621GbUJV7Qvs%JNv&$%Q17s_X%s-41vAPfIR>;x0Wlqr5?09S>x#%Qkt>?(&XjFRY}*L6BeQ3 z<6XEBh^S7>AbwGm@XP{RkeEKj6@_o%oV?hDuUpUJ+r#JZO?!IUc;r0R?>mi)*ZpQ) z#((dn=A#i_&EQn|hd)N$#A*fjBFuiHcYvo?@y1 z5|fV=a^a~d!c-%ZbMNqkMKiSzM{Yq=7_c&1H!mXk60Uv32dV;vMg&-kQ)Q{+PFtwc zj|-uQ;b^gts??J*9VxxOro}W~Q9j4Em|zSRv)(WSO9$F$s=Ydu%Q+5DOid~lwk&we zY%W(Z@ofdwPHncEZzZgmqS|!gTj3wQq9rxQy+^eNYKr1mj&?tm@wkO*9@UtnRMG>c aR{jt9+;fr}hV%pg00001^@s67{VYS000c7NklQEG_j zup^)eW&WUIApqy$=APz8jE@awGp)!bsTjDbrJO`$x^ZR^dr;>)LW>{ zs70vpsD38v)19rI=GNk1b(0?Js9~rjsQsu*K;@SD40RB-3^gKU-MYC7G!Bw{fZsqp zih4iIi;Hr_xZ033Iu{sQxLS=}yBXgLMn40d++>aQ0#%8D1EbGZp7+ z5=mK?t31BkVYbGOxE9`i748x`YgCMwL$qMsChbSGSE1`p{nSmadR zcQ#R)(?!~dmtD0+D2!K zR9%!Xp1oOJzm(vbLvT^$IKp@+W2=-}qTzTgVtQ!#Y7Gxz}stUIm<1;oBQ^Sh2X{F4ibaOOx;5ZGSNK z0maF^@(UtV$=p6DXLgRURwF95C=|U8?osGhgOED*b z7woJ_PWXBD>V-NjQAm{~T%sjyJ{5tn2f{G%?J!KRSrrGvQ1(^`YLA5B!~eycY(e5_ z*%aa{at13SxC(=7JT7$IQF~R3sy`Nn%EMv!$-8ZEAryB*yB1k&stni)=)8-ODo41g zkJu~roIgAih94tb=YsL%iH5@^b~kU9M-=aqgXIrbtxMpFy5mekFm#edF9z7RQ6V}R zBIhbXs~pMzt0VWy1Fi$^fh+1xxLDoK09&5&MJl(q#THjPm(0=z2H2Yfm^a&E)V+a5 zbi>08u;bJsDRUKR9(INSc7XyuWv(JsD+BB*0hS)FO&l&7MdViuur@-<-EHw>kHRGY zqoT}3fDv2-m{NhBG8X}+rgOEZ;amh*DqN?jEfQdqxdj08`Sr=C-KmT)qU1 z+9Cl)a1mgXxhQiHVB}l`m;-RpmKy?0*|yl?FXvJkFxuu!fKlcmz$kN(a}i*saM3nr z0!;a~_%Xqy24IxA2rz<+08=B-Q|2PT)O4;EaxP^6qixOv7-cRh?*T?zZU`{nIM-at zTKYWr9rJ=tppQ9I#Z#mLgINVB!pO-^FOcvFw6NhV0gztuO?g ztoA*C-52Q-Z-P#xB4HAY3KQVd%dz1S4PA3vHp0aa=zAO?FCt zC_GaTyVBg2F!bBr3U@Zy2iJgIAt>1sf$JWA9kh{;L+P*HfUBX1Zy{4MgNbDfBV_ly z!y#+753arsZUt@366jIC0klaC@ckuk!qu=pAyf7&QmiBUT^L1&tOHzsK)4n|pmrVT zs2($4=?s~VejTFHbFdDOwG;_58LkIj1Fh@{glkO#F1>a==ymJS$z;gdedT1zPx4Kj ztjS`y_C}%af-RtpehdQDt3a<=W5C4$)9W@QAse;WUry$WYmr51ml9lkeunUrE`-3e zmq1SgSOPNEE-Mf+AGJ$g0M;3@w!$Ej;hMh=v=I+Lpz^n%Pg^MgwyqOkNyu2c^of)C z1~ALor3}}+RiF*K4+4{(1%1j3pif1>sv0r^mTZ?5Jd-It!tfPfiG_p$AY*Vfak%FG z4z#;wLtw&E&?}w+eKG^=#jF7HQzr8rV0mY<1YAJ_uGz~$E13p?F^fPSzXSn$8UcI$ z8er9{5w5iv0qf8%70zV71T1IBB1N}R5Kp%NO0=5wJalZt8;xYp;b{1K) zHY>2wW-`Sl{=NpR%iu3(u6l&)rc%%cSA#aV7WCowfbFR4wcc{LQZv~o1u_`}EJA3>ki`?9CKYTA!rhO)if*zRdd}Kn zEPfYbhoVE~!FI_2YbC5qAj1kq;xP6%J8+?2PAs?`V3}nyFVD#sV3+uP`pi}{$l9U^ zSz}_M9f7RgnnRhaoIJgT8us!1aB&4!*vYF07Hp&}L zCRlop0oK4DL@ISz{2_BPlezc;xj2|I z23RlDNpi9LgTG_#(w%cMaS)%N`e>~1&a3<{Xy}>?WbF>OOLuO+j&hc^YohQ$4F&ze z+hwnro1puQjnKm;vFG~o>`kCeUIlkA-2tI?WBKCFLMBY=J{hpSsQ=PDtU$=duS_hq zHpymHt^uuV1q@uc4bFb{MdG*|VoW@15Osrqt2@8ll0qO=j*uOXn{M0UJX#SUztui9FN4)K3{9!y8PC-AHHvpVTU;x|-7P+taAtyglk#rjlH2 z5Gq8ik}BPaGiM{#Woyg;*&N9R2{J0V+WGB69cEtH7F?U~Kbi6ksi*`CFXsi931q7Y zGO82?whBhN%w1iDetv%~wM*Y;E^)@Vl?VDj-f*RX>{;o_=$fU!&KAXbuadYZ46Zbg z&6jMF=49$uL^73y;;N5jaHYv)BTyfh&`qVLYn?`o6BCA_z-0niZz=qPG!vonK3MW_ zo$V96zM!+kJRs{P-5-rQVse0VBH*n6A58)4uc&gfHMa{gIhV2fGf{st>E8sKyP-$8zp~wJX^A*@DI&-;8>gANXZj zU)R+Y)PB?=)a|Kj>8NXEu^S_h^7R`~Q&7*Kn!xyvzVv&^>?^iu;S~R2e-2fJx-oUb cX)(b1KSk$MOV07*qoM6N<$f&6$jw%VRuvdN2+38CZWny1cRtlsl+0_KtW)EU14Ei(F!UtWuj4IK+3{sK@>rh zs1Z;=(DD&U6+tlyL?UnHVN^&g6QhFi2#HS+*qz;(>63G(`|jRtW|nz$Pv7qTovP!^ zP_jES{mr@O-02w%!^a?^1ZP!_KmQiz0L~jZ=W@Qt`8wzOoclQsAS<5YdH;a(4bGLE zk8s}1If(PSIgVi!XE!5kA?~z*sobvNyohr;=Q_@h2@$6Flyej3J)D-6YfheRGl`HEcPk|~huT_2-U?PfL=4BPV)f1o!%rQ!NMt_MYw-5bUSwQ9Z&zC>u zOrl~UJglJNa%f50Ok}?WB{on`Ci`p^Y!xBA?m@rcJXLxtrE0FhRF3d*ir>yzO|BD$ z3V}HpFcCh6bTzY}Nt_(W%QYd3NG)jJ4<`F<1Od) zfQblTdC&h2lCz`>y?>|9o2CdvC8qZeIZt%jN;B7Hdn2l*k4M4MFEtq`q_#5?}c$b$pf_3y{Y!cRDafZBEj-*OD|gz#PBDeu3QoueOesLzB+O zxjf2wvf6Wwz>@AiOo2mO4=TkAV+g~%_n&R;)l#!cBxjuoD$aS-`IIJv7cdX%2{WT7 zOm%5rs(wqyPE^k5SIpUZ!&Lq4<~%{*>_Hu$2|~Xa;iX*tz8~G6O3uFOS?+)tWtdi| zV2b#;zRN!m@H&jd=!$7YY6_}|=!IU@=SjvGDFtL;aCtw06U;-v^0%k0FOyESt z1Wv$={b_H&8FiRV?MrzoHWd>%v6KTRU;-v^Miiz+@q`(BoT!+<37CKhoKb)|8!+RG z6BQFU^@fRW;s8!mOf2QViKQGk0TVER6EG1`#;Nm39Do^PoT!+<37AD!%oJe86(=et zZ~|sLzU>V-qYiU6V8$0GmU7_K8|Fd0B?+9Un1BhKAz#V~Fk^`mJtlCX#{^8^M8!me z8Yg;8-~>!e<-iG;h*0B1kBKm}hItVGY6WnjVpgnTTAC$rqQ^v)4KvOtpY|sIj@WYg zyw##ZZ5AC2IKNC;^hwg9BPk0wLStlmBr;E|$5GoAo$&Ui_;S9WY62n3)i49|T%C#i017z3J=$RF|KyZWnci*@lW4 z=AKhNN6+m`Q!V3Ye68|8y@%=am>YD0nG99M)NWc20%)gwO!96j7muR}Fr&54SxKP2 zP30S~lt=a*qDlbu3+Av57=9v&vr<6g0&`!8E2fq>I|EJGKs}t|{h7+KT@)LfIV-3K zK)r_fr2?}FFyn*MYoLC>oV-J~eavL2ho4a4^r{E-8m2hi>~hA?_vIG4a*KT;2eyl1 zh_hUvUJpNCFwBvRq5BI*srSle>c6%n`#VNsyC|MGa{(P&08p=C9+WUw9Hl<1o9T4M zdD=_C0F7#o8A_bRR?sFNmU0R6tW`ElnF8p53IdHo#S9(JoZCz}fHwJ6F<&?qrpVqE zte|m%89JQD+XwaPU#%#lVs-@-OL);|MdfINd6!XwP2h(eyafTUsoRkA%&@fe?9m@jw-v(yTTiV2(*fthQH9}SqmsRPVnwwbV$1E(_lkmo&S zF-truCU914_$jpqjr(>Ha4HkM4YMT>m~NosUu&UZ>zirfHo%N6PPs9^_o$WqPA0#5 z%tG>qFCL+b*0s?sZ;Sht0nE7Kl>OVXy=gjWxxK;OJ3yGd7-pZf7JYNcZo2*1SF`u6 zHJyRRxGw9mDlOiXqVMsNe#WX`fC`vrtjSQ%KmLcl(lC>ZOQzG^%iql2w-f_K@r?OE zwCICifM#L-HJyc7Gm>Ern?+Sk3&|Khmu4(~3qa$(m6Ub^U0E5RHq49za|XklN#?kP zl;EstdW?(_4D>kwjWy2f!LM)y?F94kyU3`W!6+AyId-89v}sXJpuic^NLL7GJItl~ zsiuB98AI-(#Mnm|=A-R6&2fwJ0JVSY#Q>&3$zFh|@;#%0qeF=j5Ajq@4i0tIIW z&}sk$&fGwoJpe&u-JeGLi^r?dO`m=y(QO{@h zQqAC7$rvz&5+mo3IqE?h=a~6m>%r5Quapvzq;{y~p zJpyXOBgD9VrW7@#p6l7O?o3feml(DtSL>D^R) zZUY%T2b0-vBAFN7VB;M88!~HuOXi4KcI6aRQ&h|XQ0A?m%j2=l1f0cGP}h(oVfJ`N zz#PpmFC*ieab)zJK<4?^k=g%OjPnkANzbAbmGZHoVRk*mTfm75s_cWVa`l*f$B@xu z5E*?&@seIo#*Y~1rBm!7sF9~~u6Wrj5oICUOuz}CS)jdNIznfzCA(stJ(7$c^e5wN z?lt>eYgbA!kvAR7zYSD&*r1$b|(@;9dcZ^67R0 zXAXJKa|5Sdmj!g578Nwt6d$sXuc&MWezA0Whd`94$h{{?1IwXP4)Tx4obDK%xoFZ_Z zjjHJ_P@R_e5blG@yEjnaJb`l;s%Lb2&=8$&Ct-fV`E^4CUs)=jTk!I}2d&n!f@)bm z@ z_4Dc86+3l2*p|~;o-Sb~oXb_RuLmoifDU^&Te$*FevycC0*nE3Xws8gsWp|Rj2>SM zns)qcYj?^2sd8?N!_w~4v+f-HCF|a$TNZDoNl$I1Uq87euoNgKb6&r26TNrfkUa@o zfdiFA@p{K&mH3b8i!lcoz)V{n8Q@g(vR4ns4r6w;K z>1~ecQR0-<^J|Ndg5fvVUM9g;lbu-){#ghGw(fg>L zh)T5Ljb%lWE;V9L!;Cqk>AV1(rULYF07ZBJbGb9qbSoLAd;in9{)95YqX$J43-dY7YU*k~vrM25 zxh5_IqO0LYZW%oxQ5HOzmk4x{atE*vipUk}sh88$b2tn?!ujEHn`tQLe&vo}nMb&{ zio`xzZ&GG6&ZyN3jnaQy#iVqXE9VT(3tWY$n-)uWDQ|tc{`?fq2F`oQ{;d3aWPg4Hp-(iE{ry>MIPWL> iW8 -#import -#import "AppDelegate.h" - -int main(int argc, char *argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/shared_preferences/shared_preferences_ios/example/ios/RunnerTests/SharedPreferencesTests.m b/packages/shared_preferences/shared_preferences_ios/example/ios/RunnerTests/SharedPreferencesTests.m deleted file mode 100644 index 792f6c111b82..000000000000 --- a/packages/shared_preferences/shared_preferences_ios/example/ios/RunnerTests/SharedPreferencesTests.m +++ /dev/null @@ -1,18 +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 shared_preferences_ios; -@import XCTest; - -@interface SharedPreferencesTests : XCTestCase -@end - -@implementation SharedPreferencesTests - -- (void)testPlugin { - FLTSharedPreferencesPlugin *plugin = [[FLTSharedPreferencesPlugin alloc] init]; - XCTAssertNotNil(plugin); -} - -@end diff --git a/packages/shared_preferences/shared_preferences_ios/example/lib/main.dart b/packages/shared_preferences/shared_preferences_ios/example/lib/main.dart deleted file mode 100644 index bb513b09f6d5..000000000000 --- a/packages/shared_preferences/shared_preferences_ios/example/lib/main.dart +++ /dev/null @@ -1,93 +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. - -// ignore_for_file: public_member_api_docs - -import 'package:flutter/material.dart'; -import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; - -void main() { - runApp(const MyApp()); -} - -class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return const MaterialApp( - title: 'SharedPreferences Demo', - home: SharedPreferencesDemo(), - ); - } -} - -class SharedPreferencesDemo extends StatefulWidget { - const SharedPreferencesDemo({Key? key}) : super(key: key); - - @override - SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); -} - -class SharedPreferencesDemoState extends State { - final SharedPreferencesStorePlatform _prefs = - SharedPreferencesStorePlatform.instance; - late Future _counter; - - // Includes the prefix because this is using the platform interface directly, - // but the prefix (which the native code assumes is present) is added by the - // app-facing package. - static const String _prefKey = 'flutter.counter'; - - Future _incrementCounter() async { - final Map values = await _prefs.getAll(); - final int counter = ((values[_prefKey] as int?) ?? 0) + 1; - - setState(() { - _counter = _prefs.setValue('Int', _prefKey, counter).then((bool success) { - return counter; - }); - }); - } - - @override - void initState() { - super.initState(); - _counter = _prefs.getAll().then((Map values) { - return (values[_prefKey] as int?) ?? 0; - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('SharedPreferences Demo'), - ), - body: Center( - child: FutureBuilder( - future: _counter, - builder: (BuildContext context, AsyncSnapshot snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.waiting: - return const CircularProgressIndicator(); - default: - if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); - } else { - return Text( - 'Button tapped ${snapshot.data} time${snapshot.data == 1 ? '' : 's'}.\n\n' - 'This should persist across restarts.', - ); - } - } - })), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), - ); - } -} diff --git a/packages/shared_preferences/shared_preferences_ios/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_ios/example/pubspec.yaml deleted file mode 100644 index 446cea1e0508..000000000000 --- a/packages/shared_preferences/shared_preferences_ios/example/pubspec.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: shared_preferences_example -description: Demonstrates how to use the shared_preferences plugin. -publish_to: none - -environment: - sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" - -dependencies: - flutter: - sdk: flutter - shared_preferences_ios: - # When depending on this package from a real application you should use: - # shared_preferences_ios: ^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: ../ - shared_preferences_platform_interface: ^2.0.0 - -dev_dependencies: - flutter_driver: - sdk: flutter - flutter_test: - sdk: flutter - integration_test: - sdk: flutter - -flutter: - uses-material-design: true diff --git a/packages/shared_preferences/shared_preferences_ios/ios/Assets/.gitkeep b/packages/shared_preferences/shared_preferences_ios/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/shared_preferences/shared_preferences_ios/ios/Classes/FLTSharedPreferencesPlugin.m b/packages/shared_preferences/shared_preferences_ios/ios/Classes/FLTSharedPreferencesPlugin.m deleted file mode 100644 index bb11da2b5406..000000000000 --- a/packages/shared_preferences/shared_preferences_ios/ios/Classes/FLTSharedPreferencesPlugin.m +++ /dev/null @@ -1,68 +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 "FLTSharedPreferencesPlugin.h" -#import "messages.g.h" - -static NSMutableDictionary *getAllPrefs() { - NSString *appDomain = [[NSBundle mainBundle] bundleIdentifier]; - NSDictionary *prefs = [[NSUserDefaults standardUserDefaults] persistentDomainForName:appDomain]; - NSMutableDictionary *filteredPrefs = [NSMutableDictionary dictionary]; - if (prefs != nil) { - for (NSString *candidateKey in prefs) { - if ([candidateKey hasPrefix:@"flutter."]) { - [filteredPrefs setObject:prefs[candidateKey] forKey:candidateKey]; - } - } - } - return filteredPrefs; -} - -@interface FLTSharedPreferencesPlugin () -@end - -@implementation FLTSharedPreferencesPlugin - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FLTSharedPreferencesPlugin *plugin = [[FLTSharedPreferencesPlugin alloc] init]; - UserDefaultsApiSetup(registrar.messenger, plugin); -} - -// Must not return nil unless "error" is set. -- (nullable NSDictionary *)getAllWithError: - (FlutterError *_Nullable __autoreleasing *_Nonnull)error { - return getAllPrefs(); -} - -- (void)clearWithError:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - for (NSString *key in getAllPrefs()) { - [defaults removeObjectForKey:key]; - } -} - -- (void)removeKey:(nonnull NSString *)key - error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { - [[NSUserDefaults standardUserDefaults] removeObjectForKey:key]; -} - -- (void)setBoolKey:(nonnull NSString *)key - value:(nonnull NSNumber *)value - error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { - [[NSUserDefaults standardUserDefaults] setBool:value.boolValue forKey:key]; -} - -- (void)setDoubleKey:(nonnull NSString *)key - value:(nonnull NSNumber *)value - error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { - [[NSUserDefaults standardUserDefaults] setDouble:value.doubleValue forKey:key]; -} - -- (void)setValueKey:(nonnull NSString *)key - value:(nonnull NSString *)value - error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { - [[NSUserDefaults standardUserDefaults] setValue:value forKey:key]; -} - -@end diff --git a/packages/shared_preferences/shared_preferences_ios/ios/Classes/messages.g.h b/packages/shared_preferences/shared_preferences_ios/ios/Classes/messages.g.h deleted file mode 100644 index 592402344a04..000000000000 --- a/packages/shared_preferences/shared_preferences_ios/ios/Classes/messages.g.h +++ /dev/null @@ -1,33 +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. -// Autogenerated from Pigeon (v1.0.16), do not edit directly. -// See also: https://pub.dev/packages/pigeon -#import -@protocol FlutterBinaryMessenger; -@protocol FlutterMessageCodec; -@class FlutterError; -@class FlutterStandardTypedData; - -NS_ASSUME_NONNULL_BEGIN - -/// The codec used by UserDefaultsApi. -NSObject *UserDefaultsApiGetCodec(void); - -@protocol UserDefaultsApi -- (void)removeKey:(NSString *)key error:(FlutterError *_Nullable *_Nonnull)error; -- (void)setBoolKey:(NSString *)key - value:(NSNumber *)value - error:(FlutterError *_Nullable *_Nonnull)error; -- (void)setDoubleKey:(NSString *)key - value:(NSNumber *)value - error:(FlutterError *_Nullable *_Nonnull)error; -- (void)setValueKey:(NSString *)key value:(id)value error:(FlutterError *_Nullable *_Nonnull)error; -- (nullable NSDictionary *)getAllWithError:(FlutterError *_Nullable *_Nonnull)error; -- (void)clearWithError:(FlutterError *_Nullable *_Nonnull)error; -@end - -extern void UserDefaultsApiSetup(id binaryMessenger, - NSObject *_Nullable api); - -NS_ASSUME_NONNULL_END diff --git a/packages/shared_preferences/shared_preferences_ios/ios/Classes/messages.g.m b/packages/shared_preferences/shared_preferences_ios/ios/Classes/messages.g.m deleted file mode 100644 index ea8c45c7a66c..000000000000 --- a/packages/shared_preferences/shared_preferences_ios/ios/Classes/messages.g.m +++ /dev/null @@ -1,178 +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. -// Autogenerated from Pigeon (v1.0.16), do not edit directly. -// See also: https://pub.dev/packages/pigeon -#import "messages.g.h" -#import - -#if !__has_feature(objc_arc) -#error File requires ARC to be enabled. -#endif - -static NSDictionary *wrapResult(id result, FlutterError *error) { - NSDictionary *errorDict = (NSDictionary *)[NSNull null]; - if (error) { - errorDict = @{ - @"code" : (error.code ? error.code : [NSNull null]), - @"message" : (error.message ? error.message : [NSNull null]), - @"details" : (error.details ? error.details : [NSNull null]), - }; - } - return @{ - @"result" : (result ? result : [NSNull null]), - @"error" : errorDict, - }; -} - -@interface UserDefaultsApiCodecReader : FlutterStandardReader -@end -@implementation UserDefaultsApiCodecReader -@end - -@interface UserDefaultsApiCodecWriter : FlutterStandardWriter -@end -@implementation UserDefaultsApiCodecWriter -@end - -@interface UserDefaultsApiCodecReaderWriter : FlutterStandardReaderWriter -@end -@implementation UserDefaultsApiCodecReaderWriter -- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { - return [[UserDefaultsApiCodecWriter alloc] initWithData:data]; -} -- (FlutterStandardReader *)readerWithData:(NSData *)data { - return [[UserDefaultsApiCodecReader alloc] initWithData:data]; -} -@end - -NSObject *UserDefaultsApiGetCodec() { - static dispatch_once_t s_pred = 0; - static FlutterStandardMessageCodec *s_sharedObject = nil; - dispatch_once(&s_pred, ^{ - UserDefaultsApiCodecReaderWriter *readerWriter = - [[UserDefaultsApiCodecReaderWriter alloc] init]; - s_sharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; - }); - return s_sharedObject; -} - -void UserDefaultsApiSetup(id binaryMessenger, - NSObject *api) { - { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.UserDefaultsApi.remove" - binaryMessenger:binaryMessenger - codec:UserDefaultsApiGetCodec()]; - if (api) { - NSCAssert([api respondsToSelector:@selector(removeKey:error:)], - @"UserDefaultsApi api (%@) doesn't respond to @selector(removeKey:error:)", api); - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - NSArray *args = message; - NSString *arg_key = args[0]; - FlutterError *error; - [api removeKey:arg_key error:&error]; - callback(wrapResult(nil, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } - { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.UserDefaultsApi.setBool" - binaryMessenger:binaryMessenger - codec:UserDefaultsApiGetCodec()]; - if (api) { - NSCAssert([api respondsToSelector:@selector(setBoolKey:value:error:)], - @"UserDefaultsApi api (%@) doesn't respond to @selector(setBoolKey:value:error:)", - api); - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - NSArray *args = message; - NSString *arg_key = args[0]; - NSNumber *arg_value = args[1]; - FlutterError *error; - [api setBoolKey:arg_key value:arg_value error:&error]; - callback(wrapResult(nil, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } - { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.UserDefaultsApi.setDouble" - binaryMessenger:binaryMessenger - codec:UserDefaultsApiGetCodec()]; - if (api) { - NSCAssert([api respondsToSelector:@selector(setDoubleKey:value:error:)], - @"UserDefaultsApi api (%@) doesn't respond to @selector(setDoubleKey:value:error:)", - api); - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - NSArray *args = message; - NSString *arg_key = args[0]; - NSNumber *arg_value = args[1]; - FlutterError *error; - [api setDoubleKey:arg_key value:arg_value error:&error]; - callback(wrapResult(nil, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } - { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.UserDefaultsApi.setValue" - binaryMessenger:binaryMessenger - codec:UserDefaultsApiGetCodec()]; - if (api) { - NSCAssert([api respondsToSelector:@selector(setValueKey:value:error:)], - @"UserDefaultsApi api (%@) doesn't respond to @selector(setValueKey:value:error:)", - api); - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - NSArray *args = message; - NSString *arg_key = args[0]; - id arg_value = args[1]; - FlutterError *error; - [api setValueKey:arg_key value:arg_value error:&error]; - callback(wrapResult(nil, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } - { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.UserDefaultsApi.getAll" - binaryMessenger:binaryMessenger - codec:UserDefaultsApiGetCodec()]; - if (api) { - NSCAssert([api respondsToSelector:@selector(getAllWithError:)], - @"UserDefaultsApi api (%@) doesn't respond to @selector(getAllWithError:)", api); - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FlutterError *error; - NSDictionary *output = [api getAllWithError:&error]; - callback(wrapResult(output, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } - { - FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.UserDefaultsApi.clear" - binaryMessenger:binaryMessenger - codec:UserDefaultsApiGetCodec()]; - if (api) { - NSCAssert([api respondsToSelector:@selector(clearWithError:)], - @"UserDefaultsApi api (%@) doesn't respond to @selector(clearWithError:)", api); - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FlutterError *error; - [api clearWithError:&error]; - callback(wrapResult(nil, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } -} diff --git a/packages/shared_preferences/shared_preferences_ios/ios/shared_preferences_ios.podspec b/packages/shared_preferences/shared_preferences_ios/ios/shared_preferences_ios.podspec deleted file mode 100644 index 4126ed0a8cc2..000000000000 --- a/packages/shared_preferences/shared_preferences_ios/ios/shared_preferences_ios.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'shared_preferences_ios' - s.version = '0.0.1' - s.summary = 'Flutter Shared Preferences' - s.description = <<-DESC -Wraps NSUserDefaults, providing a persistent store for simple key-value pairs. - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_ios' } - s.documentation_url = 'https://pub.dev/packages/shared_preferences' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.platform = :ios, '9.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } -end - diff --git a/packages/shared_preferences/shared_preferences_ios/pubspec.yaml b/packages/shared_preferences/shared_preferences_ios/pubspec.yaml deleted file mode 100644 index 45b1aae2c473..000000000000 --- a/packages/shared_preferences/shared_preferences_ios/pubspec.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: shared_preferences_ios -description: iOS implementation of the shared_preferences plugin -repository: https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_ios -issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.1.1 - -environment: - sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" - -flutter: - plugin: - implements: shared_preferences - platforms: - ios: - dartPluginClass: SharedPreferencesIOS - pluginClass: FLTSharedPreferencesPlugin - -dependencies: - flutter: - sdk: flutter - shared_preferences_platform_interface: ^2.0.0 - -dev_dependencies: - flutter_test: - sdk: flutter - pigeon: ^1.0.16 diff --git a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md index 5d59660b4d6b..3c5a398546d1 100644 --- a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md @@ -1,5 +1,14 @@ ## NEXT +* Updates minimum Flutter version to 3.0. + +## 2.1.3 + +* Updates code for stricter lint checks. + +## 2.1.2 + +* Updates code for stricter lint checks. * Updates code for `no_leading_underscores_for_local_identifiers` lint. * Updates minimum Flutter version to 2.10. diff --git a/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart b/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart index d51be33baeed..a904c824d4fe 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart @@ -66,9 +66,11 @@ class SharedPreferencesDemoState extends State { future: _counter, builder: (BuildContext context, AsyncSnapshot snapshot) { switch (snapshot.connectionState) { + case ConnectionState.none: case ConnectionState.waiting: return const CircularProgressIndicator(); - default: + case ConnectionState.active: + case ConnectionState.done: if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } else { diff --git a/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml index 9418c0581ed7..98ff24a84682 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart b/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart index b6a9a5bca4ff..1cc1c41f871b 100644 --- a/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart +++ b/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart @@ -7,7 +7,7 @@ import 'dart:convert' show json; import 'package:file/file.dart'; import 'package:file/local.dart'; -import 'package:flutter/foundation.dart' show visibleForTesting; +import 'package:flutter/foundation.dart' show debugPrint, visibleForTesting; import 'package:path/path.dart' as path; import 'package:path_provider_linux/path_provider_linux.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; @@ -74,7 +74,7 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { try { final File? localDataFile = await _getLocalDataFile(); if (localDataFile == null) { - print('Unable to determine where to write preferences.'); + debugPrint('Unable to determine where to write preferences.'); return false; } if (!localDataFile.existsSync()) { @@ -83,7 +83,7 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { final String stringMap = json.encode(preferences); localDataFile.writeAsStringSync(stringMap); } catch (e) { - print('Error saving preferences to disk: $e'); + debugPrint('Error saving preferences to disk: $e'); return false; } return true; diff --git a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml index 88889dcde5fd..21203a877586 100644 --- a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml @@ -2,11 +2,11 @@ name: shared_preferences_linux description: Linux implementation of the shared_preferences plugin repository: https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_linux 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.3 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/shared_preferences/shared_preferences_macos/AUTHORS b/packages/shared_preferences/shared_preferences_macos/AUTHORS deleted file mode 100644 index 493a0b4ef9c2..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/AUTHORS +++ /dev/null @@ -1,66 +0,0 @@ -# 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. -The Chromium Authors -German Saprykin -Benjamin Sauer -larsenthomasj@gmail.com -Ali Bitek -Pol Batlló -Anatoly Pulyaevskiy -Hayden Flinner -Stefano Rodriguez -Salvatore Giordano -Brian Armstrong -Paul DeMarco -Fabricio Nogueira -Simon Lightfoot -Ashton Thomas -Thomas Danner -Diego Velásquez -Hajime Nakamura -Tuyển Vũ Xuân -Miguel Ruivo -Sarthak Verma -Mike Diarmid -Invertase -Elliot Hesp -Vince Varga -Aawaz Gyawali -EUI Limited -Katarina Sheremet -Thomas Stockx -Sarbagya Dhaubanjar -Ozkan Eksi -Rishab Nayak -ko2ic -Jonathan Younger -Jose Sanchez -Debkanchan Samadder -Audrius Karosevicius -Lukasz Piliszczuk -SoundReply Solutions GmbH -Rafal Wachol -Pau Picas -Christian Weder -Alexandru Tuca -Christian Weder -Rhodes Davis Jr. -Luigi Agosti -Quentin Le Guennec -Koushik Ravikumar -Nissim Dsilva -Giancarlo Rocha -Ryo Miyake -Théo Champion -Kazuki Yamaguchi -Eitan Schwartz -Chris Rutkowski -Juan Alvarez -Aleksandr Yurkovskiy -Anton Borries -Alex Li -Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md b/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md deleted file mode 100644 index fc8a78af95b9..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md +++ /dev/null @@ -1,81 +0,0 @@ -## NEXT - -* Updates code for `no_leading_underscores_for_local_identifiers` lint. -* Updates minimum Flutter version to 2.10. - -## 2.0.4 - -* Removes unnecessary imports. -* Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors - lint warnings. - -## 2.0.3 - -* Switches to an in-package method channel implementation. -* Fixes newly enabled analyzer options. - -## 2.0.2 - -* Add native unit tests. -* Updated installation instructions in README. - -## 2.0.1 - -* Add `implements` to the pubspec. - -## 2.0.0 - -* Migrate to null safety. - -## 0.0.1+12 - -* Update Flutter SDK constraint. - -## 0.0.1+11 - -* Remove unused `test` dependency. -* Update Dart SDK constraint in example. - -## 0.0.1+10 - -* Remove iOS and Android folders from the example app. - -## 0.0.1+9 - -* Remove Android folder from `shared_preferences_macos`. - -## 0.0.1+8 - -* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). - -## 0.0.1+7 - -* Fix CocoaPods podspec lint warnings. - -## 0.0.1+6 - -* Make the pedantic dev_dependency explicit. - -## 0.0.1+5 - -* Fix readme - -## 0.0.1+4 - -* Bump gradle version to avoid bugs with android projects - -## 0.0.1+3 - -* Update README. - -## 0.0.1+2 - -* Remove unused onMethodCall method. - -## 0.0.1+1 - -* Add an android/ folder with no-op implementation to workaround https://github.com/flutter/flutter/issues/46898. - -## 0.0.1 - -* Initial open source release. diff --git a/packages/shared_preferences/shared_preferences_macos/LICENSE b/packages/shared_preferences/shared_preferences_macos/LICENSE deleted file mode 100644 index c6823b81eb84..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -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/shared_preferences/shared_preferences_macos/README.md b/packages/shared_preferences/shared_preferences_macos/README.md deleted file mode 100644 index e9cd7f25be03..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# shared\_preferences\_macos - -The macos implementation of [`shared_preferences`][1]. - -## Usage - -This package is [endorsed][2], which means you can simply use `shared_preferences` -normally. This package will be automatically included in your app when you do. - -[1]: https://pub.dev/packages/shared_preferences -[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/shared_preferences/shared_preferences_macos/example/README.md b/packages/shared_preferences/shared_preferences_macos/example/README.md deleted file mode 100644 index 96b8bb17dbff..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/example/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# 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/shared_preferences/shared_preferences_macos/example/macos/RunnerTests/RunnerTests.swift b/packages/shared_preferences/shared_preferences_macos/example/macos/RunnerTests/RunnerTests.swift deleted file mode 100644 index 7da66cbc80df..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/example/macos/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,88 +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 FlutterMacOS -import XCTest -import shared_preferences_macos - -class RunnerTests: XCTestCase { - func testHandlesCommitNoOp() throws { - let plugin = SharedPreferencesPlugin() - let call = FlutterMethodCall(methodName: "commit", arguments: nil) - var called = false - plugin.handle( - call, - result: { (result: Any?) -> Void in - called = true - XCTAssert(result as? Bool == true) - }) - XCTAssert(called) - } - - func testSetAndGet() throws { - let plugin = SharedPreferencesPlugin() - let setCall = FlutterMethodCall( - methodName: "setInt", - arguments: [ - "key": "flutter.foo", - "value": 42, - ]) - plugin.handle( - setCall, - result: { (result: Any?) -> Void in - XCTAssert(result as? Bool == true) - }) - - var value: Int? - plugin.handle( - FlutterMethodCall(methodName: "getAll", arguments: nil), - result: { (result: Any?) -> Void in - if let prefs = result as? [String: Any] { - value = prefs["flutter.foo"] as? Int - } - }) - XCTAssertEqual(value, 42) - } - - func testClear() throws { - let plugin = SharedPreferencesPlugin() - let setCall = FlutterMethodCall( - methodName: "setInt", - arguments: [ - "key": "flutter.foo", - "value": 42, - ]) - plugin.handle(setCall, result: { (result: Any?) -> Void in }) - - // Make sure there is something to clear, so the test can't pass due to a set failure. - let getCall = FlutterMethodCall(methodName: "getAll", arguments: nil) - var value: Int? - plugin.handle( - getCall, - result: { (result: Any?) -> Void in - if let prefs = result as? [String: Any] { - value = prefs["flutter.foo"] as? Int - } - }) - XCTAssertEqual(value, 42) - - // Clear the value. - plugin.handle( - FlutterMethodCall(methodName: "clear", arguments: nil), - result: { (result: Any?) -> Void in - XCTAssert(result as? Bool == true) - }) - - // Get the value again, which should clear |value|. - plugin.handle( - getCall, - result: { (result: Any?) -> Void in - if let prefs = result as? [String: Any] { - value = prefs["flutter.foo"] as? Int - XCTAssert(prefs.isEmpty) - } - }) - XCTAssertEqual(value, nil) - } -} diff --git a/packages/shared_preferences/shared_preferences_macos/example/test_driver/integration_test.dart b/packages/shared_preferences/shared_preferences_macos/example/test_driver/integration_test.dart deleted file mode 100644 index 4f10f2a522f3..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/example/test_driver/integration_test.dart +++ /dev/null @@ -1,7 +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 'package:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); diff --git a/packages/shared_preferences/shared_preferences_macos/lib/shared_preferences_macos.dart b/packages/shared_preferences/shared_preferences_macos/lib/shared_preferences_macos.dart deleted file mode 100644 index a97fe131af5c..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/lib/shared_preferences_macos.dart +++ /dev/null @@ -1,53 +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:async'; - -import 'package:flutter/services.dart'; -import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; - -const MethodChannel _kChannel = - MethodChannel('plugins.flutter.io/shared_preferences_macos'); - -/// The macOS implementation of [SharedPreferencesStorePlatform]. -/// -/// This class implements the `package:shared_preferences` functionality for macOS. -class SharedPreferencesMacOS extends SharedPreferencesStorePlatform { - /// Registers this class as the default instance of [SharedPreferencesStorePlatform]. - static void registerWith() { - SharedPreferencesStorePlatform.instance = SharedPreferencesMacOS(); - } - - @override - Future remove(String key) async { - return (await _kChannel.invokeMethod( - 'remove', - {'key': key}, - ))!; - } - - @override - Future setValue(String valueType, String key, Object value) async { - return (await _kChannel.invokeMethod( - 'set$valueType', - {'key': key, 'value': value}, - ))!; - } - - @override - Future clear() async { - return (await _kChannel.invokeMethod('clear'))!; - } - - @override - Future> getAll() async { - final Map? preferences = - await _kChannel.invokeMapMethod('getAll'); - - if (preferences == null) { - return {}; - } - return preferences; - } -} diff --git a/packages/shared_preferences/shared_preferences_macos/macos/Classes/SharedPreferencesPlugin.swift b/packages/shared_preferences/shared_preferences_macos/macos/Classes/SharedPreferencesPlugin.swift deleted file mode 100644 index 91b42441adda..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/macos/Classes/SharedPreferencesPlugin.swift +++ /dev/null @@ -1,61 +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 FlutterMacOS -import Foundation - -public class SharedPreferencesPlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel( - name: "plugins.flutter.io/shared_preferences_macos", - binaryMessenger: registrar.messenger) - let instance = SharedPreferencesPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "getAll": - result(getAllPrefs()) - case "setBool", - "setInt", - "setDouble", - "setString", - "setStringList": - let arguments = call.arguments as! [String: Any] - let key = arguments["key"] as! String - UserDefaults.standard.set(arguments["value"], forKey: key) - result(true) - case "commit": - // UserDefaults does not need to be synchronized. - result(true) - case "remove": - let arguments = call.arguments as! [String: Any] - let key = arguments["key"] as! String - UserDefaults.standard.removeObject(forKey: key) - result(true) - case "clear": - let defaults = UserDefaults.standard - for (key, _) in getAllPrefs() { - defaults.removeObject(forKey: key) - } - result(true) - default: - result(FlutterMethodNotImplemented) - } - } -} - -/// Returns all preferences stored by this plugin. -private func getAllPrefs() -> [String: Any] { - var filteredPrefs: [String: Any] = [:] - if let appDomain = Bundle.main.bundleIdentifier, - let prefs = UserDefaults.standard.persistentDomain(forName: appDomain) - { - for (key, value) in prefs where key.hasPrefix("flutter.") { - filteredPrefs[key] = value - } - } - return filteredPrefs -} diff --git a/packages/shared_preferences/shared_preferences_macos/test/shared_preferences_macos_test.dart b/packages/shared_preferences/shared_preferences_macos/test/shared_preferences_macos_test.dart deleted file mode 100644 index cd858f48179d..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/test/shared_preferences_macos_test.dart +++ /dev/null @@ -1,117 +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 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences_macos/shared_preferences_macos.dart'; -import 'package:shared_preferences_platform_interface/method_channel_shared_preferences.dart'; -import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group(MethodChannelSharedPreferencesStore, () { - const MethodChannel channel = MethodChannel( - 'plugins.flutter.io/shared_preferences_macos', - ); - - const Map kTestValues = { - 'flutter.String': 'hello world', - 'flutter.Bool': true, - 'flutter.Int': 42, - 'flutter.Double': 3.14159, - 'flutter.StringList': ['foo', 'bar'], - }; - // Create a dummy in-memory implementation to back the mocked method channel - // API to simplify validation of the expected calls. - late InMemorySharedPreferencesStore testData; - - final List log = []; - late SharedPreferencesStorePlatform store; - - setUp(() async { - testData = InMemorySharedPreferencesStore.empty(); - - channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - if (methodCall.method == 'getAll') { - return await testData.getAll(); - } - if (methodCall.method == 'remove') { - final String key = (methodCall.arguments['key'] as String?)!; - return await testData.remove(key); - } - if (methodCall.method == 'clear') { - return await testData.clear(); - } - final RegExp setterRegExp = RegExp(r'set(.*)'); - final Match? match = setterRegExp.matchAsPrefix(methodCall.method); - if (match?.groupCount == 1) { - final String valueType = match!.group(1)!; - final String key = (methodCall.arguments['key'] as String?)!; - final Object value = (methodCall.arguments['value'] as Object?)!; - return await testData.setValue(valueType, key, value); - } - fail('Unexpected method call: ${methodCall.method}'); - }); - log.clear(); - }); - - test('registers instance', () { - SharedPreferencesMacOS.registerWith(); - expect(SharedPreferencesStorePlatform.instance, - isA()); - }); - - test('getAll', () async { - store = SharedPreferencesMacOS(); - testData = InMemorySharedPreferencesStore.withData(kTestValues); - expect(await store.getAll(), kTestValues); - expect(log.single.method, 'getAll'); - }); - - test('remove', () async { - store = SharedPreferencesMacOS(); - testData = InMemorySharedPreferencesStore.withData(kTestValues); - expect(await store.remove('flutter.String'), true); - expect(await store.remove('flutter.Bool'), true); - expect(await store.remove('flutter.Int'), true); - expect(await store.remove('flutter.Double'), true); - expect(await testData.getAll(), { - 'flutter.StringList': ['foo', 'bar'], - }); - - expect(log, hasLength(4)); - for (final MethodCall call in log) { - expect(call.method, 'remove'); - } - }); - - test('setValue', () async { - store = SharedPreferencesMacOS(); - expect(await testData.getAll(), isEmpty); - for (final String key in kTestValues.keys) { - final Object value = kTestValues[key]!; - expect(await store.setValue(key.split('.').last, key, value), true); - } - expect(await testData.getAll(), kTestValues); - - expect(log, hasLength(5)); - expect(log[0].method, 'setString'); - expect(log[1].method, 'setBool'); - expect(log[2].method, 'setInt'); - expect(log[3].method, 'setDouble'); - expect(log[4].method, 'setStringList'); - }); - - test('clear', () async { - store = SharedPreferencesMacOS(); - testData = InMemorySharedPreferencesStore.withData(kTestValues); - expect(await testData.getAll(), isNotEmpty); - expect(await store.clear(), true); - expect(await testData.getAll(), isEmpty); - expect(log.single.method, 'clear'); - }); - }); -} diff --git a/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md b/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md index 3ef89c396222..38cdf083ccda 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md @@ -1,6 +1,6 @@ ## NEXT -* Updates minimum Flutter version to 2.10. +* Updates minimum Flutter version to 3.0. ## 2.1.0 diff --git a/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml b/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml index b55eb1ccceb2..59d6409cff7a 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml @@ -6,7 +6,7 @@ version: 2.1.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart index 31ee89e4c3f6..296592e70bb0 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart @@ -31,25 +31,36 @@ void main() { setUp(() async { testData = InMemorySharedPreferencesStore.empty(); - channel.setMockMethodCallHandler((MethodCall methodCall) async { + Map getArgumentDictionary(MethodCall call) { + return (call.arguments as Map) + .cast(); + } + + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); if (methodCall.method == 'getAll') { - return await testData.getAll(); + return testData.getAll(); } if (methodCall.method == 'remove') { - final String key = (methodCall.arguments['key'] as String?)!; - return await testData.remove(key); + final Map arguments = + getArgumentDictionary(methodCall); + final String key = arguments['key']! as String; + return testData.remove(key); } if (methodCall.method == 'clear') { - return await testData.clear(); + return testData.clear(); } final RegExp setterRegExp = RegExp(r'set(.*)'); final Match? match = setterRegExp.matchAsPrefix(methodCall.method); if (match?.groupCount == 1) { final String valueType = match!.group(1)!; - final String key = (methodCall.arguments['key'] as String?)!; - final Object value = (methodCall.arguments['value'] as Object?)!; - return await testData.setValue(valueType, key, value); + final Map arguments = + getArgumentDictionary(methodCall); + final String key = arguments['key']! as String; + final Object value = arguments['value']!; + return testData.setValue(valueType, key, value); } fail('Unexpected method call: ${methodCall.method}'); }); @@ -108,3 +119,9 @@ void main() { }); }); } + +/// 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/shared_preferences/shared_preferences_web/CHANGELOG.md b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md index 8c8411da6fff..6332663b4b47 100644 --- a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md @@ -1,6 +1,6 @@ ## NEXT -* Updates minimum Flutter version to 2.10. +* Updates minimum Flutter version to 3.0. ## 2.0.4 diff --git a/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml index 050275489efa..52cfa1b436fb 100644 --- a/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml @@ -3,7 +3,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences_web/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/pubspec.yaml index b64f37d10da6..942fe12a39a1 100644 --- a/packages/shared_preferences/shared_preferences_web/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_web/pubspec.yaml @@ -6,7 +6,7 @@ version: 2.0.4 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/shared_preferences/shared_preferences_web/test/tests_exist_elsewhere_test.dart b/packages/shared_preferences/shared_preferences_web/test/tests_exist_elsewhere_test.dart index 442c50144727..cc32e6c72f1e 100644 --- a/packages/shared_preferences/shared_preferences_web/test/tests_exist_elsewhere_test.dart +++ b/packages/shared_preferences/shared_preferences_web/test/tests_exist_elsewhere_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: avoid_print + import 'package:flutter_test/flutter_test.dart'; void main() { diff --git a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md index 935a5ee54c2b..b99e3dd6f6ec 100644 --- a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md @@ -1,5 +1,14 @@ ## NEXT +* Updates minimum Flutter version to 3.0. + +## 2.1.3 + +* Updates code for stricter lint checks. + +## 2.1.2 + +* Updates code for stricter lint checks. * Updates code for `no_leading_underscores_for_local_identifiers` lint. * Updates minimum Flutter version to 2.10. diff --git a/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart b/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart index 74d5e4c68772..e442c4b69ee5 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart @@ -66,9 +66,11 @@ class SharedPreferencesDemoState extends State { future: _counter, builder: (BuildContext context, AsyncSnapshot snapshot) { switch (snapshot.connectionState) { + case ConnectionState.none: case ConnectionState.waiting: return const CircularProgressIndicator(); - default: + case ConnectionState.active: + case ConnectionState.done: if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } else { diff --git a/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml index 43c2145b32ae..bb51f7fbef18 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart b/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart index a60d2ed09926..5cdb30c04e04 100644 --- a/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart +++ b/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart @@ -7,7 +7,7 @@ import 'dart:convert' show json; import 'package:file/file.dart'; import 'package:file/local.dart'; -import 'package:flutter/foundation.dart' show visibleForTesting; +import 'package:flutter/foundation.dart' show debugPrint, visibleForTesting; import 'package:path/path.dart' as path; import 'package:path_provider_windows/path_provider_windows.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; @@ -80,7 +80,7 @@ class SharedPreferencesWindows extends SharedPreferencesStorePlatform { try { final File? localDataFile = await _getLocalDataFile(); if (localDataFile == null) { - print('Unable to determine where to write preferences.'); + debugPrint('Unable to determine where to write preferences.'); return false; } if (!localDataFile.existsSync()) { @@ -89,7 +89,7 @@ class SharedPreferencesWindows extends SharedPreferencesStorePlatform { final String stringMap = json.encode(preferences); localDataFile.writeAsStringSync(stringMap); } catch (e) { - print('Error saving preferences to disk: $e'); + debugPrint('Error saving preferences to disk: $e'); return false; } return true; diff --git a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml index 032e0fb213df..03fc31c6301e 100644 --- a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml @@ -2,11 +2,11 @@ name: shared_preferences_windows description: Windows implementation of shared_preferences repository: https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_windows 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.3 environment: sdk: '>=2.12.0 <3.0.0' - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index 18a0289eb43f..4079520d9120 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,3 +1,16 @@ +## 6.1.9 + +* Updates minimum Flutter version to 3.0. +* Updates iOS minimum version in README. + +## 6.1.8 + +* Updates code for stricter lint checks. + +## 6.1.7 + +* Updates code for new analysis options. + ## 6.1.6 * Updates imports for `prefer_relative_imports`. diff --git a/packages/url_launcher/url_launcher/README.md b/packages/url_launcher/url_launcher/README.md index e9e4dae476cc..b394e4ad6395 100644 --- a/packages/url_launcher/url_launcher/README.md +++ b/packages/url_launcher/url_launcher/README.md @@ -6,9 +6,9 @@ A Flutter plugin for launching a URL. -| | Android | iOS | Linux | macOS | Web | Windows | -|-------------|---------|------|-------|--------|-----|-------------| -| **Support** | SDK 16+ | 9.0+ | Any | 10.11+ | Any | Windows 10+ | +| | Android | iOS | Linux | macOS | Web | Windows | +|-------------|---------|-------|-------|--------|-----|-------------| +| **Support** | SDK 16+ | 11.0+ | Any | 10.11+ | Any | Windows 10+ | ## Usage @@ -38,7 +38,7 @@ void main() => runApp( Future _launchUrl() async { if (!await launchUrl(_url)) { - throw 'Could not launch $_url'; + throw Exception('Could not launch $_url'); } } ``` @@ -198,10 +198,10 @@ final String filePath = testFile.absolute.path; final Uri uri = Uri.file(filePath); if (!File(uri.toFilePath()).existsSync()) { - throw '$uri does not exist!'; + throw Exception('$uri does not exist!'); } if (!await launchUrl(uri)) { - throw 'Could not launch $uri'; + throw Exception('Could not launch $uri'); } ``` diff --git a/packages/url_launcher/url_launcher/example/android/gradle.properties b/packages/url_launcher/url_launcher/example/android/gradle.properties index a6738207fd15..e5611e4c7fa0 100644 --- a/packages/url_launcher/url_launcher/example/android/gradle.properties +++ b/packages/url_launcher/url_launcher/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.enableR8=true diff --git a/packages/url_launcher/url_launcher/example/ios/Flutter/AppFrameworkInfo.plist b/packages/url_launcher/url_launcher/example/ios/Flutter/AppFrameworkInfo.plist index 3a9c234f96d4..9b41e7d87980 100644 --- a/packages/url_launcher/url_launcher/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/url_launcher/url_launcher/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/url_launcher/url_launcher/example/ios/Podfile b/packages/url_launcher/url_launcher/example/ios/Podfile index f7d6a5e68c3a..d207307f86d7 100644 --- a/packages/url_launcher/url_launcher/example/ios/Podfile +++ b/packages/url_launcher/url_launcher/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/project.pbxproj b/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/project.pbxproj index 7855640c017e..0b8010748e09 100644 --- a/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -169,7 +169,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1100; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -213,10 +213,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -227,6 +229,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -340,7 +343,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -390,7 +393,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c5f1a9de4a30..ad0ebfab1b88 100644 --- a/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/url_launcher/url_launcher/example/lib/basic.dart b/packages/url_launcher/url_launcher/example/lib/basic.dart index 987ca2134318..422e2aae460c 100644 --- a/packages/url_launcher/url_launcher/example/lib/basic.dart +++ b/packages/url_launcher/url_launcher/example/lib/basic.dart @@ -28,7 +28,7 @@ void main() => runApp( Future _launchUrl() async { if (!await launchUrl(_url)) { - throw 'Could not launch $_url'; + throw Exception('Could not launch $_url'); } } // #enddocregion basic-example diff --git a/packages/url_launcher/url_launcher/example/lib/encoding.dart b/packages/url_launcher/url_launcher/example/lib/encoding.dart index 24c724466a77..575eb5f42387 100644 --- a/packages/url_launcher/url_launcher/example/lib/encoding.dart +++ b/packages/url_launcher/url_launcher/example/lib/encoding.dart @@ -22,8 +22,14 @@ 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 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 [ diff --git a/packages/url_launcher/url_launcher/example/lib/files.dart b/packages/url_launcher/url_launcher/example/lib/files.dart index d48440670406..7f9d20669ee7 100644 --- a/packages/url_launcher/url_launcher/example/lib/files.dart +++ b/packages/url_launcher/url_launcher/example/lib/files.dart @@ -38,10 +38,10 @@ Future _openFile() async { final Uri uri = Uri.file(filePath); if (!File(uri.toFilePath()).existsSync()) { - throw '$uri does not exist!'; + throw Exception('$uri does not exist!'); } if (!await launchUrl(uri)) { - throw 'Could not launch $uri'; + throw Exception('Could not launch $uri'); } // #enddocregion file } diff --git a/packages/url_launcher/url_launcher/example/lib/main.dart b/packages/url_launcher/url_launcher/example/lib/main.dart index a538940f1a68..9b005cf98db0 100644 --- a/packages/url_launcher/url_launcher/example/lib/main.dart +++ b/packages/url_launcher/url_launcher/example/lib/main.dart @@ -58,7 +58,7 @@ class _MyHomePageState extends State { url, mode: LaunchMode.externalApplication, )) { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -69,7 +69,7 @@ class _MyHomePageState extends State { webViewConfiguration: const WebViewConfiguration( headers: {'my_header_key': 'my_header_value'}), )) { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -79,7 +79,7 @@ class _MyHomePageState extends State { mode: LaunchMode.inAppWebView, webViewConfiguration: const WebViewConfiguration(enableJavaScript: false), )) { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -89,7 +89,7 @@ class _MyHomePageState extends State { mode: LaunchMode.inAppWebView, webViewConfiguration: const WebViewConfiguration(enableDomStorage: false), )) { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -196,7 +196,6 @@ class _MyHomePageState extends State { onPressed: () => setState(() { _launched = _launchInWebViewOrVC(toLaunch); Timer(const Duration(seconds: 5), () { - print('Closing WebView after 5 seconds...'); closeInAppWebView(); }); }), diff --git a/packages/url_launcher/url_launcher/example/pubspec.yaml b/packages/url_launcher/url_launcher/example/pubspec.yaml index 573dc0d9ed01..83900bfdef75 100644 --- a/packages/url_launcher/url_launcher/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher/lib/src/legacy_api.dart b/packages/url_launcher/url_launcher/lib/src/legacy_api.dart index f6faf3fa3d0e..9f6d2dca001e 100644 --- a/packages/url_launcher/url_launcher/lib/src/legacy_api.dart +++ b/packages/url_launcher/url_launcher/lib/src/legacy_api.dart @@ -130,7 +130,7 @@ Future launch( /// details. @Deprecated('Use canLaunchUrl instead') Future canLaunch(String urlString) async { - return await UrlLauncherPlatform.instance.canLaunch(urlString); + return UrlLauncherPlatform.instance.canLaunch(urlString); } /// Closes the current WebView, if one was previously opened via a call to [launch]. @@ -143,12 +143,11 @@ Future canLaunch(String urlString) async { /// WebView/SafariViewController available to be closed. @Deprecated('Use closeInAppWebView instead') Future closeWebView() async { - return await UrlLauncherPlatform.instance.closeWebView(); + return UrlLauncherPlatform.instance.closeWebView(); } /// 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. -// TODO(ianh): Remove this once we roll stable in late 2021. T? _ambiguate(T? value) => value; diff --git a/packages/url_launcher/url_launcher/lib/src/url_launcher_string.dart b/packages/url_launcher/url_launcher/lib/src/url_launcher_string.dart index cf96ebc095da..45193ff17cb3 100644 --- a/packages/url_launcher/url_launcher/lib/src/url_launcher_string.dart +++ b/packages/url_launcher/url_launcher/lib/src/url_launcher_string.dart @@ -30,7 +30,7 @@ Future launchUrlString( throw ArgumentError.value(urlString, 'urlString', 'To use an in-app web view, you must provide an http(s) URL.'); } - return await UrlLauncherPlatform.instance.launchUrl( + return UrlLauncherPlatform.instance.launchUrl( urlString, LaunchOptions( mode: convertLaunchMode(mode), @@ -53,5 +53,5 @@ Future launchUrlString( /// others will immediately fail if the URL can't be parsed according to the /// official standards that define URL formats. Future canLaunchUrlString(String urlString) async { - return await UrlLauncherPlatform.instance.canLaunch(urlString); + return UrlLauncherPlatform.instance.canLaunch(urlString); } diff --git a/packages/url_launcher/url_launcher/lib/src/url_launcher_uri.dart b/packages/url_launcher/url_launcher/lib/src/url_launcher_uri.dart index 30321026ceb9..b3ce6c279f39 100644 --- a/packages/url_launcher/url_launcher/lib/src/url_launcher_uri.dart +++ b/packages/url_launcher/url_launcher/lib/src/url_launcher_uri.dart @@ -50,7 +50,7 @@ Future launchUrl( throw ArgumentError.value(url, 'url', 'To use an in-app web view, you must provide an http(s) URL.'); } - return await UrlLauncherPlatform.instance.launchUrl( + return UrlLauncherPlatform.instance.launchUrl( url.toString(), LaunchOptions( mode: convertLaunchMode(mode), @@ -75,7 +75,7 @@ Future launchUrl( /// that are always assumed to be supported (such as http(s)), as web pages /// are never allowed to query installed applications. Future canLaunchUrl(Uri url) async { - return await UrlLauncherPlatform.instance.canLaunch(url.toString()); + return UrlLauncherPlatform.instance.canLaunch(url.toString()); } /// Closes the current in-app web view, if one was previously opened by @@ -84,5 +84,5 @@ Future canLaunchUrl(Uri url) async { /// If [launchUrl] was never called with [LaunchMode.inAppWebView], then this /// call will have no effect. Future closeInAppWebView() async { - return await UrlLauncherPlatform.instance.closeWebView(); + return UrlLauncherPlatform.instance.closeWebView(); } diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml index 8efda4afb580..e4f6d4c7c5c4 100644 --- a/packages/url_launcher/url_launcher/pubspec.yaml +++ b/packages/url_launcher/url_launcher/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes. repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 6.1.6 +version: 6.1.9 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.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 11d7d8f17c09..b2fde31d526d 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 @@ -188,7 +188,7 @@ void main() { }); test('cannot launch a non-web in webview', () async { - expect(() async => await launch('tel:555-555-5555', forceWebView: true), + expect(() async => launch('tel:555-555-5555', forceWebView: true), throwsA(isA())); }); @@ -211,16 +211,14 @@ void main() { test('cannot send e-mail with forceSafariVC: true', () async { expect( - () async => await launch( - 'mailto:gmail-noreply@google.com?subject=Hello', + () async => launch('mailto:gmail-noreply@google.com?subject=Hello', forceSafariVC: true), throwsA(isA())); }); test('cannot send e-mail with forceWebView: true', () async { expect( - () async => await launch( - 'mailto:gmail-noreply@google.com?subject=Hello', + () async => launch('mailto:gmail-noreply@google.com?subject=Hello', forceWebView: true), throwsA(isA())); }); @@ -305,7 +303,7 @@ void main() { test('cannot open non-parseable url with forceSafariVC: true', () async { expect( - () async => await launch( + () async => launch( 'rdp://full%20address=s:mypc:3389&audiomode=i:2&disable%20themes=i:1', forceSafariVC: true), throwsA(isA())); @@ -313,7 +311,7 @@ void main() { test('cannot open non-parseable url with forceWebView: true', () async { expect( - () async => await launch( + () async => launch( 'rdp://full%20address=s:mypc:3389&audiomode=i:2&disable%20themes=i:1', forceWebView: true), throwsA(isA())); @@ -323,8 +321,6 @@ void main() { /// This removes the type information from a value so that it can be cast /// to another type even if that cast is redundant. -/// /// We use this so that APIs whose type have become more descriptive can still /// be used on the stable branch where they require a cast. -// TODO(ianh): Remove this once we roll stable in late 2021. Object? _anonymize(T? value) => value; diff --git a/packages/url_launcher/url_launcher/test/src/url_launcher_string_test.dart b/packages/url_launcher/url_launcher/test/src/url_launcher_string_test.dart index 0dcbc34b7dd6..64065ff99f9a 100644 --- a/packages/url_launcher/url_launcher/test/src/url_launcher_string_test.dart +++ b/packages/url_launcher/url_launcher/test/src/url_launcher_string_test.dart @@ -222,7 +222,7 @@ void main() { test('cannot launch a non-web URL in a webview', () async { expect( - () async => await launchUrlString('tel:555-555-5555', + () async => launchUrlString('tel:555-555-5555', mode: LaunchMode.inAppWebView), throwsA(isA())); }); diff --git a/packages/url_launcher/url_launcher/test/src/url_launcher_uri_test.dart b/packages/url_launcher/url_launcher/test/src/url_launcher_uri_test.dart index 7685aefdd4ee..d71d07fc8fc4 100644 --- a/packages/url_launcher/url_launcher/test/src/url_launcher_uri_test.dart +++ b/packages/url_launcher/url_launcher/test/src/url_launcher_uri_test.dart @@ -223,7 +223,7 @@ void main() { test('cannot launch a non-web URL in a webview', () async { expect( - () async => await launchUrl(Uri(scheme: 'tel', path: '555-555-5555'), + () async => launchUrl(Uri(scheme: 'tel', path: '555-555-5555'), mode: LaunchMode.inAppWebView), throwsA(isA())); }); diff --git a/packages/url_launcher/url_launcher_android/CHANGELOG.md b/packages/url_launcher/url_launcher_android/CHANGELOG.md index 934d8da556b7..1062de50c4ca 100644 --- a/packages/url_launcher/url_launcher_android/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_android/CHANGELOG.md @@ -1,3 +1,15 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 6.0.23 + +* Updates code for stricter lint checks. + +## 6.0.22 + +* Updates code for new analysis options. + ## 6.0.21 * Updates androidx.annotation to 1.2.0. diff --git a/packages/url_launcher/url_launcher_android/android/build.gradle b/packages/url_launcher/url_launcher_android/android/build.gradle index dbd68d99c1a2..63d81249ed8e 100644 --- a/packages/url_launcher/url_launcher_android/android/build.gradle +++ b/packages/url_launcher/url_launcher_android/android/build.gradle @@ -29,9 +29,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' } @@ -51,7 +49,7 @@ android { dependencies { compileOnly 'androidx.annotation:annotation:1.2.0' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:4.8.0' + testImplementation 'org.mockito:mockito-core:5.1.1' testImplementation 'androidx.test:core:1.0.0' testImplementation 'org.robolectric:robolectric:4.3' } diff --git a/packages/url_launcher/url_launcher_android/example/android/gradle.properties b/packages/url_launcher/url_launcher_android/example/android/gradle.properties index a6738207fd15..e5611e4c7fa0 100644 --- a/packages/url_launcher/url_launcher_android/example/android/gradle.properties +++ b/packages/url_launcher/url_launcher_android/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.enableR8=true diff --git a/packages/url_launcher/url_launcher_android/example/lib/main.dart b/packages/url_launcher/url_launcher_android/example/lib/main.dart index 672ae4a27665..7a77c86aef72 100644 --- a/packages/url_launcher/url_launcher_android/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_android/example/lib/main.dart @@ -63,7 +63,7 @@ class _MyHomePageState extends State { universalLinksOnly: false, headers: {}, )) { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -77,7 +77,7 @@ class _MyHomePageState extends State { universalLinksOnly: false, headers: {'my_header_key': 'my_header_value'}, )) { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -91,7 +91,7 @@ class _MyHomePageState extends State { universalLinksOnly: false, headers: {}, )) { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -105,7 +105,7 @@ class _MyHomePageState extends State { universalLinksOnly: false, headers: {}, )) { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -202,7 +202,6 @@ class _MyHomePageState extends State { onPressed: () => setState(() { _launched = _launchInWebView(toLaunch); Timer(const Duration(seconds: 5), () { - print('Closing WebView after 5 seconds...'); launcher.closeWebView(); }); }), diff --git a/packages/url_launcher/url_launcher_android/example/pubspec.yaml b/packages/url_launcher/url_launcher_android/example/pubspec.yaml index 6c922c7a0f7d..33fc9f06ed63 100644 --- a/packages/url_launcher/url_launcher_android/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_android/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart b/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart index 1aa093a36451..bd4c2a5ff45b 100644 --- a/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart +++ b/packages/url_launcher/url_launcher_android/lib/url_launcher_android.dart @@ -33,7 +33,7 @@ class UrlLauncherAndroid extends UrlLauncherPlatform { // returns true, then there is a browser, which means that there is // at least one handler for the original URL. if (scheme == 'http' || scheme == 'https') { - return await _canLaunchUrl('$scheme://flutter.dev'); + return _canLaunchUrl('$scheme://flutter.dev'); } } return canLaunchSpecificUrl; diff --git a/packages/url_launcher/url_launcher_android/pubspec.yaml b/packages/url_launcher/url_launcher_android/pubspec.yaml index e97fde31b2e0..599274a95ebc 100644 --- a/packages/url_launcher/url_launcher_android/pubspec.yaml +++ b/packages/url_launcher/url_launcher_android/pubspec.yaml @@ -2,11 +2,11 @@ name: url_launcher_android description: Android implementation of the url_launcher plugin. repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 6.0.21 +version: 6.0.23 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart b/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart index eebd8cd4c059..18db61e0b9fa 100644 --- a/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart +++ b/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart @@ -16,7 +16,9 @@ void main() { setUp(() { log = []; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); // Return null explicitly instead of relying on the implicit null @@ -32,7 +34,9 @@ void main() { group('canLaunch', () { test('calls through', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); return true; }); @@ -59,9 +63,12 @@ void main() { test('checks a generic URL if an http URL returns false', () async { const String specificUrl = 'http://example.com/'; const String genericUrl = 'http://flutter.dev'; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); - return methodCall.arguments['url'] != specificUrl; + return (methodCall.arguments as Map)['url'] != + specificUrl; }); final UrlLauncherAndroid launcher = UrlLauncherAndroid(); @@ -69,15 +76,18 @@ void main() { expect(canLaunch, true); expect(log.length, 2); - expect(log[1].arguments['url'], genericUrl); + expect((log[1].arguments as Map)['url'], genericUrl); }); test('checks a generic URL if an https URL returns false', () async { const String specificUrl = 'https://example.com/'; const String genericUrl = 'https://flutter.dev'; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); - return methodCall.arguments['url'] != specificUrl; + return (methodCall.arguments as Map)['url'] != + specificUrl; }); final UrlLauncherAndroid launcher = UrlLauncherAndroid(); @@ -85,11 +95,13 @@ void main() { expect(canLaunch, true); expect(log.length, 2); - expect(log[1].arguments['url'], genericUrl); + expect((log[1].arguments as Map)['url'], genericUrl); }); test('does not a generic URL if a non-web URL returns false', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); return false; }); @@ -286,3 +298,9 @@ void main() { }); }); } + +/// 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_ios/CHANGELOG.md b/packages/url_launcher/url_launcher_ios/CHANGELOG.md index cf018da4f59d..86546d45566d 100644 --- a/packages/url_launcher/url_launcher_ios/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_ios/CHANGELOG.md @@ -1,5 +1,10 @@ -## NEXT +## 6.1.0 +* Updates minimum Flutter version to 3.3 and iOS 11. + +## 6.0.18 + +* Updates code for stricter lint checks. * Updates minimum Flutter version to 2.10. ## 6.0.17 diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/url_launcher/url_launcher_ios/example/ios/Flutter/AppFrameworkInfo.plist index 3a9c234f96d4..9b41e7d87980 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/url_launcher/url_launcher_ios/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Podfile b/packages/url_launcher/url_launcher_ios/example/ios/Podfile index 3924e59aa0f9..ec43b513b0d1 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/Podfile +++ b/packages/url_launcher/url_launcher_ios/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj index 595f85d9a75b..d61abc724469 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -269,7 +269,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1100; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -339,10 +339,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -353,6 +355,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -517,7 +520,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -567,7 +570,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c5f1a9de4a30..ad0ebfab1b88 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/url_launcher/url_launcher_ios/example/lib/main.dart b/packages/url_launcher/url_launcher_ios/example/lib/main.dart index 7aa3a4b74e83..f01624ff87c6 100644 --- a/packages/url_launcher/url_launcher_ios/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_ios/example/lib/main.dart @@ -14,7 +14,7 @@ void main() { } class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); + const MyApp({super.key}); @override Widget build(BuildContext context) { @@ -29,7 +29,7 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - const MyHomePage({Key? key, required this.title}) : super(key: key); + const MyHomePage({super.key, required this.title}); final String title; @override @@ -53,7 +53,7 @@ class _MyHomePageState extends State { headers: {'my_header_key': 'my_header_value'}, ); } else { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -70,7 +70,7 @@ class _MyHomePageState extends State { headers: {'my_header_key': 'my_header_value'}, ); } else { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -87,7 +87,7 @@ class _MyHomePageState extends State { headers: {}, ); } else { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -104,7 +104,7 @@ class _MyHomePageState extends State { headers: {}, ); } else { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -155,7 +155,7 @@ class _MyHomePageState extends State { headers: {}, ); } else { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -226,7 +226,6 @@ class _MyHomePageState extends State { onPressed: () => setState(() { _launched = _launchInWebViewOrVC(toLaunch); Timer(const Duration(seconds: 5), () { - print('Closing WebView after 5 seconds...'); UrlLauncherPlatform.instance.closeWebView(); }); }), diff --git a/packages/url_launcher/url_launcher_ios/example/pubspec.yaml b/packages/url_launcher/url_launcher_ios/example/pubspec.yaml index 9a134c747fa4..21b191ad0cce 100644 --- a/packages/url_launcher/url_launcher_ios/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_ios/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the url_launcher plugin. publish_to: none environment: - sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + sdk: '>=2.18.0 <3.0.0' + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m b/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m index af720c87b8b2..375d5e2a2354 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m @@ -6,7 +6,6 @@ #import "FLTURLLauncherPlugin.h" -API_AVAILABLE(ios(9.0)) @interface FLTURLLaunchSession : NSObject @property(copy, nonatomic) FlutterResult flutterResult; @@ -30,7 +29,7 @@ - (instancetype)initWithUrl:url withFlutterResult:result { } - (void)safariViewController:(SFSafariViewController *)controller - didCompleteInitialLoad:(BOOL)didLoadSuccessfully API_AVAILABLE(ios(9.0)) { + didCompleteInitialLoad:(BOOL)didLoadSuccessfully { if (didLoadSuccessfully) { self.flutterResult(@YES); } else { @@ -41,7 +40,7 @@ - (void)safariViewController:(SFSafariViewController *)controller } } -- (void)safariViewControllerDidFinish:(SFSafariViewController *)controller API_AVAILABLE(ios(9.0)) { +- (void)safariViewControllerDidFinish:(SFSafariViewController *)controller { [controller dismissViewControllerAnimated:YES completion:nil]; self.didFinish(); } @@ -52,7 +51,6 @@ - (void)close { @end -API_AVAILABLE(ios(9.0)) @interface FLTURLLauncherPlugin () @property(strong, nonatomic) FLTURLLaunchSession *currentSession; @@ -99,24 +97,16 @@ - (void)launchURL:(NSString *)urlString NSURL *url = [NSURL URLWithString:urlString]; UIApplication *application = [UIApplication sharedApplication]; - if (@available(iOS 10.0, *)) { - NSNumber *universalLinksOnly = call.arguments[@"universalLinksOnly"] ?: @0; - NSDictionary *options = @{UIApplicationOpenURLOptionUniversalLinksOnly : universalLinksOnly}; - [application openURL:url - options:options - completionHandler:^(BOOL success) { - result(@(success)); - }]; - } else { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - BOOL success = [application openURL:url]; -#pragma clang diagnostic pop - result(@(success)); - } + NSNumber *universalLinksOnly = call.arguments[@"universalLinksOnly"] ?: @0; + NSDictionary *options = @{UIApplicationOpenURLOptionUniversalLinksOnly : universalLinksOnly}; + [application openURL:url + options:options + completionHandler:^(BOOL success) { + result(@(success)); + }]; } -- (void)launchURLInVC:(NSString *)urlString result:(FlutterResult)result API_AVAILABLE(ios(9.0)) { +- (void)launchURLInVC:(NSString *)urlString result:(FlutterResult)result { NSURL *url = [NSURL URLWithString:urlString]; self.currentSession = [[FLTURLLaunchSession alloc] initWithUrl:url withFlutterResult:result]; __weak typeof(self) weakSelf = self; @@ -128,7 +118,7 @@ - (void)launchURLInVC:(NSString *)urlString result:(FlutterResult)result API_AVA completion:nil]; } -- (void)closeWebViewWithResult:(FlutterResult)result API_AVAILABLE(ios(9.0)) { +- (void)closeWebViewWithResult:(FlutterResult)result { if (self.currentSession != nil) { [self.currentSession close]; } diff --git a/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec b/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec index 1c0e81964252..9c265694018e 100644 --- a/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec +++ b/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec @@ -16,7 +16,6 @@ A Flutter plugin for making the underlying platform (Android or iOS) launch a UR s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - - s.platform = :ios, '9.0' + s.platform = :ios, '11.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/packages/url_launcher/url_launcher_ios/pubspec.yaml b/packages/url_launcher/url_launcher_ios/pubspec.yaml index 5e06a80b4cfe..5a5c4bdc0514 100644 --- a/packages/url_launcher/url_launcher_ios/pubspec.yaml +++ b/packages/url_launcher/url_launcher_ios/pubspec.yaml @@ -2,11 +2,11 @@ name: url_launcher_ios description: iOS implementation of the url_launcher plugin. repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 6.0.17 +version: 6.1.0 environment: - sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + sdk: '>=2.18.0 <3.0.0' + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart index 8fad5807bddb..34dac1c4f925 100644 --- a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart +++ b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart @@ -14,7 +14,9 @@ void main() { const MethodChannel channel = MethodChannel('plugins.flutter.io/url_launcher_ios'); final List log = []; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); // Return null explicitly instead of relying on the implicit null @@ -206,3 +208,9 @@ void main() { }); }); } + +/// 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_linux/CHANGELOG.md b/packages/url_launcher/url_launcher_linux/CHANGELOG.md index 69d8b24ddf6f..3d955871c8c8 100644 --- a/packages/url_launcher/url_launcher_linux/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_linux/CHANGELOG.md @@ -1,5 +1,10 @@ ## NEXT +* Updates minimum Flutter version to 3.0. + +## 3.0.2 + +* Updates code for stricter lint checks. * Updates minimum Flutter version to 2.10. ## 3.0.1 diff --git a/packages/url_launcher/url_launcher_linux/example/lib/main.dart b/packages/url_launcher/url_launcher_linux/example/lib/main.dart index 0b985e78ac0d..bbe651ea05de 100644 --- a/packages/url_launcher/url_launcher_linux/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_linux/example/lib/main.dart @@ -50,7 +50,7 @@ class _MyHomePageState extends State { headers: {}, ); } else { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } diff --git a/packages/url_launcher/url_launcher_linux/example/pubspec.yaml b/packages/url_launcher/url_launcher_linux/example/pubspec.yaml index 17effeb1ffcb..ba738806af38 100644 --- a/packages/url_launcher/url_launcher_linux/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_linux/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_linux/pubspec.yaml b/packages/url_launcher/url_launcher_linux/pubspec.yaml index 99b237506c60..e455ab83bef5 100644 --- a/packages/url_launcher/url_launcher_linux/pubspec.yaml +++ b/packages/url_launcher/url_launcher_linux/pubspec.yaml @@ -2,11 +2,11 @@ name: url_launcher_linux description: Linux implementation of the url_launcher plugin. repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 3.0.1 +version: 3.0.2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/url_launcher/url_launcher_linux/test/url_launcher_linux_test.dart b/packages/url_launcher/url_launcher_linux/test/url_launcher_linux_test.dart index 7a4399dd4e6c..4e62cc446199 100644 --- a/packages/url_launcher/url_launcher_linux/test/url_launcher_linux_test.dart +++ b/packages/url_launcher/url_launcher_linux/test/url_launcher_linux_test.dart @@ -14,7 +14,9 @@ void main() { const MethodChannel channel = MethodChannel('plugins.flutter.io/url_launcher_linux'); final List log = []; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); // Return null explicitly instead of relying on the implicit null @@ -142,3 +144,9 @@ void main() { }); }); } + +/// 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_macos/CHANGELOG.md b/packages/url_launcher/url_launcher_macos/CHANGELOG.md index 7386ecced865..eb42ba920e23 100644 --- a/packages/url_launcher/url_launcher_macos/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_macos/CHANGELOG.md @@ -1,5 +1,10 @@ ## NEXT +* Updates minimum Flutter version to 3.0. + +## 3.0.2 + +* Updates code for stricter lint checks. * Updates minimum Flutter version to 2.10. ## 3.0.1 diff --git a/packages/url_launcher/url_launcher_macos/example/lib/main.dart b/packages/url_launcher/url_launcher_macos/example/lib/main.dart index 0b985e78ac0d..bbe651ea05de 100644 --- a/packages/url_launcher/url_launcher_macos/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_macos/example/lib/main.dart @@ -50,7 +50,7 @@ class _MyHomePageState extends State { headers: {}, ); } else { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } diff --git a/packages/url_launcher/url_launcher_macos/example/pubspec.yaml b/packages/url_launcher/url_launcher_macos/example/pubspec.yaml index 3b802ea229ba..688cac3a6b0e 100644 --- a/packages/url_launcher/url_launcher_macos/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_macos/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_macos/pubspec.yaml b/packages/url_launcher/url_launcher_macos/pubspec.yaml index eaf210a367b6..2ec915fc2ddb 100644 --- a/packages/url_launcher/url_launcher_macos/pubspec.yaml +++ b/packages/url_launcher/url_launcher_macos/pubspec.yaml @@ -2,11 +2,11 @@ name: url_launcher_macos description: macOS implementation of the url_launcher plugin. repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 3.0.1 +version: 3.0.2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/url_launcher/url_launcher_macos/test/url_launcher_macos_test.dart b/packages/url_launcher/url_launcher_macos/test/url_launcher_macos_test.dart index 0a28aea678c3..26011fa6779a 100644 --- a/packages/url_launcher/url_launcher_macos/test/url_launcher_macos_test.dart +++ b/packages/url_launcher/url_launcher_macos/test/url_launcher_macos_test.dart @@ -14,7 +14,9 @@ void main() { const MethodChannel channel = MethodChannel('plugins.flutter.io/url_launcher_macos'); final List log = []; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); // Return null explicitly instead of relying on the implicit null @@ -142,3 +144,9 @@ void main() { }); }); } + +/// 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/CHANGELOG.md b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md index d45ca36e3906..fecd2a45c4cb 100644 --- a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 2.1.1 * Updates imports for `prefer_relative_imports`. 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 da8aa1570bad..bddadad893a7 100644 --- a/packages/url_launcher/url_launcher_platform_interface/lib/link.dart +++ b/packages/url_launcher/url_launcher_platform_interface/lib/link.dart @@ -109,5 +109,4 @@ Future pushRouteNameToFramework(Object? _, String routeName) { /// /// We use this so that APIs that have become non-nullable can still be used /// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. 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 4364e116c508..ab37dc32eedd 100644 --- a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml +++ b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml @@ -8,7 +8,7 @@ version: 2.1.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart b/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart index c8ec08c53095..9ccdd84ae890 100644 --- a/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart +++ b/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart @@ -48,7 +48,9 @@ void main() { const MethodChannel channel = MethodChannel('plugins.flutter.io/url_launcher'); final List log = []; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); // Return null explicitly instead of relying on the implicit null @@ -323,3 +325,9 @@ class ExtendsUrlLauncherPlatform extends UrlLauncherPlatform { @override final LinkDelegate? linkDelegate = null; } + +/// 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_web/CHANGELOG.md b/packages/url_launcher/url_launcher_web/CHANGELOG.md index 5454338bde51..51b2de90b88a 100644 --- a/packages/url_launcher/url_launcher_web/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_web/CHANGELOG.md @@ -1,5 +1,10 @@ ## NEXT +* Updates minimum Flutter version to 3.0. + +## 2.0.14 + +* Updates code for stricter lint checks. * Updates minimum Flutter version to 2.10. ## 2.0.13 diff --git a/packages/url_launcher/url_launcher_web/example/integration_test/link_widget_test.dart b/packages/url_launcher/url_launcher_web/example/integration_test/link_widget_test.dart index 6b19861c5ce5..5f4239ab0ba9 100644 --- a/packages/url_launcher/url_launcher_web/example/integration_test/link_widget_test.dart +++ b/packages/url_launcher/url_launcher_web/example/integration_test/link_widget_test.dart @@ -25,7 +25,7 @@ void main() { uri: uri, target: LinkTarget.blank, builder: (BuildContext context, FollowLink? followLink) { - return Container(width: 100, height: 100); + return const SizedBox(width: 100, height: 100); }, )), )); @@ -43,7 +43,7 @@ void main() { uri: uri2, target: LinkTarget.self, builder: (BuildContext context, FollowLink? followLink) { - return Container(width: 100, height: 100); + return const SizedBox(width: 100, height: 100); }, )), )); @@ -60,7 +60,7 @@ void main() { uri: uri3, target: LinkTarget.self, builder: (BuildContext context, FollowLink? followLink) { - return Container(width: 100, height: 100); + return const SizedBox(width: 100, height: 100); }, )), )); @@ -113,7 +113,7 @@ void main() { uri: null, target: LinkTarget.defaultTarget, builder: (BuildContext context, FollowLink? followLink) { - return Container(width: 100, height: 100); + return const SizedBox(width: 100, height: 100); }, )), )); @@ -149,7 +149,7 @@ void main() { await tester.scrollUntilVisible( find.text('#${itemCount - 1}'), - 2500, + 800, maxScrolls: 1000, ); }); diff --git a/packages/url_launcher/url_launcher_web/example/pubspec.yaml b/packages/url_launcher/url_launcher_web/example/pubspec.yaml index f972b2857ecf..ca1b0d6634a7 100644 --- a/packages/url_launcher/url_launcher_web/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/example/pubspec.yaml @@ -3,7 +3,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_web/lib/src/link.dart b/packages/url_launcher/url_launcher_web/lib/src/link.dart index 112d07ea7571..78c049c03def 100644 --- a/packages/url_launcher/url_launcher_web/lib/src/link.dart +++ b/packages/url_launcher/url_launcher_web/lib/src/link.dart @@ -247,9 +247,13 @@ class LinkViewController extends PlatformViewController { return '_self'; case LinkTarget.blank: return '_blank'; - default: - throw Exception('Unknown LinkTarget value $target.'); } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + return '_self'; } @override diff --git a/packages/url_launcher/url_launcher_web/pubspec.yaml b/packages/url_launcher/url_launcher_web/pubspec.yaml index 6d4c80689427..8c8214ef6e4b 100644 --- a/packages/url_launcher/url_launcher_web/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/pubspec.yaml @@ -2,11 +2,11 @@ name: url_launcher_web description: Web platform implementation of url_launcher repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 2.0.13 +version: 2.0.14 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/url_launcher/url_launcher_web/test/tests_exist_elsewhere_test.dart b/packages/url_launcher/url_launcher_web/test/tests_exist_elsewhere_test.dart index 442c50144727..cc32e6c72f1e 100644 --- a/packages/url_launcher/url_launcher_web/test/tests_exist_elsewhere_test.dart +++ b/packages/url_launcher/url_launcher_web/test/tests_exist_elsewhere_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: avoid_print + import 'package:flutter_test/flutter_test.dart'; void main() { diff --git a/packages/url_launcher/url_launcher_windows/CHANGELOG.md b/packages/url_launcher/url_launcher_windows/CHANGELOG.md index a5952feb4978..abb3ab10db57 100644 --- a/packages/url_launcher/url_launcher_windows/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_windows/CHANGELOG.md @@ -1,5 +1,11 @@ -## NEXT +## 3.0.3 +* Converts internal implentation to Pigeon. +* Updates minimum Flutter version to 3.0. + +## 3.0.2 + +* Updates code for stricter lint checks. * Updates minimum Flutter version to 2.10. ## 3.0.1 diff --git a/packages/url_launcher/url_launcher_windows/example/lib/main.dart b/packages/url_launcher/url_launcher_windows/example/lib/main.dart index 0b985e78ac0d..bbe651ea05de 100644 --- a/packages/url_launcher/url_launcher_windows/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_windows/example/lib/main.dart @@ -50,7 +50,7 @@ class _MyHomePageState extends State { headers: {}, ); } else { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } diff --git a/packages/url_launcher/url_launcher_windows/example/pubspec.yaml b/packages/url_launcher/url_launcher_windows/example/pubspec.yaml index 966d32c779e8..231d3d0848bc 100644 --- a/packages/url_launcher/url_launcher_windows/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_windows/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_windows/lib/src/messages.g.dart b/packages/url_launcher/url_launcher_windows/lib/src/messages.g.dart new file mode 100644 index 000000000000..a1d46c11267d --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/lib/src/messages.g.dart @@ -0,0 +1,71 @@ +// 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 (v5.0.1), 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 +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +class UrlLauncherApi { + /// Constructor for [UrlLauncherApi]. 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. + UrlLauncherApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + Future canLaunchUrl(String arg_url) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_url]) 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 bool?)!; + } + } + + Future launchUrl(String arg_url) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UrlLauncherApi.launchUrl', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_url]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } +} diff --git a/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart b/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart index b0ee8cb1a0b4..41c403e56f8e 100644 --- a/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart +++ b/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart @@ -2,17 +2,21 @@ // 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/foundation.dart'; import 'package:url_launcher_platform_interface/link.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; -const MethodChannel _channel = - MethodChannel('plugins.flutter.io/url_launcher_windows'); +import 'src/messages.g.dart'; /// An implementation of [UrlLauncherPlatform] for Windows. class UrlLauncherWindows extends UrlLauncherPlatform { + /// Creates a new plugin implementation instance. + UrlLauncherWindows({ + @visibleForTesting UrlLauncherApi? api, + }) : _hostApi = api ?? UrlLauncherApi(); + + final UrlLauncherApi _hostApi; + /// Registers this class as the default instance of [UrlLauncherPlatform]. static void registerWith() { UrlLauncherPlatform.instance = UrlLauncherWindows(); @@ -23,10 +27,7 @@ class UrlLauncherWindows extends UrlLauncherPlatform { @override Future canLaunch(String url) { - return _channel.invokeMethod( - 'canLaunch', - {'url': url}, - ).then((bool? value) => value ?? false); + return _hostApi.canLaunchUrl(url); } @override @@ -39,16 +40,9 @@ class UrlLauncherWindows extends UrlLauncherPlatform { required bool universalLinksOnly, required Map headers, String? webOnlyWindowName, - }) { - return _channel.invokeMethod( - 'launch', - { - 'url': url, - 'enableJavaScript': enableJavaScript, - 'enableDomStorage': enableDomStorage, - 'universalLinksOnly': universalLinksOnly, - 'headers': headers, - }, - ).then((bool? value) => value ?? false); + }) async { + await _hostApi.launchUrl(url); + // Failure is handled via a PlatformException from `launchUrl`. + return true; } } diff --git a/packages/url_launcher/url_launcher_windows/pigeons/copyright.txt b/packages/url_launcher/url_launcher_windows/pigeons/copyright.txt new file mode 100644 index 000000000000..1236b63caf3a --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/pigeons/copyright.txt @@ -0,0 +1,3 @@ +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. diff --git a/packages/url_launcher/url_launcher_windows/pigeons/messages.dart b/packages/url_launcher/url_launcher_windows/pigeons/messages.dart new file mode 100644 index 000000000000..9607cdffc686 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/pigeons/messages.dart @@ -0,0 +1,18 @@ +// 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:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + cppOptions: CppOptions(namespace: 'url_launcher_windows'), + cppHeaderOut: 'windows/messages.g.h', + cppSourceOut: 'windows/messages.g.cpp', + copyrightHeader: 'pigeons/copyright.txt', +)) +@HostApi(dartHostTestHandler: 'TestUrlLauncherApi') +abstract class UrlLauncherApi { + bool canLaunchUrl(String url); + void launchUrl(String url); +} diff --git a/packages/url_launcher/url_launcher_windows/pubspec.yaml b/packages/url_launcher/url_launcher_windows/pubspec.yaml index b35b62e1d82e..de4f5edd69eb 100644 --- a/packages/url_launcher/url_launcher_windows/pubspec.yaml +++ b/packages/url_launcher/url_launcher_windows/pubspec.yaml @@ -2,11 +2,11 @@ name: url_launcher_windows description: Windows implementation of the url_launcher plugin. repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 3.0.1 +version: 3.0.3 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -24,4 +24,5 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + pigeon: ^5.0.1 test: ^1.16.3 diff --git a/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart b/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart index 8b55b29bb530..7f48f64fa92c 100644 --- a/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart +++ b/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart @@ -5,140 +5,101 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; +import 'package:url_launcher_windows/src/messages.g.dart'; import 'package:url_launcher_windows/url_launcher_windows.dart'; void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('$UrlLauncherWindows', () { - const MethodChannel channel = - MethodChannel('plugins.flutter.io/url_launcher_windows'); - final List log = []; - channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - - // Return null explicitly instead of relying on the implicit null - // returned by the method channel if no return statement is specified. - return null; - }); + late _FakeUrlLauncherApi api; + late UrlLauncherWindows plugin; - test('registers instance', () { - UrlLauncherWindows.registerWith(); - expect(UrlLauncherPlatform.instance, isA()); - }); + setUp(() { + api = _FakeUrlLauncherApi(); + plugin = UrlLauncherWindows(api: api); + }); - tearDown(() { - log.clear(); - }); + test('registers instance', () { + UrlLauncherWindows.registerWith(); + expect(UrlLauncherPlatform.instance, isA()); + }); - test('canLaunch', () async { - final UrlLauncherWindows launcher = UrlLauncherWindows(); - await launcher.canLaunch('http://example.com/'); - expect( - log, - [ - isMethodCall('canLaunch', arguments: { - 'url': 'http://example.com/', - }) - ], - ); - }); + group('canLaunch', () { + test('handles true', () async { + api.canLaunch = true; - test('canLaunch should return false if platform returns null', () async { - final UrlLauncherWindows launcher = UrlLauncherWindows(); - final bool canLaunch = await launcher.canLaunch('http://example.com/'); + final bool result = await plugin.canLaunch('http://example.com/'); - expect(canLaunch, false); + expect(result, isTrue); + expect(api.argument, 'http://example.com/'); }); - test('launch', () async { - final UrlLauncherWindows launcher = UrlLauncherWindows(); - await launcher.launch( - 'http://example.com/', - useSafariVC: true, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: const {}, - ); - expect( - log, - [ - isMethodCall('launch', arguments: { - 'url': 'http://example.com/', - 'enableJavaScript': false, - 'enableDomStorage': false, - 'universalLinksOnly': false, - 'headers': {}, - }) - ], - ); - }); + test('handles false', () async { + api.canLaunch = false; - test('launch with headers', () async { - final UrlLauncherWindows launcher = UrlLauncherWindows(); - await launcher.launch( - 'http://example.com/', - useSafariVC: true, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: const {'key': 'value'}, - ); - expect( - log, - [ - isMethodCall('launch', arguments: { - 'url': 'http://example.com/', - 'enableJavaScript': false, - 'enableDomStorage': false, - 'universalLinksOnly': false, - 'headers': {'key': 'value'}, - }) - ], - ); + final bool result = await plugin.canLaunch('http://example.com/'); + + expect(result, isFalse); + expect(api.argument, 'http://example.com/'); }); + }); + + group('launch', () { + test('handles success', () async { + api.canLaunch = true; - test('launch universal links only', () async { - final UrlLauncherWindows launcher = UrlLauncherWindows(); - await launcher.launch( - 'http://example.com/', - useSafariVC: false, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: true, - headers: const {}, - ); expect( - log, - [ - isMethodCall('launch', arguments: { - 'url': 'http://example.com/', - 'enableJavaScript': false, - 'enableDomStorage': false, - 'universalLinksOnly': true, - 'headers': {}, - }) - ], - ); + plugin.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ), + completes); + expect(api.argument, 'http://example.com/'); }); - test('launch should return false if platform returns null', () async { - final UrlLauncherWindows launcher = UrlLauncherWindows(); - final bool launched = await launcher.launch( - 'http://example.com/', - useSafariVC: true, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: const {}, - ); - - expect(launched, false); + test('handles failure', () async { + api.canLaunch = false; + + await expectLater( + plugin.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ), + throwsA(isA())); + expect(api.argument, 'http://example.com/'); }); }); } + +class _FakeUrlLauncherApi implements UrlLauncherApi { + /// The argument that was passed to an API call. + String? argument; + + /// Controls the behavior of the fake implementations. + /// + /// - [canLaunchUrl] returns this value. + /// - [launchUrl] throws if this is false. + bool canLaunch = false; + + @override + Future canLaunchUrl(String url) async { + argument = url; + return canLaunch; + } + + @override + Future launchUrl(String url) async { + argument = url; + if (!canLaunch) { + throw PlatformException(code: 'Failed'); + } + } +} diff --git a/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt b/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt index a4185acff6a1..a34bcb3d35da 100644 --- a/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt +++ b/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt @@ -5,6 +5,8 @@ project(${PROJECT_NAME} LANGUAGES CXX) set(PLUGIN_NAME "${PROJECT_NAME}_plugin") list(APPEND PLUGIN_SOURCES + "messages.g.cpp" + "messages.g.h" "system_apis.cpp" "system_apis.h" "url_launcher_plugin.cpp" diff --git a/packages/url_launcher/url_launcher_windows/windows/messages.g.cpp b/packages/url_launcher/url_launcher_windows/windows/messages.g.cpp new file mode 100644 index 000000000000..eb1cf792931f --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/messages.g.cpp @@ -0,0 +1,113 @@ +// 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 (v5.0.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#undef _HAS_EXCEPTIONS + +#include "messages.g.h" + +#include +#include +#include +#include + +#include +#include +#include + +namespace url_launcher_windows { + +/// The codec used by UrlLauncherApi. +const flutter::StandardMessageCodec& UrlLauncherApi::GetCodec() { + return flutter::StandardMessageCodec::GetInstance( + &flutter::StandardCodecSerializer::GetInstance()); +} + +// Sets up an instance of `UrlLauncherApi` to handle messages through the +// `binary_messenger`. +void UrlLauncherApi::SetUp(flutter::BinaryMessenger* binary_messenger, + UrlLauncherApi* api) { + { + auto channel = + std::make_unique>( + binary_messenger, "dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_url_arg = args.at(0); + if (encodable_url_arg.IsNull()) { + reply(WrapError("url_arg unexpectedly null.")); + return; + } + const auto& url_arg = std::get(encodable_url_arg); + ErrorOr output = api->CanLaunchUrl(url_arg); + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + flutter::EncodableList wrapped; + wrapped.push_back( + flutter::EncodableValue(std::move(output).TakeValue())); + reply(flutter::EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = + std::make_unique>( + binary_messenger, "dev.flutter.pigeon.UrlLauncherApi.launchUrl", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_url_arg = args.at(0); + if (encodable_url_arg.IsNull()) { + reply(WrapError("url_arg unexpectedly null.")); + return; + } + const auto& url_arg = std::get(encodable_url_arg); + std::optional output = api->LaunchUrl(url_arg); + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + flutter::EncodableList wrapped; + wrapped.push_back(flutter::EncodableValue()); + reply(flutter::EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } +} + +flutter::EncodableValue UrlLauncherApi::WrapError( + std::string_view error_message) { + return flutter::EncodableValue(flutter::EncodableList{ + flutter::EncodableValue(std::string(error_message)), + flutter::EncodableValue("Error"), flutter::EncodableValue()}); +} +flutter::EncodableValue UrlLauncherApi::WrapError(const FlutterError& error) { + return flutter::EncodableValue(flutter::EncodableList{ + flutter::EncodableValue(error.message()), + flutter::EncodableValue(error.code()), error.details()}); +} + +} // namespace url_launcher_windows diff --git a/packages/url_launcher/url_launcher_windows/windows/messages.g.h b/packages/url_launcher/url_launcher_windows/windows/messages.g.h new file mode 100644 index 000000000000..cb8e95f8d065 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/messages.g.h @@ -0,0 +1,86 @@ +// 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 (v5.0.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#ifndef PIGEON_H_ +#define PIGEON_H_ +#include +#include +#include +#include + +#include +#include +#include + +namespace url_launcher_windows { + +// Generated class from Pigeon. + +class FlutterError { + public: + explicit FlutterError(const std::string& code) : code_(code) {} + explicit FlutterError(const std::string& code, const std::string& message) + : code_(code), message_(message) {} + explicit FlutterError(const std::string& code, const std::string& message, + const flutter::EncodableValue& details) + : code_(code), message_(message), details_(details) {} + + const std::string& code() const { return code_; } + const std::string& message() const { return message_; } + const flutter::EncodableValue& details() const { return details_; } + + private: + std::string code_; + std::string message_; + flutter::EncodableValue details_; +}; + +template +class ErrorOr { + public: + ErrorOr(const T& rhs) { new (&v_) T(rhs); } + ErrorOr(const T&& rhs) { v_ = std::move(rhs); } + ErrorOr(const FlutterError& rhs) { new (&v_) FlutterError(rhs); } + ErrorOr(const FlutterError&& rhs) { v_ = std::move(rhs); } + + bool has_error() const { return std::holds_alternative(v_); } + const T& value() const { return std::get(v_); }; + const FlutterError& error() const { return std::get(v_); }; + + private: + friend class UrlLauncherApi; + ErrorOr() = default; + T TakeValue() && { return std::get(std::move(v_)); } + + std::variant v_; +}; + +// Generated interface from Pigeon that represents a handler of messages from +// Flutter. +class UrlLauncherApi { + public: + UrlLauncherApi(const UrlLauncherApi&) = delete; + UrlLauncherApi& operator=(const UrlLauncherApi&) = delete; + virtual ~UrlLauncherApi(){}; + virtual ErrorOr CanLaunchUrl(const std::string& url) = 0; + virtual std::optional LaunchUrl(const std::string& url) = 0; + + // The codec used by UrlLauncherApi. + static const flutter::StandardMessageCodec& GetCodec(); + // Sets up an instance of `UrlLauncherApi` to handle messages through the + // `binary_messenger`. + static void SetUp(flutter::BinaryMessenger* binary_messenger, + UrlLauncherApi* api); + static flutter::EncodableValue WrapError(std::string_view error_message); + static flutter::EncodableValue WrapError(const FlutterError& error); + + protected: + UrlLauncherApi() = default; +}; + +} // namespace url_launcher_windows + +#endif // PIGEON_H_ diff --git a/packages/url_launcher/url_launcher_windows/windows/system_apis.cpp b/packages/url_launcher/url_launcher_windows/windows/system_apis.cpp index abd690b6e47f..cde95ee1b399 100644 --- a/packages/url_launcher/url_launcher_windows/windows/system_apis.cpp +++ b/packages/url_launcher/url_launcher_windows/windows/system_apis.cpp @@ -5,7 +5,7 @@ #include -namespace url_launcher_plugin { +namespace url_launcher_windows { SystemApis::SystemApis() {} @@ -35,4 +35,4 @@ HINSTANCE SystemApisImpl::ShellExecuteW(HWND hwnd, LPCWSTR operation, show_flags); } -} // namespace url_launcher_plugin +} // namespace url_launcher_windows diff --git a/packages/url_launcher/url_launcher_windows/windows/system_apis.h b/packages/url_launcher/url_launcher_windows/windows/system_apis.h index 7b56704d8e04..c56c4100180b 100644 --- a/packages/url_launcher/url_launcher_windows/windows/system_apis.h +++ b/packages/url_launcher/url_launcher_windows/windows/system_apis.h @@ -3,7 +3,7 @@ // found in the LICENSE file. #include -namespace url_launcher_plugin { +namespace url_launcher_windows { // An interface wrapping system APIs used by the plugin, for mocking. class SystemApis { @@ -53,4 +53,4 @@ class SystemApisImpl : public SystemApis { int show_flags); }; -} // namespace url_launcher_plugin +} // namespace url_launcher_windows diff --git a/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp b/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp index 191d51a0caa8..9dd2be5347b5 100644 --- a/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp +++ b/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp @@ -9,11 +9,13 @@ #include #include +#include #include +#include "messages.g.h" #include "url_launcher_plugin.h" -namespace url_launcher_plugin { +namespace url_launcher_windows { namespace test { namespace { @@ -42,30 +44,10 @@ class MockSystemApis : public SystemApis { (override)); }; -class MockMethodResult : public flutter::MethodResult<> { - public: - MOCK_METHOD(void, SuccessInternal, (const EncodableValue* result), - (override)); - MOCK_METHOD(void, ErrorInternal, - (const std::string& error_code, const std::string& error_message, - const EncodableValue* details), - (override)); - MOCK_METHOD(void, NotImplementedInternal, (), (override)); -}; - -std::unique_ptr CreateArgumentsWithUrl(const std::string& url) { - EncodableMap args = { - {EncodableValue("url"), EncodableValue(url)}, - }; - return std::make_unique(args); -} - } // namespace TEST(UrlLauncherPlugin, CanLaunchSuccessTrue) { std::unique_ptr system = std::make_unique(); - std::unique_ptr result = - std::make_unique(); // Return success values from the registery commands. HKEY fake_key = reinterpret_cast(1); @@ -73,20 +55,16 @@ TEST(UrlLauncherPlugin, CanLaunchSuccessTrue) { .WillOnce(DoAll(SetArgPointee<4>(fake_key), Return(ERROR_SUCCESS))); EXPECT_CALL(*system, RegQueryValueExW).WillOnce(Return(ERROR_SUCCESS)); EXPECT_CALL(*system, RegCloseKey(fake_key)).WillOnce(Return(ERROR_SUCCESS)); - // Expect a success response. - EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(true)))); UrlLauncherPlugin plugin(std::move(system)); - plugin.HandleMethodCall( - flutter::MethodCall("canLaunch", - CreateArgumentsWithUrl("https://some.url.com")), - std::move(result)); + ErrorOr result = plugin.CanLaunchUrl("https://some.url.com"); + + ASSERT_FALSE(result.has_error()); + EXPECT_TRUE(result.value()); } TEST(UrlLauncherPlugin, CanLaunchQueryFailure) { std::unique_ptr system = std::make_unique(); - std::unique_ptr result = - std::make_unique(); // Return success values from the registery commands, except for the query, // to simulate a scheme that is in the registry, but has no URL handler. @@ -95,68 +73,52 @@ TEST(UrlLauncherPlugin, CanLaunchQueryFailure) { .WillOnce(DoAll(SetArgPointee<4>(fake_key), Return(ERROR_SUCCESS))); EXPECT_CALL(*system, RegQueryValueExW).WillOnce(Return(ERROR_FILE_NOT_FOUND)); EXPECT_CALL(*system, RegCloseKey(fake_key)).WillOnce(Return(ERROR_SUCCESS)); - // Expect a success response. - EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(false)))); UrlLauncherPlugin plugin(std::move(system)); - plugin.HandleMethodCall( - flutter::MethodCall("canLaunch", - CreateArgumentsWithUrl("https://some.url.com")), - std::move(result)); + ErrorOr result = plugin.CanLaunchUrl("https://some.url.com"); + + ASSERT_FALSE(result.has_error()); + EXPECT_FALSE(result.value()); } TEST(UrlLauncherPlugin, CanLaunchHandlesOpenFailure) { std::unique_ptr system = std::make_unique(); - std::unique_ptr result = - std::make_unique(); // Return failure for opening. EXPECT_CALL(*system, RegOpenKeyExW).WillOnce(Return(ERROR_BAD_PATHNAME)); - // Expect a success response. - EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(false)))); UrlLauncherPlugin plugin(std::move(system)); - plugin.HandleMethodCall( - flutter::MethodCall("canLaunch", - CreateArgumentsWithUrl("https://some.url.com")), - std::move(result)); + ErrorOr result = plugin.CanLaunchUrl("https://some.url.com"); + + ASSERT_FALSE(result.has_error()); + EXPECT_FALSE(result.value()); } TEST(UrlLauncherPlugin, LaunchSuccess) { std::unique_ptr system = std::make_unique(); - std::unique_ptr result = - std::make_unique(); // Return a success value (>32) from launching. EXPECT_CALL(*system, ShellExecuteW) .WillOnce(Return(reinterpret_cast(33))); - // Expect a success response. - EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(true)))); UrlLauncherPlugin plugin(std::move(system)); - plugin.HandleMethodCall( - flutter::MethodCall("launch", - CreateArgumentsWithUrl("https://some.url.com")), - std::move(result)); + std::optional error = plugin.LaunchUrl("https://some.url.com"); + + EXPECT_FALSE(error.has_value()); } TEST(UrlLauncherPlugin, LaunchReportsFailure) { std::unique_ptr system = std::make_unique(); - std::unique_ptr result = - std::make_unique(); // Return a faile value (<=32) from launching. EXPECT_CALL(*system, ShellExecuteW) .WillOnce(Return(reinterpret_cast(32))); - // Expect an error response. - EXPECT_CALL(*result, ErrorInternal); UrlLauncherPlugin plugin(std::move(system)); - plugin.HandleMethodCall( - flutter::MethodCall("launch", - CreateArgumentsWithUrl("https://some.url.com")), - std::move(result)); + std::optional error = plugin.LaunchUrl("https://some.url.com"); + + EXPECT_TRUE(error.has_value()); } } // namespace test -} // namespace url_launcher_plugin +} // namespace url_launcher_windows diff --git a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp index d5f201219c75..1dfee16c4445 100644 --- a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp +++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp @@ -13,7 +13,9 @@ #include #include -namespace url_launcher_plugin { +#include "messages.g.h" + +namespace url_launcher_windows { namespace { @@ -62,18 +64,9 @@ std::string GetUrlArgument(const flutter::MethodCall<>& method_call) { // static void UrlLauncherPlugin::RegisterWithRegistrar( flutter::PluginRegistrar* registrar) { - auto channel = std::make_unique>( - registrar->messenger(), "plugins.flutter.io/url_launcher_windows", - &flutter::StandardMethodCodec::GetInstance()); - std::unique_ptr plugin = std::make_unique(); - - channel->SetMethodCallHandler( - [plugin_pointer = plugin.get()](const auto& call, auto result) { - plugin_pointer->HandleMethodCall(call, std::move(result)); - }); - + UrlLauncherApi::SetUp(registrar->messenger(), plugin.get()); registrar->AddPlugin(std::move(plugin)); } @@ -85,37 +78,7 @@ UrlLauncherPlugin::UrlLauncherPlugin(std::unique_ptr system_apis) UrlLauncherPlugin::~UrlLauncherPlugin() = default; -void UrlLauncherPlugin::HandleMethodCall( - const flutter::MethodCall<>& method_call, - std::unique_ptr> result) { - if (method_call.method_name().compare("launch") == 0) { - std::string url = GetUrlArgument(method_call); - if (url.empty()) { - result->Error("argument_error", "No URL provided"); - return; - } - - std::optional error = LaunchUrl(url); - if (error) { - result->Error("open_error", error.value()); - return; - } - result->Success(EncodableValue(true)); - } else if (method_call.method_name().compare("canLaunch") == 0) { - std::string url = GetUrlArgument(method_call); - if (url.empty()) { - result->Error("argument_error", "No URL provided"); - return; - } - - bool can_launch = CanLaunchUrl(url); - result->Success(EncodableValue(can_launch)); - } else { - result->NotImplemented(); - } -} - -bool UrlLauncherPlugin::CanLaunchUrl(const std::string& url) { +ErrorOr UrlLauncherPlugin::CanLaunchUrl(const std::string& url) { size_t separator_location = url.find(":"); if (separator_location == std::string::npos) { return false; @@ -134,7 +97,7 @@ bool UrlLauncherPlugin::CanLaunchUrl(const std::string& url) { return has_handler; } -std::optional UrlLauncherPlugin::LaunchUrl( +std::optional UrlLauncherPlugin::LaunchUrl( const std::string& url) { std::wstring url_wide = Utf16FromUtf8(url); @@ -147,9 +110,9 @@ std::optional UrlLauncherPlugin::LaunchUrl( std::ostringstream error_message; error_message << "Failed to open " << url << ": ShellExecute error code " << status; - return std::optional(error_message.str()); + return FlutterError("open_error", error_message.str()); } return std::nullopt; } -} // namespace url_launcher_plugin +} // namespace url_launcher_windows diff --git a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h index 45e70e5fc067..e51cde67ab79 100644 --- a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h +++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h @@ -10,11 +10,12 @@ #include #include +#include "messages.g.h" #include "system_apis.h" -namespace url_launcher_plugin { +namespace url_launcher_windows { -class UrlLauncherPlugin : public flutter::Plugin { +class UrlLauncherPlugin : public flutter::Plugin, public UrlLauncherApi { public: static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar); @@ -31,18 +32,12 @@ class UrlLauncherPlugin : public flutter::Plugin { UrlLauncherPlugin(const UrlLauncherPlugin&) = delete; UrlLauncherPlugin& operator=(const UrlLauncherPlugin&) = delete; - // Called when a method is called on the plugin channel. - void HandleMethodCall(const flutter::MethodCall<>& method_call, - std::unique_ptr> result); + // UrlLauncherApi: + ErrorOr CanLaunchUrl(const std::string& url) override; + std::optional LaunchUrl(const std::string& url) override; private: - // Returns whether or not the given URL has a registered handler. - bool CanLaunchUrl(const std::string& url); - - // Attempts to launch the given URL. On failure, returns an error string. - std::optional LaunchUrl(const std::string& url); - std::unique_ptr system_apis_; }; -} // namespace url_launcher_plugin +} // namespace url_launcher_windows diff --git a/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp b/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp index 05de586d8fe0..726709386fa6 100644 --- a/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp +++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp @@ -9,7 +9,7 @@ void UrlLauncherWindowsRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar) { - url_launcher_plugin::UrlLauncherPlugin::RegisterWithRegistrar( + url_launcher_windows::UrlLauncherPlugin::RegisterWithRegistrar( flutter::PluginRegistrarManager::GetInstance() ->GetRegistrar(registrar)); } diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 0885f28f9db8..eed3b6bc2346 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,5 +1,26 @@ ## NEXT +* Updates minimum Flutter version to 3.0. + +## 2.5.1 + +* Updates code for stricter lint checks. + +## 2.5.0 + +* Exposes `VideoScrubber` so it can be used to make custom video progress indicators + +## 2.4.10 + +* Adds compatibilty with version 6.0 of the platform interface. + +## 2.4.9 + +* Fixes file URI construction. + +## 2.4.8 + +* Updates code for new analysis options. * Updates code for `no_leading_underscores_for_local_identifiers` lint. ## 2.4.7 diff --git a/packages/video_player/video_player/example/android/app/build.gradle b/packages/video_player/video_player/example/android/app/build.gradle index 338eeb8944f7..8b2086b6c05e 100644 --- a/packages/video_player/video_player/example/android/app/build.gradle +++ b/packages/video_player/video_player/example/android/app/build.gradle @@ -60,7 +60,7 @@ flutter { dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'org.robolectric:robolectric:4.8.1' - testImplementation 'org.mockito:mockito-core:4.7.0' + testImplementation 'org.mockito:mockito-core:5.0.0' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' } diff --git a/packages/video_player/video_player/example/android/gradle.properties b/packages/video_player/video_player/example/android/gradle.properties index a6738207fd15..e5611e4c7fa0 100644 --- a/packages/video_player/video_player/example/android/gradle.properties +++ b/packages/video_player/video_player/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.enableR8=true diff --git a/packages/video_player/video_player/example/pubspec.yaml b/packages/video_player/video_player/example/pubspec.yaml index 7b6aa09329fa..0b30e9fb01e7 100644 --- a/packages/video_player/video_player/example/pubspec.yaml +++ b/packages/video_player/video_player/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index c1f4886282f8..5720e2d9d136 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -245,12 +245,11 @@ class VideoPlayerController extends ValueNotifier { /// Constructs a [VideoPlayerController] playing a video from a file. /// - /// This will load the file from the file-URI given by: - /// `'file://${file.path}'`. + /// This will load the file from a file:// URI constructed from [file]'s path. VideoPlayerController.file(File file, {Future? closedCaptionFile, this.videoPlayerOptions}) : _closedCaptionFileFuture = closedCaptionFile, - dataSource = 'file://${file.path}', + dataSource = Uri.file(file.absolute.path).toString(), dataSourceType = DataSourceType.file, package = null, formatHint = null, @@ -542,7 +541,7 @@ class VideoPlayerController extends ValueNotifier { if (_isDisposed) { return null; } - return await _videoPlayerPlatform.getPosition(_textureId); + return _videoPlayerPlatform.getPosition(_textureId); } /// Sets the video's current timestamp to be at [moment]. The next @@ -698,17 +697,13 @@ class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) { - switch (state) { - case AppLifecycleState.paused: - _wasPlayingBeforePause = _controller.value.isPlaying; - _controller.pause(); - break; - case AppLifecycleState.resumed: - if (_wasPlayingBeforePause) { - _controller.play(); - } - break; - default: + if (state == AppLifecycleState.paused) { + _wasPlayingBeforePause = _controller.value.isPlaying; + _controller.pause(); + } else if (state == AppLifecycleState.resumed) { + if (_wasPlayingBeforePause) { + _controller.play(); + } } } @@ -836,20 +831,29 @@ class VideoProgressColors { final Color backgroundColor; } -class _VideoScrubber extends StatefulWidget { - const _VideoScrubber({ +/// A scrubber to control [VideoPlayerController]s +class VideoScrubber extends StatefulWidget { + /// Create a [VideoScrubber] handler with the given [child]. + /// + /// [controller] is the [VideoPlayerController] that will be controlled by + /// this scrubber. + const VideoScrubber({ + Key? key, required this.child, required this.controller, - }); + }) : super(key: key); + /// The widget that will be displayed inside the gesture detector. final Widget child; + + /// The [VideoPlayerController] that will be controlled by this scrubber. final VideoPlayerController controller; @override - _VideoScrubberState createState() => _VideoScrubberState(); + State createState() => _VideoScrubberState(); } -class _VideoScrubberState extends State<_VideoScrubber> { +class _VideoScrubberState extends State { bool _controllerWasPlaying = false; VideoPlayerController get controller => widget.controller; @@ -1014,7 +1018,7 @@ class _VideoProgressIndicatorState extends State { child: progressIndicator, ); if (widget.allowScrubbing) { - return _VideoScrubber( + return VideoScrubber( controller: controller, child: paddedProgressIndicator, ); @@ -1096,5 +1100,4 @@ class ClosedCaption extends StatelessWidget { /// /// We use this so that APIs that have become non-nullable can still be used /// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. T? _ambiguate(T? value) => value; diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 7e2df60d3cdd..d75456ace469 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, and web. repository: https://github.com/flutter/plugins/tree/main/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.4.7 +version: 2.5.1 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -25,7 +25,7 @@ dependencies: html: ^0.15.0 video_player_android: ^2.3.5 video_player_avfoundation: ^2.2.17 - video_player_platform_interface: ^5.1.1 + video_player_platform_interface: ">=5.1.1 <7.0.0" video_player_web: ^2.0.0 dev_dependencies: 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 8e5e98b68f18..663fc9f8e897 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -7,6 +7,7 @@ import 'dart:io'; import 'dart:math' as math; import 'dart:typed_data'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -105,6 +106,13 @@ class _FakeClosedCaptionFile extends ClosedCaptionFile { } void main() { + late FakeVideoPlayerPlatform fakeVideoPlayerPlatform; + + setUp(() { + fakeVideoPlayerPlatform = FakeVideoPlayerPlatform(); + VideoPlayerPlatform.instance = fakeVideoPlayerPlatform; + }); + void verifyPlayStateRespondsToLifecycle( VideoPlayerController controller, { required bool shouldPlayInBackground, @@ -234,13 +242,6 @@ void main() { }); group('VideoPlayerController', () { - late FakeVideoPlayerPlatform fakeVideoPlayerPlatform; - - setUp(() { - fakeVideoPlayerPlatform = FakeVideoPlayerPlatform(); - VideoPlayerPlatform.instance = fakeVideoPlayerPlatform; - }); - group('initialize', () { test('started app lifecycle observing', () async { final VideoPlayerController controller = VideoPlayerController.network( @@ -341,8 +342,21 @@ void main() { VideoPlayerController.file(File('a.avi')); await controller.initialize(); - expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'file://a.avi'); - }); + final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; + expect(uri.startsWith('file:///'), true, reason: 'Actual string: $uri'); + expect(uri.endsWith('/a.avi'), true, reason: 'Actual string: $uri'); + }, skip: kIsWeb /* Web does not support file assets. */); + + test('file with special characters', () async { + final VideoPlayerController controller = + 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, + reason: 'Actual string: $uri'); + }, skip: kIsWeb /* Web does not support file assets. */); test('successful initialize on controller with error clears error', () async { @@ -389,7 +403,7 @@ void main() { await controller.initialize(); await controller.dispose(); - expect(() async => await controller.dispose(), returnsNormally); + expect(() async => controller.dispose(), returnsNormally); }); test('play', () async { @@ -1001,24 +1015,17 @@ void main() { }); group('VideoPlayerOptions', () { - late FakeVideoPlayerPlatform fakeVideoPlayerPlatform; - - setUp(() { - fakeVideoPlayerPlatform = FakeVideoPlayerPlatform(); - VideoPlayerPlatform.instance = fakeVideoPlayerPlatform; - }); - test('setMixWithOthers', () async { - final VideoPlayerController controller = VideoPlayerController.file( - File(''), + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true)); await controller.initialize(); expect(controller.videoPlayerOptions!.mixWithOthers, true); }); test('true allowBackgroundPlayback continues playback', () async { - final VideoPlayerController controller = VideoPlayerController.file( - File(''), + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', videoPlayerOptions: VideoPlayerOptions( allowBackgroundPlayback: true, ), @@ -1032,8 +1039,8 @@ void main() { }); test('false allowBackgroundPlayback pauses playback', () async { - final VideoPlayerController controller = VideoPlayerController.file( - File(''), + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', videoPlayerOptions: VideoPlayerOptions(), ); await controller.initialize(); @@ -1146,6 +1153,11 @@ class FakeVideoPlayerPlatform extends VideoPlayerPlatform { Future setMixWithOthers(bool mixWithOthers) async { calls.add('setMixWithOthers'); } + + @override + Widget buildView(int textureId) { + return Texture(textureId: textureId); + } } /// This allows a value of type T or T? to be treated as a value of type T?. diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index 4d3f72da6fbd..56024c4ba233 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,5 +1,12 @@ ## NEXT +* Updates minimum Flutter version to 3.0. + +## 2.3.10 + +* Adds compatibilty with version 6.0 of the platform interface. +* Fixes file URI construction in example. +* Updates code for new analysis options. * Updates code for `no_leading_underscores_for_local_identifiers` lint. * Updates minimum Flutter version to 2.10. * Fixes violations of new analysis option use_named_constants. diff --git a/packages/video_player/video_player_android/android/build.gradle b/packages/video_player/video_player_android/android/build.gradle index 2677050d303b..903ee219d881 100644 --- a/packages/video_player/video_player_android/android/build.gradle +++ b/packages/video_player/video_player_android/android/build.gradle @@ -34,9 +34,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -50,7 +48,7 @@ android { implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.18.1' testImplementation 'junit:junit:4.13.2' testImplementation 'androidx.test:core:1.3.0' - testImplementation 'org.mockito:mockito-inline:4.7.0' + testImplementation 'org.mockito:mockito-inline:5.0.0' testImplementation 'org.robolectric:robolectric:4.8.1' } diff --git a/packages/video_player/video_player_android/example/android/app/build.gradle b/packages/video_player/video_player_android/example/android/app/build.gradle index 93caaa5c7c61..80de4a1ee27d 100644 --- a/packages/video_player/video_player_android/example/android/app/build.gradle +++ b/packages/video_player/video_player_android/example/android/app/build.gradle @@ -60,7 +60,7 @@ flutter { dependencies { testImplementation 'junit:junit:4.13' testImplementation 'org.robolectric:robolectric:4.8.2' - testImplementation 'org.mockito:mockito-core:4.7.0' + testImplementation 'org.mockito:mockito-core:5.0.0' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' } diff --git a/packages/video_player/video_player_android/example/android/gradle.properties b/packages/video_player/video_player_android/example/android/gradle.properties index a6738207fd15..e5611e4c7fa0 100644 --- a/packages/video_player/video_player_android/example/android/gradle.properties +++ b/packages/video_player/video_player_android/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.enableR8=true diff --git a/packages/video_player/video_player_android/example/lib/mini_controller.dart b/packages/video_player/video_player_android/example/lib/mini_controller.dart index 61959efe1e10..fb79a77fb2cb 100644 --- a/packages/video_player/video_player_android/example/lib/mini_controller.dart +++ b/packages/video_player/video_player_android/example/lib/mini_controller.dart @@ -150,7 +150,7 @@ class MiniController extends ValueNotifier { /// Constructs a [MiniController] playing a video from obtained from a file. MiniController.file(File file) - : dataSource = 'file://${file.path}', + : dataSource = Uri.file(file.absolute.path).toString(), dataSourceType = DataSourceType.file, package = null, super(VideoPlayerValue(duration: Duration.zero)); @@ -318,7 +318,7 @@ class MiniController extends ValueNotifier { /// The position in the current video. Future get position async { - return await _platform.getPosition(_textureId); + return _platform.getPosition(_textureId); } /// Sets the video's current timestamp to be at [position]. diff --git a/packages/video_player/video_player_android/example/pubspec.yaml b/packages/video_player/video_player_android/example/pubspec.yaml index d935244ed924..16ffe17e7ba3 100644 --- a/packages/video_player/video_player_android/example/pubspec.yaml +++ b/packages/video_player/video_player_android/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: @@ -16,7 +16,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: ../ - video_player_platform_interface: ">=4.2.0 <6.0.0" + video_player_platform_interface: ">=5.1.1 <7.0.0" dev_dependencies: flutter_driver: diff --git a/packages/video_player/video_player_android/pigeons/messages.dart b/packages/video_player/video_player_android/pigeons/messages.dart index bf552f9369df..90c9fbb61ea0 100644 --- a/packages/video_player/video_player_android/pigeons/messages.dart +++ b/packages/video_player/video_player_android/pigeons/messages.dart @@ -6,7 +6,7 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions( dartOut: 'lib/src/messages.g.dart', - dartTestOut: 'test/test_api.dart', + dartTestOut: 'test/test_api.g.dart', javaOut: 'android/src/main/java/io/flutter/plugins/videoplayer/Messages.java', javaOptions: JavaOptions( package: 'io.flutter.plugins.videoplayer', diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index 1df1deeebf22..3f46ec8a4d79 100644 --- a/packages/video_player/video_player_android/pubspec.yaml +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -2,11 +2,11 @@ name: video_player_android description: Android implementation of the video_player plugin. repository: https://github.com/flutter/plugins/tree/main/packages/video_player/video_player_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.3.9 +version: 2.3.10 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -20,7 +20,7 @@ flutter: dependencies: flutter: sdk: flutter - video_player_platform_interface: ^5.1.1 + video_player_platform_interface: ">=5.1.1 <7.0.0" dev_dependencies: flutter_test: diff --git a/packages/video_player/video_player_android/test/android_video_player_test.dart b/packages/video_player/video_player_android/test/android_video_player_test.dart index fad9617ddad9..6aa24e5c1808 100644 --- a/packages/video_player/video_player_android/test/android_video_player_test.dart +++ b/packages/video_player/video_player_android/test/android_video_player_test.dart @@ -12,7 +12,7 @@ import 'package:video_player_android/src/messages.g.dart'; import 'package:video_player_android/video_player_android.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; -import 'test_api.dart'; +import 'test_api.g.dart'; class _ApiLogger implements TestHostVideoPlayerApi { final List log = []; @@ -234,16 +234,16 @@ void main() { }); test('videoEventsFor', () async { - _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .setMockMessageHandler( 'flutter.io/videoPlayer/videoEvents123', (ByteData? message) async { final MethodCall methodCall = const StandardMethodCodec().decodeMethodCall(message); if (methodCall.method == 'listen') { - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -255,8 +255,8 @@ void main() { }), (ByteData? data) {}); - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -269,8 +269,8 @@ void main() { }), (ByteData? data) {}); - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -279,8 +279,8 @@ void main() { }), (ByteData? data) {}); - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -293,8 +293,8 @@ void main() { }), (ByteData? data) {}); - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -303,8 +303,8 @@ void main() { }), (ByteData? data) {}); - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -360,5 +360,4 @@ void main() { /// /// We use this so that APIs that have become non-nullable can still be used /// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. T? _ambiguate(T? value) => value; diff --git a/packages/video_player/video_player_android/test/test_api.dart b/packages/video_player/video_player_android/test/test_api.g.dart similarity index 100% rename from packages/video_player/video_player_android/test/test_api.dart rename to packages/video_player/video_player_android/test/test_api.g.dart diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index b1cc1ce13927..b8564c0a2236 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,5 +1,12 @@ ## NEXT +* Updates minimum Flutter version to 3.0. + +## 2.3.8 + +* Adds compatibilty with version 6.0 of the platform interface. +* Fixes file URI construction in example. +* Updates code for new analysis options. * Adds an integration test for a bug where the aspect ratios of some HLS videos are incorrectly inverted. * Removes an unnecessary override in example code. @@ -10,7 +17,7 @@ ## 2.3.6 -* Fixes a bug in iOS 16 where videos from protected live streams are not shown. +* Fixes a bug in iOS 16 where videos from protected live streams are not shown. * Updates minimum Flutter version to 2.10. * Fixes violations of new analysis option use_named_constants. * Fixes avoid_redundant_argument_values lint warnings and minor typos. diff --git a/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart index 61959efe1e10..fb79a77fb2cb 100644 --- a/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart +++ b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart @@ -150,7 +150,7 @@ class MiniController extends ValueNotifier { /// Constructs a [MiniController] playing a video from obtained from a file. MiniController.file(File file) - : dataSource = 'file://${file.path}', + : dataSource = Uri.file(file.absolute.path).toString(), dataSourceType = DataSourceType.file, package = null, super(VideoPlayerValue(duration: Duration.zero)); @@ -318,7 +318,7 @@ class MiniController extends ValueNotifier { /// The position in the current video. Future get position async { - return await _platform.getPosition(_textureId); + return _platform.getPosition(_textureId); } /// Sets the video's current timestamp to be at [position]. diff --git a/packages/video_player/video_player_avfoundation/example/pubspec.yaml b/packages/video_player/video_player_avfoundation/example/pubspec.yaml index 9123b15aa721..422fb91e35e5 100644 --- a/packages/video_player/video_player_avfoundation/example/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: @@ -16,7 +16,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: ../ - video_player_platform_interface: ">=4.2.0 <6.0.0" + video_player_platform_interface: ">=4.2.0 <7.0.0" dev_dependencies: flutter_driver: diff --git a/packages/video_player/video_player_avfoundation/pigeons/messages.dart b/packages/video_player/video_player_avfoundation/pigeons/messages.dart index e6eda5960f29..695ff34e3ebd 100644 --- a/packages/video_player/video_player_avfoundation/pigeons/messages.dart +++ b/packages/video_player/video_player_avfoundation/pigeons/messages.dart @@ -6,7 +6,7 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions( dartOut: 'lib/src/messages.g.dart', - dartTestOut: 'test/test_api.dart', + dartTestOut: 'test/test_api.g.dart', objcHeaderOut: 'ios/Classes/messages.g.h', objcSourceOut: 'ios/Classes/messages.g.m', objcOptions: ObjcOptions( diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index 6da166791281..a5204137af20 100644 --- a/packages/video_player/video_player_avfoundation/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -2,11 +2,11 @@ name: video_player_avfoundation description: iOS implementation of the video_player plugin. repository: https://github.com/flutter/plugins/tree/main/packages/video_player/video_player_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.3.7 +version: 2.3.8 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - video_player_platform_interface: ">=4.2.0 <6.0.0" + video_player_platform_interface: ">=4.2.0 <7.0.0" dev_dependencies: flutter_test: diff --git a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart index ea81d438ad75..c01373f05424 100644 --- a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart +++ b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart @@ -12,7 +12,7 @@ import 'package:video_player_avfoundation/src/messages.g.dart'; import 'package:video_player_avfoundation/video_player_avfoundation.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; -import 'test_api.dart'; +import 'test_api.g.dart'; class _ApiLogger implements TestHostVideoPlayerApi { final List log = []; @@ -234,16 +234,16 @@ void main() { }); test('videoEventsFor', () async { - _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .setMockMessageHandler( 'flutter.io/videoPlayer/videoEvents123', (ByteData? message) async { final MethodCall methodCall = const StandardMethodCodec().decodeMethodCall(message); if (methodCall.method == 'listen') { - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -255,8 +255,8 @@ void main() { }), (ByteData? data) {}); - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -265,8 +265,8 @@ void main() { }), (ByteData? data) {}); - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -279,8 +279,8 @@ void main() { }), (ByteData? data) {}); - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -289,8 +289,8 @@ void main() { }), (ByteData? data) {}); - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -339,5 +339,4 @@ void main() { /// /// We use this so that APIs that have become non-nullable can still be used /// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. T? _ambiguate(T? value) => value; diff --git a/packages/video_player/video_player_avfoundation/test/test_api.dart b/packages/video_player/video_player_avfoundation/test/test_api.g.dart similarity index 100% rename from packages/video_player/video_player_avfoundation/test/test_api.dart rename to packages/video_player/video_player_avfoundation/test/test_api.g.dart diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index 05cb63835da4..e1acbf578027 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,3 +1,11 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 6.0.1 + +* Fixes comment describing file URI construction. + ## 6.0.0 * **BREAKING CHANGE**: Removes `MethodChannelVideoPlayer`. The default diff --git a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart index 92099eb6635a..d3df9b25df53 100644 --- a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart +++ b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart @@ -114,7 +114,7 @@ class DataSource { /// The [sourceType] is always required. /// /// The [uri] argument takes the form of `'https://example.com/video.mp4'` or - /// `'file://${file.path}'`. + /// `'file:///absolute/path/to/local/video.mp4`. /// /// The [formatHint] argument can be null. /// diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index 56e132dbb3a7..8c6a8f400bb2 100644 --- a/packages/video_player/video_player_platform_interface/pubspec.yaml +++ b/packages/video_player/video_player_platform_interface/pubspec.yaml @@ -4,11 +4,11 @@ repository: https://github.com/flutter/plugins/tree/main/packages/video_player/v issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%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: 6.0.0 +version: 6.0.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index 52cfd319349b..42355439ce12 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,5 +1,10 @@ ## NEXT +* Updates minimum Flutter version to 3.0. + +## 2.0.13 + +* Adds compatibilty with version 6.0 of the platform interface. * Updates minimum Flutter version to 2.10. ## 2.0.12 diff --git a/packages/video_player/video_player_web/example/pubspec.yaml b/packages/video_player/video_player_web/example/pubspec.yaml index 57728f323d55..c4de1ce54c1a 100644 --- a/packages/video_player/video_player_web/example/pubspec.yaml +++ b/packages/video_player/video_player_web/example/pubspec.yaml @@ -3,13 +3,13 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: sdk: flutter js: ^0.6.0 - video_player_platform_interface: ">=4.2.0 <6.0.0" + video_player_platform_interface: ">=4.2.0 <7.0.0" video_player_web: path: ../ diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index ad6c9bfa198f..5e603034dd28 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -2,11 +2,11 @@ name: video_player_web description: Web platform implementation of video_player. repository: https://github.com/flutter/plugins/tree/main/packages/video_player/video_player_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.0.12 +version: 2.0.13 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -21,7 +21,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - video_player_platform_interface: ">=4.2.0 <6.0.0" + video_player_platform_interface: ">=4.2.0 <7.0.0" dev_dependencies: flutter_test: diff --git a/packages/video_player/video_player_web/test/tests_exist_elsewhere_test.dart b/packages/video_player/video_player_web/test/tests_exist_elsewhere_test.dart index 442c50144727..cc32e6c72f1e 100644 --- a/packages/video_player/video_player_web/test/tests_exist_elsewhere_test.dart +++ b/packages/video_player/video_player_web/test/tests_exist_elsewhere_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: avoid_print + import 'package:flutter_test/flutter_test.dart'; void main() { diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index f278cf9f9d5b..84f890790128 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,11 +1,27 @@ -## NEXT +## 4.0.4 -* Updates code for `no_leading_underscores_for_local_identifiers` lint. -* Updates minimum Flutter version to 2.10. -* Fixes avoid_redundant_argument_values lint warnings and minor typos. -* Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/104231). +* Adds examples of accessing platform-specific features for each class. + +## 4.0.3 + +* Updates example code for `use_build_context_synchronously` lint. + +## 4.0.2 + +* Updates code for stricter lint checks. + +## 4.0.1 + +* Exposes `WebResourceErrorType` from platform interface. + +## 4.0.0 + +* **BREAKING CHANGE** Updates implementation to use the `2.0.0` release of + `webview_flutter_platform_interface`. See `Usage` section in the README for updated usage. See + `Migrating from 3.0 to 4.0` section in the README for details on migrating to this version. +* Updates minimum Flutter version to 3.0.0. +* Updates code for new analysis options. * Updates references to the obsolete master branch. -* Fixes typo from lowercase to uppercase. ## 3.0.4 diff --git a/packages/webview_flutter/webview_flutter/README.md b/packages/webview_flutter/webview_flutter/README.md index ffe91441326d..b30b8bc20fa1 100644 --- a/packages/webview_flutter/webview_flutter/README.md +++ b/packages/webview_flutter/webview_flutter/README.md @@ -1,10 +1,12 @@ # WebView for Flutter + + [![pub package](https://img.shields.io/pub/v/webview_flutter.svg)](https://pub.dev/packages/webview_flutter) A Flutter plugin that provides a WebView widget. -On iOS the WebView widget is backed by a [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview); +On iOS the WebView widget is backed by a [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview). On Android the WebView widget is backed by a [WebView](https://developer.android.com/reference/android/webkit/WebView). | | Android | iOS | @@ -12,30 +14,60 @@ On Android the WebView widget is backed by a [WebView](https://developer.android | **Support** | SDK 19+ or 20+ | 9.0+ | ## Usage -Add `webview_flutter` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). If you are targeting Android, make sure to read the *Android Platform Views* section below to choose the platform view mode that best suits your needs. - -You can now include a WebView widget in your widget tree. See the -[WebView](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebView-class.html) -widget's Dartdoc for more details on how to use the widget. +Add `webview_flutter` as a [dependency in your pubspec.yaml file](https://pub.dev/packages/webview_flutter/install). + +You can now display a WebView by: + +1. Instantiating a [WebViewController](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewController-class.html). + + +```dart +controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(const Color(0x00000000)) + ..setNavigationDelegate( + NavigationDelegate( + onProgress: (int progress) { + // Update loading bar. + }, + onPageStarted: (String url) {}, + onPageFinished: (String url) {}, + onWebResourceError: (WebResourceError error) {}, + onNavigationRequest: (NavigationRequest request) { + if (request.url.startsWith('https://www.youtube.com/')) { + return NavigationDecision.prevent; + } + return NavigationDecision.navigate; + }, + ), + ) + ..loadRequest(Uri.parse('https://flutter.dev')); +``` -## Android Platform Views -This plugin uses -[Platform Views](https://flutter.dev/docs/development/platform-integration/platform-views) to embed -the Android’s webview within the Flutter app. It supports two modes: -*hybrid composition* (the current default) and *virtual display*. +2. Passing the controller to a [WebViewWidget](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewWidget-class.html). -Here are some points to consider when choosing between the two: + +```dart +@override +Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Flutter Simple Example')), + body: WebViewWidget(controller: controller), + ); +} +``` -* *Hybrid composition* has built-in keyboard support while *virtual display* has multiple -[keyboard issues](https://github.com/flutter/flutter/issues?q=is%3Aopen+label%3Avd-only+label%3A%22p%3A+webview-keyboard%22). -* *Hybrid composition* requires Android SDK 19+ while *virtual display* requires Android SDK 20+. -* *Hybrid composition* and *virtual display* have different - [performance tradeoffs](https://flutter.dev/docs/development/platform-integration/platform-views#performance). +See the Dartdocs for [WebViewController](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewController-class.html) +and [WebViewWidget](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewWidget-class.html) +for more details. +### Android Platform Views -### Using Hybrid Composition +This plugin uses +[Platform Views](https://flutter.dev/docs/development/platform-integration/platform-views) to embed +the Android’s WebView within the Flutter app. -The mode is currently enabled by default. You should however make sure to set the correct `minSdkVersion` in `android/app/build.gradle` if it was previously lower than 19: +You should however make sure to set the correct `minSdkVersion` in `android/app/build.gradle` if it was previously lower than 19: ```groovy android { @@ -45,47 +77,67 @@ android { } ``` -### Using Virtual displays +### Platform-Specific Features -1. Set the correct `minSdkVersion` in `android/app/build.gradle` (if it was previously lower than 20): +Many classes have a subclass or an underlying implementation that provides access to platform-specific +features. - ```groovy - android { - defaultConfig { - minSdkVersion 20 - } - } - ``` +To access platform-specific features, start by adding the platform implementation packages to your +app or package: -2. Set `WebView.platform = AndroidWebView();` in `initState()`. - For example: +* **Android**: [webview_flutter_android](https://pub.dev/packages/webview_flutter_android/install) +* **iOS**: [webview_flutter_wkwebview](https://pub.dev/packages/webview_flutter_wkwebview/install) - ```dart - import 'dart:io'; +Next, add the imports of the implementation packages to your app or package: - import 'package:webview_flutter/webview_flutter.dart'; + +```dart +// Import for Android features. +import 'package:webview_flutter_android/webview_flutter_android.dart'; +// Import for iOS features. +import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; +``` - class WebViewExample extends StatefulWidget { - @override - WebViewExampleState createState() => WebViewExampleState(); - } +Now, additional features can be accessed through the platform implementations. Classes +[WebViewController], [WebViewWidget], [NavigationDelegate], and [WebViewCookieManager] pass their +functionality to a class provided by the current platform. Below are a couple of ways to access +additional functionality provided by the platform and is followed by an example. + +1. Pass a creation params class provided by a platform implementation to a `fromPlatformCreationParams` + constructor (e.g. `WebViewController.fromPlatformCreationParams`, + `WebViewWidget.fromPlatformCreationParams`, etc.). +2. Call methods on a platform implementation of a class by using the `platform` field (e.g. + `WebViewController.platform`, `WebViewWidget.platform`, etc.). + +Below is an example of setting additional iOS and Android parameters on the `WebViewController`. + + +```dart +late final PlatformWebViewControllerCreationParams params; +if (WebViewPlatform.instance is WebKitWebViewPlatform) { + params = WebKitWebViewControllerCreationParams( + allowsInlineMediaPlayback: true, + mediaTypesRequiringUserAction: const {}, + ); +} else { + params = const PlatformWebViewControllerCreationParams(); +} - class WebViewExampleState extends State { - @override - void initState() { - super.initState(); - // Enable virtual display. - if (Platform.isAndroid) WebView.platform = AndroidWebView(); - } - - @override - Widget build(BuildContext context) { - return WebView( - initialUrl: 'https://flutter.dev', - ); - } - } - ``` +final WebViewController controller = + WebViewController.fromPlatformCreationParams(params); +// ··· +if (controller.platform is AndroidWebViewController) { + AndroidWebViewController.enableDebugging(true); + (controller.platform as AndroidWebViewController) + .setMediaPlaybackRequiresUserGesture(false); +} +``` + +See https://pub.dev/documentation/webview_flutter_android/latest/webview_flutter_android/webview_flutter_android-library.html +for more details on Android features. + +See https://pub.dev/documentation/webview_flutter_wkwebview/latest/webview_flutter_wkwebview/webview_flutter_wkwebview-library.html +for more details on iOS features. ### Enable Material Components for Android @@ -95,4 +147,82 @@ follow the steps described in the [Enabling Material Components instructions](ht ### Setting custom headers on POST requests Currently, setting custom headers when making a post request with the WebViewController's `loadRequest` method is not supported on Android. -If you require this functionality, a workaround is to make the request manually, and then load the response data using `loadHTMLString` instead. +If you require this functionality, a workaround is to make the request manually, and then load the response data using `loadHtmlString` instead. + +## Migrating from 3.0 to 4.0 + +### Instantiating WebViewController + +In version 3.0 and below, `WebViewController` could only be retrieved in a callback after the +`WebView` was added to the widget tree. Now, `WebViewController` must be instantiated and can be +used before it is added to the widget tree. See `Usage` section above for an example. + +### Replacing WebView Functionality + +The `WebView` class has been removed and its functionality has been split into `WebViewController` +and `WebViewWidget`. + +`WebViewController` handles all functionality that is associated with the underlying web view +provided by each platform. (e.g., loading a url, setting the background color of the underlying +platform view, or clearing the cache). + +`WebViewWidget` takes a `WebViewController` and handles all Flutter widget related functionality +(e.g., layout direction, gesture recognizers). + +See the Dartdocs for [WebViewController](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewController-class.html) +and [WebViewWidget](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewWidget-class.html) +for more details. + +### PlatformView Implementation on Android + +The PlatformView implementation for Android is currently no longer configurable. It uses Texture +Layer Hybrid Composition on versions 23+ and automatically fallbacks to Hybrid Composition for +version 19-23. See https://github.com/flutter/flutter/issues/108106 for progress on manually +switching to Hybrid Composition on versions 23+. + +### API Changes + +Below is a non-exhaustive list of changes to the API: + +* `WebViewController.clearCache` no longer clears local storage. Please use + `WebViewController.clearLocalStorage`. +* `WebViewController.clearCache` no longer reloads the page. +* `WebViewController.loadUrl` has been removed. Please use `WebViewController.loadRequest`. +* `WebViewController.evaluateJavascript` has been removed. Please use + `WebViewController.runJavaScript` or `WebViewController.runJavaScriptReturningResult`. +* `WebViewController.getScrollX` and `WebViewController.getScrollY` have been removed and have + been replaced by `WebViewController.getScrollPosition`. +* `WebViewController.runJavaScriptReturningResult` now returns an `Object` and not a `String`. This + will attempt to return a `bool` or `num` if the return value can be parsed. +* `CookieManager` is replaced by `WebViewCookieManager`. +* `NavigationDelegate.onWebResourceError` callback includes errors that are not from the main frame. + Use the `WebResourceError.isForMainFrame` field to filter errors. +* The following fields from `WebView` have been moved to `NavigationDelegate`. They can be added to + a WebView with `WebViewController.setNavigationDelegate`. + * `WebView.navigationDelegate` -> `NavigationDelegate.onNavigationRequest` + * `WebView.onPageStarted` -> `NavigationDelegate.onPageStarted` + * `WebView.onPageFinished` -> `NavigationDelegate.onPageFinished` + * `WebView.onProgress` -> `NavigationDelegate.onProgress` + * `WebView.onWebResourceError` -> `NavigationDelegate.onWebResourceError` +* The following fields from `WebView` have been moved to `WebViewController`: + * `WebView.javascriptMode` -> `WebViewController.setJavaScriptMode` + * `WebView.javascriptChannels` -> + `WebViewController.addJavaScriptChannel`/`WebViewController.removeJavaScriptChannel` + * `WebView.zoomEnabled` -> `WebViewController.enableZoom` + * `WebView.userAgent` -> `WebViewController.setUserAgent` + * `WebView.backgroundColor` -> `WebViewController.setBackgroundColor` +* The following features have been moved to an Android implementation class. See section + `Platform-Specific Features` for details on accessing Android platform-specific features. + * `WebView.debuggingEnabled` -> `static AndroidWebViewController.enableDebugging` + * `WebView.initialMediaPlaybackPolicy` -> `AndroidWebViewController.setMediaPlaybackRequiresUserGesture` +* The following features have been moved to an iOS implementation class. See section + `Platform-Specific Features` for details on accessing iOS platform-specific features. + * `WebView.gestureNavigationEnabled` -> `WebKitWebViewController.setAllowsBackForwardNavigationGestures` + * `WebView.initialMediaPlaybackPolicy` -> `WebKitWebViewControllerCreationParams.mediaTypesRequiringUserAction` + * `WebView.allowsInlineMediaPlayback` -> `WebKitWebViewControllerCreationParams.allowsInlineMediaPlayback` + + +[WebViewController]: https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewController-class.html +[WebViewWidget]: https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewWidget-class.html +[NavigationDelegate]: https://pub.dev/documentation/webview_flutter/latest/webview_flutter/NavigationDelegate-class.html +[WebViewCookieManager]: https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewCookieManager-class.html \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter/example/android/gradle.properties b/packages/webview_flutter/webview_flutter/example/android/gradle.properties index a6738207fd15..e5611e4c7fa0 100644 --- a/packages/webview_flutter/webview_flutter/example/android/gradle.properties +++ b/packages/webview_flutter/webview_flutter/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.enableR8=true diff --git a/packages/webview_flutter/webview_flutter/example/build.excerpt.yaml b/packages/webview_flutter/webview_flutter/example/build.excerpt.yaml new file mode 100644 index 000000000000..46c1e754361f --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/build.excerpt.yaml @@ -0,0 +1,15 @@ +targets: + $default: + sources: + include: + - lib/** + # Some default includes that aren't really used here but will prevent + # false-negative warnings: + - $package$ + - lib/$lib$ + exclude: + - '**/.*/**' + - '**/build/**' + builders: + code_excerpter|code_excerpter: + enabled: true \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter/example/integration_test/legacy/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/example/integration_test/legacy/webview_flutter_test.dart new file mode 100644 index 000000000000..14539105d5d3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/integration_test/legacy/webview_flutter_test.dart @@ -0,0 +1,1382 @@ +// 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. + +// This test is run using `flutter drive` by the CI (see /script/tool/README.md +// in this repository for details on driving that tooling manually), but can +// also be run using `flutter test` directly during development. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231) +// ignore: unnecessary_import +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:webview_flutter/src/webview_flutter_legacy.dart'; + +Future main() async { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + final HttpServer server = await HttpServer.bind(InternetAddress.anyIPv4, 0); + server.forEach((HttpRequest request) { + if (request.uri.path == '/hello.txt') { + request.response.writeln('Hello, world.'); + } else if (request.uri.path == '/secondary.txt') { + request.response.writeln('How are you today?'); + } else if (request.uri.path == '/headers') { + request.response.writeln('${request.headers}'); + } else if (request.uri.path == '/favicon.ico') { + request.response.statusCode = HttpStatus.notFound; + } else { + fail('unexpected request: ${request.method} ${request.uri}'); + } + request.response.close(); + }); + final String prefixUrl = 'http://${server.address.address}:${server.port}'; + final String primaryUrl = '$prefixUrl/hello.txt'; + final String secondaryUrl = '$prefixUrl/secondary.txt'; + final String headersUrl = '$prefixUrl/headers'; + + testWidgets('initialUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageFinishedCompleter = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: pageFinishedCompleter.complete, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageFinishedCompleter.future; + + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }); + + testWidgets('loadUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = StreamController(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoads.add(url); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + + await controller.loadUrl(secondaryUrl); + await expectLater( + pageLoads.stream.firstWhere((String url) => url == secondaryUrl), + completion(secondaryUrl), + ); + }); + + testWidgets('evaluateJavascript', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + // ignore: deprecated_member_use + final String result = await controller.evaluateJavascript('1 + 1'); + expect(result, equals('2')); + }); + + testWidgets('loadUrl with headers', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageStarts = StreamController(); + final StreamController pageLoads = StreamController(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarts.add(url); + }, + onPageFinished: (String url) { + pageLoads.add(url); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final Map headers = { + 'test_header': 'flutter_test_header' + }; + await controller.loadUrl(headersUrl, headers: headers); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, headersUrl); + + await pageStarts.stream.firstWhere((String url) => url == currentUrl); + await pageLoads.stream.firstWhere((String url) => url == currentUrl); + + final String content = await controller + .runJavascriptReturningResult('document.documentElement.innerText'); + expect(content.contains('flutter_test_header'), isTrue); + }); + + testWidgets('JavascriptChannel', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final Completer channelCompleter = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + // This is the data URL for: '' + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'Echo', + onMessageReceived: (JavascriptMessage message) { + channelCompleter.complete(message.message); + }, + ), + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + expect(channelCompleter.isCompleted, isFalse); + await controller.runJavascript('Echo.postMessage("hello");'); + + await expectLater(channelCompleter.future, completion('hello')); + }); + + testWidgets('resize webview', (WidgetTester tester) async { + final Completer initialResizeCompleter = Completer(); + final Completer buttonTapResizeCompleter = Completer(); + final Completer onPageFinished = Completer(); + + bool resizeButtonTapped = false; + await tester.pumpWidget(ResizableWebView( + onResize: (_) { + if (resizeButtonTapped) { + buttonTapResizeCompleter.complete(); + } else { + initialResizeCompleter.complete(); + } + }, + onPageFinished: () => onPageFinished.complete(), + )); + await onPageFinished.future; + // Wait for a potential call to resize after page is loaded. + await initialResizeCompleter.future.timeout( + const Duration(seconds: 3), + onTimeout: () => null, + ); + + resizeButtonTapped = true; + await tester.tap(find.byKey(const ValueKey('resizeButton'))); + await tester.pumpAndSettle(); + expect(buttonTapResizeCompleter.future, completes); + }); + + testWidgets('set custom userAgent', (WidgetTester tester) async { + final Completer controllerCompleter1 = + Completer(); + final GlobalKey globalKey = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent1', + onWebViewCreated: (WebViewController controller) { + controllerCompleter1.complete(controller); + }, + ), + ), + ); + final WebViewController controller1 = await controllerCompleter1.future; + final String customUserAgent1 = await _getUserAgent(controller1); + expect(customUserAgent1, 'Custom_User_Agent1'); + // rebuild the WebView with a different user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent2', + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller1); + expect(customUserAgent2, 'Custom_User_Agent2'); + }); + + testWidgets('use default platform userAgent after webView is rebuilt', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final GlobalKey globalKey = GlobalKey(); + // Build the webView with no user agent to get the default platform user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: globalKey, + initialUrl: primaryUrl, + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String defaultPlatformUserAgent = await _getUserAgent(controller); + // rebuild the WebView with a custom user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent', + ), + ), + ); + final String customUserAgent = await _getUserAgent(controller); + expect(customUserAgent, 'Custom_User_Agent'); + // rebuilds the WebView with no user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller); + expect(customUserAgent2, defaultPlatformUserAgent); + }); + + group('Video playback policy', () { + late String videoTestBase64; + setUpAll(() async { + final ByteData videoData = + await rootBundle.load('assets/sample_video.mp4'); + final String base64VideoData = + base64Encode(Uint8List.view(videoData.buffer)); + final String videoTest = ''' + + Video auto play + + + + + + + '''; + videoTestBase64 = base64Encode(const Utf8Encoder().convert(videoTest)); + }); + + testWidgets('Auto media playback', (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(false)); + + controllerCompleter = Completer(); + pageLoaded = Completer(); + + // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + controller = await controllerCompleter.future; + await pageLoaded.future; + + isPaused = await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(true)); + }); + + testWidgets('Changes to initialMediaPlaybackPolicy are ignored', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(false)); + + pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + await controller.reload(); + + await pageLoaded.future; + + isPaused = await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(false)); + }); + + testWidgets('Video plays inline when allowsInlineMediaPlayback is true', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageLoaded = Completer(); + final Completer videoPlaying = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'VideoTestTime', + onMessageReceived: (JavascriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1 && !videoPlaying.isCompleted) { + videoPlaying.complete(null); + } + }, + ), + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + allowsInlineMediaPlayback: true, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + // Pump once to trigger the video play. + await tester.pump(); + + // Makes sure we get the correct event that indicates the video is actually playing. + await videoPlaying.future; + + final String fullScreen = + await controller.runJavascriptReturningResult('isFullScreen();'); + expect(fullScreen, _webviewBool(false)); + }); + + // allowsInlineMediaPlayback is a noop on Android, so it is skipped. + testWidgets( + 'Video plays full screen when allowsInlineMediaPlayback is false', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageLoaded = Completer(); + final Completer videoPlaying = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'VideoTestTime', + onMessageReceived: (JavascriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1 && !videoPlaying.isCompleted) { + videoPlaying.complete(null); + } + }, + ), + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + // Pump once to trigger the video play. + await tester.pump(); + + // Makes sure we get the correct event that indicates the video is actually playing. + await videoPlaying.future; + + final String fullScreen = + await controller.runJavascriptReturningResult('isFullScreen();'); + expect(fullScreen, _webviewBool(true)); + }, skip: Platform.isAndroid); + }); + + group('Audio playback policy', () { + late String audioTestBase64; + setUpAll(() async { + final ByteData audioData = + await rootBundle.load('assets/sample_audio.ogg'); + final String base64AudioData = + base64Encode(Uint8List.view(audioData.buffer)); + final String audioTest = ''' + + Audio auto play + + + + + + + '''; + audioTestBase64 = base64Encode(const Utf8Encoder().convert(audioTest)); + }); + + testWidgets('Auto media playback', (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageStarted = Completer(); + Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(false)); + + controllerCompleter = Completer(); + pageStarted = Completer(); + pageLoaded = Completer(); + + // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + isPaused = await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(true)); + }); + + testWidgets('Changes to initialMediaPlaybackPolicy are ignored', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageStarted = Completer(); + Completer pageLoaded = Completer(); + + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(false)); + + pageStarted = Completer(); + pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + await controller.reload(); + + await pageStarted.future; + await pageLoaded.future; + + isPaused = await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(false)); + }); + }); + + testWidgets('getTitle', (WidgetTester tester) async { + const String getTitleTest = ''' + + Some title + + + + + '''; + final String getTitleTestBase64 = + base64Encode(const Utf8Encoder().convert(getTitleTest)); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + // On at least iOS, it does not appear to be guaranteed that the native + // code has the title when the page load completes. Execute some JavaScript + // before checking the title to ensure that the page has been fully parsed + // and processed. + await controller.runJavascript('1;'); + + final String? title = await controller.getTitle(); + expect(title, 'Some title'); + }); + + group('Programmatic Scroll', () { + testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { + const String scrollTestPage = ''' + + + + + + +
+ + + '''; + + final String scrollTestPageBase64 = + base64Encode(const Utf8Encoder().convert(scrollTestPage)); + + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + int scrollPosX = await controller.getScrollX(); + int scrollPosY = await controller.getScrollY(); + + // Check scrollTo() + const int X_SCROLL = 123; + const int Y_SCROLL = 321; + // Get the initial position; this ensures that scrollTo is actually + // changing something, but also gives the native view's scroll position + // time to settle. + expect(scrollPosX, isNot(X_SCROLL)); + expect(scrollPosX, isNot(Y_SCROLL)); + + await controller.scrollTo(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL); + expect(scrollPosY, Y_SCROLL); + + // Check scrollBy() (on top of scrollTo()) + await controller.scrollBy(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL * 2); + expect(scrollPosY, Y_SCROLL * 2); + }); + }); + + // Minimal end-to-end testing of the legacy Android implementation. + group('AndroidWebView (virtual display)', () { + setUpAll(() { + WebView.platform = AndroidWebView(); + }); + + tearDownAll(() { + WebView.platform = null; + }); + + testWidgets('initialUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageFinishedCompleter = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: pageFinishedCompleter.complete, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageFinishedCompleter.future; + + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }); + }, skip: !Platform.isAndroid); + + group('NavigationDelegate', () { + const String blankPage = ''; + final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + '${base64Encode(const Utf8Encoder().convert(blankPage))}'; + + testWidgets('can allow requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller.runJavascript('location.href = "$secondaryUrl"'); + + await pageLoads.stream.first; // Wait for the next page load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }); + + testWidgets('onWebResourceError', (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://www.notawebsite..com', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + ), + ), + ); + + final WebResourceError error = await errorCompleter.future; + expect(error, isNotNull); + + if (Platform.isIOS) { + expect(error.domain, isNotNull); + expect(error.failingUrl, isNull); + } else if (Platform.isAndroid) { + expect(error.errorType, isNotNull); + expect(error.failingUrl?.startsWith('https://www.notawebsite..com'), + isTrue); + } + }); + + testWidgets('onWebResourceError is not called with valid url', + (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }); + + testWidgets( + 'onWebResourceError only called for main frame', + (WidgetTester tester) async { + const String iframeTest = ''' + + + + WebResourceError test + + + + + + '''; + final String iframeTestBase64 = + base64Encode(const Utf8Encoder().convert(iframeTest)); + + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,$iframeTestBase64', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }, + ); + + testWidgets('can block requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .runJavascript('location.href = "https://www.youtube.com/"'); + + // There should never be any second page load, since our new URL is + // blocked. Still wait for a potential page change for some time in order + // to give the test a chance to fail. + await pageLoads.stream.first + .timeout(const Duration(milliseconds: 500), onTimeout: () => ''); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, isNot(contains('youtube.com'))); + }); + + testWidgets('supports asynchronous decisions', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) async { + NavigationDecision decision = NavigationDecision.prevent; + decision = await Future.delayed( + const Duration(milliseconds: 10), + () => NavigationDecision.navigate); + return decision; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller.runJavascript('location.href = "$secondaryUrl"'); + + await pageLoads.stream.first; // Wait for second page to load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }); + }); + + testWidgets('launches with gestureNavigationEnabled on iOS', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: SizedBox( + width: 400, + height: 300, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + gestureNavigationEnabled: true, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }); + + testWidgets('target _blank opens in same window', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageLoaded = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller.runJavascript('window.open("$primaryUrl", "_blank")'); + await pageLoaded.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }); + + testWidgets( + 'can open new window and go back', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(); + }, + initialUrl: primaryUrl, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + expect(controller.currentUrl(), completion(primaryUrl)); + await pageLoaded.future; + pageLoaded = Completer(); + + await controller.runJavascript('window.open("$secondaryUrl")'); + await pageLoaded.future; + pageLoaded = Completer(); + expect(controller.currentUrl(), completion(secondaryUrl)); + + expect(controller.canGoBack(), completion(true)); + await controller.goBack(); + await pageLoaded.future; + await expectLater(controller.currentUrl(), completion(primaryUrl)); + }, + ); + + testWidgets( + 'clearCache should clear local storage', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + + Completer pageLoadCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (_) => pageLoadCompleter.complete(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + + await pageLoadCompleter.future; + pageLoadCompleter = Completer(); + + final WebViewController controller = await controllerCompleter.future; + await controller.runJavascript('localStorage.setItem("myCat", "Tom");'); + final String myCatItem = await controller.runJavascriptReturningResult( + 'localStorage.getItem("myCat");', + ); + expect(myCatItem, _webviewString('Tom')); + + await controller.clearCache(); + await pageLoadCompleter.future; + + late final String? nullItem; + try { + nullItem = await controller.runJavascriptReturningResult( + 'localStorage.getItem("myCat");', + ); + } catch (exception) { + if (defaultTargetPlatform == TargetPlatform.iOS && + exception is ArgumentError && + (exception.message as String).contains( + 'Result of JavaScript execution returned a `null` value.')) { + nullItem = ''; + } + } + expect(nullItem, _webviewNull()); + }, + ); +} + +// JavaScript booleans evaluate to different string values on Android and iOS. +// This utility method returns the string boolean value of the current platform. +String _webviewBool(bool value) { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return value ? '1' : '0'; + } + return value ? 'true' : 'false'; +} + +// JavaScript `null` evaluate to different string values on Android and iOS. +// This utility method returns the string boolean value of the current platform. +String _webviewNull() { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return ''; + } + return 'null'; +} + +// JavaScript String evaluate to different string values on Android and iOS. +// This utility method returns the string boolean value of the current platform. +String _webviewString(String value) { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return value; + } + return '"$value"'; +} + +/// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. +Future _getUserAgent(WebViewController controller) async { + return _runJavascriptReturningResult(controller, 'navigator.userAgent;'); +} + +Future _runJavascriptReturningResult( + WebViewController controller, String js) async { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return controller.runJavascriptReturningResult(js); + } + return jsonDecode(await controller.runJavascriptReturningResult(js)) + as String; +} + +class ResizableWebView extends StatefulWidget { + const ResizableWebView({ + super.key, + required this.onResize, + required this.onPageFinished, + }); + + final JavascriptMessageHandler onResize; + final VoidCallback onPageFinished; + + @override + State createState() => ResizableWebViewState(); +} + +class ResizableWebViewState extends State { + double webViewWidth = 200; + double webViewHeight = 200; + + static const String resizePage = ''' + + Resize test + + + + + + '''; + + @override + Widget build(BuildContext context) { + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizePage)); + return Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: webViewWidth, + height: webViewHeight, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + javascriptChannels: { + JavascriptChannel( + name: 'Resize', + onMessageReceived: widget.onResize, + ), + }, + onPageFinished: (_) => widget.onPageFinished(), + javascriptMode: JavascriptMode.unrestricted, + ), + ), + TextButton( + key: const Key('resizeButton'), + onPressed: () { + setState(() { + webViewWidth += 100.0; + webViewHeight += 100.0; + }); + }, + child: const Text('ResizeButton'), + ), + ], + ), + ); + } +} diff --git a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart index 63f43848508b..7763327df582 100644 --- a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -19,6 +19,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_android/webview_flutter_android.dart'; +import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; Future main() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -43,163 +45,93 @@ Future main() async { final String secondaryUrl = '$prefixUrl/secondary.txt'; final String headersUrl = '$prefixUrl/headers'; - testWidgets('initialUrl', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final Completer pageFinishedCompleter = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageFinished: pageFinishedCompleter.complete, - ), - ), - ); + testWidgets('loadRequest', (WidgetTester tester) async { + final Completer pageFinished = Completer(); + + final WebViewController controller = WebViewController() + ..setNavigationDelegate( + NavigationDelegate(onPageFinished: (_) => pageFinished.complete()), + ) + ..loadRequest(Uri.parse(primaryUrl)); - final WebViewController controller = await controllerCompleter.future; - await pageFinishedCompleter.future; + await tester.pumpWidget(WebViewWidget(controller: controller)); + + await pageFinished.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); }); - testWidgets('loadUrl', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageLoads = StreamController(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageFinished: (String url) { - pageLoads.add(url); - }, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; + testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async { + final Completer pageFinished = Completer(); - await controller.loadUrl(secondaryUrl); - await expectLater( - pageLoads.stream.firstWhere((String url) => url == secondaryUrl), - completion(secondaryUrl), - ); - }); + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate(onPageFinished: (_) => pageFinished.complete()), + ) + ..loadRequest(Uri.parse(primaryUrl)); - testWidgets('evaluateJavascript', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - ), - ), + await tester.pumpWidget(WebViewWidget(controller: controller)); + + await pageFinished.future; + + await expectLater( + controller.runJavaScriptReturningResult('1 + 1'), + completion(2), ); - final WebViewController controller = await controllerCompleter.future; - // ignore: deprecated_member_use - final String result = await controller.evaluateJavascript('1 + 1'); - expect(result, equals('2')); }); - testWidgets('loadUrl with headers', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageStarts = StreamController(); - final StreamController pageLoads = StreamController(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (String url) { - pageStarts.add(url); - }, - onPageFinished: (String url) { - pageLoads.add(url); - }, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; + testWidgets('loadRequest with headers', (WidgetTester tester) async { final Map headers = { 'test_header': 'flutter_test_header' }; - await controller.loadUrl(headersUrl, headers: headers); - final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, headersUrl); - await pageStarts.stream.firstWhere((String url) => url == currentUrl); - await pageLoads.stream.firstWhere((String url) => url == currentUrl); + final StreamController pageLoads = StreamController(); - final String content = await controller - .runJavascriptReturningResult('document.documentElement.innerText'); + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate(onPageFinished: (String url) => pageLoads.add(url)), + ); + + await tester.pumpWidget(WebViewWidget(controller: controller)); + + controller.loadRequest(Uri.parse(headersUrl), headers: headers); + + await pageLoads.stream.firstWhere((String url) => url == headersUrl); + + final String content = await controller.runJavaScriptReturningResult( + 'document.documentElement.innerText', + ) as String; expect(content.contains('flutter_test_header'), isTrue); }); testWidgets('JavascriptChannel', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final Completer pageStarted = Completer(); - final Completer pageLoaded = Completer(); + final Completer pageFinished = Completer(); + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate(onPageFinished: (_) => pageFinished.complete()), + ); + final Completer channelCompleter = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - // This is the data URL for: '' - initialUrl: - 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - javascriptChannels: { - JavascriptChannel( - name: 'Echo', - onMessageReceived: (JavascriptMessage message) { - channelCompleter.complete(message.message); - }, - ), - }, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), - ), + await controller.addJavaScriptChannel( + 'Echo', + onMessageReceived: (JavaScriptMessage message) { + channelCompleter.complete(message.message); + }, + ); + + await controller.loadHtmlString( + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', ); - final WebViewController controller = await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - expect(channelCompleter.isCompleted, isFalse); - await controller.runJavascript('Echo.postMessage("hello");'); + await tester.pumpWidget(WebViewWidget(controller: controller)); + await pageFinished.future; + + await controller.runJavaScript('Echo.postMessage("hello");'); await expectLater(channelCompleter.future, completion('hello')); }); @@ -210,7 +142,7 @@ Future main() async { bool resizeButtonTapped = false; await tester.pumpWidget(ResizableWebView( - onResize: (_) { + onResize: () { if (resizeButtonTapped) { buttonTapResizeCompleter.complete(); } else { @@ -219,6 +151,7 @@ Future main() async { }, onPageFinished: () => onPageFinished.complete(), )); + await onPageFinished.future; // Wait for a potential call to resize after page is loaded. await initialResizeCompleter.future.timeout( @@ -227,98 +160,30 @@ Future main() async { ); resizeButtonTapped = true; + await tester.tap(find.byKey(const ValueKey('resizeButton'))); await tester.pumpAndSettle(); - expect(buttonTapResizeCompleter.future, completes); + + await expectLater(buttonTapResizeCompleter.future, completes); }); testWidgets('set custom userAgent', (WidgetTester tester) async { - final Completer controllerCompleter1 = - Completer(); - final GlobalKey globalKey = GlobalKey(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: globalKey, - initialUrl: 'about:blank', - javascriptMode: JavascriptMode.unrestricted, - userAgent: 'Custom_User_Agent1', - onWebViewCreated: (WebViewController controller) { - controllerCompleter1.complete(controller); - }, - ), - ), - ); - final WebViewController controller1 = await controllerCompleter1.future; - final String customUserAgent1 = await _getUserAgent(controller1); - expect(customUserAgent1, 'Custom_User_Agent1'); - // rebuild the WebView with a different user agent. - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: globalKey, - initialUrl: 'about:blank', - javascriptMode: JavascriptMode.unrestricted, - userAgent: 'Custom_User_Agent2', - ), - ), - ); + final Completer pageFinished = Completer(); - final String customUserAgent2 = await _getUserAgent(controller1); - expect(customUserAgent2, 'Custom_User_Agent2'); - }); + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageFinished.complete(), + )) + ..setUserAgent('Custom_User_Agent1') + ..loadRequest(Uri.parse('about:blank')); - testWidgets('use default platform userAgent after webView is rebuilt', - (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final GlobalKey globalKey = GlobalKey(); - // Build the webView with no user agent to get the default platform user agent. - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: globalKey, - initialUrl: primaryUrl, - javascriptMode: JavascriptMode.unrestricted, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - final String defaultPlatformUserAgent = await _getUserAgent(controller); - // rebuild the WebView with a custom user agent. - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: globalKey, - initialUrl: 'about:blank', - javascriptMode: JavascriptMode.unrestricted, - userAgent: 'Custom_User_Agent', - ), - ), - ); - final String customUserAgent = await _getUserAgent(controller); - expect(customUserAgent, 'Custom_User_Agent'); - // rebuilds the WebView with no user agent. - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: globalKey, - initialUrl: 'about:blank', - javascriptMode: JavascriptMode.unrestricted, - ), - ), - ); + await tester.pumpWidget(WebViewWidget(controller: controller)); + + await pageFinished.future; - final String customUserAgent2 = await _getUserAgent(controller); - expect(customUserAgent2, defaultPlatformUserAgent); + final String customUserAgent = await _getUserAgent(controller); + expect(customUserAgent, 'Custom_User_Agent1'); }); group('Video playback policy', () { @@ -362,219 +227,156 @@ Future main() async { }); testWidgets('Auto media playback', (WidgetTester tester) async { - Completer controllerCompleter = - Completer(); Completer pageLoaded = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - ), - ), - ); - WebViewController controller = await controllerCompleter.future; - await pageLoaded.future; + late PlatformWebViewControllerCreationParams params; + if (defaultTargetPlatform == TargetPlatform.iOS) { + params = WebKitWebViewControllerCreationParams( + mediaTypesRequiringUserAction: const {}, + ); + } else { + params = const PlatformWebViewControllerCreationParams(); + } - String isPaused = - await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(false)); + WebViewController controller = + WebViewController.fromPlatformCreationParams(params) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate(onPageFinished: (_) => pageLoaded.complete()), + ); - controllerCompleter = Completer(); - pageLoaded = Completer(); + if (controller.platform is AndroidWebViewController) { + (controller.platform as AndroidWebViewController) + .setMediaPlaybackRequiresUserGesture(false); + } - // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), - ), + await controller.loadRequest( + Uri.parse('data:text/html;charset=utf-8;base64,$videoTestBase64'), ); - controller = await controllerCompleter.future; - await pageLoaded.future; - - isPaused = await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(true)); - }); - - testWidgets('Changes to initialMediaPlaybackPolicy are ignored', - (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - Completer pageLoaded = Completer(); + await tester.pumpWidget(WebViewWidget(controller: controller)); - final GlobalKey key = GlobalKey(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = - await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(false)); + bool isPaused = + await controller.runJavaScriptReturningResult('isPaused();') as bool; + expect(isPaused, false); pageLoaded = Completer(); + controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate(onPageFinished: (_) => pageLoaded.complete()), + ) + ..loadRequest( + Uri.parse('data:text/html;charset=utf-8;base64,$videoTestBase64'), + ); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), - ), - ); - - await controller.reload(); + await tester.pumpWidget(WebViewWidget(controller: controller)); await pageLoaded.future; - isPaused = await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(false)); + isPaused = + await controller.runJavaScriptReturningResult('isPaused();') as bool; + expect(isPaused, true); }); - testWidgets('Video plays inline when allowsInlineMediaPlayback is true', - (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); + testWidgets('Video plays inline', (WidgetTester tester) async { final Completer pageLoaded = Completer(); final Completer videoPlaying = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - javascriptChannels: { - JavascriptChannel( - name: 'VideoTestTime', - onMessageReceived: (JavascriptMessage message) { - final double currentTime = double.parse(message.message); - // Let it play for at least 1 second to make sure the related video's properties are set. - if (currentTime > 1 && !videoPlaying.isCompleted) { - videoPlaying.complete(null); - } - }, - ), - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - allowsInlineMediaPlayback: true, - ), - ), + late PlatformWebViewControllerCreationParams params; + if (defaultTargetPlatform == TargetPlatform.iOS) { + params = WebKitWebViewControllerCreationParams( + mediaTypesRequiringUserAction: const {}, + allowsInlineMediaPlayback: true, + ); + } else { + params = const PlatformWebViewControllerCreationParams(); + } + final WebViewController controller = + WebViewController.fromPlatformCreationParams(params) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate(onPageFinished: (_) => pageLoaded.complete()), + ) + ..addJavaScriptChannel( + 'VideoTestTime', + onMessageReceived: (JavaScriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1 && !videoPlaying.isCompleted) { + videoPlaying.complete(null); + } + }, + ); + + if (controller.platform is AndroidWebViewController) { + (controller.platform as AndroidWebViewController) + .setMediaPlaybackRequiresUserGesture(false); + } + + await controller.loadRequest( + Uri.parse('data:text/html;charset=utf-8;base64,$videoTestBase64'), ); - final WebViewController controller = await controllerCompleter.future; - await pageLoaded.future; - // Pump once to trigger the video play. - await tester.pump(); + await tester.pumpWidget(WebViewWidget(controller: controller)); + await tester.pumpAndSettle(); + + await pageLoaded.future; // Makes sure we get the correct event that indicates the video is actually playing. await videoPlaying.future; - final String fullScreen = - await controller.runJavascriptReturningResult('isFullScreen();'); - expect(fullScreen, _webviewBool(false)); + final bool fullScreen = await controller + .runJavaScriptReturningResult('isFullScreen();') as bool; + expect(fullScreen, false); }); // allowsInlineMediaPlayback is a noop on Android, so it is skipped. testWidgets( 'Video plays full screen when allowsInlineMediaPlayback is false', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); final Completer pageLoaded = Completer(); final Completer videoPlaying = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - javascriptChannels: { - JavascriptChannel( - name: 'VideoTestTime', - onMessageReceived: (JavascriptMessage message) { - final double currentTime = double.parse(message.message); - // Let it play for at least 1 second to make sure the related video's properties are set. - if (currentTime > 1 && !videoPlaying.isCompleted) { - videoPlaying.complete(null); - } - }, + final WebViewController controller = + WebViewController.fromPlatformCreationParams( + WebKitWebViewControllerCreationParams( + mediaTypesRequiringUserAction: const {}, + ), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate(onPageFinished: (_) => pageLoaded.complete()), + ) + ..addJavaScriptChannel( + 'VideoTestTime', + onMessageReceived: (JavaScriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1 && !videoPlaying.isCompleted) { + videoPlaying.complete(null); + } + }, + ) + ..loadRequest( + Uri.parse( + 'data:text/html;charset=utf-8;base64,$videoTestBase64', ), - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - await pageLoaded.future; + ); - // Pump once to trigger the video play. - await tester.pump(); + await tester.pumpWidget(WebViewWidget(controller: controller)); + await tester.pumpAndSettle(); + + await pageLoaded.future; // Makes sure we get the correct event that indicates the video is actually playing. await videoPlaying.future; - final String fullScreen = - await controller.runJavascriptReturningResult('isFullScreen();'); - expect(fullScreen, _webviewBool(true)); + final bool fullScreen = await controller + .runJavaScriptReturningResult('isFullScreen();') as bool; + expect(fullScreen, true); }, skip: Platform.isAndroid); }); @@ -610,138 +412,60 @@ Future main() async { }); testWidgets('Auto media playback', (WidgetTester tester) async { - Completer controllerCompleter = - Completer(); - Completer pageStarted = Completer(); Completer pageLoaded = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - ), - ), - ); - WebViewController controller = await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; + late PlatformWebViewControllerCreationParams params; + if (defaultTargetPlatform == TargetPlatform.iOS) { + params = WebKitWebViewControllerCreationParams( + mediaTypesRequiringUserAction: const {}, + ); + } else { + params = const PlatformWebViewControllerCreationParams(); + } - String isPaused = - await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(false)); + WebViewController controller = + WebViewController.fromPlatformCreationParams(params) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate(onPageFinished: (_) => pageLoaded.complete()), + ); - controllerCompleter = Completer(); - pageStarted = Completer(); - pageLoaded = Completer(); + if (controller.platform is AndroidWebViewController) { + (controller.platform as AndroidWebViewController) + .setMediaPlaybackRequiresUserGesture(false); + } - // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), - ), + await controller.loadRequest( + Uri.parse('data:text/html;charset=utf-8;base64,$audioTestBase64'), ); - controller = await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - - isPaused = await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(true)); - }); - - testWidgets('Changes to initialMediaPlaybackPolicy are ignored', - (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - Completer pageStarted = Completer(); - Completer pageLoaded = Completer(); + await tester.pumpWidget(WebViewWidget(controller: controller)); + await tester.pumpAndSettle(); - final GlobalKey key = GlobalKey(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - await pageStarted.future; await pageLoaded.future; - String isPaused = - await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(false)); + bool isPaused = + await controller.runJavaScriptReturningResult('isPaused();') as bool; + expect(isPaused, false); - pageStarted = Completer(); pageLoaded = Completer(); + controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate(onPageFinished: (_) => pageLoaded.complete()), + ) + ..loadRequest( + Uri.parse('data:text/html;charset=utf-8;base64,$audioTestBase64'), + ); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), - ), - ); - - await controller.reload(); + await tester.pumpWidget(WebViewWidget(controller: controller)); + await tester.pumpAndSettle(); - await pageStarted.future; await pageLoaded.future; - isPaused = await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(false)); + isPaused = + await controller.runJavaScriptReturningResult('isPaused();') as bool; + expect(isPaused, true); }); }); @@ -756,39 +480,26 @@ Future main() async { '''; final String getTitleTestBase64 = base64Encode(const Utf8Encoder().convert(getTitleTest)); - final Completer pageStarted = Completer(); final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', - javascriptMode: JavascriptMode.unrestricted, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - await pageStarted.future; + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageLoaded.complete(), + )) + ..loadRequest( + Uri.parse('data:text/html;charset=utf-8;base64,$getTitleTestBase64'), + ); + + await tester.pumpWidget(WebViewWidget(controller: controller)); + await pageLoaded.future; // On at least iOS, it does not appear to be guaranteed that the native // code has the title when the page load completes. Execute some JavaScript // before checking the title to ensure that the page has been fully parsed // and processed. - await controller.runJavascript('1;'); + await controller.runJavaScript('1;'); final String? title = await controller.getTitle(); expect(title, 'Some title'); @@ -821,32 +532,22 @@ Future main() async { base64Encode(const Utf8Encoder().convert(scrollTestPage)); final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - initialUrl: - 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), - ), - ); + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageLoaded.complete(), + )) + ..loadRequest(Uri.parse( + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + )); + + await tester.pumpWidget(WebViewWidget(controller: controller)); - final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; await tester.pumpAndSettle(const Duration(seconds: 3)); - int scrollPosX = await controller.getScrollX(); - int scrollPosY = await controller.getScrollY(); + Offset scrollPos = await controller.getScrollPosition(); // Check scrollTo() const int X_SCROLL = 123; @@ -854,95 +555,51 @@ Future main() async { // Get the initial position; this ensures that scrollTo is actually // changing something, but also gives the native view's scroll position // time to settle. - expect(scrollPosX, isNot(X_SCROLL)); - expect(scrollPosX, isNot(Y_SCROLL)); + expect(scrollPos.dx, isNot(X_SCROLL)); + expect(scrollPos.dy, isNot(Y_SCROLL)); await controller.scrollTo(X_SCROLL, Y_SCROLL); - scrollPosX = await controller.getScrollX(); - scrollPosY = await controller.getScrollY(); - expect(scrollPosX, X_SCROLL); - expect(scrollPosY, Y_SCROLL); + scrollPos = await controller.getScrollPosition(); + expect(scrollPos.dx, X_SCROLL); + expect(scrollPos.dy, Y_SCROLL); // Check scrollBy() (on top of scrollTo()) await controller.scrollBy(X_SCROLL, Y_SCROLL); - scrollPosX = await controller.getScrollX(); - scrollPosY = await controller.getScrollY(); - expect(scrollPosX, X_SCROLL * 2); - expect(scrollPosY, Y_SCROLL * 2); + scrollPos = await controller.getScrollPosition(); + expect(scrollPos.dx, X_SCROLL * 2); + expect(scrollPos.dy, Y_SCROLL * 2); }); }); - // Minimal end-to-end testing of the legacy Android implementation. - group('AndroidWebView (virtual display)', () { - setUpAll(() { - WebView.platform = AndroidWebView(); - }); - - tearDownAll(() { - WebView.platform = null; - }); - - testWidgets('initialUrl', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final Completer pageFinishedCompleter = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageFinished: pageFinishedCompleter.complete, - ), - ), - ); - - final WebViewController controller = await controllerCompleter.future; - await pageFinishedCompleter.future; - - final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, primaryUrl); - }); - }, skip: !Platform.isAndroid); - group('NavigationDelegate', () { const String blankPage = ''; final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' '${base64Encode(const Utf8Encoder().convert(blankPage))}'; testWidgets('can allow requests', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageLoads = - StreamController.broadcast(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: blankPageEncoded, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - navigationDelegate: (NavigationRequest request) { - return (request.url.contains('youtube.com')) - ? NavigationDecision.prevent - : NavigationDecision.navigate; - }, - onPageFinished: (String url) => pageLoads.add(url), - ), - ), - ); + Completer pageLoaded = Completer(); + + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageLoaded.complete(), + onNavigationRequest: (NavigationRequest navigationRequest) { + return (navigationRequest.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + )); + + await tester.pumpWidget(WebViewWidget(controller: controller)); - await pageLoads.stream.first; // Wait for initial page load. - final WebViewController controller = await controllerCompleter.future; - await controller.runJavascript('location.href = "$secondaryUrl"'); + controller.loadRequest(Uri.parse(blankPageEncoded)); + + await pageLoaded.future; // Wait for initial page load. + + pageLoaded = Completer(); + await controller.runJavaScript('location.href = "$secondaryUrl"'); + await pageLoaded.future; // Wait for the next page load. - await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); expect(currentUrl, secondaryUrl); }); @@ -951,30 +608,18 @@ Future main() async { final Completer errorCompleter = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'https://www.notawebsite..com', - onWebResourceError: (WebResourceError error) { - errorCompleter.complete(error); - }, - ), - ), - ); + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate(onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + })) + ..loadRequest(Uri.parse('https://www.notawebsite..com')); + + await tester.pumpWidget(WebViewWidget(controller: controller)); final WebResourceError error = await errorCompleter.future; expect(error, isNotNull); - - if (Platform.isIOS) { - expect(error.domain, isNotNull); - expect(error.failingUrl, isNull); - } else if (Platform.isAndroid) { - expect(error.errorType, isNotNull); - expect(error.failingUrl?.startsWith('https://www.notawebsite..com'), - isTrue); - } }); testWidgets('onWebResourceError is not called with valid url', @@ -983,190 +628,99 @@ Future main() async { Completer(); final Completer pageFinishCompleter = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: - 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', - onWebResourceError: (WebResourceError error) { - errorCompleter.complete(error); - }, - onPageFinished: (_) => pageFinishCompleter.complete(), - ), - ), - ); + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageFinishCompleter.complete(), + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + )) + ..loadRequest( + Uri.parse('data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+'), + ); + + await tester.pumpWidget(WebViewWidget(controller: controller)); expect(errorCompleter.future, doesNotComplete); await pageFinishCompleter.future; }); - testWidgets( - 'onWebResourceError only called for main frame', - (WidgetTester tester) async { - const String iframeTest = ''' - - - - WebResourceError test - - - - - - '''; - final String iframeTestBase64 = - base64Encode(const Utf8Encoder().convert(iframeTest)); - - final Completer errorCompleter = - Completer(); - final Completer pageFinishCompleter = Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: - 'data:text/html;charset=utf-8;base64,$iframeTestBase64', - onWebResourceError: (WebResourceError error) { - errorCompleter.complete(error); - }, - onPageFinished: (_) => pageFinishCompleter.complete(), - ), - ), - ); - - expect(errorCompleter.future, doesNotComplete); - await pageFinishCompleter.future; - }, - ); - testWidgets('can block requests', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageLoads = - StreamController.broadcast(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: blankPageEncoded, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - navigationDelegate: (NavigationRequest request) { - return (request.url.contains('youtube.com')) + Completer pageLoaded = Completer(); + + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageLoaded.complete(), + onNavigationRequest: (NavigationRequest navigationRequest) { + return (navigationRequest.url.contains('youtube.com')) ? NavigationDecision.prevent : NavigationDecision.navigate; - }, - onPageFinished: (String url) => pageLoads.add(url), - ), - ), - ); + })); - await pageLoads.stream.first; // Wait for initial page load. - final WebViewController controller = await controllerCompleter.future; + await tester.pumpWidget(WebViewWidget(controller: controller)); + + controller.loadRequest(Uri.parse(blankPageEncoded)); + + await pageLoaded.future; // Wait for initial page load. + + pageLoaded = Completer(); await controller - .runJavascript('location.href = "https://www.youtube.com/"'); + .runJavaScript('location.href = "https://www.youtube.com/"'); // There should never be any second page load, since our new URL is // blocked. Still wait for a potential page change for some time in order // to give the test a chance to fail. - await pageLoads.stream.first + await pageLoaded.future .timeout(const Duration(milliseconds: 500), onTimeout: () => ''); final String? currentUrl = await controller.currentUrl(); expect(currentUrl, isNot(contains('youtube.com'))); }); testWidgets('supports asynchronous decisions', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageLoads = - StreamController.broadcast(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: blankPageEncoded, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - navigationDelegate: (NavigationRequest request) async { + Completer pageLoaded = Completer(); + + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageLoaded.complete(), + onNavigationRequest: (NavigationRequest navigationRequest) async { NavigationDecision decision = NavigationDecision.prevent; decision = await Future.delayed( const Duration(milliseconds: 10), () => NavigationDecision.navigate); return decision; - }, - onPageFinished: (String url) => pageLoads.add(url), - ), - ), - ); + })); + + await tester.pumpWidget(WebViewWidget(controller: controller)); + + controller.loadRequest(Uri.parse(blankPageEncoded)); - await pageLoads.stream.first; // Wait for initial page load. - final WebViewController controller = await controllerCompleter.future; - await controller.runJavascript('location.href = "$secondaryUrl"'); + await pageLoaded.future; // Wait for initial page load. + + pageLoaded = Completer(); + await controller.runJavaScript('location.href = "$secondaryUrl"'); + await pageLoaded.future; // Wait for second page to load. - await pageLoads.stream.first; // Wait for second page to load. final String? currentUrl = await controller.currentUrl(); expect(currentUrl, secondaryUrl); }); }); - testWidgets('launches with gestureNavigationEnabled on iOS', - (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: SizedBox( - width: 400, - height: 300, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - gestureNavigationEnabled: true, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - ), - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, primaryUrl); - }); - testWidgets('target _blank opens in same window', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); final Completer pageLoaded = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - await controller.runJavascript('window.open("$primaryUrl", "_blank")'); + + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageLoaded.complete(), + )); + + await tester.pumpWidget(WebViewWidget(controller: controller)); + + await controller.runJavaScript('window.open("$primaryUrl", "_blank")'); await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); @@ -1175,31 +729,22 @@ Future main() async { testWidgets( 'can open new window and go back', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); Completer pageLoaded = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (String url) { - pageLoaded.complete(); - }, - initialUrl: primaryUrl, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; + + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageLoaded.complete(), + )) + ..loadRequest(Uri.parse(primaryUrl)); + + await tester.pumpWidget(WebViewWidget(controller: controller)); + expect(controller.currentUrl(), completion(primaryUrl)); await pageLoaded.future; pageLoaded = Completer(); - await controller.runJavascript('window.open("$secondaryUrl")'); + await controller.runJavaScript('window.open("$secondaryUrl")'); await pageLoaded.future; pageLoaded = Completer(); expect(controller.currentUrl(), completion(secondaryUrl)); @@ -1212,46 +757,39 @@ Future main() async { ); testWidgets( - 'clearCache should clear local storage', + 'clearLocalStorage', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - Completer pageLoadCompleter = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (_) => pageLoadCompleter.complete(), - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - ), - ), - ); + final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => pageLoadCompleter.complete(), + )) + ..loadRequest(Uri.parse(primaryUrl)); + + await tester.pumpWidget(WebViewWidget(controller: controller)); await pageLoadCompleter.future; pageLoadCompleter = Completer(); - final WebViewController controller = await controllerCompleter.future; - await controller.runJavascript('localStorage.setItem("myCat", "Tom");'); - final String myCatItem = await controller.runJavascriptReturningResult( + await controller.runJavaScript('localStorage.setItem("myCat", "Tom");'); + final String myCatItem = await controller.runJavaScriptReturningResult( 'localStorage.getItem("myCat");', - ); - expect(myCatItem, _webviewString('Tom')); + ) as String; + expect(myCatItem, _webViewString('Tom')); + + await controller.clearLocalStorage(); - await controller.clearCache(); + // Reload page to have changes take effect. + await controller.reload(); await pageLoadCompleter.future; late final String? nullItem; try { - nullItem = await controller.runJavascriptReturningResult( + nullItem = await controller.runJavaScriptReturningResult( 'localStorage.getItem("myCat");', - ); + ) as String; } catch (exception) { if (defaultTargetPlatform == TargetPlatform.iOS && exception is ArgumentError && @@ -1260,23 +798,14 @@ Future main() async { nullItem = ''; } } - expect(nullItem, _webviewNull()); + expect(nullItem, _webViewNull()); }, ); } -// JavaScript booleans evaluate to different string values on Android and iOS. -// This utility method returns the string boolean value of the current platform. -String _webviewBool(bool value) { - if (defaultTargetPlatform == TargetPlatform.iOS) { - return value ? '1' : '0'; - } - return value ? 'true' : 'false'; -} - // JavaScript `null` evaluate to different string values on Android and iOS. // This utility method returns the string boolean value of the current platform. -String _webviewNull() { +String _webViewNull() { if (defaultTargetPlatform == TargetPlatform.iOS) { return ''; } @@ -1285,7 +814,7 @@ String _webviewNull() { // JavaScript String evaluate to different string values on Android and iOS. // This utility method returns the string boolean value of the current platform. -String _webviewString(String value) { +String _webViewString(String value) { if (defaultTargetPlatform == TargetPlatform.iOS) { return value; } @@ -1298,20 +827,24 @@ Future _getUserAgent(WebViewController controller) async { } Future _runJavascriptReturningResult( - WebViewController controller, String js) async { + WebViewController controller, + String js, +) async { if (defaultTargetPlatform == TargetPlatform.iOS) { - return await controller.runJavascriptReturningResult(js); + return await controller.runJavaScriptReturningResult(js) as String; } - return jsonDecode(await controller.runJavascriptReturningResult(js)) + return jsonDecode(await controller.runJavaScriptReturningResult(js) as String) as String; } class ResizableWebView extends StatefulWidget { - const ResizableWebView( - {Key? key, required this.onResize, required this.onPageFinished}) - : super(key: key); + const ResizableWebView({ + super.key, + required this.onResize, + required this.onPageFinished, + }); - final JavascriptMessageHandler onResize; + final VoidCallback onResize; final VoidCallback onPageFinished; @override @@ -1319,6 +852,23 @@ class ResizableWebView extends StatefulWidget { } class ResizableWebViewState extends State { + late final WebViewController controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onPageFinished: (_) => widget.onPageFinished(), + )) + ..addJavaScriptChannel( + 'Resize', + onMessageReceived: (_) { + widget.onResize(); + }, + ) + ..loadRequest( + Uri.parse( + 'data:text/html;charset=utf-8;base64,${base64Encode(const Utf8Encoder().convert(resizePage))}', + ), + ); + double webViewWidth = 200; double webViewHeight = 200; @@ -1341,28 +891,14 @@ class ResizableWebViewState extends State { @override Widget build(BuildContext context) { - final String resizeTestBase64 = - base64Encode(const Utf8Encoder().convert(resizePage)); return Directionality( textDirection: TextDirection.ltr, child: Column( children: [ SizedBox( - width: webViewWidth, - height: webViewHeight, - child: WebView( - initialUrl: - 'data:text/html;charset=utf-8;base64,$resizeTestBase64', - javascriptChannels: { - JavascriptChannel( - name: 'Resize', - onMessageReceived: widget.onResize, - ), - }, - onPageFinished: (_) => widget.onPageFinished(), - javascriptMode: JavascriptMode.unrestricted, - ), - ), + width: webViewWidth, + height: webViewHeight, + child: WebViewWidget(controller: controller)), TextButton( key: const Key('resizeButton'), onPressed: () { diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart index 79197b02315c..ec1ce4eef16c 100644 --- a/packages/webview_flutter/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart @@ -12,6 +12,12 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:webview_flutter/webview_flutter.dart'; +// #docregion platform_imports +// Import for Android features. +import 'package:webview_flutter_android/webview_flutter_android.dart'; +// Import for iOS features. +import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; +// #enddocregion platform_imports void main() => runApp(const MaterialApp(home: WebViewExample())); @@ -71,24 +77,86 @@ const String kTransparentBackgroundPage = ''' '''; class WebViewExample extends StatefulWidget { - const WebViewExample({Key? key, this.cookieManager}) : super(key: key); - - final CookieManager? cookieManager; + const WebViewExample({super.key}); @override State createState() => _WebViewExampleState(); } class _WebViewExampleState extends State { - final Completer _controller = - Completer(); + late final WebViewController _controller; @override void initState() { super.initState(); - if (Platform.isAndroid) { - WebView.platform = SurfaceAndroidWebView(); + + // #docregion platform_features + late final PlatformWebViewControllerCreationParams params; + if (WebViewPlatform.instance is WebKitWebViewPlatform) { + params = WebKitWebViewControllerCreationParams( + allowsInlineMediaPlayback: true, + mediaTypesRequiringUserAction: const {}, + ); + } else { + params = const PlatformWebViewControllerCreationParams(); + } + + final WebViewController controller = + WebViewController.fromPlatformCreationParams(params); + // #enddocregion platform_features + + controller + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(const Color(0x00000000)) + ..setNavigationDelegate( + NavigationDelegate( + onProgress: (int progress) { + debugPrint('WebView is loading (progress : $progress%)'); + }, + onPageStarted: (String url) { + debugPrint('Page started loading: $url'); + }, + onPageFinished: (String url) { + debugPrint('Page finished loading: $url'); + }, + onWebResourceError: (WebResourceError error) { + debugPrint(''' +Page resource error: + code: ${error.errorCode} + description: ${error.description} + errorType: ${error.errorType} + isForMainFrame: ${error.isForMainFrame} + '''); + }, + onNavigationRequest: (NavigationRequest request) { + if (request.url.startsWith('https://www.youtube.com/')) { + debugPrint('blocking navigation to ${request.url}'); + return NavigationDecision.prevent; + } + debugPrint('allowing navigation to ${request.url}'); + return NavigationDecision.navigate; + }, + ), + ) + ..addJavaScriptChannel( + 'Toaster', + onMessageReceived: (JavaScriptMessage message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message.message)), + ); + }, + ) + ..loadRequest(Uri.parse('https://flutter.dev')); + + // #docregion platform_features + if (controller.platform is AndroidWebViewController) { + AndroidWebViewController.enableDebugging(true); + (controller.platform as AndroidWebViewController) + .setMediaPlaybackRequiresUserGesture(false); } + // #enddocregion platform_features + + _controller = controller; } @override @@ -99,77 +167,27 @@ class _WebViewExampleState extends State { title: const Text('Flutter WebView example'), // This drop down menu demonstrates that Flutter widgets can be shown over the web view. actions: [ - NavigationControls(_controller.future), - SampleMenu(_controller.future, widget.cookieManager), + NavigationControls(webViewController: _controller), + SampleMenu(webViewController: _controller), ], ), - body: WebView( - initialUrl: 'https://flutter.dev', - javascriptMode: JavascriptMode.unrestricted, - onWebViewCreated: (WebViewController webViewController) { - _controller.complete(webViewController); - }, - onProgress: (int progress) { - print('WebView is loading (progress : $progress%)'); - }, - javascriptChannels: { - _toasterJavascriptChannel(context), - }, - navigationDelegate: (NavigationRequest request) { - if (request.url.startsWith('https://www.youtube.com/')) { - print('blocking navigation to $request}'); - return NavigationDecision.prevent; - } - print('allowing navigation to $request'); - return NavigationDecision.navigate; - }, - onPageStarted: (String url) { - print('Page started loading: $url'); - }, - onPageFinished: (String url) { - print('Page finished loading: $url'); - }, - gestureNavigationEnabled: true, - backgroundColor: const Color(0x00000000), - ), + body: WebViewWidget(controller: _controller), floatingActionButton: favoriteButton(), ); } - JavascriptChannel _toasterJavascriptChannel(BuildContext context) { - return JavascriptChannel( - name: 'Toaster', - onMessageReceived: (JavascriptMessage message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message.message)), - ); - }); - } - Widget favoriteButton() { - return FutureBuilder( - future: _controller.future, - builder: (BuildContext context, - AsyncSnapshot controller) { - return FloatingActionButton( - onPressed: () async { - String? url; - if (controller.hasData) { - url = await controller.data!.currentUrl(); - } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - controller.hasData - ? 'Favorited $url' - : 'Unable to favorite', - ), - ), - ); - }, - child: const Icon(Icons.favorite), + return FloatingActionButton( + onPressed: () async { + final String? url = await _controller.currentUrl(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Favorited $url')), ); - }); + } + }, + child: const Icon(Icons.favorite), + ); } } @@ -190,172 +208,170 @@ enum MenuOptions { } class SampleMenu extends StatelessWidget { - SampleMenu(this.controller, CookieManager? cookieManager, {Key? key}) - : cookieManager = cookieManager ?? CookieManager(), - super(key: key); + SampleMenu({ + super.key, + required this.webViewController, + }); - final Future controller; - late final CookieManager cookieManager; + final WebViewController webViewController; + late final WebViewCookieManager cookieManager = WebViewCookieManager(); @override Widget build(BuildContext context) { - return FutureBuilder( - future: controller, - builder: - (BuildContext context, AsyncSnapshot controller) { - return PopupMenuButton( - key: const ValueKey('ShowPopupMenu'), - onSelected: (MenuOptions value) { - switch (value) { - case MenuOptions.showUserAgent: - _onShowUserAgent(controller.data!, context); - break; - case MenuOptions.listCookies: - _onListCookies(controller.data!, context); - break; - case MenuOptions.clearCookies: - _onClearCookies(context); - break; - case MenuOptions.addToCache: - _onAddToCache(controller.data!, context); - break; - case MenuOptions.listCache: - _onListCache(controller.data!, context); - break; - case MenuOptions.clearCache: - _onClearCache(controller.data!, context); - break; - case MenuOptions.navigationDelegate: - _onNavigationDelegateExample(controller.data!, context); - break; - case MenuOptions.doPostRequest: - _onDoPostRequest(controller.data!, context); - break; - case MenuOptions.loadLocalFile: - _onLoadLocalFileExample(controller.data!, context); - break; - case MenuOptions.loadFlutterAsset: - _onLoadFlutterAssetExample(controller.data!, context); - break; - case MenuOptions.loadHtmlString: - _onLoadHtmlStringExample(controller.data!, context); - break; - case MenuOptions.transparentBackground: - _onTransparentBackground(controller.data!, context); - break; - case MenuOptions.setCookie: - _onSetCookie(controller.data!, context); - break; - } - }, - itemBuilder: (BuildContext context) => >[ - PopupMenuItem( - value: MenuOptions.showUserAgent, - enabled: controller.hasData, - child: const Text('Show user agent'), - ), - const PopupMenuItem( - value: MenuOptions.listCookies, - child: Text('List cookies'), - ), - const PopupMenuItem( - value: MenuOptions.clearCookies, - child: Text('Clear cookies'), - ), - const PopupMenuItem( - value: MenuOptions.addToCache, - child: Text('Add to cache'), - ), - const PopupMenuItem( - value: MenuOptions.listCache, - child: Text('List cache'), - ), - const PopupMenuItem( - value: MenuOptions.clearCache, - child: Text('Clear cache'), - ), - const PopupMenuItem( - value: MenuOptions.navigationDelegate, - child: Text('Navigation Delegate example'), - ), - const PopupMenuItem( - value: MenuOptions.doPostRequest, - child: Text('Post Request'), - ), - const PopupMenuItem( - value: MenuOptions.loadHtmlString, - child: Text('Load HTML string'), - ), - const PopupMenuItem( - value: MenuOptions.loadLocalFile, - child: Text('Load local file'), - ), - const PopupMenuItem( - value: MenuOptions.loadFlutterAsset, - child: Text('Load Flutter Asset'), - ), - const PopupMenuItem( - key: ValueKey('ShowTransparentBackgroundExample'), - value: MenuOptions.transparentBackground, - child: Text('Transparent background example'), - ), - const PopupMenuItem( - value: MenuOptions.setCookie, - child: Text('Set cookie'), - ), - ], - ); + return PopupMenuButton( + key: const ValueKey('ShowPopupMenu'), + onSelected: (MenuOptions value) { + switch (value) { + case MenuOptions.showUserAgent: + _onShowUserAgent(); + break; + case MenuOptions.listCookies: + _onListCookies(context); + break; + case MenuOptions.clearCookies: + _onClearCookies(context); + break; + case MenuOptions.addToCache: + _onAddToCache(context); + break; + case MenuOptions.listCache: + _onListCache(); + break; + case MenuOptions.clearCache: + _onClearCache(context); + break; + case MenuOptions.navigationDelegate: + _onNavigationDelegateExample(); + break; + case MenuOptions.doPostRequest: + _onDoPostRequest(); + break; + case MenuOptions.loadLocalFile: + _onLoadLocalFileExample(); + break; + case MenuOptions.loadFlutterAsset: + _onLoadFlutterAssetExample(); + break; + case MenuOptions.loadHtmlString: + _onLoadHtmlStringExample(); + break; + case MenuOptions.transparentBackground: + _onTransparentBackground(); + break; + case MenuOptions.setCookie: + _onSetCookie(); + break; + } }, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem( + value: MenuOptions.showUserAgent, + child: Text('Show user agent'), + ), + const PopupMenuItem( + value: MenuOptions.listCookies, + child: Text('List cookies'), + ), + const PopupMenuItem( + value: MenuOptions.clearCookies, + child: Text('Clear cookies'), + ), + const PopupMenuItem( + value: MenuOptions.addToCache, + child: Text('Add to cache'), + ), + const PopupMenuItem( + value: MenuOptions.listCache, + child: Text('List cache'), + ), + const PopupMenuItem( + value: MenuOptions.clearCache, + child: Text('Clear cache'), + ), + const PopupMenuItem( + value: MenuOptions.navigationDelegate, + child: Text('Navigation Delegate example'), + ), + const PopupMenuItem( + value: MenuOptions.doPostRequest, + child: Text('Post Request'), + ), + const PopupMenuItem( + value: MenuOptions.loadHtmlString, + child: Text('Load HTML string'), + ), + const PopupMenuItem( + value: MenuOptions.loadLocalFile, + child: Text('Load local file'), + ), + const PopupMenuItem( + value: MenuOptions.loadFlutterAsset, + child: Text('Load Flutter Asset'), + ), + const PopupMenuItem( + key: ValueKey('ShowTransparentBackgroundExample'), + value: MenuOptions.transparentBackground, + child: Text('Transparent background example'), + ), + const PopupMenuItem( + value: MenuOptions.setCookie, + child: Text('Set cookie'), + ), + ], ); } - Future _onShowUserAgent( - WebViewController controller, BuildContext context) async { + Future _onShowUserAgent() { // Send a message with the user agent string to the Toaster JavaScript channel we registered // with the WebView. - await controller.runJavascript( - 'Toaster.postMessage("User Agent: " + navigator.userAgent);'); + return webViewController.runJavaScript( + 'Toaster.postMessage("User Agent: " + navigator.userAgent);', + ); } - Future _onListCookies( - WebViewController controller, BuildContext context) async { - final String cookies = - await controller.runJavascriptReturningResult('document.cookie'); - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Column( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Cookies:'), - _getCookieList(cookies), - ], - ), - )); + Future _onListCookies(BuildContext context) async { + final String cookies = await webViewController + .runJavaScriptReturningResult('document.cookie') as String; + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Cookies:'), + _getCookieList(cookies), + ], + ), + )); + } } - Future _onAddToCache( - WebViewController controller, BuildContext context) async { - await controller.runJavascript( - 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Added a test entry to cache.'), - )); + Future _onAddToCache(BuildContext context) async { + await webViewController.runJavaScript( + 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";', + ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Added a test entry to cache.'), + )); + } } - Future _onListCache( - WebViewController controller, BuildContext context) async { - await controller.runJavascript('caches.keys()' + Future _onListCache() { + return webViewController.runJavaScript('caches.keys()' // ignore: missing_whitespace_between_adjacent_strings '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' '.then((caches) => Toaster.postMessage(caches))'); } - Future _onClearCache( - WebViewController controller, BuildContext context) async { - await controller.clearCache(); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Cache cleared.'), - )); + Future _onClearCache(BuildContext context) async { + await webViewController.clearCache(); + await webViewController.clearLocalStorage(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Cache cleared.'), + )); + } } Future _onClearCookies(BuildContext context) async { @@ -364,58 +380,60 @@ class SampleMenu extends StatelessWidget { if (!hadCookies) { message = 'There are no cookies.'; } - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(message), - )); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(message), + )); + } } - Future _onNavigationDelegateExample( - WebViewController controller, BuildContext context) async { - final String contentBase64 = - base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); - await controller.loadUrl('data:text/html;base64,$contentBase64'); + Future _onNavigationDelegateExample() { + final String contentBase64 = base64Encode( + const Utf8Encoder().convert(kNavigationExamplePage), + ); + return webViewController.loadRequest( + Uri.parse('data:text/html;base64,$contentBase64'), + ); } - Future _onSetCookie( - WebViewController controller, BuildContext context) async { + Future _onSetCookie() async { await cookieManager.setCookie( const WebViewCookie( - name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'), + name: 'foo', + value: 'bar', + domain: 'httpbin.org', + path: '/anything', + ), ); - await controller.loadUrl('https://httpbin.org/anything'); + await webViewController.loadRequest(Uri.parse( + 'https://httpbin.org/anything', + )); } - Future _onDoPostRequest( - WebViewController controller, BuildContext context) async { - final WebViewRequest request = WebViewRequest( - uri: Uri.parse('https://httpbin.org/post'), - method: WebViewRequestMethod.post, + Future _onDoPostRequest() { + return webViewController.loadRequest( + Uri.parse('https://httpbin.org/post'), + method: LoadRequestMethod.post, headers: {'foo': 'bar', 'Content-Type': 'text/plain'}, body: Uint8List.fromList('Test Body'.codeUnits), ); - await controller.loadRequest(request); } - Future _onLoadLocalFileExample( - WebViewController controller, BuildContext context) async { + Future _onLoadLocalFileExample() async { final String pathToIndex = await _prepareLocalFile(); - - await controller.loadFile(pathToIndex); + await webViewController.loadFile(pathToIndex); } - Future _onLoadFlutterAssetExample( - WebViewController controller, BuildContext context) async { - await controller.loadFlutterAsset('assets/www/index.html'); + Future _onLoadFlutterAssetExample() { + return webViewController.loadFlutterAsset('assets/www/index.html'); } - Future _onLoadHtmlStringExample( - WebViewController controller, BuildContext context) async { - await controller.loadHtmlString(kLocalExamplePage); + Future _onLoadHtmlStringExample() { + return webViewController.loadHtmlString(kLocalExamplePage); } - Future _onTransparentBackground( - WebViewController controller, BuildContext context) async { - await controller.loadHtmlString(kTransparentBackgroundPage); + Future _onTransparentBackground() { + return webViewController.loadHtmlString(kTransparentBackgroundPage); } Widget _getCookieList(String cookies) { @@ -445,65 +463,47 @@ class SampleMenu extends StatelessWidget { } class NavigationControls extends StatelessWidget { - const NavigationControls(this._webViewControllerFuture, {Key? key}) - : assert(_webViewControllerFuture != null), - super(key: key); + const NavigationControls({super.key, required this.webViewController}); - final Future _webViewControllerFuture; + final WebViewController webViewController; @override Widget build(BuildContext context) { - return FutureBuilder( - future: _webViewControllerFuture, - builder: - (BuildContext context, AsyncSnapshot snapshot) { - final bool webViewReady = - snapshot.connectionState == ConnectionState.done; - final WebViewController? controller = snapshot.data; - return Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_back_ios), - onPressed: !webViewReady - ? null - : () async { - if (await controller!.canGoBack()) { - await controller.goBack(); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('No back history item')), - ); - return; - } - }, - ), - IconButton( - icon: const Icon(Icons.arrow_forward_ios), - onPressed: !webViewReady - ? null - : () async { - if (await controller!.canGoForward()) { - await controller.goForward(); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('No forward history item')), - ); - return; - } - }, - ), - IconButton( - icon: const Icon(Icons.replay), - onPressed: !webViewReady - ? null - : () { - controller!.reload(); - }, - ), - ], - ); - }, + return Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: () async { + if (await webViewController.canGoBack()) { + await webViewController.goBack(); + } else { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No back history item')), + ); + } + } + }, + ), + IconButton( + icon: const Icon(Icons.arrow_forward_ios), + onPressed: () async { + if (await webViewController.canGoForward()) { + await webViewController.goForward(); + } else { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No forward history item')), + ); + } + } + }, + ), + IconButton( + icon: const Icon(Icons.replay), + onPressed: () => webViewController.reload(), + ), + ], ); } } diff --git a/packages/webview_flutter/webview_flutter/example/lib/simple_example.dart b/packages/webview_flutter/webview_flutter/example/lib/simple_example.dart new file mode 100644 index 000000000000..dfee9e6bd23a --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/lib/simple_example.dart @@ -0,0 +1,59 @@ +// 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 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +void main() => runApp(const MaterialApp(home: WebViewExample())); + +class WebViewExample extends StatefulWidget { + const WebViewExample({super.key}); + + @override + State createState() => _WebViewExampleState(); +} + +class _WebViewExampleState extends State { + late final WebViewController controller; + + @override + void initState() { + super.initState(); + + // #docregion webview_controller + controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(const Color(0x00000000)) + ..setNavigationDelegate( + NavigationDelegate( + onProgress: (int progress) { + // Update loading bar. + }, + onPageStarted: (String url) {}, + onPageFinished: (String url) {}, + onWebResourceError: (WebResourceError error) {}, + onNavigationRequest: (NavigationRequest request) { + if (request.url.startsWith('https://www.youtube.com/')) { + return NavigationDecision.prevent; + } + return NavigationDecision.navigate; + }, + ), + ) + ..loadRequest(Uri.parse('https://flutter.dev')); + // #enddocregion webview_controller + } + + // #docregion webview_widget + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Flutter Simple Example')), + body: WebViewWidget(controller: controller), + ); + } + // #enddocregion webview_widget +} diff --git a/packages/webview_flutter/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/webview_flutter/example/pubspec.yaml index 6b01b53ee4a3..4d8d7889d733 100644 --- a/packages/webview_flutter/webview_flutter/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the webview_flutter plugin. publish_to: none environment: - sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + sdk: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" dependencies: flutter: @@ -17,8 +17,11 @@ 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: ../ + webview_flutter_android: ^3.0.0 + webview_flutter_wkwebview: ^3.0.0 dev_dependencies: + build_runner: ^2.1.5 espresso: ^0.2.0 flutter_driver: sdk: flutter @@ -26,6 +29,7 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter + webview_flutter_platform_interface: ^2.0.0 flutter: uses-material-design: true diff --git a/packages/webview_flutter/webview_flutter/example/test/main_test.dart b/packages/webview_flutter/webview_flutter/example/test/main_test.dart index 867633366e1a..7857022c14a0 100644 --- a/packages/webview_flutter/webview_flutter/example/test/main_test.dart +++ b/packages/webview_flutter/webview_flutter/example/test/main_test.dart @@ -4,17 +4,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter_example/main.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; void main() { + setUp(() { + WebViewPlatform.instance = FakeWebViewPlatform(); + }); + testWidgets('Test snackbar from ScaffoldMessenger', (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: WebViewExample(cookieManager: FakeCookieManager()), - ), - ); + await tester.pumpWidget(const MaterialApp(home: WebViewExample())); expect(find.byIcon(Icons.favorite), findsOneWidget); await tester.tap(find.byIcon(Icons.favorite)); await tester.pump(); @@ -22,18 +22,95 @@ void main() { }); } -class FakeCookieManager implements CookieManager { - factory FakeCookieManager() { - return _instance ??= FakeCookieManager._(); +class FakeWebViewPlatform extends WebViewPlatform { + @override + PlatformWebViewController createPlatformWebViewController( + PlatformWebViewControllerCreationParams params, + ) { + return FakeWebViewController(params); + } + + @override + PlatformWebViewWidget createPlatformWebViewWidget( + PlatformWebViewWidgetCreationParams params, + ) { + return FakeWebViewWidget(params); + } + + @override + PlatformWebViewCookieManager createPlatformCookieManager( + PlatformWebViewCookieManagerCreationParams params, + ) { + return FakeCookieManager(params); + } + + @override + PlatformNavigationDelegate createPlatformNavigationDelegate( + PlatformNavigationDelegateCreationParams params, + ) { + return FakeNavigationDelegate(params); + } +} + +class FakeWebViewController extends PlatformWebViewController { + FakeWebViewController(super.params) : super.implementation(); + + @override + Future setJavaScriptMode(JavaScriptMode javaScriptMode) async {} + + @override + Future setBackgroundColor(Color color) async {} + + @override + Future setPlatformNavigationDelegate( + PlatformNavigationDelegate handler, + ) async {} + + @override + Future addJavaScriptChannel( + JavaScriptChannelParams javaScriptChannelParams) async {} + + @override + Future loadRequest(LoadRequestParams params) async {} + + @override + Future currentUrl() async { + return 'https://www.google.com'; + } +} + +class FakeCookieManager extends PlatformWebViewCookieManager { + FakeCookieManager(super.params) : super.implementation(); +} + +class FakeWebViewWidget extends PlatformWebViewWidget { + FakeWebViewWidget(super.params) : super.implementation(); + + @override + Widget build(BuildContext context) { + return Container(); } +} - FakeCookieManager._(); +class FakeNavigationDelegate extends PlatformNavigationDelegate { + FakeNavigationDelegate(super.params) : super.implementation(); - static FakeCookieManager? _instance; + @override + Future setOnNavigationRequest( + NavigationRequestCallback onNavigationRequest, + ) async {} + + @override + Future setOnPageFinished(PageEventCallback onPageFinished) async {} + + @override + Future setOnPageStarted(PageEventCallback onPageStarted) async {} @override - Future clearCookies() => throw UnimplementedError(); + Future setOnProgress(ProgressCallback onProgress) async {} @override - Future setCookie(WebViewCookie cookie) => throw UnimplementedError(); + Future setOnWebResourceError( + WebResourceErrorCallback onWebResourceError, + ) async {} } diff --git a/packages/webview_flutter/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/webview_flutter/lib/src/legacy/platform_interface.dart similarity index 89% rename from packages/webview_flutter/webview_flutter/lib/platform_interface.dart rename to packages/webview_flutter/webview_flutter/lib/src/legacy/platform_interface.dart index 48f74346fe61..e036d2ef88a5 100644 --- a/packages/webview_flutter/webview_flutter/lib/platform_interface.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/legacy/platform_interface.dart @@ -5,7 +5,7 @@ /// Re-export the classes from the webview_flutter_platform_interface through /// the `platform_interface.dart` file so we don't accidentally break any /// non-endorsed existing implementations of the interface. -export 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' +export 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart' show AutoMediaPlaybackPolicy, CreationParams, diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview.dart b/packages/webview_flutter/webview_flutter/lib/src/legacy/webview.dart similarity index 98% rename from packages/webview_flutter/webview_flutter/lib/src/webview.dart rename to packages/webview_flutter/webview_flutter/lib/src/legacy/webview.dart index 7de8e281711b..d210e1e7669a 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/webview.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/legacy/webview.dart @@ -8,10 +8,12 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; -import 'package:webview_flutter_android/webview_android_cookie_manager.dart'; -import 'package:webview_flutter_android/webview_surface_android.dart'; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; -import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; +// ignore: implementation_imports +import 'package:webview_flutter_android/src/webview_flutter_android_legacy.dart'; +// ignore: implementation_imports +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; +// ignore: implementation_imports +import 'package:webview_flutter_wkwebview/src/webview_flutter_wkwebview_legacy.dart'; /// Optional callback invoked when a web view is first created. [controller] is /// the [WebViewController] for the created web view. @@ -76,7 +78,7 @@ class WebView extends StatefulWidget { /// /// The `javascriptMode` and `autoMediaPlaybackPolicy` parameters must not be null. const WebView({ - Key? key, + super.key, this.onWebViewCreated, this.initialUrl, this.initialCookies = const [], @@ -98,8 +100,7 @@ class WebView extends StatefulWidget { this.backgroundColor, }) : assert(javascriptMode != null), assert(initialMediaPlaybackPolicy != null), - assert(allowsInlineMediaPlayback != null), - super(key: key); + assert(allowsInlineMediaPlayback != null); static WebViewPlatform? _platform; @@ -126,6 +127,7 @@ class WebView extends StatefulWidget { case TargetPlatform.iOS: _platform = CupertinoWebView(); break; + // ignore: no_default_cases default: throw UnsupportedError( "Trying to use the default webview implementation for $defaultTargetPlatform but there isn't a default one"); diff --git a/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart b/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart new file mode 100644 index 000000000000..3237fa41c0bb --- /dev/null +++ b/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart @@ -0,0 +1,153 @@ +// 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:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'webview_controller.dart'; + +/// Callbacks for accepting or rejecting navigation changes, and for tracking +/// the progress of navigation requests. +/// +/// See [WebViewController.setNavigationDelegate]. +/// +/// ## Platform-Specific Features +/// This class contains an underlying implementation provided by the current +/// platform. Once a platform implementation is imported, the examples below +/// can be followed to use features provided by a platform's implementation. +/// +/// {@macro webview_flutter.NavigationDelegate.fromPlatformCreationParams} +/// +/// Below is an example of accessing the platform-specific implementation for +/// iOS and Android: +/// +/// ```dart +/// final NavigationDelegate navigationDelegate = NavigationDelegate(); +/// +/// if (WebViewPlatform.instance is WebKitWebViewPlatform) { +/// final WebKitNavigationDelegate webKitDelegate = +/// navigationDelegate.platform as WebKitNavigationDelegate; +/// } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { +/// final AndroidNavigationDelegate androidDelegate = +/// navigationDelegate.platform as AndroidNavigationDelegate; +/// } +/// ``` +class NavigationDelegate { + /// Constructs a [NavigationDelegate]. + NavigationDelegate({ + FutureOr Function(NavigationRequest request)? + onNavigationRequest, + void Function(String url)? onPageStarted, + void Function(String url)? onPageFinished, + void Function(int progress)? onProgress, + void Function(WebResourceError error)? onWebResourceError, + }) : this.fromPlatformCreationParams( + const PlatformNavigationDelegateCreationParams(), + onNavigationRequest: onNavigationRequest, + onPageStarted: onPageStarted, + onPageFinished: onPageFinished, + onProgress: onProgress, + onWebResourceError: onWebResourceError, + ); + + /// Constructs a [NavigationDelegate] from creation params for a specific + /// platform. + /// + /// {@template webview_flutter.NavigationDelegate.fromPlatformCreationParams} + /// Below is an example of setting platform-specific creation parameters for + /// iOS and Android: + /// + /// ```dart + /// PlatformNavigationDelegateCreationParams params = + /// const PlatformNavigationDelegateCreationParams(); + /// + /// if (WebViewPlatform.instance is WebKitWebViewPlatform) { + /// params = WebKitNavigationDelegateCreationParams + /// .fromPlatformNavigationDelegateCreationParams( + /// params, + /// ); + /// } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { + /// params = AndroidNavigationDelegateCreationParams + /// .fromPlatformNavigationDelegateCreationParams( + /// params, + /// ); + /// } + /// + /// final NavigationDelegate navigationDelegate = + /// NavigationDelegate.fromPlatformCreationParams( + /// params, + /// ); + /// ``` + /// {@endtemplate} + NavigationDelegate.fromPlatformCreationParams( + PlatformNavigationDelegateCreationParams params, { + FutureOr Function(NavigationRequest request)? + onNavigationRequest, + void Function(String url)? onPageStarted, + void Function(String url)? onPageFinished, + void Function(int progress)? onProgress, + void Function(WebResourceError error)? onWebResourceError, + }) : this.fromPlatform( + PlatformNavigationDelegate(params), + onNavigationRequest: onNavigationRequest, + onPageStarted: onPageStarted, + onPageFinished: onPageFinished, + onProgress: onProgress, + onWebResourceError: onWebResourceError, + ); + + /// Constructs a [NavigationDelegate] from a specific platform implementation. + NavigationDelegate.fromPlatform( + this.platform, { + this.onNavigationRequest, + this.onPageStarted, + this.onPageFinished, + this.onProgress, + this.onWebResourceError, + }) { + if (onNavigationRequest != null) { + platform.setOnNavigationRequest(onNavigationRequest!); + } + if (onPageStarted != null) { + platform.setOnPageStarted(onPageStarted!); + } + if (onPageFinished != null) { + platform.setOnPageFinished(onPageFinished!); + } + if (onProgress != null) { + platform.setOnProgress(onProgress!); + } + if (onWebResourceError != null) { + platform.setOnWebResourceError(onWebResourceError!); + } + } + + /// Implementation of [PlatformNavigationDelegate] for the current platform. + final PlatformNavigationDelegate platform; + + /// Invoked when a decision for a navigation request is pending. + /// + /// When a navigation is initiated by the WebView (e.g when a user clicks a + /// link) this delegate is called and has to decide how to proceed with the + /// navigation. + /// + /// *Important*: Some platforms may also trigger this callback from calls to + /// [WebViewController.loadRequest]. + /// + /// See [NavigationDecision]. + final NavigationRequestCallback? onNavigationRequest; + + /// Invoked when a page has started loading. + final PageEventCallback? onPageStarted; + + /// Invoked when a page has finished loading. + final PageEventCallback? onPageFinished; + + /// Invoked when a page is loading to report the progress. + final ProgressCallback? onProgress; + + /// Invoked when a resource loading error occurred. + final WebResourceErrorCallback? onWebResourceError; +} diff --git a/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_cookie_manager.dart b/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_cookie_manager.dart deleted file mode 100644 index a1091fa3c7b1..000000000000 --- a/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_cookie_manager.dart +++ /dev/null @@ -1,37 +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 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; - -/// Manages cookies pertaining to all WebViews. -class WebViewCookieManager { - /// Constructs a [WebViewCookieManager]. - WebViewCookieManager() - : this.fromPlatformCreationParams( - const PlatformWebViewCookieManagerCreationParams(), - ); - - /// Constructs a [WebViewCookieManager] from creation params for a specific - /// platform. - WebViewCookieManager.fromPlatformCreationParams( - PlatformWebViewCookieManagerCreationParams params, - ) : this.fromPlatform(PlatformWebViewCookieManager(params)); - - /// Constructs a [WebViewCookieManager] from a specific platform - /// implementation. - WebViewCookieManager.fromPlatform(this.platform); - - /// Implementation of [PlatformWebViewCookieManager] for the current platform. - final PlatformWebViewCookieManager platform; - - /// Clears all cookies for all WebViews. - /// - /// Returns true if cookies were present before clearing, else false. - Future clearCookies() => platform.clearCookies(); - - /// Sets a cookie for all WebView instances. - /// - /// This is a no op on iOS versions below 11. - Future setCookie(WebViewCookie cookie) => platform.setCookie(cookie); -} diff --git a/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_widget.dart b/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_widget.dart deleted file mode 100644 index 06e4f78028df..000000000000 --- a/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_widget.dart +++ /dev/null @@ -1,64 +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 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; - -import 'webview_controller.dart'; - -/// Displays a native WebView as a Widget. -class WebViewWidget extends StatelessWidget { - /// Constructs a [WebViewWidget]. - WebViewWidget({ - Key? key, - required WebViewController controller, - TextDirection layoutDirection = TextDirection.ltr, - Set> gestureRecognizers = - const >{}, - }) : this.fromPlatformCreationParams( - key: key, - params: PlatformWebViewWidgetCreationParams( - controller: controller.platform, - layoutDirection: layoutDirection, - gestureRecognizers: gestureRecognizers, - ), - ); - - /// Constructs a [WebViewWidget] from creation params for a specific - /// platform. - WebViewWidget.fromPlatformCreationParams({ - Key? key, - required PlatformWebViewWidgetCreationParams params, - }) : this.fromPlatform(key: key, platform: PlatformWebViewWidget(params)); - - /// Constructs a [WebViewWidget] from a specific platform implementation. - WebViewWidget.fromPlatform({Key? key, required this.platform}) - : super(key: key); - - /// Implementation of [PlatformWebViewWidget] for the current platform. - final PlatformWebViewWidget platform; - - /// The layout direction to use for the embedded WebView. - late final TextDirection layoutDirection = platform.params.layoutDirection; - - /// Specifies which gestures should be consumed by the web view. - /// - /// It is possible for other gesture recognizers to be competing with the web - /// view on pointer events, e.g if the web view is inside a [ListView] the - /// [ListView] will want to handle vertical drags. The web view will claim - /// gestures that are recognized by any of the recognizers on this list. - /// - /// When `gestureRecognizers` is empty (default), the web view will only - /// handle pointer events for gestures that were not claimed by any other - /// gesture recognizer. - late final Set> gestureRecognizers = - platform.params.gestureRecognizers; - - @override - Widget build(BuildContext context) { - return platform.build(context); - } -} diff --git a/packages/webview_flutter/webview_flutter/lib/src/v4/webview_flutter.dart b/packages/webview_flutter/webview_flutter/lib/src/v4/webview_flutter.dart deleted file mode 100644 index f4a0b207e27a..000000000000 --- a/packages/webview_flutter/webview_flutter/lib/src/v4/webview_flutter.dart +++ /dev/null @@ -1,12 +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. - -library webview_flutter; - -export 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart' - show JavaScriptMessage, LoadRequestMethod, WebViewCookie; - -export 'src/webview_controller.dart'; -export 'src/webview_cookie_manager.dart'; -export 'src/webview_widget.dart'; diff --git a/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_controller.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart similarity index 76% rename from packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_controller.dart rename to packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart index bd03b247027e..a112f1522579 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/v4/src/webview_controller.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart @@ -2,20 +2,49 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:math'; - // TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231) // ignore: unnecessary_import import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'navigation_delegate.dart'; +import 'webview_widget.dart'; /// Controls a WebView provided by the host platform. /// /// Pass this to a [WebViewWidget] to display the WebView. +/// +/// A [WebViewController] can only be used by a single [WebViewWidget] at a +/// time. +/// +/// ## Platform-Specific Features +/// This class contains an underlying implementation provided by the current +/// platform. Once a platform implementation is imported, the examples below +/// can be followed to use features provided by a platform's implementation. +/// +/// {@macro webview_flutter.WebViewController.fromPlatformCreationParams} +/// +/// Below is an example of accessing the platform-specific implementation for +/// iOS and Android: +/// +/// ```dart +/// final WebViewController webViewController = WebViewController(); +/// +/// if (WebViewPlatform.instance is WebKitWebViewPlatform) { +/// final WebKitWebViewController webKitController = +/// webViewController.platform as WebKitWebViewController; +/// } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { +/// final AndroidWebViewController androidController = +/// webViewController.platform as AndroidWebViewController; +/// } +/// ``` class WebViewController { /// Constructs a [WebViewController]. + /// + /// See [WebViewController.fromPlatformCreationParams] for setting parameters + /// for a specific platform. WebViewController() : this.fromPlatformCreationParams( const PlatformWebViewControllerCreationParams(), @@ -23,6 +52,33 @@ class WebViewController { /// Constructs a [WebViewController] from creation params for a specific /// platform. + /// + /// {@template webview_flutter.WebViewCookieManager.fromPlatformCreationParams} + /// Below is an example of setting platform-specific creation parameters for + /// iOS and Android: + /// + /// ```dart + /// PlatformWebViewControllerCreationParams params = + /// const PlatformWebViewControllerCreationParams(); + /// + /// if (WebViewPlatform.instance is WebKitWebViewPlatform) { + /// params = WebKitWebViewControllerCreationParams + /// .fromPlatformWebViewControllerCreationParams( + /// params, + /// ); + /// } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { + /// params = AndroidWebViewControllerCreationParams + /// .fromPlatformWebViewControllerCreationParams( + /// params, + /// ); + /// } + /// + /// final WebViewController webViewController = + /// WebViewController.fromPlatformCreationParams( + /// params, + /// ); + /// ``` + /// {@endtemplate} WebViewController.fromPlatformCreationParams( PlatformWebViewControllerCreationParams params, ) : this.fromPlatform(PlatformWebViewController(params)); @@ -125,6 +181,12 @@ class WebViewController { return platform.reload(); } + /// Sets the [NavigationDelegate] containing the callback methods that are + /// called during navigation events. + Future setNavigationDelegate(NavigationDelegate delegate) { + return platform.setPlatformNavigationDelegate(delegate.platform); + } + /// Clears all caches used by the WebView. /// /// The following caches are cleared: @@ -233,9 +295,8 @@ class WebViewController { /// Returns the current scroll position of this view. /// /// Scroll position is measured from the top left. - Future getScrollPosition() async { - final Point position = await platform.getScrollPosition(); - return Offset(position.x.toDouble(), position.y.toDouble()); + Future getScrollPosition() { + return platform.getScrollPosition(); } /// Whether to support zooming using the on-screen zoom controls and gestures. diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview_cookie_manager.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_cookie_manager.dart new file mode 100644 index 000000000000..353d7554fcb2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/lib/src/webview_cookie_manager.dart @@ -0,0 +1,89 @@ +// 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:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +/// Manages cookies pertaining to all WebViews. +/// +/// ## Platform-Specific Features +/// This class contains an underlying implementation provided by the current +/// platform. Once a platform implementation is imported, the examples below +/// can be followed to use features provided by a platform's implementation. +/// +/// {@macro webview_flutter.WebViewCookieManager.fromPlatformCreationParams} +/// +/// Below is an example of accessing the platform-specific implementation for +/// iOS and Android: +/// +/// ```dart +/// final WebViewCookieManager cookieManager = WebViewCookieManager(); +/// +/// if (WebViewPlatform.instance is WebKitWebViewPlatform) { +/// final WebKitWebViewCookieManager webKitManager = +/// cookieManager.platform as WebKitWebViewCookieManager; +/// } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { +/// final AndroidWebViewCookieManager androidManager = +/// cookieManager.platform as AndroidWebViewCookieManager; +/// } +/// ``` +class WebViewCookieManager { + /// Constructs a [WebViewCookieManager]. + /// + /// See [WebViewCookieManager.fromPlatformCreationParams] for setting + /// parameters for a specific platform. + WebViewCookieManager() + : this.fromPlatformCreationParams( + const PlatformWebViewCookieManagerCreationParams(), + ); + + /// Constructs a [WebViewCookieManager] from creation params for a specific + /// platform. + /// + /// {@template webview_flutter.WebViewCookieManager.fromPlatformCreationParams} + /// Below is an example of setting platform-specific creation parameters for + /// iOS and Android: + /// + /// ```dart + /// PlatformWebViewCookieManagerCreationParams params = + /// const PlatformWebViewCookieManagerCreationParams(); + /// + /// if (WebViewPlatform.instance is WebKitWebViewPlatform) { + /// params = WebKitWebViewCookieManagerCreationParams + /// .fromPlatformWebViewCookieManagerCreationParams( + /// params, + /// ); + /// } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { + /// params = AndroidWebViewCookieManagerCreationParams + /// .fromPlatformWebViewCookieManagerCreationParams( + /// params, + /// ); + /// } + /// + /// final WebViewCookieManager webViewCookieManager = + /// WebViewCookieManager.fromPlatformCreationParams( + /// params, + /// ); + /// ``` + /// {@endtemplate} + WebViewCookieManager.fromPlatformCreationParams( + PlatformWebViewCookieManagerCreationParams params, + ) : this.fromPlatform(PlatformWebViewCookieManager(params)); + + /// Constructs a [WebViewCookieManager] from a specific platform + /// implementation. + WebViewCookieManager.fromPlatform(this.platform); + + /// Implementation of [PlatformWebViewCookieManager] for the current platform. + final PlatformWebViewCookieManager platform; + + /// Clears all cookies for all WebViews. + /// + /// Returns true if cookies were present before clearing, else false. + Future clearCookies() => platform.clearCookies(); + + /// Sets a cookie for all WebView instances. + /// + /// This is a no op on iOS versions below 11. + Future setCookie(WebViewCookie cookie) => platform.setCookie(cookie); +} diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview_flutter_legacy.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_flutter_legacy.dart new file mode 100644 index 000000000000..d040fc2e71d8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/lib/src/webview_flutter_legacy.dart @@ -0,0 +1,9 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'package:webview_flutter_android/src/webview_flutter_android_legacy.dart'; +export 'package:webview_flutter_wkwebview/src/webview_flutter_wkwebview_legacy.dart'; + +export 'legacy/platform_interface.dart'; +export 'legacy/webview.dart'; diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview_widget.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_widget.dart new file mode 100644 index 000000000000..440d0f6654ec --- /dev/null +++ b/packages/webview_flutter/webview_flutter/lib/src/webview_widget.dart @@ -0,0 +1,122 @@ +// 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/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'webview_controller.dart'; + +/// Displays a native WebView as a Widget. +/// +/// ## Platform-Specific Features +/// This class contains an underlying implementation provided by the current +/// platform. Once a platform implementation is imported, the examples below +/// can be followed to use features provided by a platform's implementation. +/// +/// {@macro webview_flutter.WebViewWidget.fromPlatformCreationParams} +/// +/// Below is an example of accessing the platform-specific implementation for +/// iOS and Android: +/// +/// ```dart +/// final WebViewController controller = WebViewController(); +/// +/// final WebViewWidget webViewWidget = WebViewWidget(controller: controller); +/// +/// if (WebViewPlatform.instance is WebKitWebViewPlatform) { +/// final WebKitWebViewWidget webKitWidget = +/// webViewWidget.platform as WebKitWebViewWidget; +/// } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { +/// final AndroidWebViewWidget androidWidget = +/// webViewWidget.platform as AndroidWebViewWidget; +/// } +/// ``` +class WebViewWidget extends StatelessWidget { + /// Constructs a [WebViewWidget]. + /// + /// See [WebViewWidget.fromPlatformCreationParams] for setting parameters for + /// a specific platform. + WebViewWidget({ + Key? key, + required WebViewController controller, + TextDirection layoutDirection = TextDirection.ltr, + Set> gestureRecognizers = + const >{}, + }) : this.fromPlatformCreationParams( + key: key, + params: PlatformWebViewWidgetCreationParams( + controller: controller.platform, + layoutDirection: layoutDirection, + gestureRecognizers: gestureRecognizers, + ), + ); + + /// Constructs a [WebViewWidget] from creation params for a specific platform. + /// + /// {@template webview_flutter.WebViewWidget.fromPlatformCreationParams} + /// Below is an example of setting platform-specific creation parameters for + /// iOS and Android: + /// + /// ```dart + /// final WebViewController controller = WebViewController(); + /// + /// PlatformWebViewWidgetCreationParams params = + /// PlatformWebViewWidgetCreationParams( + /// controller: controller.platform, + /// layoutDirection: TextDirection.ltr, + /// gestureRecognizers: const >{}, + /// ); + /// + /// if (WebViewPlatform.instance is WebKitWebViewPlatform) { + /// params = WebKitWebViewWidgetCreationParams + /// .fromPlatformWebViewWidgetCreationParams( + /// params, + /// ); + /// } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { + /// params = AndroidWebViewWidgetCreationParams + /// .fromPlatformWebViewWidgetCreationParams( + /// params, + /// ); + /// } + /// + /// final WebViewWidget webViewWidget = + /// WebViewWidget.fromPlatformCreationParams( + /// params: params, + /// ); + /// ``` + /// {@endtemplate} + WebViewWidget.fromPlatformCreationParams({ + Key? key, + required PlatformWebViewWidgetCreationParams params, + }) : this.fromPlatform(key: key, platform: PlatformWebViewWidget(params)); + + /// Constructs a [WebViewWidget] from a specific platform implementation. + WebViewWidget.fromPlatform({super.key, required this.platform}); + + /// Implementation of [PlatformWebViewWidget] for the current platform. + final PlatformWebViewWidget platform; + + /// The layout direction to use for the embedded WebView. + late final TextDirection layoutDirection = platform.params.layoutDirection; + + /// Specifies which gestures should be consumed by the web view. + /// + /// It is possible for other gesture recognizers to be competing with the web + /// view on pointer events, e.g if the web view is inside a [ListView] the + /// [ListView] will want to handle vertical drags. The web view will claim + /// gestures that are recognized by any of the recognizers on this list. + /// + /// When `gestureRecognizers` is empty (default), the web view will only + /// handle pointer events for gestures that were not claimed by any other + /// gesture recognizer. + late final Set> gestureRecognizers = + platform.params.gestureRecognizers; + + @override + Widget build(BuildContext context) { + return platform.build(context); + } +} diff --git a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart index ba38771e5107..112966d47760 100644 --- a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart @@ -2,9 +2,29 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -export 'package:webview_flutter_android/webview_android.dart'; -export 'package:webview_flutter_android/webview_surface_android.dart'; -export 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; +library webview_flutter; -export 'platform_interface.dart'; -export 'src/webview.dart'; +export 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' + show + JavaScriptMessage, + JavaScriptMode, + LoadRequestMethod, + NavigationDecision, + NavigationRequest, + NavigationRequestCallback, + PageEventCallback, + PlatformNavigationDelegateCreationParams, + PlatformWebViewControllerCreationParams, + PlatformWebViewCookieManagerCreationParams, + PlatformWebViewWidgetCreationParams, + ProgressCallback, + WebResourceError, + WebResourceErrorCallback, + WebResourceErrorType, + WebViewCookie, + WebViewPlatform; + +export 'src/navigation_delegate.dart'; +export 'src/webview_controller.dart'; +export 'src/webview_cookie_manager.dart'; +export 'src/webview_widget.dart'; diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index a02b0323e7ab..5cef1a731739 100644 --- a/packages/webview_flutter/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/pubspec.yaml @@ -2,11 +2,11 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. repository: https://github.com/flutter/plugins/tree/main/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 3.0.4 +version: 4.0.4 environment: - sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + sdk: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" flutter: plugin: @@ -19,9 +19,9 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_android: ^2.8.0 - webview_flutter_platform_interface: ^1.9.3 - webview_flutter_wkwebview: ^2.7.0 + webview_flutter_android: ^3.0.0 + webview_flutter_platform_interface: ^2.0.0 + webview_flutter_wkwebview: ^3.0.0 dev_dependencies: build_runner: ^2.1.5 @@ -29,4 +29,5 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter - mockito: ^5.0.16 + mockito: ^5.3.2 + plugin_platform_interface: ^2.1.3 diff --git a/packages/webview_flutter/webview_flutter/test/legacy/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/test/legacy/webview_flutter_test.dart new file mode 100644 index 000000000000..4db70113dfb2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/test/legacy/webview_flutter_test.dart @@ -0,0 +1,1367 @@ +// 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/src/foundation/basic_types.dart'; +import 'package:flutter/src/gestures/recognizer.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:webview_flutter/src/webview_flutter_legacy.dart'; +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; + +import 'webview_flutter_test.mocks.dart'; + +typedef VoidCallback = void Function(); + +@GenerateMocks([WebViewPlatform, WebViewPlatformController]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late MockWebViewPlatform mockWebViewPlatform; + late MockWebViewPlatformController mockWebViewPlatformController; + late MockWebViewCookieManagerPlatform mockWebViewCookieManagerPlatform; + + setUp(() { + mockWebViewPlatformController = MockWebViewPlatformController(); + mockWebViewPlatform = MockWebViewPlatform(); + mockWebViewCookieManagerPlatform = MockWebViewCookieManagerPlatform(); + when(mockWebViewPlatform.build( + context: anyNamed('context'), + creationParams: anyNamed('creationParams'), + webViewPlatformCallbacksHandler: + anyNamed('webViewPlatformCallbacksHandler'), + javascriptChannelRegistry: anyNamed('javascriptChannelRegistry'), + onWebViewPlatformCreated: anyNamed('onWebViewPlatformCreated'), + gestureRecognizers: anyNamed('gestureRecognizers'), + )).thenAnswer((Invocation invocation) { + final WebViewPlatformCreatedCallback onWebViewPlatformCreated = + invocation.namedArguments[const Symbol('onWebViewPlatformCreated')] + as WebViewPlatformCreatedCallback; + return TestPlatformWebView( + mockWebViewPlatformController: mockWebViewPlatformController, + onWebViewPlatformCreated: onWebViewPlatformCreated, + ); + }); + + WebView.platform = mockWebViewPlatform; + WebViewCookieManagerPlatform.instance = mockWebViewCookieManagerPlatform; + }); + + tearDown(() { + mockWebViewCookieManagerPlatform.reset(); + }); + + testWidgets('Create WebView', (WidgetTester tester) async { + await tester.pumpWidget(const WebView()); + }); + + testWidgets('Initial url', (WidgetTester tester) async { + await tester.pumpWidget(const WebView(initialUrl: 'https://youtube.com')); + + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect(params.initialUrl, 'https://youtube.com'); + }); + + testWidgets('Javascript mode', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + javascriptMode: JavascriptMode.unrestricted, + )); + + final CreationParams unrestrictedparams = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect( + unrestrictedparams.webSettings!.javascriptMode, + JavascriptMode.unrestricted, + ); + + await tester.pumpWidget(const WebView()); + + final CreationParams disabledparams = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect(disabledparams.webSettings!.javascriptMode, JavascriptMode.disabled); + }); + + testWidgets('Load file', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + await controller!.loadFile('/test/path/index.html'); + + verify(mockWebViewPlatformController.loadFile( + '/test/path/index.html', + )); + }); + + testWidgets('Load file with empty path', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + expect(() => controller!.loadFile(''), throwsAssertionError); + }); + + testWidgets('Load Flutter asset', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + await controller!.loadFlutterAsset('assets/index.html'); + + verify(mockWebViewPlatformController.loadFlutterAsset( + 'assets/index.html', + )); + }); + + testWidgets('Load Flutter asset with empty key', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + expect(() => controller!.loadFlutterAsset(''), throwsAssertionError); + }); + + testWidgets('Load HTML string without base URL', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + await controller!.loadHtmlString('

This is a test paragraph.

'); + + verify(mockWebViewPlatformController.loadHtmlString( + '

This is a test paragraph.

', + )); + }); + + testWidgets('Load HTML string with base URL', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + await controller!.loadHtmlString( + '

This is a test paragraph.

', + baseUrl: 'https://flutter.dev', + ); + + verify(mockWebViewPlatformController.loadHtmlString( + '

This is a test paragraph.

', + baseUrl: 'https://flutter.dev', + )); + }); + + testWidgets('Load HTML string with empty string', + (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + expect(() => controller!.loadHtmlString(''), throwsAssertionError); + }); + + testWidgets('Load url', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + await controller!.loadUrl('https://flutter.io'); + + verify(mockWebViewPlatformController.loadUrl( + 'https://flutter.io', + argThat(isNull), + )); + }); + + testWidgets('Invalid urls', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect(params.initialUrl, isNull); + + expect(() => controller!.loadUrl(''), throwsA(anything)); + expect(() => controller!.loadUrl('flutter.io'), throwsA(anything)); + }); + + testWidgets('Headers in loadUrl', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + final Map headers = { + 'CACHE-CONTROL': 'ABC' + }; + await controller!.loadUrl('https://flutter.io', headers: headers); + + verify(mockWebViewPlatformController.loadUrl( + 'https://flutter.io', + {'CACHE-CONTROL': 'ABC'}, + )); + }); + + testWidgets('loadRequest', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect(controller, isNotNull); + + final WebViewRequest req = WebViewRequest( + uri: Uri.parse('https://flutter.dev'), + method: WebViewRequestMethod.post, + headers: {'foo': 'bar'}, + body: Uint8List.fromList('Test Body'.codeUnits), + ); + + await controller!.loadRequest(req); + + verify(mockWebViewPlatformController.loadRequest(req)); + }); + + testWidgets('Clear Cache', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + await controller!.clearCache(); + + verify(mockWebViewPlatformController.clearCache()); + }); + + testWidgets('Can go back', (WidgetTester tester) async { + when(mockWebViewPlatformController.canGoBack()) + .thenAnswer((_) => Future.value(true)); + + WebViewController? controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + expect(controller!.canGoBack(), completion(true)); + }); + + testWidgets("Can't go forward", (WidgetTester tester) async { + when(mockWebViewPlatformController.canGoForward()) + .thenAnswer((_) => Future.value(false)); + + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + expect(controller!.canGoForward(), completion(false)); + }); + + testWidgets('Go back', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + await controller!.goBack(); + verify(mockWebViewPlatformController.goBack()); + }); + + testWidgets('Go forward', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + await controller!.goForward(); + verify(mockWebViewPlatformController.goForward()); + }); + + testWidgets('Current URL', (WidgetTester tester) async { + when(mockWebViewPlatformController.currentUrl()) + .thenAnswer((_) => Future.value('https://youtube.com')); + + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + expect(await controller!.currentUrl(), 'https://youtube.com'); + }); + + testWidgets('Reload url', (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + await controller.reload(); + verify(mockWebViewPlatformController.reload()); + }); + + testWidgets('evaluate Javascript', (WidgetTester tester) async { + when(mockWebViewPlatformController.evaluateJavascript('fake js string')) + .thenAnswer((_) => Future.value('fake js string')); + + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect( + // ignore: deprecated_member_use_from_same_package + await controller.evaluateJavascript('fake js string'), + 'fake js string', + reason: 'should get the argument'); + }); + + testWidgets('evaluate Javascript with JavascriptMode disabled', + (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect( + // ignore: deprecated_member_use_from_same_package + () => controller.evaluateJavascript('fake js string'), + throwsA(anything), + ); + }); + + testWidgets('runJavaScript', (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + await controller.runJavascript('fake js string'); + verify(mockWebViewPlatformController.runJavascript('fake js string')); + }); + + testWidgets('runJavaScript with JavascriptMode disabled', + (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect( + () => controller.runJavascript('fake js string'), + throwsA(anything), + ); + }); + + testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async { + when(mockWebViewPlatformController + .runJavascriptReturningResult('fake js string')) + .thenAnswer((_) => Future.value('fake js string')); + + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect(await controller.runJavascriptReturningResult('fake js string'), + 'fake js string', + reason: 'should get the argument'); + }); + + testWidgets('runJavaScriptReturningResult with JavascriptMode disabled', + (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect( + () => controller.runJavascriptReturningResult('fake js string'), + throwsA(anything), + ); + }); + + testWidgets('Cookies can be cleared once', (WidgetTester tester) async { + await tester.pumpWidget( + const WebView( + initialUrl: 'https://flutter.io', + ), + ); + final CookieManager cookieManager = CookieManager(); + final bool hasCookies = await cookieManager.clearCookies(); + expect(hasCookies, true); + }); + + testWidgets('Cookies can be set', (WidgetTester tester) async { + const WebViewCookie cookie = + WebViewCookie(name: 'foo', value: 'bar', domain: 'flutter.dev'); + + await tester.pumpWidget( + const WebView( + initialUrl: 'https://flutter.io', + ), + ); + final CookieManager cookieManager = CookieManager(); + await cookieManager.setCookie(cookie); + expect(mockWebViewCookieManagerPlatform.setCookieCalls, + [cookie]); + }); + + testWidgets('Initial JavaScript channels', (WidgetTester tester) async { + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: { + JavascriptChannel( + name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), + }, + ), + ); + + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect(params.javascriptChannelNames, + unorderedEquals(['Tts', 'Alarm'])); + }); + + test('Only valid JavaScript channel names are allowed', () { + void noOp(JavascriptMessage msg) {} + JavascriptChannel(name: 'Tts1', onMessageReceived: noOp); + JavascriptChannel(name: '_Alarm', onMessageReceived: noOp); + JavascriptChannel(name: 'foo_bar_', onMessageReceived: noOp); + + VoidCallback createChannel(String name) { + return () { + JavascriptChannel(name: name, onMessageReceived: noOp); + }; + } + + expect(createChannel('1Alarm'), throwsAssertionError); + expect(createChannel('foo.bar'), throwsAssertionError); + expect(createChannel(''), throwsAssertionError); + }); + + testWidgets('Unique JavaScript channel names are required', + (WidgetTester tester) async { + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: { + JavascriptChannel( + name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), + }, + ), + ); + expect(tester.takeException(), isNot(null)); + }); + + testWidgets('JavaScript channels update', (WidgetTester tester) async { + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: { + JavascriptChannel( + name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), + }, + ), + ); + + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: { + JavascriptChannel( + name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm2', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm3', onMessageReceived: (JavascriptMessage msg) {}), + }, + ), + ); + + final JavascriptChannelRegistry channelRegistry = captureBuildArgs( + mockWebViewPlatform, + javascriptChannelRegistry: true, + ).first as JavascriptChannelRegistry; + + expect( + channelRegistry.channels.keys, + unorderedEquals(['Tts', 'Alarm2', 'Alarm3']), + ); + }); + + testWidgets('Remove all JavaScript channels and then add', + (WidgetTester tester) async { + // This covers a specific bug we had where after updating javascriptChannels to null, + // updating it again with a subset of the previously registered channels fails as the + // widget's cache of current channel wasn't properly updated when updating javascriptChannels to + // null. + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: { + JavascriptChannel( + name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + }, + ), + ); + + await tester.pumpWidget( + const WebView( + initialUrl: 'https://youtube.com', + ), + ); + + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: { + JavascriptChannel( + name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + }, + ), + ); + + final JavascriptChannelRegistry channelRegistry = captureBuildArgs( + mockWebViewPlatform, + javascriptChannelRegistry: true, + ).last as JavascriptChannelRegistry; + + expect(channelRegistry.channels.keys, unorderedEquals(['Tts'])); + }); + + testWidgets('JavaScript channel messages', (WidgetTester tester) async { + final List ttsMessagesReceived = []; + final List alarmMessagesReceived = []; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: { + JavascriptChannel( + name: 'Tts', + onMessageReceived: (JavascriptMessage msg) { + ttsMessagesReceived.add(msg.message); + }), + JavascriptChannel( + name: 'Alarm', + onMessageReceived: (JavascriptMessage msg) { + alarmMessagesReceived.add(msg.message); + }), + }, + ), + ); + + final JavascriptChannelRegistry channelRegistry = captureBuildArgs( + mockWebViewPlatform, + javascriptChannelRegistry: true, + ).single as JavascriptChannelRegistry; + + expect(ttsMessagesReceived, isEmpty); + expect(alarmMessagesReceived, isEmpty); + + channelRegistry.onJavascriptChannelMessage('Tts', 'Hello'); + channelRegistry.onJavascriptChannelMessage('Tts', 'World'); + + expect(ttsMessagesReceived, ['Hello', 'World']); + }); + + group('$PageStartedCallback', () { + testWidgets('onPageStarted is not null', (WidgetTester tester) async { + String? returnedUrl; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageStarted: (String url) { + returnedUrl = url; + }, + )); + + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single as WebViewPlatformCallbacksHandler; + + handler.onPageStarted('https://youtube.com'); + + expect(returnedUrl, 'https://youtube.com'); + }); + + testWidgets('onPageStarted is null', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + )); + + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single as WebViewPlatformCallbacksHandler; + + // The platform side will always invoke a call for onPageStarted. This is + // to test that it does not crash on a null callback. + handler.onPageStarted('https://youtube.com'); + }); + + testWidgets('onPageStarted changed', (WidgetTester tester) async { + String? returnedUrl; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageStarted: (String url) {}, + )); + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageStarted: (String url) { + returnedUrl = url; + }, + )); + + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).last as WebViewPlatformCallbacksHandler; + handler.onPageStarted('https://youtube.com'); + + expect(returnedUrl, 'https://youtube.com'); + }); + }); + + group('$PageFinishedCallback', () { + testWidgets('onPageFinished is not null', (WidgetTester tester) async { + String? returnedUrl; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageFinished: (String url) { + returnedUrl = url; + }, + )); + + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single as WebViewPlatformCallbacksHandler; + handler.onPageFinished('https://youtube.com'); + + expect(returnedUrl, 'https://youtube.com'); + }); + + testWidgets('onPageFinished is null', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + )); + + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single as WebViewPlatformCallbacksHandler; + // The platform side will always invoke a call for onPageFinished. This is + // to test that it does not crash on a null callback. + handler.onPageFinished('https://youtube.com'); + }); + + testWidgets('onPageFinished changed', (WidgetTester tester) async { + String? returnedUrl; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageFinished: (String url) {}, + )); + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageFinished: (String url) { + returnedUrl = url; + }, + )); + + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).last as WebViewPlatformCallbacksHandler; + handler.onPageFinished('https://youtube.com'); + + expect(returnedUrl, 'https://youtube.com'); + }); + }); + + group('$PageLoadingCallback', () { + testWidgets('onLoadingProgress is not null', (WidgetTester tester) async { + int? loadingProgress; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onProgress: (int progress) { + loadingProgress = progress; + }, + )); + + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single as WebViewPlatformCallbacksHandler; + handler.onProgress(50); + + expect(loadingProgress, 50); + }); + + testWidgets('onLoadingProgress is null', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + )); + + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).single as WebViewPlatformCallbacksHandler; + + // This is to test that it does not crash on a null callback. + handler.onProgress(50); + }); + + testWidgets('onLoadingProgress changed', (WidgetTester tester) async { + int? loadingProgress; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onProgress: (int progress) {}, + )); + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onProgress: (int progress) { + loadingProgress = progress; + }, + )); + + final WebViewPlatformCallbacksHandler handler = captureBuildArgs( + mockWebViewPlatform, + webViewPlatformCallbacksHandler: true, + ).last as WebViewPlatformCallbacksHandler; + handler.onProgress(50); + + expect(loadingProgress, 50); + }); + }); + + group('navigationDelegate', () { + testWidgets('hasNavigationDelegate', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + )); + + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect(params.webSettings!.hasNavigationDelegate, false); + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + navigationDelegate: (NavigationRequest r) => + NavigationDecision.navigate, + )); + + final WebSettings updateSettings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .single as WebSettings; + + expect(updateSettings.hasNavigationDelegate, true); + }); + + testWidgets('Block navigation', (WidgetTester tester) async { + final List navigationRequests = []; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + navigationDelegate: (NavigationRequest request) { + navigationRequests.add(request); + // Only allow navigating to https://flutter.dev + return request.url == 'https://flutter.dev' + ? NavigationDecision.navigate + : NavigationDecision.prevent; + })); + + final List args = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + webViewPlatformCallbacksHandler: true, + ); + + final CreationParams params = args[0] as CreationParams; + expect(params.webSettings!.hasNavigationDelegate, true); + + final WebViewPlatformCallbacksHandler handler = + args[1] as WebViewPlatformCallbacksHandler; + + // The navigation delegate only allows navigation to https://flutter.dev + // so we should still be in https://youtube.com. + expect( + handler.onNavigationRequest( + url: 'https://www.google.com', + isForMainFrame: true, + ), + completion(false), + ); + + expect(navigationRequests.length, 1); + expect(navigationRequests[0].url, 'https://www.google.com'); + expect(navigationRequests[0].isForMainFrame, true); + + expect( + handler.onNavigationRequest( + url: 'https://flutter.dev', + isForMainFrame: true, + ), + completion(true), + ); + }); + }); + + group('debuggingEnabled', () { + testWidgets('enable debugging', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + debuggingEnabled: true, + )); + + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect(params.webSettings!.debuggingEnabled, true); + }); + + testWidgets('defaults to false', (WidgetTester tester) async { + await tester.pumpWidget(const WebView()); + + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect(params.webSettings!.debuggingEnabled, false); + }); + + testWidgets('can be changed', (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + await tester.pumpWidget(WebView(key: key)); + + await tester.pumpWidget(WebView( + key: key, + debuggingEnabled: true, + )); + + final WebSettings enabledSettings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .last as WebSettings; + expect(enabledSettings.debuggingEnabled, true); + + await tester.pumpWidget(WebView( + key: key, + )); + + final WebSettings disabledSettings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .last as WebSettings; + expect(disabledSettings.debuggingEnabled, false); + }); + }); + + group('zoomEnabled', () { + testWidgets('Enable zoom', (WidgetTester tester) async { + await tester.pumpWidget(const WebView()); + + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect(params.webSettings!.zoomEnabled, isTrue); + }); + + testWidgets('defaults to true', (WidgetTester tester) async { + await tester.pumpWidget(const WebView()); + + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect(params.webSettings!.zoomEnabled, isTrue); + }); + + testWidgets('can be changed', (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + await tester.pumpWidget(WebView(key: key)); + + await tester.pumpWidget(WebView( + key: key, + )); + + final WebSettings enabledSettings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .last as WebSettings; + // Zoom defaults to true, so no changes are made to settings. + expect(enabledSettings.zoomEnabled, isNull); + + await tester.pumpWidget(WebView( + key: key, + zoomEnabled: false, + )); + + final WebSettings disabledSettings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .last as WebSettings; + expect(disabledSettings.zoomEnabled, isFalse); + }); + }); + + group('Background color', () { + testWidgets('Defaults to null', (WidgetTester tester) async { + await tester.pumpWidget(const WebView()); + + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect(params.backgroundColor, null); + }); + + testWidgets('Can be transparent', (WidgetTester tester) async { + const Color transparentColor = Color(0x00000000); + + await tester.pumpWidget(const WebView( + backgroundColor: transparentColor, + )); + + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect(params.backgroundColor, transparentColor); + }); + }); + + group('Custom platform implementation', () { + setUp(() { + WebView.platform = MyWebViewPlatform(); + }); + tearDownAll(() { + WebView.platform = null; + }); + + testWidgets('creation', (WidgetTester tester) async { + await tester.pumpWidget( + const WebView( + initialUrl: 'https://youtube.com', + gestureNavigationEnabled: true, + ), + ); + + final MyWebViewPlatform builder = WebView.platform as MyWebViewPlatform; + final MyWebViewPlatformController platform = builder.lastPlatformBuilt!; + + expect( + platform.creationParams, + MatchesCreationParams(CreationParams( + initialUrl: 'https://youtube.com', + webSettings: WebSettings( + javascriptMode: JavascriptMode.disabled, + hasNavigationDelegate: false, + debuggingEnabled: false, + userAgent: const WebSetting.of(null), + gestureNavigationEnabled: true, + zoomEnabled: true, + ), + ))); + }); + + testWidgets('loadUrl', (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + final MyWebViewPlatform builder = WebView.platform as MyWebViewPlatform; + final MyWebViewPlatformController platform = builder.lastPlatformBuilt!; + + final Map headers = { + 'header': 'value', + }; + + await controller.loadUrl('https://google.com', headers: headers); + + expect(platform.lastUrlLoaded, 'https://google.com'); + expect(platform.lastRequestHeaders, headers); + }); + }); + + testWidgets('Set UserAgent', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + javascriptMode: JavascriptMode.unrestricted, + )); + + final CreationParams params = captureBuildArgs( + mockWebViewPlatform, + creationParams: true, + ).single as CreationParams; + + expect(params.webSettings!.userAgent.value, isNull); + + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'UA', + )); + + final WebSettings settings = + verify(mockWebViewPlatformController.updateSettings(captureAny)) + .captured + .last as WebSettings; + expect(settings.userAgent.value, 'UA'); + }); +} + +List captureBuildArgs( + MockWebViewPlatform mockWebViewPlatform, { + bool context = false, + bool creationParams = false, + bool webViewPlatformCallbacksHandler = false, + bool javascriptChannelRegistry = false, + bool onWebViewPlatformCreated = false, + bool gestureRecognizers = false, +}) { + return verify(mockWebViewPlatform.build( + context: context ? captureAnyNamed('context') : anyNamed('context'), + creationParams: creationParams + ? captureAnyNamed('creationParams') + : anyNamed('creationParams'), + webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler + ? captureAnyNamed('webViewPlatformCallbacksHandler') + : anyNamed('webViewPlatformCallbacksHandler'), + javascriptChannelRegistry: javascriptChannelRegistry + ? captureAnyNamed('javascriptChannelRegistry') + : anyNamed('javascriptChannelRegistry'), + onWebViewPlatformCreated: onWebViewPlatformCreated + ? captureAnyNamed('onWebViewPlatformCreated') + : anyNamed('onWebViewPlatformCreated'), + gestureRecognizers: gestureRecognizers + ? captureAnyNamed('gestureRecognizers') + : anyNamed('gestureRecognizers'), + )).captured; +} + +// This Widget ensures that onWebViewPlatformCreated is only called once when +// making multiple calls to `WidgetTester.pumpWidget` with different parameters +// for the WebView. +class TestPlatformWebView extends StatefulWidget { + const TestPlatformWebView({ + super.key, + required this.mockWebViewPlatformController, + this.onWebViewPlatformCreated, + }); + + final MockWebViewPlatformController mockWebViewPlatformController; + final WebViewPlatformCreatedCallback? onWebViewPlatformCreated; + + @override + State createState() => TestPlatformWebViewState(); +} + +class TestPlatformWebViewState extends State { + @override + void initState() { + super.initState(); + final WebViewPlatformCreatedCallback? onWebViewPlatformCreated = + widget.onWebViewPlatformCreated; + if (onWebViewPlatformCreated != null) { + onWebViewPlatformCreated(widget.mockWebViewPlatformController); + } + } + + @override + Widget build(BuildContext context) { + return Container(); + } +} + +class MyWebViewPlatform implements WebViewPlatform { + MyWebViewPlatformController? lastPlatformBuilt; + + @override + Widget build({ + BuildContext? context, + CreationParams? creationParams, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + required JavascriptChannelRegistry javascriptChannelRegistry, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + }) { + assert(onWebViewPlatformCreated != null); + lastPlatformBuilt = MyWebViewPlatformController( + creationParams, gestureRecognizers, webViewPlatformCallbacksHandler); + onWebViewPlatformCreated!(lastPlatformBuilt); + return Container(); + } + + @override + Future clearCookies() { + return Future.sync(() => true); + } +} + +class MyWebViewPlatformController extends WebViewPlatformController { + MyWebViewPlatformController(this.creationParams, this.gestureRecognizers, + WebViewPlatformCallbacksHandler platformHandler) + : super(platformHandler); + + CreationParams? creationParams; + Set>? gestureRecognizers; + + String? lastUrlLoaded; + Map? lastRequestHeaders; + + @override + Future loadUrl(String url, Map? headers) async { + equals(1, 1); + lastUrlLoaded = url; + lastRequestHeaders = headers; + } +} + +class MatchesWebSettings extends Matcher { + MatchesWebSettings(this._webSettings); + + final WebSettings? _webSettings; + + @override + Description describe(Description description) => + description.add('$_webSettings'); + + @override + bool matches( + covariant WebSettings webSettings, Map matchState) { + return _webSettings!.javascriptMode == webSettings.javascriptMode && + _webSettings!.hasNavigationDelegate == + webSettings.hasNavigationDelegate && + _webSettings!.debuggingEnabled == webSettings.debuggingEnabled && + _webSettings!.gestureNavigationEnabled == + webSettings.gestureNavigationEnabled && + _webSettings!.userAgent == webSettings.userAgent && + _webSettings!.zoomEnabled == webSettings.zoomEnabled; + } +} + +class MatchesCreationParams extends Matcher { + MatchesCreationParams(this._creationParams); + + final CreationParams _creationParams; + + @override + Description describe(Description description) => + description.add('$_creationParams'); + + @override + bool matches(covariant CreationParams creationParams, + Map matchState) { + return _creationParams.initialUrl == creationParams.initialUrl && + MatchesWebSettings(_creationParams.webSettings) + .matches(creationParams.webSettings!, matchState) && + orderedEquals(_creationParams.javascriptChannelNames) + .matches(creationParams.javascriptChannelNames, matchState); + } +} + +class MockWebViewCookieManagerPlatform extends WebViewCookieManagerPlatform { + List setCookieCalls = []; + + @override + Future clearCookies() async => true; + + @override + Future setCookie(WebViewCookie cookie) async { + setCookieCalls.add(cookie); + } + + void reset() { + setCookieCalls = []; + } +} diff --git a/packages/webview_flutter/webview_flutter/test/legacy/webview_flutter_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/legacy/webview_flutter_test.mocks.dart new file mode 100644 index 000000000000..a40cf34828ae --- /dev/null +++ b/packages/webview_flutter/webview_flutter/test/legacy/webview_flutter_test.mocks.dart @@ -0,0 +1,346 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter/test/legacy/webview_flutter_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i9; + +import 'package:flutter/foundation.dart' as _i3; +import 'package:flutter/gestures.dart' as _i8; +import 'package:flutter/widgets.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_platform_interface/src/legacy/platform_interface/javascript_channel_registry.dart' + as _i7; +import 'package:webview_flutter_platform_interface/src/legacy/platform_interface/webview_platform.dart' + as _i4; +import 'package:webview_flutter_platform_interface/src/legacy/platform_interface/webview_platform_callbacks_handler.dart' + as _i6; +import 'package:webview_flutter_platform_interface/src/legacy/platform_interface/webview_platform_controller.dart' + as _i10; +import 'package:webview_flutter_platform_interface/src/legacy/types/types.dart' + as _i5; + +// 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 + +class _FakeWidget_0 extends _i1.SmartFake implements _i2.Widget { + _FakeWidget_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +/// A class which mocks [WebViewPlatform]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewPlatform extends _i1.Mock implements _i4.WebViewPlatform { + MockWebViewPlatform() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Widget build({ + required _i2.BuildContext? context, + required _i5.CreationParams? creationParams, + required _i6.WebViewPlatformCallbacksHandler? + webViewPlatformCallbacksHandler, + required _i7.JavascriptChannelRegistry? javascriptChannelRegistry, + _i4.WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set<_i3.Factory<_i8.OneSequenceGestureRecognizer>>? gestureRecognizers, + }) => + (super.noSuchMethod( + Invocation.method( + #build, + [], + { + #context: context, + #creationParams: creationParams, + #webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler, + #javascriptChannelRegistry: javascriptChannelRegistry, + #onWebViewPlatformCreated: onWebViewPlatformCreated, + #gestureRecognizers: gestureRecognizers, + }, + ), + returnValue: _FakeWidget_0( + this, + Invocation.method( + #build, + [], + { + #context: context, + #creationParams: creationParams, + #webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler, + #javascriptChannelRegistry: javascriptChannelRegistry, + #onWebViewPlatformCreated: onWebViewPlatformCreated, + #gestureRecognizers: gestureRecognizers, + }, + ), + ), + ) as _i2.Widget); + @override + _i9.Future clearCookies() => (super.noSuchMethod( + Invocation.method( + #clearCookies, + [], + ), + returnValue: _i9.Future.value(false), + ) as _i9.Future); +} + +/// A class which mocks [WebViewPlatformController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewPlatformController extends _i1.Mock + implements _i10.WebViewPlatformController { + MockWebViewPlatformController() { + _i1.throwOnMissingStub(this); + } + + @override + _i9.Future loadFile(String? absoluteFilePath) => (super.noSuchMethod( + Invocation.method( + #loadFile, + [absoluteFilePath], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future loadFlutterAsset(String? key) => (super.noSuchMethod( + Invocation.method( + #loadFlutterAsset, + [key], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future loadHtmlString( + String? html, { + String? baseUrl, + }) => + (super.noSuchMethod( + Invocation.method( + #loadHtmlString, + [html], + {#baseUrl: baseUrl}, + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future loadUrl( + String? url, + Map? headers, + ) => + (super.noSuchMethod( + Invocation.method( + #loadUrl, + [ + url, + headers, + ], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future loadRequest(_i5.WebViewRequest? request) => + (super.noSuchMethod( + Invocation.method( + #loadRequest, + [request], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future updateSettings(_i5.WebSettings? setting) => + (super.noSuchMethod( + Invocation.method( + #updateSettings, + [setting], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future currentUrl() => (super.noSuchMethod( + Invocation.method( + #currentUrl, + [], + ), + returnValue: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future canGoBack() => (super.noSuchMethod( + Invocation.method( + #canGoBack, + [], + ), + returnValue: _i9.Future.value(false), + ) as _i9.Future); + @override + _i9.Future canGoForward() => (super.noSuchMethod( + Invocation.method( + #canGoForward, + [], + ), + returnValue: _i9.Future.value(false), + ) as _i9.Future); + @override + _i9.Future goBack() => (super.noSuchMethod( + Invocation.method( + #goBack, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future goForward() => (super.noSuchMethod( + Invocation.method( + #goForward, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future reload() => (super.noSuchMethod( + Invocation.method( + #reload, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future clearCache() => (super.noSuchMethod( + Invocation.method( + #clearCache, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future evaluateJavascript(String? javascript) => + (super.noSuchMethod( + Invocation.method( + #evaluateJavascript, + [javascript], + ), + returnValue: _i9.Future.value(''), + ) as _i9.Future); + @override + _i9.Future runJavascript(String? javascript) => (super.noSuchMethod( + Invocation.method( + #runJavascript, + [javascript], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future runJavascriptReturningResult(String? javascript) => + (super.noSuchMethod( + Invocation.method( + #runJavascriptReturningResult, + [javascript], + ), + returnValue: _i9.Future.value(''), + ) as _i9.Future); + @override + _i9.Future addJavascriptChannels(Set? javascriptChannelNames) => + (super.noSuchMethod( + Invocation.method( + #addJavascriptChannels, + [javascriptChannelNames], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future removeJavascriptChannels( + Set? javascriptChannelNames) => + (super.noSuchMethod( + Invocation.method( + #removeJavascriptChannels, + [javascriptChannelNames], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future getTitle() => (super.noSuchMethod( + Invocation.method( + #getTitle, + [], + ), + returnValue: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future scrollTo( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollTo, + [ + x, + y, + ], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future scrollBy( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollBy, + [ + x, + y, + ], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future getScrollX() => (super.noSuchMethod( + Invocation.method( + #getScrollX, + [], + ), + returnValue: _i9.Future.value(0), + ) as _i9.Future); + @override + _i9.Future getScrollY() => (super.noSuchMethod( + Invocation.method( + #getScrollY, + [], + ), + returnValue: _i9.Future.value(0), + ) as _i9.Future); +} diff --git a/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.dart new file mode 100644 index 000000000000..839454eaa605 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.dart @@ -0,0 +1,91 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'navigation_delegate_test.mocks.dart'; + +@GenerateMocks([WebViewPlatform, PlatformNavigationDelegate]) +void main() { + group('NavigationDelegate', () { + test('onNavigationRequest', () async { + WebViewPlatform.instance = TestWebViewPlatform(); + + NavigationDecision onNavigationRequest(NavigationRequest request) { + return NavigationDecision.navigate; + } + + final NavigationDelegate delegate = NavigationDelegate( + onNavigationRequest: onNavigationRequest, + ); + + verify(delegate.platform.setOnNavigationRequest(onNavigationRequest)); + }); + + test('onPageStarted', () async { + WebViewPlatform.instance = TestWebViewPlatform(); + + void onPageStarted(String url) {} + + final NavigationDelegate delegate = NavigationDelegate( + onPageStarted: onPageStarted, + ); + + verify(delegate.platform.setOnPageStarted(onPageStarted)); + }); + + test('onPageFinished', () async { + WebViewPlatform.instance = TestWebViewPlatform(); + + void onPageFinished(String url) {} + + final NavigationDelegate delegate = NavigationDelegate( + onPageFinished: onPageFinished, + ); + + verify(delegate.platform.setOnPageFinished(onPageFinished)); + }); + + test('onProgress', () async { + WebViewPlatform.instance = TestWebViewPlatform(); + + void onProgress(int progress) {} + + final NavigationDelegate delegate = NavigationDelegate( + onProgress: onProgress, + ); + + verify(delegate.platform.setOnProgress(onProgress)); + }); + + test('onWebResourceError', () async { + WebViewPlatform.instance = TestWebViewPlatform(); + + void onWebResourceError(WebResourceError error) {} + + final NavigationDelegate delegate = NavigationDelegate( + onWebResourceError: onWebResourceError, + ); + + verify(delegate.platform.setOnWebResourceError(onWebResourceError)); + }); + }); +} + +class TestWebViewPlatform extends WebViewPlatform { + @override + PlatformNavigationDelegate createPlatformNavigationDelegate( + PlatformNavigationDelegateCreationParams params, + ) { + return TestMockPlatformNavigationDelegate(); + } +} + +class TestMockPlatformNavigationDelegate extends MockPlatformNavigationDelegate + with MockPlatformInterfaceMixin {} diff --git a/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.mocks.dart new file mode 100644 index 000000000000..a7ac41e558c3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.mocks.dart @@ -0,0 +1,231 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter/test/navigation_delegate_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i8; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_platform_interface/src/platform_navigation_delegate.dart' + as _i3; +import 'package:webview_flutter_platform_interface/src/platform_webview_controller.dart' + as _i4; +import 'package:webview_flutter_platform_interface/src/platform_webview_cookie_manager.dart' + as _i2; +import 'package:webview_flutter_platform_interface/src/platform_webview_widget.dart' + as _i5; +import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i6; +import 'package:webview_flutter_platform_interface/src/webview_platform.dart' + as _i7; + +// 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 + +class _FakePlatformWebViewCookieManager_0 extends _i1.SmartFake + implements _i2.PlatformWebViewCookieManager { + _FakePlatformWebViewCookieManager_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlatformNavigationDelegate_1 extends _i1.SmartFake + implements _i3.PlatformNavigationDelegate { + _FakePlatformNavigationDelegate_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlatformWebViewController_2 extends _i1.SmartFake + implements _i4.PlatformWebViewController { + _FakePlatformWebViewController_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlatformWebViewWidget_3 extends _i1.SmartFake + implements _i5.PlatformWebViewWidget { + _FakePlatformWebViewWidget_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlatformNavigationDelegateCreationParams_4 extends _i1.SmartFake + implements _i6.PlatformNavigationDelegateCreationParams { + _FakePlatformNavigationDelegateCreationParams_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [WebViewPlatform]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewPlatform extends _i1.Mock implements _i7.WebViewPlatform { + MockWebViewPlatform() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.PlatformWebViewCookieManager createPlatformCookieManager( + _i6.PlatformWebViewCookieManagerCreationParams? params) => + (super.noSuchMethod( + Invocation.method( + #createPlatformCookieManager, + [params], + ), + returnValue: _FakePlatformWebViewCookieManager_0( + this, + Invocation.method( + #createPlatformCookieManager, + [params], + ), + ), + ) as _i2.PlatformWebViewCookieManager); + @override + _i3.PlatformNavigationDelegate createPlatformNavigationDelegate( + _i6.PlatformNavigationDelegateCreationParams? params) => + (super.noSuchMethod( + Invocation.method( + #createPlatformNavigationDelegate, + [params], + ), + returnValue: _FakePlatformNavigationDelegate_1( + this, + Invocation.method( + #createPlatformNavigationDelegate, + [params], + ), + ), + ) as _i3.PlatformNavigationDelegate); + @override + _i4.PlatformWebViewController createPlatformWebViewController( + _i6.PlatformWebViewControllerCreationParams? params) => + (super.noSuchMethod( + Invocation.method( + #createPlatformWebViewController, + [params], + ), + returnValue: _FakePlatformWebViewController_2( + this, + Invocation.method( + #createPlatformWebViewController, + [params], + ), + ), + ) as _i4.PlatformWebViewController); + @override + _i5.PlatformWebViewWidget createPlatformWebViewWidget( + _i6.PlatformWebViewWidgetCreationParams? params) => + (super.noSuchMethod( + Invocation.method( + #createPlatformWebViewWidget, + [params], + ), + returnValue: _FakePlatformWebViewWidget_3( + this, + Invocation.method( + #createPlatformWebViewWidget, + [params], + ), + ), + ) as _i5.PlatformWebViewWidget); +} + +/// A class which mocks [PlatformNavigationDelegate]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPlatformNavigationDelegate extends _i1.Mock + implements _i3.PlatformNavigationDelegate { + MockPlatformNavigationDelegate() { + _i1.throwOnMissingStub(this); + } + + @override + _i6.PlatformNavigationDelegateCreationParams get params => + (super.noSuchMethod( + Invocation.getter(#params), + returnValue: _FakePlatformNavigationDelegateCreationParams_4( + this, + Invocation.getter(#params), + ), + ) as _i6.PlatformNavigationDelegateCreationParams); + @override + _i8.Future setOnNavigationRequest( + _i3.NavigationRequestCallback? onNavigationRequest) => + (super.noSuchMethod( + Invocation.method( + #setOnNavigationRequest, + [onNavigationRequest], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future setOnPageStarted(_i3.PageEventCallback? onPageStarted) => + (super.noSuchMethod( + Invocation.method( + #setOnPageStarted, + [onPageStarted], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future setOnPageFinished(_i3.PageEventCallback? onPageFinished) => + (super.noSuchMethod( + Invocation.method( + #setOnPageFinished, + [onPageFinished], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future setOnProgress(_i3.ProgressCallback? onProgress) => + (super.noSuchMethod( + Invocation.method( + #setOnProgress, + [onProgress], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override + _i8.Future setOnWebResourceError( + _i3.WebResourceErrorCallback? onWebResourceError) => + (super.noSuchMethod( + Invocation.method( + #setOnWebResourceError, + [onWebResourceError], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} diff --git a/packages/webview_flutter/webview_flutter/test/v4/webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/v4/webview_controller_test.mocks.dart deleted file mode 100644 index f0fb4b47fc6e..000000000000 --- a/packages/webview_flutter/webview_flutter/test/v4/webview_controller_test.mocks.dart +++ /dev/null @@ -1,203 +0,0 @@ -// Mocks generated by Mockito 5.3.0 from annotations -// in webview_flutter/test/v4/webview_controller_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; -import 'dart:math' as _i3; -import 'dart:ui' as _i7; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_platform_interface/v4/src/platform_navigation_delegate.dart' - as _i6; -import 'package:webview_flutter_platform_interface/v4/src/platform_webview_controller.dart' - as _i4; -import 'package:webview_flutter_platform_interface/v4/src/webview_platform.dart' - as _i2; - -// 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 - -class _FakePlatformWebViewControllerCreationParams_0 extends _i1.SmartFake - implements _i2.PlatformWebViewControllerCreationParams { - _FakePlatformWebViewControllerCreationParams_0( - Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); -} - -class _FakePoint_1 extends _i1.SmartFake - implements _i3.Point { - _FakePoint_1(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); -} - -/// A class which mocks [PlatformWebViewController]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockPlatformWebViewController extends _i1.Mock - implements _i4.PlatformWebViewController { - MockPlatformWebViewController() { - _i1.throwOnMissingStub(this); - } - - @override - _i2.PlatformWebViewControllerCreationParams get params => - (super.noSuchMethod(Invocation.getter(#params), - returnValue: _FakePlatformWebViewControllerCreationParams_0( - this, Invocation.getter(#params))) - as _i2.PlatformWebViewControllerCreationParams); - @override - _i5.Future loadFile(String? absoluteFilePath) => (super.noSuchMethod( - Invocation.method(#loadFile, [absoluteFilePath]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value()) as _i5.Future); - @override - _i5.Future loadFlutterAsset(String? key) => (super.noSuchMethod( - Invocation.method(#loadFlutterAsset, [key]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value()) as _i5.Future); - @override - _i5.Future loadHtmlString(String? html, {String? baseUrl}) => - (super.noSuchMethod( - Invocation.method(#loadHtmlString, [html], {#baseUrl: baseUrl}), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value()) - as _i5.Future); - @override - _i5.Future loadRequest(_i2.LoadRequestParams? params) => - (super.noSuchMethod(Invocation.method(#loadRequest, [params]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value()) - as _i5.Future); - @override - _i5.Future currentUrl() => - (super.noSuchMethod(Invocation.method(#currentUrl, []), - returnValue: _i5.Future.value()) as _i5.Future); - @override - _i5.Future canGoBack() => - (super.noSuchMethod(Invocation.method(#canGoBack, []), - returnValue: _i5.Future.value(false)) as _i5.Future); - @override - _i5.Future canGoForward() => - (super.noSuchMethod(Invocation.method(#canGoForward, []), - returnValue: _i5.Future.value(false)) as _i5.Future); - @override - _i5.Future goBack() => (super.noSuchMethod( - Invocation.method(#goBack, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value()) as _i5.Future); - @override - _i5.Future goForward() => (super.noSuchMethod( - Invocation.method(#goForward, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value()) as _i5.Future); - @override - _i5.Future reload() => (super.noSuchMethod( - Invocation.method(#reload, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value()) as _i5.Future); - @override - _i5.Future clearCache() => (super.noSuchMethod( - Invocation.method(#clearCache, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value()) as _i5.Future); - @override - _i5.Future clearLocalStorage() => (super.noSuchMethod( - Invocation.method(#clearLocalStorage, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value()) as _i5.Future); - @override - _i5.Future setPlatformNavigationDelegate( - _i6.PlatformNavigationDelegate? handler) => - (super.noSuchMethod( - Invocation.method(#setPlatformNavigationDelegate, [handler]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value()) - as _i5.Future); - @override - _i5.Future runJavaScript(String? javaScript) => (super.noSuchMethod( - Invocation.method(#runJavaScript, [javaScript]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value()) as _i5.Future); - @override - _i5.Future runJavaScriptReturningResult(String? javaScript) => - (super.noSuchMethod( - Invocation.method(#runJavaScriptReturningResult, [javaScript]), - returnValue: _i5.Future.value('')) as _i5.Future); - @override - _i5.Future addJavaScriptChannel( - _i4.JavaScriptChannelParams? javaScriptChannelParams) => - (super.noSuchMethod( - Invocation.method(#addJavaScriptChannel, [javaScriptChannelParams]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: - _i5.Future.value()) as _i5.Future); - @override - _i5.Future removeJavaScriptChannel(String? javaScriptChannelName) => - (super.noSuchMethod( - Invocation.method(#removeJavaScriptChannel, [javaScriptChannelName]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: - _i5.Future.value()) as _i5.Future); - @override - _i5.Future getTitle() => - (super.noSuchMethod(Invocation.method(#getTitle, []), - returnValue: _i5.Future.value()) as _i5.Future); - @override - _i5.Future scrollTo(int? x, int? y) => (super.noSuchMethod( - Invocation.method(#scrollTo, [x, y]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value()) as _i5.Future); - @override - _i5.Future scrollBy(int? x, int? y) => (super.noSuchMethod( - Invocation.method(#scrollBy, [x, y]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value()) as _i5.Future); - @override - _i5.Future<_i3.Point> getScrollPosition() => - (super.noSuchMethod(Invocation.method(#getScrollPosition, []), - returnValue: _i5.Future<_i3.Point>.value(_FakePoint_1( - this, Invocation.method(#getScrollPosition, [])))) - as _i5.Future<_i3.Point>); - @override - _i5.Future enableDebugging(bool? enabled) => (super.noSuchMethod( - Invocation.method(#enableDebugging, [enabled]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value()) as _i5.Future); - @override - _i5.Future enableGestureNavigation(bool? enabled) => (super - .noSuchMethod(Invocation.method(#enableGestureNavigation, [enabled]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value()) - as _i5.Future); - @override - _i5.Future enableZoom(bool? enabled) => (super.noSuchMethod( - Invocation.method(#enableZoom, [enabled]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value()) as _i5.Future); - @override - _i5.Future setBackgroundColor(_i7.Color? color) => (super.noSuchMethod( - Invocation.method(#setBackgroundColor, [color]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value()) as _i5.Future); - @override - _i5.Future setJavaScriptMode(_i2.JavaScriptMode? javaScriptMode) => - (super.noSuchMethod( - Invocation.method(#setJavaScriptMode, [javaScriptMode]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value()) - as _i5.Future); - @override - _i5.Future setUserAgent(String? userAgent) => (super.noSuchMethod( - Invocation.method(#setUserAgent, [userAgent]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value()) as _i5.Future); -} diff --git a/packages/webview_flutter/webview_flutter/test/v4/webview_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/v4/webview_widget_test.mocks.dart deleted file mode 100644 index e481d752be5d..000000000000 --- a/packages/webview_flutter/webview_flutter/test/v4/webview_widget_test.mocks.dart +++ /dev/null @@ -1,246 +0,0 @@ -// Mocks generated by Mockito 5.3.0 from annotations -// in webview_flutter/test/v4/webview_widget_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i7; -import 'dart:math' as _i3; -import 'dart:ui' as _i9; - -import 'package:flutter/foundation.dart' as _i5; -import 'package:flutter/widgets.dart' as _i4; -import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_platform_interface/v4/src/platform_navigation_delegate.dart' - as _i8; -import 'package:webview_flutter_platform_interface/v4/src/platform_webview_controller.dart' - as _i6; -import 'package:webview_flutter_platform_interface/v4/src/platform_webview_widget.dart' - as _i10; -import 'package:webview_flutter_platform_interface/v4/src/webview_platform.dart' - as _i2; - -// 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 - -class _FakePlatformWebViewControllerCreationParams_0 extends _i1.SmartFake - implements _i2.PlatformWebViewControllerCreationParams { - _FakePlatformWebViewControllerCreationParams_0( - Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); -} - -class _FakePoint_1 extends _i1.SmartFake - implements _i3.Point { - _FakePoint_1(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); -} - -class _FakePlatformWebViewWidgetCreationParams_2 extends _i1.SmartFake - implements _i2.PlatformWebViewWidgetCreationParams { - _FakePlatformWebViewWidgetCreationParams_2( - Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); -} - -class _FakeWidget_3 extends _i1.SmartFake implements _i4.Widget { - _FakeWidget_3(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); - - @override - String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => - super.toString(); -} - -/// A class which mocks [PlatformWebViewController]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockPlatformWebViewController extends _i1.Mock - implements _i6.PlatformWebViewController { - MockPlatformWebViewController() { - _i1.throwOnMissingStub(this); - } - - @override - _i2.PlatformWebViewControllerCreationParams get params => - (super.noSuchMethod(Invocation.getter(#params), - returnValue: _FakePlatformWebViewControllerCreationParams_0( - this, Invocation.getter(#params))) - as _i2.PlatformWebViewControllerCreationParams); - @override - _i7.Future loadFile(String? absoluteFilePath) => (super.noSuchMethod( - Invocation.method(#loadFile, [absoluteFilePath]), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value()) as _i7.Future); - @override - _i7.Future loadFlutterAsset(String? key) => (super.noSuchMethod( - Invocation.method(#loadFlutterAsset, [key]), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value()) as _i7.Future); - @override - _i7.Future loadHtmlString(String? html, {String? baseUrl}) => - (super.noSuchMethod( - Invocation.method(#loadHtmlString, [html], {#baseUrl: baseUrl}), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value()) - as _i7.Future); - @override - _i7.Future loadRequest(_i2.LoadRequestParams? params) => - (super.noSuchMethod(Invocation.method(#loadRequest, [params]), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value()) - as _i7.Future); - @override - _i7.Future currentUrl() => - (super.noSuchMethod(Invocation.method(#currentUrl, []), - returnValue: _i7.Future.value()) as _i7.Future); - @override - _i7.Future canGoBack() => - (super.noSuchMethod(Invocation.method(#canGoBack, []), - returnValue: _i7.Future.value(false)) as _i7.Future); - @override - _i7.Future canGoForward() => - (super.noSuchMethod(Invocation.method(#canGoForward, []), - returnValue: _i7.Future.value(false)) as _i7.Future); - @override - _i7.Future goBack() => (super.noSuchMethod( - Invocation.method(#goBack, []), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value()) as _i7.Future); - @override - _i7.Future goForward() => (super.noSuchMethod( - Invocation.method(#goForward, []), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value()) as _i7.Future); - @override - _i7.Future reload() => (super.noSuchMethod( - Invocation.method(#reload, []), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value()) as _i7.Future); - @override - _i7.Future clearCache() => (super.noSuchMethod( - Invocation.method(#clearCache, []), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value()) as _i7.Future); - @override - _i7.Future clearLocalStorage() => (super.noSuchMethod( - Invocation.method(#clearLocalStorage, []), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value()) as _i7.Future); - @override - _i7.Future setPlatformNavigationDelegate( - _i8.PlatformNavigationDelegate? handler) => - (super.noSuchMethod( - Invocation.method(#setPlatformNavigationDelegate, [handler]), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value()) - as _i7.Future); - @override - _i7.Future runJavaScript(String? javaScript) => (super.noSuchMethod( - Invocation.method(#runJavaScript, [javaScript]), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value()) as _i7.Future); - @override - _i7.Future runJavaScriptReturningResult(String? javaScript) => - (super.noSuchMethod( - Invocation.method(#runJavaScriptReturningResult, [javaScript]), - returnValue: _i7.Future.value('')) as _i7.Future); - @override - _i7.Future addJavaScriptChannel( - _i6.JavaScriptChannelParams? javaScriptChannelParams) => - (super.noSuchMethod( - Invocation.method(#addJavaScriptChannel, [javaScriptChannelParams]), - returnValue: _i7.Future.value(), - returnValueForMissingStub: - _i7.Future.value()) as _i7.Future); - @override - _i7.Future removeJavaScriptChannel(String? javaScriptChannelName) => - (super.noSuchMethod( - Invocation.method(#removeJavaScriptChannel, [javaScriptChannelName]), - returnValue: _i7.Future.value(), - returnValueForMissingStub: - _i7.Future.value()) as _i7.Future); - @override - _i7.Future getTitle() => - (super.noSuchMethod(Invocation.method(#getTitle, []), - returnValue: _i7.Future.value()) as _i7.Future); - @override - _i7.Future scrollTo(int? x, int? y) => (super.noSuchMethod( - Invocation.method(#scrollTo, [x, y]), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value()) as _i7.Future); - @override - _i7.Future scrollBy(int? x, int? y) => (super.noSuchMethod( - Invocation.method(#scrollBy, [x, y]), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value()) as _i7.Future); - @override - _i7.Future<_i3.Point> getScrollPosition() => - (super.noSuchMethod(Invocation.method(#getScrollPosition, []), - returnValue: _i7.Future<_i3.Point>.value(_FakePoint_1( - this, Invocation.method(#getScrollPosition, [])))) - as _i7.Future<_i3.Point>); - @override - _i7.Future enableDebugging(bool? enabled) => (super.noSuchMethod( - Invocation.method(#enableDebugging, [enabled]), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value()) as _i7.Future); - @override - _i7.Future enableGestureNavigation(bool? enabled) => (super - .noSuchMethod(Invocation.method(#enableGestureNavigation, [enabled]), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value()) - as _i7.Future); - @override - _i7.Future enableZoom(bool? enabled) => (super.noSuchMethod( - Invocation.method(#enableZoom, [enabled]), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value()) as _i7.Future); - @override - _i7.Future setBackgroundColor(_i9.Color? color) => (super.noSuchMethod( - Invocation.method(#setBackgroundColor, [color]), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value()) as _i7.Future); - @override - _i7.Future setJavaScriptMode(_i2.JavaScriptMode? javaScriptMode) => - (super.noSuchMethod( - Invocation.method(#setJavaScriptMode, [javaScriptMode]), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value()) - as _i7.Future); - @override - _i7.Future setUserAgent(String? userAgent) => (super.noSuchMethod( - Invocation.method(#setUserAgent, [userAgent]), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value()) as _i7.Future); -} - -/// A class which mocks [PlatformWebViewWidget]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockPlatformWebViewWidget extends _i1.Mock - implements _i10.PlatformWebViewWidget { - MockPlatformWebViewWidget() { - _i1.throwOnMissingStub(this); - } - - @override - _i2.PlatformWebViewWidgetCreationParams get params => - (super.noSuchMethod(Invocation.getter(#params), - returnValue: _FakePlatformWebViewWidgetCreationParams_2( - this, Invocation.getter(#params))) - as _i2.PlatformWebViewWidgetCreationParams); - @override - _i4.Widget build(_i4.BuildContext? context) => - (super.noSuchMethod(Invocation.method(#build, [context]), - returnValue: - _FakeWidget_3(this, Invocation.method(#build, [context]))) - as _i4.Widget); -} diff --git a/packages/webview_flutter/webview_flutter/test/v4/webview_controller_test.dart b/packages/webview_flutter/webview_flutter/test/webview_controller_test.dart similarity index 91% rename from packages/webview_flutter/webview_flutter/test/v4/webview_controller_test.dart rename to packages/webview_flutter/webview_flutter/test/webview_controller_test.dart index f767a2e48d5e..f11884bb2acf 100644 --- a/packages/webview_flutter/webview_flutter/test/v4/webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_controller_test.dart @@ -2,19 +2,18 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:math'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:webview_flutter/src/v4/src/webview_controller.dart'; -import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'webview_controller_test.mocks.dart'; -@GenerateMocks([PlatformWebViewController]) +@GenerateMocks([PlatformWebViewController, PlatformNavigationDelegate]) void main() { test('loadFile', () async { final MockPlatformWebViewController mockPlatformWebViewController = @@ -286,9 +285,7 @@ void main() { final MockPlatformWebViewController mockPlatformWebViewController = MockPlatformWebViewController(); when(mockPlatformWebViewController.getScrollPosition()).thenAnswer( - (_) => Future>.value( - const Point(2, 3), - ), + (_) => Future.value(const Offset(2, 3)), ); final WebViewController webViewController = WebViewController.fromPlatform( @@ -350,4 +347,22 @@ void main() { await webViewController.setUserAgent('userAgent'); verify(mockPlatformWebViewController.setUserAgent('userAgent')); }); + + test('setNavigationDelegate', () async { + final MockPlatformWebViewController mockPlatformWebViewController = + MockPlatformWebViewController(); + final WebViewController webViewController = WebViewController.fromPlatform( + mockPlatformWebViewController, + ); + + final MockPlatformNavigationDelegate mockPlatformNavigationDelegate = + MockPlatformNavigationDelegate(); + final NavigationDelegate navigationDelegate = + NavigationDelegate.fromPlatform(mockPlatformNavigationDelegate); + + await webViewController.setNavigationDelegate(navigationDelegate); + verify(mockPlatformWebViewController.setPlatformNavigationDelegate( + mockPlatformNavigationDelegate, + )); + }); } diff --git a/packages/webview_flutter/webview_flutter/test/webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/webview_controller_test.mocks.dart new file mode 100644 index 000000000000..2bb1ef691321 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/test/webview_controller_test.mocks.dart @@ -0,0 +1,417 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter/test/webview_controller_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; +import 'dart:ui' as _i3; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_platform_interface/src/platform_navigation_delegate.dart' + as _i6; +import 'package:webview_flutter_platform_interface/src/platform_webview_controller.dart' + as _i4; +import 'package:webview_flutter_platform_interface/src/webview_platform.dart' + as _i2; + +// 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 + +class _FakePlatformWebViewControllerCreationParams_0 extends _i1.SmartFake + implements _i2.PlatformWebViewControllerCreationParams { + _FakePlatformWebViewControllerCreationParams_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeObject_1 extends _i1.SmartFake implements Object { + _FakeObject_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeOffset_2 extends _i1.SmartFake implements _i3.Offset { + _FakeOffset_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlatformNavigationDelegateCreationParams_3 extends _i1.SmartFake + implements _i2.PlatformNavigationDelegateCreationParams { + _FakePlatformNavigationDelegateCreationParams_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [PlatformWebViewController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPlatformWebViewController extends _i1.Mock + implements _i4.PlatformWebViewController { + MockPlatformWebViewController() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.PlatformWebViewControllerCreationParams get params => (super.noSuchMethod( + Invocation.getter(#params), + returnValue: _FakePlatformWebViewControllerCreationParams_0( + this, + Invocation.getter(#params), + ), + ) as _i2.PlatformWebViewControllerCreationParams); + @override + _i5.Future loadFile(String? absoluteFilePath) => (super.noSuchMethod( + Invocation.method( + #loadFile, + [absoluteFilePath], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future loadFlutterAsset(String? key) => (super.noSuchMethod( + Invocation.method( + #loadFlutterAsset, + [key], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future loadHtmlString( + String? html, { + String? baseUrl, + }) => + (super.noSuchMethod( + Invocation.method( + #loadHtmlString, + [html], + {#baseUrl: baseUrl}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future loadRequest(_i2.LoadRequestParams? params) => + (super.noSuchMethod( + Invocation.method( + #loadRequest, + [params], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future currentUrl() => (super.noSuchMethod( + Invocation.method( + #currentUrl, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future canGoBack() => (super.noSuchMethod( + Invocation.method( + #canGoBack, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future canGoForward() => (super.noSuchMethod( + Invocation.method( + #canGoForward, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future goBack() => (super.noSuchMethod( + Invocation.method( + #goBack, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future goForward() => (super.noSuchMethod( + Invocation.method( + #goForward, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future reload() => (super.noSuchMethod( + Invocation.method( + #reload, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future clearCache() => (super.noSuchMethod( + Invocation.method( + #clearCache, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future clearLocalStorage() => (super.noSuchMethod( + Invocation.method( + #clearLocalStorage, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setPlatformNavigationDelegate( + _i6.PlatformNavigationDelegate? handler) => + (super.noSuchMethod( + Invocation.method( + #setPlatformNavigationDelegate, + [handler], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future runJavaScript(String? javaScript) => (super.noSuchMethod( + Invocation.method( + #runJavaScript, + [javaScript], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future runJavaScriptReturningResult(String? javaScript) => + (super.noSuchMethod( + Invocation.method( + #runJavaScriptReturningResult, + [javaScript], + ), + returnValue: _i5.Future.value(_FakeObject_1( + this, + Invocation.method( + #runJavaScriptReturningResult, + [javaScript], + ), + )), + ) as _i5.Future); + @override + _i5.Future addJavaScriptChannel( + _i4.JavaScriptChannelParams? javaScriptChannelParams) => + (super.noSuchMethod( + Invocation.method( + #addJavaScriptChannel, + [javaScriptChannelParams], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeJavaScriptChannel(String? javaScriptChannelName) => + (super.noSuchMethod( + Invocation.method( + #removeJavaScriptChannel, + [javaScriptChannelName], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getTitle() => (super.noSuchMethod( + Invocation.method( + #getTitle, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future scrollTo( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollTo, + [ + x, + y, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future scrollBy( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollBy, + [ + x, + y, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future<_i3.Offset> getScrollPosition() => (super.noSuchMethod( + Invocation.method( + #getScrollPosition, + [], + ), + returnValue: _i5.Future<_i3.Offset>.value(_FakeOffset_2( + this, + Invocation.method( + #getScrollPosition, + [], + ), + )), + ) as _i5.Future<_i3.Offset>); + @override + _i5.Future enableZoom(bool? enabled) => (super.noSuchMethod( + Invocation.method( + #enableZoom, + [enabled], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setBackgroundColor(_i3.Color? color) => (super.noSuchMethod( + Invocation.method( + #setBackgroundColor, + [color], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setJavaScriptMode(_i2.JavaScriptMode? javaScriptMode) => + (super.noSuchMethod( + Invocation.method( + #setJavaScriptMode, + [javaScriptMode], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setUserAgent(String? userAgent) => (super.noSuchMethod( + Invocation.method( + #setUserAgent, + [userAgent], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [PlatformNavigationDelegate]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPlatformNavigationDelegate extends _i1.Mock + implements _i6.PlatformNavigationDelegate { + MockPlatformNavigationDelegate() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.PlatformNavigationDelegateCreationParams get params => + (super.noSuchMethod( + Invocation.getter(#params), + returnValue: _FakePlatformNavigationDelegateCreationParams_3( + this, + Invocation.getter(#params), + ), + ) as _i2.PlatformNavigationDelegateCreationParams); + @override + _i5.Future setOnNavigationRequest( + _i6.NavigationRequestCallback? onNavigationRequest) => + (super.noSuchMethod( + Invocation.method( + #setOnNavigationRequest, + [onNavigationRequest], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setOnPageStarted(_i6.PageEventCallback? onPageStarted) => + (super.noSuchMethod( + Invocation.method( + #setOnPageStarted, + [onPageStarted], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setOnPageFinished(_i6.PageEventCallback? onPageFinished) => + (super.noSuchMethod( + Invocation.method( + #setOnPageFinished, + [onPageFinished], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setOnProgress(_i6.ProgressCallback? onProgress) => + (super.noSuchMethod( + Invocation.method( + #setOnProgress, + [onProgress], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setOnWebResourceError( + _i6.WebResourceErrorCallback? onWebResourceError) => + (super.noSuchMethod( + Invocation.method( + #setOnWebResourceError, + [onWebResourceError], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} diff --git a/packages/webview_flutter/webview_flutter/test/v4/webview_cookie_manager_test.dart b/packages/webview_flutter/webview_flutter/test/webview_cookie_manager_test.dart similarity index 90% rename from packages/webview_flutter/webview_flutter/test/v4/webview_cookie_manager_test.dart rename to packages/webview_flutter/webview_flutter/test/webview_cookie_manager_test.dart index e8152407fb92..babf74b18922 100644 --- a/packages/webview_flutter/webview_flutter/test/v4/webview_cookie_manager_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_cookie_manager_test.dart @@ -5,8 +5,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:webview_flutter/src/v4/src/webview_cookie_manager.dart'; -import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'webview_cookie_manager_test.mocks.dart'; diff --git a/packages/webview_flutter/webview_flutter/test/v4/webview_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/webview_cookie_manager_test.mocks.dart similarity index 55% rename from packages/webview_flutter/webview_flutter/test/v4/webview_cookie_manager_test.mocks.dart rename to packages/webview_flutter/webview_flutter/test/webview_cookie_manager_test.mocks.dart index 4bca8b6a1f12..7cae6632d157 100644 --- a/packages/webview_flutter/webview_flutter/test/v4/webview_cookie_manager_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_cookie_manager_test.mocks.dart @@ -1,14 +1,14 @@ -// Mocks generated by Mockito 5.3.0 from annotations -// in webview_flutter/test/v4/webview_cookie_manager_test.dart. +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter/test/webview_cookie_manager_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_platform_interface/v4/src/platform_webview_cookie_manager.dart' +import 'package:webview_flutter_platform_interface/src/platform_webview_cookie_manager.dart' as _i3; -import 'package:webview_flutter_platform_interface/v4/src/webview_platform.dart' +import 'package:webview_flutter_platform_interface/src/webview_platform.dart' as _i2; // ignore_for_file: type=lint @@ -25,8 +25,12 @@ import 'package:webview_flutter_platform_interface/v4/src/webview_platform.dart' class _FakePlatformWebViewCookieManagerCreationParams_0 extends _i1.SmartFake implements _i2.PlatformWebViewCookieManagerCreationParams { _FakePlatformWebViewCookieManagerCreationParams_0( - Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } /// A class which mocks [PlatformWebViewCookieManager]. @@ -40,17 +44,28 @@ class MockPlatformWebViewCookieManager extends _i1.Mock @override _i2.PlatformWebViewCookieManagerCreationParams get params => - (super.noSuchMethod(Invocation.getter(#params), - returnValue: _FakePlatformWebViewCookieManagerCreationParams_0( - this, Invocation.getter(#params))) - as _i2.PlatformWebViewCookieManagerCreationParams); + (super.noSuchMethod( + Invocation.getter(#params), + returnValue: _FakePlatformWebViewCookieManagerCreationParams_0( + this, + Invocation.getter(#params), + ), + ) as _i2.PlatformWebViewCookieManagerCreationParams); @override - _i4.Future clearCookies() => - (super.noSuchMethod(Invocation.method(#clearCookies, []), - returnValue: _i4.Future.value(false)) as _i4.Future); + _i4.Future clearCookies() => (super.noSuchMethod( + Invocation.method( + #clearCookies, + [], + ), + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override _i4.Future setCookie(_i2.WebViewCookie? cookie) => (super.noSuchMethod( - Invocation.method(#setCookie, [cookie]), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value()) as _i4.Future); + Invocation.method( + #setCookie, + [cookie], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); } diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart index b10366c82024..68b60ec82896 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart @@ -2,1366 +2,45 @@ // 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/src/foundation/basic_types.dart'; -import 'package:flutter/src/gestures/recognizer.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; -import 'package:webview_flutter/webview_flutter.dart'; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; - -import 'webview_flutter_test.mocks.dart'; - -typedef VoidCallback = void Function(); +import 'package:webview_flutter/webview_flutter.dart' as main_file; -@GenerateMocks([WebViewPlatform, WebViewPlatformController]) void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - late MockWebViewPlatform mockWebViewPlatform; - late MockWebViewPlatformController mockWebViewPlatformController; - late MockWebViewCookieManagerPlatform mockWebViewCookieManagerPlatform; - - setUp(() { - mockWebViewPlatformController = MockWebViewPlatformController(); - mockWebViewPlatform = MockWebViewPlatform(); - mockWebViewCookieManagerPlatform = MockWebViewCookieManagerPlatform(); - when(mockWebViewPlatform.build( - context: anyNamed('context'), - creationParams: anyNamed('creationParams'), - webViewPlatformCallbacksHandler: - anyNamed('webViewPlatformCallbacksHandler'), - javascriptChannelRegistry: anyNamed('javascriptChannelRegistry'), - onWebViewPlatformCreated: anyNamed('onWebViewPlatformCreated'), - gestureRecognizers: anyNamed('gestureRecognizers'), - )).thenAnswer((Invocation invocation) { - final WebViewPlatformCreatedCallback onWebViewPlatformCreated = - invocation.namedArguments[const Symbol('onWebViewPlatformCreated')] - as WebViewPlatformCreatedCallback; - return TestPlatformWebView( - mockWebViewPlatformController: mockWebViewPlatformController, - onWebViewPlatformCreated: onWebViewPlatformCreated, - ); + group('webview_flutter', () { + test('ensure webview_flutter.dart exports classes from platform interface', + () { + // ignore: unnecessary_statements + main_file.JavaScriptMessage; + // ignore: unnecessary_statements + main_file.JavaScriptMode; + // ignore: unnecessary_statements + main_file.LoadRequestMethod; + // ignore: unnecessary_statements + main_file.NavigationDecision; + // ignore: unnecessary_statements + main_file.NavigationRequest; + // ignore: unnecessary_statements + main_file.NavigationRequestCallback; + // ignore: unnecessary_statements + main_file.PageEventCallback; + // ignore: unnecessary_statements + main_file.PlatformNavigationDelegateCreationParams; + // ignore: unnecessary_statements + main_file.PlatformWebViewControllerCreationParams; + // ignore: unnecessary_statements + main_file.PlatformWebViewCookieManagerCreationParams; + // ignore: unnecessary_statements + main_file.PlatformWebViewWidgetCreationParams; + // ignore: unnecessary_statements + main_file.ProgressCallback; + // ignore: unnecessary_statements + main_file.WebResourceError; + // ignore: unnecessary_statements + main_file.WebResourceErrorCallback; + // ignore: unnecessary_statements + main_file.WebViewCookie; + // ignore: unnecessary_statements + main_file.WebResourceErrorType; }); - - WebView.platform = mockWebViewPlatform; - WebViewCookieManagerPlatform.instance = mockWebViewCookieManagerPlatform; - }); - - tearDown(() { - mockWebViewCookieManagerPlatform.reset(); - }); - - testWidgets('Create WebView', (WidgetTester tester) async { - await tester.pumpWidget(const WebView()); - }); - - testWidgets('Initial url', (WidgetTester tester) async { - await tester.pumpWidget(const WebView(initialUrl: 'https://youtube.com')); - - final CreationParams params = captureBuildArgs( - mockWebViewPlatform, - creationParams: true, - ).single as CreationParams; - - expect(params.initialUrl, 'https://youtube.com'); - }); - - testWidgets('Javascript mode', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - javascriptMode: JavascriptMode.unrestricted, - )); - - final CreationParams unrestrictedparams = captureBuildArgs( - mockWebViewPlatform, - creationParams: true, - ).single as CreationParams; - - expect( - unrestrictedparams.webSettings!.javascriptMode, - JavascriptMode.unrestricted, - ); - - await tester.pumpWidget(const WebView()); - - final CreationParams disabledparams = captureBuildArgs( - mockWebViewPlatform, - creationParams: true, - ).single as CreationParams; - - expect(disabledparams.webSettings!.javascriptMode, JavascriptMode.disabled); - }); - - testWidgets('Load file', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - await controller!.loadFile('/test/path/index.html'); - - verify(mockWebViewPlatformController.loadFile( - '/test/path/index.html', - )); - }); - - testWidgets('Load file with empty path', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - expect(() => controller!.loadFile(''), throwsAssertionError); - }); - - testWidgets('Load Flutter asset', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - await controller!.loadFlutterAsset('assets/index.html'); - - verify(mockWebViewPlatformController.loadFlutterAsset( - 'assets/index.html', - )); - }); - - testWidgets('Load Flutter asset with empty key', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - expect(() => controller!.loadFlutterAsset(''), throwsAssertionError); }); - - testWidgets('Load HTML string without base URL', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - await controller!.loadHtmlString('

This is a test paragraph.

'); - - verify(mockWebViewPlatformController.loadHtmlString( - '

This is a test paragraph.

', - )); - }); - - testWidgets('Load HTML string with base URL', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - await controller!.loadHtmlString( - '

This is a test paragraph.

', - baseUrl: 'https://flutter.dev', - ); - - verify(mockWebViewPlatformController.loadHtmlString( - '

This is a test paragraph.

', - baseUrl: 'https://flutter.dev', - )); - }); - - testWidgets('Load HTML string with empty string', - (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - expect(() => controller!.loadHtmlString(''), throwsAssertionError); - }); - - testWidgets('Load url', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - await controller!.loadUrl('https://flutter.io'); - - verify(mockWebViewPlatformController.loadUrl( - 'https://flutter.io', - argThat(isNull), - )); - }); - - testWidgets('Invalid urls', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - final CreationParams params = captureBuildArgs( - mockWebViewPlatform, - creationParams: true, - ).single as CreationParams; - - expect(params.initialUrl, isNull); - - expect(() => controller!.loadUrl(''), throwsA(anything)); - expect(() => controller!.loadUrl('flutter.io'), throwsA(anything)); - }); - - testWidgets('Headers in loadUrl', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - final Map headers = { - 'CACHE-CONTROL': 'ABC' - }; - await controller!.loadUrl('https://flutter.io', headers: headers); - - verify(mockWebViewPlatformController.loadUrl( - 'https://flutter.io', - {'CACHE-CONTROL': 'ABC'}, - )); - }); - - testWidgets('loadRequest', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - expect(controller, isNotNull); - - final WebViewRequest req = WebViewRequest( - uri: Uri.parse('https://flutter.dev'), - method: WebViewRequestMethod.post, - headers: {'foo': 'bar'}, - body: Uint8List.fromList('Test Body'.codeUnits), - ); - - await controller!.loadRequest(req); - - verify(mockWebViewPlatformController.loadRequest(req)); - }); - - testWidgets('Clear Cache', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - await controller!.clearCache(); - - verify(mockWebViewPlatformController.clearCache()); - }); - - testWidgets('Can go back', (WidgetTester tester) async { - when(mockWebViewPlatformController.canGoBack()) - .thenAnswer((_) => Future.value(true)); - - WebViewController? controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - expect(controller!.canGoBack(), completion(true)); - }); - - testWidgets("Can't go forward", (WidgetTester tester) async { - when(mockWebViewPlatformController.canGoForward()) - .thenAnswer((_) => Future.value(false)); - - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - expect(controller!.canGoForward(), completion(false)); - }); - - testWidgets('Go back', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - await controller!.goBack(); - verify(mockWebViewPlatformController.goBack()); - }); - - testWidgets('Go forward', (WidgetTester tester) async { - WebViewController? controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - await controller!.goForward(); - verify(mockWebViewPlatformController.goForward()); - }); - - testWidgets('Current URL', (WidgetTester tester) async { - when(mockWebViewPlatformController.currentUrl()) - .thenAnswer((_) => Future.value('https://youtube.com')); - - WebViewController? controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - expect(await controller!.currentUrl(), 'https://youtube.com'); - }); - - testWidgets('Reload url', (WidgetTester tester) async { - late WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - await controller.reload(); - verify(mockWebViewPlatformController.reload()); - }); - - testWidgets('evaluate Javascript', (WidgetTester tester) async { - when(mockWebViewPlatformController.evaluateJavascript('fake js string')) - .thenAnswer((_) => Future.value('fake js string')); - - late WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - javascriptMode: JavascriptMode.unrestricted, - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect( - // ignore: deprecated_member_use_from_same_package - await controller.evaluateJavascript('fake js string'), - 'fake js string', - reason: 'should get the argument'); - }); - - testWidgets('evaluate Javascript with JavascriptMode disabled', - (WidgetTester tester) async { - late WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - expect( - // ignore: deprecated_member_use_from_same_package - () => controller.evaluateJavascript('fake js string'), - throwsA(anything), - ); - }); - - testWidgets('runJavaScript', (WidgetTester tester) async { - late WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - javascriptMode: JavascriptMode.unrestricted, - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - await controller.runJavascript('fake js string'); - verify(mockWebViewPlatformController.runJavascript('fake js string')); - }); - - testWidgets('runJavaScript with JavascriptMode disabled', - (WidgetTester tester) async { - late WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - expect( - () => controller.runJavascript('fake js string'), - throwsA(anything), - ); - }); - - testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async { - when(mockWebViewPlatformController - .runJavascriptReturningResult('fake js string')) - .thenAnswer((_) => Future.value('fake js string')); - - late WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - javascriptMode: JavascriptMode.unrestricted, - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - expect(await controller.runJavascriptReturningResult('fake js string'), - 'fake js string', - reason: 'should get the argument'); - }); - - testWidgets('runJavaScriptReturningResult with JavascriptMode disabled', - (WidgetTester tester) async { - late WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - expect( - () => controller.runJavascriptReturningResult('fake js string'), - throwsA(anything), - ); - }); - - testWidgets('Cookies can be cleared once', (WidgetTester tester) async { - await tester.pumpWidget( - const WebView( - initialUrl: 'https://flutter.io', - ), - ); - final CookieManager cookieManager = CookieManager(); - final bool hasCookies = await cookieManager.clearCookies(); - expect(hasCookies, true); - }); - - testWidgets('Cookies can be set', (WidgetTester tester) async { - const WebViewCookie cookie = - WebViewCookie(name: 'foo', value: 'bar', domain: 'flutter.dev'); - - await tester.pumpWidget( - const WebView( - initialUrl: 'https://flutter.io', - ), - ); - final CookieManager cookieManager = CookieManager(); - await cookieManager.setCookie(cookie); - expect(mockWebViewCookieManagerPlatform.setCookieCalls, - [cookie]); - }); - - testWidgets('Initial JavaScript channels', (WidgetTester tester) async { - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - javascriptChannels: { - JavascriptChannel( - name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), - JavascriptChannel( - name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), - }, - ), - ); - - final CreationParams params = captureBuildArgs( - mockWebViewPlatform, - creationParams: true, - ).single as CreationParams; - - expect(params.javascriptChannelNames, - unorderedEquals(['Tts', 'Alarm'])); - }); - - test('Only valid JavaScript channel names are allowed', () { - void noOp(JavascriptMessage msg) {} - JavascriptChannel(name: 'Tts1', onMessageReceived: noOp); - JavascriptChannel(name: '_Alarm', onMessageReceived: noOp); - JavascriptChannel(name: 'foo_bar_', onMessageReceived: noOp); - - VoidCallback createChannel(String name) { - return () { - JavascriptChannel(name: name, onMessageReceived: noOp); - }; - } - - expect(createChannel('1Alarm'), throwsAssertionError); - expect(createChannel('foo.bar'), throwsAssertionError); - expect(createChannel(''), throwsAssertionError); - }); - - testWidgets('Unique JavaScript channel names are required', - (WidgetTester tester) async { - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - javascriptChannels: { - JavascriptChannel( - name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), - JavascriptChannel( - name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), - }, - ), - ); - expect(tester.takeException(), isNot(null)); - }); - - testWidgets('JavaScript channels update', (WidgetTester tester) async { - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - javascriptChannels: { - JavascriptChannel( - name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), - JavascriptChannel( - name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), - }, - ), - ); - - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - javascriptChannels: { - JavascriptChannel( - name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), - JavascriptChannel( - name: 'Alarm2', onMessageReceived: (JavascriptMessage msg) {}), - JavascriptChannel( - name: 'Alarm3', onMessageReceived: (JavascriptMessage msg) {}), - }, - ), - ); - - final JavascriptChannelRegistry channelRegistry = captureBuildArgs( - mockWebViewPlatform, - javascriptChannelRegistry: true, - ).first as JavascriptChannelRegistry; - - expect( - channelRegistry.channels.keys, - unorderedEquals(['Tts', 'Alarm2', 'Alarm3']), - ); - }); - - testWidgets('Remove all JavaScript channels and then add', - (WidgetTester tester) async { - // This covers a specific bug we had where after updating javascriptChannels to null, - // updating it again with a subset of the previously registered channels fails as the - // widget's cache of current channel wasn't properly updated when updating javascriptChannels to - // null. - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - javascriptChannels: { - JavascriptChannel( - name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), - }, - ), - ); - - await tester.pumpWidget( - const WebView( - initialUrl: 'https://youtube.com', - ), - ); - - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - javascriptChannels: { - JavascriptChannel( - name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), - }, - ), - ); - - final JavascriptChannelRegistry channelRegistry = captureBuildArgs( - mockWebViewPlatform, - javascriptChannelRegistry: true, - ).last as JavascriptChannelRegistry; - - expect(channelRegistry.channels.keys, unorderedEquals(['Tts'])); - }); - - testWidgets('JavaScript channel messages', (WidgetTester tester) async { - final List ttsMessagesReceived = []; - final List alarmMessagesReceived = []; - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - javascriptChannels: { - JavascriptChannel( - name: 'Tts', - onMessageReceived: (JavascriptMessage msg) { - ttsMessagesReceived.add(msg.message); - }), - JavascriptChannel( - name: 'Alarm', - onMessageReceived: (JavascriptMessage msg) { - alarmMessagesReceived.add(msg.message); - }), - }, - ), - ); - - final JavascriptChannelRegistry channelRegistry = captureBuildArgs( - mockWebViewPlatform, - javascriptChannelRegistry: true, - ).single as JavascriptChannelRegistry; - - expect(ttsMessagesReceived, isEmpty); - expect(alarmMessagesReceived, isEmpty); - - channelRegistry.onJavascriptChannelMessage('Tts', 'Hello'); - channelRegistry.onJavascriptChannelMessage('Tts', 'World'); - - expect(ttsMessagesReceived, ['Hello', 'World']); - }); - - group('$PageStartedCallback', () { - testWidgets('onPageStarted is not null', (WidgetTester tester) async { - String? returnedUrl; - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onPageStarted: (String url) { - returnedUrl = url; - }, - )); - - final WebViewPlatformCallbacksHandler handler = captureBuildArgs( - mockWebViewPlatform, - webViewPlatformCallbacksHandler: true, - ).single as WebViewPlatformCallbacksHandler; - - handler.onPageStarted('https://youtube.com'); - - expect(returnedUrl, 'https://youtube.com'); - }); - - testWidgets('onPageStarted is null', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - )); - - final WebViewPlatformCallbacksHandler handler = captureBuildArgs( - mockWebViewPlatform, - webViewPlatformCallbacksHandler: true, - ).single as WebViewPlatformCallbacksHandler; - - // The platform side will always invoke a call for onPageStarted. This is - // to test that it does not crash on a null callback. - handler.onPageStarted('https://youtube.com'); - }); - - testWidgets('onPageStarted changed', (WidgetTester tester) async { - String? returnedUrl; - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onPageStarted: (String url) {}, - )); - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onPageStarted: (String url) { - returnedUrl = url; - }, - )); - - final WebViewPlatformCallbacksHandler handler = captureBuildArgs( - mockWebViewPlatform, - webViewPlatformCallbacksHandler: true, - ).last as WebViewPlatformCallbacksHandler; - handler.onPageStarted('https://youtube.com'); - - expect(returnedUrl, 'https://youtube.com'); - }); - }); - - group('$PageFinishedCallback', () { - testWidgets('onPageFinished is not null', (WidgetTester tester) async { - String? returnedUrl; - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onPageFinished: (String url) { - returnedUrl = url; - }, - )); - - final WebViewPlatformCallbacksHandler handler = captureBuildArgs( - mockWebViewPlatform, - webViewPlatformCallbacksHandler: true, - ).single as WebViewPlatformCallbacksHandler; - handler.onPageFinished('https://youtube.com'); - - expect(returnedUrl, 'https://youtube.com'); - }); - - testWidgets('onPageFinished is null', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - )); - - final WebViewPlatformCallbacksHandler handler = captureBuildArgs( - mockWebViewPlatform, - webViewPlatformCallbacksHandler: true, - ).single as WebViewPlatformCallbacksHandler; - // The platform side will always invoke a call for onPageFinished. This is - // to test that it does not crash on a null callback. - handler.onPageFinished('https://youtube.com'); - }); - - testWidgets('onPageFinished changed', (WidgetTester tester) async { - String? returnedUrl; - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onPageFinished: (String url) {}, - )); - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onPageFinished: (String url) { - returnedUrl = url; - }, - )); - - final WebViewPlatformCallbacksHandler handler = captureBuildArgs( - mockWebViewPlatform, - webViewPlatformCallbacksHandler: true, - ).last as WebViewPlatformCallbacksHandler; - handler.onPageFinished('https://youtube.com'); - - expect(returnedUrl, 'https://youtube.com'); - }); - }); - - group('$PageLoadingCallback', () { - testWidgets('onLoadingProgress is not null', (WidgetTester tester) async { - int? loadingProgress; - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onProgress: (int progress) { - loadingProgress = progress; - }, - )); - - final WebViewPlatformCallbacksHandler handler = captureBuildArgs( - mockWebViewPlatform, - webViewPlatformCallbacksHandler: true, - ).single as WebViewPlatformCallbacksHandler; - handler.onProgress(50); - - expect(loadingProgress, 50); - }); - - testWidgets('onLoadingProgress is null', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - )); - - final WebViewPlatformCallbacksHandler handler = captureBuildArgs( - mockWebViewPlatform, - webViewPlatformCallbacksHandler: true, - ).single as WebViewPlatformCallbacksHandler; - - // This is to test that it does not crash on a null callback. - handler.onProgress(50); - }); - - testWidgets('onLoadingProgress changed', (WidgetTester tester) async { - int? loadingProgress; - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onProgress: (int progress) {}, - )); - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onProgress: (int progress) { - loadingProgress = progress; - }, - )); - - final WebViewPlatformCallbacksHandler handler = captureBuildArgs( - mockWebViewPlatform, - webViewPlatformCallbacksHandler: true, - ).last as WebViewPlatformCallbacksHandler; - handler.onProgress(50); - - expect(loadingProgress, 50); - }); - }); - - group('navigationDelegate', () { - testWidgets('hasNavigationDelegate', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - )); - - final CreationParams params = captureBuildArgs( - mockWebViewPlatform, - creationParams: true, - ).single as CreationParams; - - expect(params.webSettings!.hasNavigationDelegate, false); - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - navigationDelegate: (NavigationRequest r) => - NavigationDecision.navigate, - )); - - final WebSettings updateSettings = - verify(mockWebViewPlatformController.updateSettings(captureAny)) - .captured - .single as WebSettings; - - expect(updateSettings.hasNavigationDelegate, true); - }); - - testWidgets('Block navigation', (WidgetTester tester) async { - final List navigationRequests = []; - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - navigationDelegate: (NavigationRequest request) { - navigationRequests.add(request); - // Only allow navigating to https://flutter.dev - return request.url == 'https://flutter.dev' - ? NavigationDecision.navigate - : NavigationDecision.prevent; - })); - - final List args = captureBuildArgs( - mockWebViewPlatform, - creationParams: true, - webViewPlatformCallbacksHandler: true, - ); - - final CreationParams params = args[0] as CreationParams; - expect(params.webSettings!.hasNavigationDelegate, true); - - final WebViewPlatformCallbacksHandler handler = - args[1] as WebViewPlatformCallbacksHandler; - - // The navigation delegate only allows navigation to https://flutter.dev - // so we should still be in https://youtube.com. - expect( - handler.onNavigationRequest( - url: 'https://www.google.com', - isForMainFrame: true, - ), - completion(false), - ); - - expect(navigationRequests.length, 1); - expect(navigationRequests[0].url, 'https://www.google.com'); - expect(navigationRequests[0].isForMainFrame, true); - - expect( - handler.onNavigationRequest( - url: 'https://flutter.dev', - isForMainFrame: true, - ), - completion(true), - ); - }); - }); - - group('debuggingEnabled', () { - testWidgets('enable debugging', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - debuggingEnabled: true, - )); - - final CreationParams params = captureBuildArgs( - mockWebViewPlatform, - creationParams: true, - ).single as CreationParams; - - expect(params.webSettings!.debuggingEnabled, true); - }); - - testWidgets('defaults to false', (WidgetTester tester) async { - await tester.pumpWidget(const WebView()); - - final CreationParams params = captureBuildArgs( - mockWebViewPlatform, - creationParams: true, - ).single as CreationParams; - - expect(params.webSettings!.debuggingEnabled, false); - }); - - testWidgets('can be changed', (WidgetTester tester) async { - final GlobalKey key = GlobalKey(); - await tester.pumpWidget(WebView(key: key)); - - await tester.pumpWidget(WebView( - key: key, - debuggingEnabled: true, - )); - - final WebSettings enabledSettings = - verify(mockWebViewPlatformController.updateSettings(captureAny)) - .captured - .last as WebSettings; - expect(enabledSettings.debuggingEnabled, true); - - await tester.pumpWidget(WebView( - key: key, - )); - - final WebSettings disabledSettings = - verify(mockWebViewPlatformController.updateSettings(captureAny)) - .captured - .last as WebSettings; - expect(disabledSettings.debuggingEnabled, false); - }); - }); - - group('zoomEnabled', () { - testWidgets('Enable zoom', (WidgetTester tester) async { - await tester.pumpWidget(const WebView()); - - final CreationParams params = captureBuildArgs( - mockWebViewPlatform, - creationParams: true, - ).single as CreationParams; - - expect(params.webSettings!.zoomEnabled, isTrue); - }); - - testWidgets('defaults to true', (WidgetTester tester) async { - await tester.pumpWidget(const WebView()); - - final CreationParams params = captureBuildArgs( - mockWebViewPlatform, - creationParams: true, - ).single as CreationParams; - - expect(params.webSettings!.zoomEnabled, isTrue); - }); - - testWidgets('can be changed', (WidgetTester tester) async { - final GlobalKey key = GlobalKey(); - await tester.pumpWidget(WebView(key: key)); - - await tester.pumpWidget(WebView( - key: key, - )); - - final WebSettings enabledSettings = - verify(mockWebViewPlatformController.updateSettings(captureAny)) - .captured - .last as WebSettings; - // Zoom defaults to true, so no changes are made to settings. - expect(enabledSettings.zoomEnabled, isNull); - - await tester.pumpWidget(WebView( - key: key, - zoomEnabled: false, - )); - - final WebSettings disabledSettings = - verify(mockWebViewPlatformController.updateSettings(captureAny)) - .captured - .last as WebSettings; - expect(disabledSettings.zoomEnabled, isFalse); - }); - }); - - group('Background color', () { - testWidgets('Defaults to null', (WidgetTester tester) async { - await tester.pumpWidget(const WebView()); - - final CreationParams params = captureBuildArgs( - mockWebViewPlatform, - creationParams: true, - ).single as CreationParams; - - expect(params.backgroundColor, null); - }); - - testWidgets('Can be transparent', (WidgetTester tester) async { - const Color transparentColor = Color(0x00000000); - - await tester.pumpWidget(const WebView( - backgroundColor: transparentColor, - )); - - final CreationParams params = captureBuildArgs( - mockWebViewPlatform, - creationParams: true, - ).single as CreationParams; - - expect(params.backgroundColor, transparentColor); - }); - }); - - group('Custom platform implementation', () { - setUp(() { - WebView.platform = MyWebViewPlatform(); - }); - tearDownAll(() { - WebView.platform = null; - }); - - testWidgets('creation', (WidgetTester tester) async { - await tester.pumpWidget( - const WebView( - initialUrl: 'https://youtube.com', - gestureNavigationEnabled: true, - ), - ); - - final MyWebViewPlatform builder = WebView.platform as MyWebViewPlatform; - final MyWebViewPlatformController platform = builder.lastPlatformBuilt!; - - expect( - platform.creationParams, - MatchesCreationParams(CreationParams( - initialUrl: 'https://youtube.com', - webSettings: WebSettings( - javascriptMode: JavascriptMode.disabled, - hasNavigationDelegate: false, - debuggingEnabled: false, - userAgent: const WebSetting.of(null), - gestureNavigationEnabled: true, - zoomEnabled: true, - ), - ))); - }); - - testWidgets('loadUrl', (WidgetTester tester) async { - late WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - final MyWebViewPlatform builder = WebView.platform as MyWebViewPlatform; - final MyWebViewPlatformController platform = builder.lastPlatformBuilt!; - - final Map headers = { - 'header': 'value', - }; - - await controller.loadUrl('https://google.com', headers: headers); - - expect(platform.lastUrlLoaded, 'https://google.com'); - expect(platform.lastRequestHeaders, headers); - }); - }); - - testWidgets('Set UserAgent', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - javascriptMode: JavascriptMode.unrestricted, - )); - - final CreationParams params = captureBuildArgs( - mockWebViewPlatform, - creationParams: true, - ).single as CreationParams; - - expect(params.webSettings!.userAgent.value, isNull); - - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - javascriptMode: JavascriptMode.unrestricted, - userAgent: 'UA', - )); - - final WebSettings settings = - verify(mockWebViewPlatformController.updateSettings(captureAny)) - .captured - .last as WebSettings; - expect(settings.userAgent.value, 'UA'); - }); -} - -List captureBuildArgs( - MockWebViewPlatform mockWebViewPlatform, { - bool context = false, - bool creationParams = false, - bool webViewPlatformCallbacksHandler = false, - bool javascriptChannelRegistry = false, - bool onWebViewPlatformCreated = false, - bool gestureRecognizers = false, -}) { - return verify(mockWebViewPlatform.build( - context: context ? captureAnyNamed('context') : anyNamed('context'), - creationParams: creationParams - ? captureAnyNamed('creationParams') - : anyNamed('creationParams'), - webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler - ? captureAnyNamed('webViewPlatformCallbacksHandler') - : anyNamed('webViewPlatformCallbacksHandler'), - javascriptChannelRegistry: javascriptChannelRegistry - ? captureAnyNamed('javascriptChannelRegistry') - : anyNamed('javascriptChannelRegistry'), - onWebViewPlatformCreated: onWebViewPlatformCreated - ? captureAnyNamed('onWebViewPlatformCreated') - : anyNamed('onWebViewPlatformCreated'), - gestureRecognizers: gestureRecognizers - ? captureAnyNamed('gestureRecognizers') - : anyNamed('gestureRecognizers'), - )).captured; -} - -// This Widget ensures that onWebViewPlatformCreated is only called once when -// making multiple calls to `WidgetTester.pumpWidget` with different parameters -// for the WebView. -class TestPlatformWebView extends StatefulWidget { - const TestPlatformWebView({ - Key? key, - required this.mockWebViewPlatformController, - this.onWebViewPlatformCreated, - }) : super(key: key); - - final MockWebViewPlatformController mockWebViewPlatformController; - final WebViewPlatformCreatedCallback? onWebViewPlatformCreated; - - @override - State createState() => TestPlatformWebViewState(); -} - -class TestPlatformWebViewState extends State { - @override - void initState() { - super.initState(); - final WebViewPlatformCreatedCallback? onWebViewPlatformCreated = - widget.onWebViewPlatformCreated; - if (onWebViewPlatformCreated != null) { - onWebViewPlatformCreated(widget.mockWebViewPlatformController); - } - } - - @override - Widget build(BuildContext context) { - return Container(); - } -} - -class MyWebViewPlatform implements WebViewPlatform { - MyWebViewPlatformController? lastPlatformBuilt; - - @override - Widget build({ - BuildContext? context, - CreationParams? creationParams, - required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - required JavascriptChannelRegistry javascriptChannelRegistry, - WebViewPlatformCreatedCallback? onWebViewPlatformCreated, - Set>? gestureRecognizers, - }) { - assert(onWebViewPlatformCreated != null); - lastPlatformBuilt = MyWebViewPlatformController( - creationParams, gestureRecognizers, webViewPlatformCallbacksHandler); - onWebViewPlatformCreated!(lastPlatformBuilt); - return Container(); - } - - @override - Future clearCookies() { - return Future.sync(() => true); - } -} - -class MyWebViewPlatformController extends WebViewPlatformController { - MyWebViewPlatformController(this.creationParams, this.gestureRecognizers, - WebViewPlatformCallbacksHandler platformHandler) - : super(platformHandler); - - CreationParams? creationParams; - Set>? gestureRecognizers; - - String? lastUrlLoaded; - Map? lastRequestHeaders; - - @override - Future loadUrl(String url, Map? headers) async { - equals(1, 1); - lastUrlLoaded = url; - lastRequestHeaders = headers; - } -} - -class MatchesWebSettings extends Matcher { - MatchesWebSettings(this._webSettings); - - final WebSettings? _webSettings; - - @override - Description describe(Description description) => - description.add('$_webSettings'); - - @override - bool matches( - covariant WebSettings webSettings, Map matchState) { - return _webSettings!.javascriptMode == webSettings.javascriptMode && - _webSettings!.hasNavigationDelegate == - webSettings.hasNavigationDelegate && - _webSettings!.debuggingEnabled == webSettings.debuggingEnabled && - _webSettings!.gestureNavigationEnabled == - webSettings.gestureNavigationEnabled && - _webSettings!.userAgent == webSettings.userAgent && - _webSettings!.zoomEnabled == webSettings.zoomEnabled; - } -} - -class MatchesCreationParams extends Matcher { - MatchesCreationParams(this._creationParams); - - final CreationParams _creationParams; - - @override - Description describe(Description description) => - description.add('$_creationParams'); - - @override - bool matches(covariant CreationParams creationParams, - Map matchState) { - return _creationParams.initialUrl == creationParams.initialUrl && - MatchesWebSettings(_creationParams.webSettings) - .matches(creationParams.webSettings!, matchState) && - orderedEquals(_creationParams.javascriptChannelNames) - .matches(creationParams.javascriptChannelNames, matchState); - } -} - -class MockWebViewCookieManagerPlatform extends WebViewCookieManagerPlatform { - List setCookieCalls = []; - - @override - Future clearCookies() async => true; - - @override - Future setCookie(WebViewCookie cookie) async { - setCookieCalls.add(cookie); - } - - void reset() { - setCookieCalls = []; - } } diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart deleted file mode 100644 index a7a21007d6be..000000000000 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.mocks.dart +++ /dev/null @@ -1,213 +0,0 @@ -// Mocks generated by Mockito 5.3.0 from annotations -// in webview_flutter/test/webview_flutter_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i9; - -import 'package:flutter/foundation.dart' as _i3; -import 'package:flutter/gestures.dart' as _i8; -import 'package:flutter/widgets.dart' as _i2; -import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_platform_interface/src/platform_interface/javascript_channel_registry.dart' - as _i7; -import 'package:webview_flutter_platform_interface/src/platform_interface/webview_platform.dart' - as _i4; -import 'package:webview_flutter_platform_interface/src/platform_interface/webview_platform_callbacks_handler.dart' - as _i6; -import 'package:webview_flutter_platform_interface/src/platform_interface/webview_platform_controller.dart' - as _i10; -import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i5; - -// 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 - -class _FakeWidget_0 extends _i1.SmartFake implements _i2.Widget { - _FakeWidget_0(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); - - @override - String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => - super.toString(); -} - -/// A class which mocks [WebViewPlatform]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockWebViewPlatform extends _i1.Mock implements _i4.WebViewPlatform { - MockWebViewPlatform() { - _i1.throwOnMissingStub(this); - } - - @override - _i2.Widget build( - {_i2.BuildContext? context, - _i5.CreationParams? creationParams, - _i6.WebViewPlatformCallbacksHandler? webViewPlatformCallbacksHandler, - _i7.JavascriptChannelRegistry? javascriptChannelRegistry, - _i4.WebViewPlatformCreatedCallback? onWebViewPlatformCreated, - Set<_i3.Factory<_i8.OneSequenceGestureRecognizer>>? - gestureRecognizers}) => - (super.noSuchMethod( - Invocation.method(#build, [], { - #context: context, - #creationParams: creationParams, - #webViewPlatformCallbacksHandler: webViewPlatformCallbacksHandler, - #javascriptChannelRegistry: javascriptChannelRegistry, - #onWebViewPlatformCreated: onWebViewPlatformCreated, - #gestureRecognizers: gestureRecognizers - }), - returnValue: _FakeWidget_0( - this, - Invocation.method(#build, [], { - #context: context, - #creationParams: creationParams, - #webViewPlatformCallbacksHandler: - webViewPlatformCallbacksHandler, - #javascriptChannelRegistry: javascriptChannelRegistry, - #onWebViewPlatformCreated: onWebViewPlatformCreated, - #gestureRecognizers: gestureRecognizers - }))) as _i2.Widget); - @override - _i9.Future clearCookies() => - (super.noSuchMethod(Invocation.method(#clearCookies, []), - returnValue: _i9.Future.value(false)) as _i9.Future); -} - -/// A class which mocks [WebViewPlatformController]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockWebViewPlatformController extends _i1.Mock - implements _i10.WebViewPlatformController { - MockWebViewPlatformController() { - _i1.throwOnMissingStub(this); - } - - @override - _i9.Future loadFile(String? absoluteFilePath) => (super.noSuchMethod( - Invocation.method(#loadFile, [absoluteFilePath]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value()) as _i9.Future); - @override - _i9.Future loadFlutterAsset(String? key) => (super.noSuchMethod( - Invocation.method(#loadFlutterAsset, [key]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value()) as _i9.Future); - @override - _i9.Future loadHtmlString(String? html, {String? baseUrl}) => - (super.noSuchMethod( - Invocation.method(#loadHtmlString, [html], {#baseUrl: baseUrl}), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value()) - as _i9.Future); - @override - _i9.Future loadUrl(String? url, Map? headers) => - (super.noSuchMethod(Invocation.method(#loadUrl, [url, headers]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value()) - as _i9.Future); - @override - _i9.Future loadRequest(_i5.WebViewRequest? request) => - (super.noSuchMethod(Invocation.method(#loadRequest, [request]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value()) - as _i9.Future); - @override - _i9.Future updateSettings(_i5.WebSettings? setting) => - (super.noSuchMethod(Invocation.method(#updateSettings, [setting]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value()) - as _i9.Future); - @override - _i9.Future currentUrl() => - (super.noSuchMethod(Invocation.method(#currentUrl, []), - returnValue: _i9.Future.value()) as _i9.Future); - @override - _i9.Future canGoBack() => - (super.noSuchMethod(Invocation.method(#canGoBack, []), - returnValue: _i9.Future.value(false)) as _i9.Future); - @override - _i9.Future canGoForward() => - (super.noSuchMethod(Invocation.method(#canGoForward, []), - returnValue: _i9.Future.value(false)) as _i9.Future); - @override - _i9.Future goBack() => (super.noSuchMethod( - Invocation.method(#goBack, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value()) as _i9.Future); - @override - _i9.Future goForward() => (super.noSuchMethod( - Invocation.method(#goForward, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value()) as _i9.Future); - @override - _i9.Future reload() => (super.noSuchMethod( - Invocation.method(#reload, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value()) as _i9.Future); - @override - _i9.Future clearCache() => (super.noSuchMethod( - Invocation.method(#clearCache, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value()) as _i9.Future); - @override - _i9.Future evaluateJavascript(String? javascript) => - (super.noSuchMethod(Invocation.method(#evaluateJavascript, [javascript]), - returnValue: _i9.Future.value('')) as _i9.Future); - @override - _i9.Future runJavascript(String? javascript) => (super.noSuchMethod( - Invocation.method(#runJavascript, [javascript]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value()) as _i9.Future); - @override - _i9.Future runJavascriptReturningResult(String? javascript) => - (super.noSuchMethod( - Invocation.method(#runJavascriptReturningResult, [javascript]), - returnValue: _i9.Future.value('')) as _i9.Future); - @override - _i9.Future addJavascriptChannels(Set? javascriptChannelNames) => - (super.noSuchMethod( - Invocation.method(#addJavascriptChannels, [javascriptChannelNames]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: - _i9.Future.value()) as _i9.Future); - @override - _i9.Future removeJavascriptChannels( - Set? javascriptChannelNames) => - (super.noSuchMethod( - Invocation.method( - #removeJavascriptChannels, [javascriptChannelNames]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value()) - as _i9.Future); - @override - _i9.Future getTitle() => - (super.noSuchMethod(Invocation.method(#getTitle, []), - returnValue: _i9.Future.value()) as _i9.Future); - @override - _i9.Future scrollTo(int? x, int? y) => (super.noSuchMethod( - Invocation.method(#scrollTo, [x, y]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value()) as _i9.Future); - @override - _i9.Future scrollBy(int? x, int? y) => (super.noSuchMethod( - Invocation.method(#scrollBy, [x, y]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value()) as _i9.Future); - @override - _i9.Future getScrollX() => - (super.noSuchMethod(Invocation.method(#getScrollX, []), - returnValue: _i9.Future.value(0)) as _i9.Future); - @override - _i9.Future getScrollY() => - (super.noSuchMethod(Invocation.method(#getScrollY, []), - returnValue: _i9.Future.value(0)) as _i9.Future); -} diff --git a/packages/webview_flutter/webview_flutter/test/v4/webview_widget_test.dart b/packages/webview_flutter/webview_flutter/test/webview_widget_test.dart similarity index 90% rename from packages/webview_flutter/webview_flutter/test/v4/webview_widget_test.dart rename to packages/webview_flutter/webview_flutter/test/webview_widget_test.dart index 455d8b371ec7..21e9f53a2260 100644 --- a/packages/webview_flutter/webview_flutter/test/v4/webview_widget_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_widget_test.dart @@ -8,8 +8,8 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:webview_flutter/src/v4/webview_flutter.dart'; -import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'webview_widget_test.mocks.dart'; @@ -77,8 +77,7 @@ class TestWebViewPlatform extends WebViewPlatform { } class TestPlatformWebViewWidget extends PlatformWebViewWidget { - TestPlatformWebViewWidget(PlatformWebViewWidgetCreationParams params) - : super.implementation(params); + TestPlatformWebViewWidget(super.params) : super.implementation(); @override Widget build(BuildContext context) { diff --git a/packages/webview_flutter/webview_flutter/test/webview_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/webview_widget_test.mocks.dart new file mode 100644 index 000000000000..0e29ede0d561 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/test/webview_widget_test.mocks.dart @@ -0,0 +1,396 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter/test/webview_widget_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i7; +import 'dart:ui' as _i3; + +import 'package:flutter/foundation.dart' as _i5; +import 'package:flutter/widgets.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_platform_interface/src/platform_navigation_delegate.dart' + as _i8; +import 'package:webview_flutter_platform_interface/src/platform_webview_controller.dart' + as _i6; +import 'package:webview_flutter_platform_interface/src/platform_webview_widget.dart' + as _i9; +import 'package:webview_flutter_platform_interface/src/webview_platform.dart' + as _i2; + +// 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 + +class _FakePlatformWebViewControllerCreationParams_0 extends _i1.SmartFake + implements _i2.PlatformWebViewControllerCreationParams { + _FakePlatformWebViewControllerCreationParams_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeObject_1 extends _i1.SmartFake implements Object { + _FakeObject_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeOffset_2 extends _i1.SmartFake implements _i3.Offset { + _FakeOffset_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlatformWebViewWidgetCreationParams_3 extends _i1.SmartFake + implements _i2.PlatformWebViewWidgetCreationParams { + _FakePlatformWebViewWidgetCreationParams_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWidget_4 extends _i1.SmartFake implements _i4.Widget { + _FakeWidget_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +/// A class which mocks [PlatformWebViewController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPlatformWebViewController extends _i1.Mock + implements _i6.PlatformWebViewController { + MockPlatformWebViewController() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.PlatformWebViewControllerCreationParams get params => (super.noSuchMethod( + Invocation.getter(#params), + returnValue: _FakePlatformWebViewControllerCreationParams_0( + this, + Invocation.getter(#params), + ), + ) as _i2.PlatformWebViewControllerCreationParams); + @override + _i7.Future loadFile(String? absoluteFilePath) => (super.noSuchMethod( + Invocation.method( + #loadFile, + [absoluteFilePath], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + @override + _i7.Future loadFlutterAsset(String? key) => (super.noSuchMethod( + Invocation.method( + #loadFlutterAsset, + [key], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + @override + _i7.Future loadHtmlString( + String? html, { + String? baseUrl, + }) => + (super.noSuchMethod( + Invocation.method( + #loadHtmlString, + [html], + {#baseUrl: baseUrl}, + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + @override + _i7.Future loadRequest(_i2.LoadRequestParams? params) => + (super.noSuchMethod( + Invocation.method( + #loadRequest, + [params], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + @override + _i7.Future currentUrl() => (super.noSuchMethod( + Invocation.method( + #currentUrl, + [], + ), + returnValue: _i7.Future.value(), + ) as _i7.Future); + @override + _i7.Future canGoBack() => (super.noSuchMethod( + Invocation.method( + #canGoBack, + [], + ), + returnValue: _i7.Future.value(false), + ) as _i7.Future); + @override + _i7.Future canGoForward() => (super.noSuchMethod( + Invocation.method( + #canGoForward, + [], + ), + returnValue: _i7.Future.value(false), + ) as _i7.Future); + @override + _i7.Future goBack() => (super.noSuchMethod( + Invocation.method( + #goBack, + [], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + @override + _i7.Future goForward() => (super.noSuchMethod( + Invocation.method( + #goForward, + [], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + @override + _i7.Future reload() => (super.noSuchMethod( + Invocation.method( + #reload, + [], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + @override + _i7.Future clearCache() => (super.noSuchMethod( + Invocation.method( + #clearCache, + [], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + @override + _i7.Future clearLocalStorage() => (super.noSuchMethod( + Invocation.method( + #clearLocalStorage, + [], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + @override + _i7.Future setPlatformNavigationDelegate( + _i8.PlatformNavigationDelegate? handler) => + (super.noSuchMethod( + Invocation.method( + #setPlatformNavigationDelegate, + [handler], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + @override + _i7.Future runJavaScript(String? javaScript) => (super.noSuchMethod( + Invocation.method( + #runJavaScript, + [javaScript], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + @override + _i7.Future runJavaScriptReturningResult(String? javaScript) => + (super.noSuchMethod( + Invocation.method( + #runJavaScriptReturningResult, + [javaScript], + ), + returnValue: _i7.Future.value(_FakeObject_1( + this, + Invocation.method( + #runJavaScriptReturningResult, + [javaScript], + ), + )), + ) as _i7.Future); + @override + _i7.Future addJavaScriptChannel( + _i6.JavaScriptChannelParams? javaScriptChannelParams) => + (super.noSuchMethod( + Invocation.method( + #addJavaScriptChannel, + [javaScriptChannelParams], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + @override + _i7.Future removeJavaScriptChannel(String? javaScriptChannelName) => + (super.noSuchMethod( + Invocation.method( + #removeJavaScriptChannel, + [javaScriptChannelName], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + @override + _i7.Future getTitle() => (super.noSuchMethod( + Invocation.method( + #getTitle, + [], + ), + returnValue: _i7.Future.value(), + ) as _i7.Future); + @override + _i7.Future scrollTo( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollTo, + [ + x, + y, + ], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + @override + _i7.Future scrollBy( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollBy, + [ + x, + y, + ], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + @override + _i7.Future<_i3.Offset> getScrollPosition() => (super.noSuchMethod( + Invocation.method( + #getScrollPosition, + [], + ), + returnValue: _i7.Future<_i3.Offset>.value(_FakeOffset_2( + this, + Invocation.method( + #getScrollPosition, + [], + ), + )), + ) as _i7.Future<_i3.Offset>); + @override + _i7.Future enableZoom(bool? enabled) => (super.noSuchMethod( + Invocation.method( + #enableZoom, + [enabled], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + @override + _i7.Future setBackgroundColor(_i3.Color? color) => (super.noSuchMethod( + Invocation.method( + #setBackgroundColor, + [color], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + @override + _i7.Future setJavaScriptMode(_i2.JavaScriptMode? javaScriptMode) => + (super.noSuchMethod( + Invocation.method( + #setJavaScriptMode, + [javaScriptMode], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + @override + _i7.Future setUserAgent(String? userAgent) => (super.noSuchMethod( + Invocation.method( + #setUserAgent, + [userAgent], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); +} + +/// A class which mocks [PlatformWebViewWidget]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPlatformWebViewWidget extends _i1.Mock + implements _i9.PlatformWebViewWidget { + MockPlatformWebViewWidget() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.PlatformWebViewWidgetCreationParams get params => (super.noSuchMethod( + Invocation.getter(#params), + returnValue: _FakePlatformWebViewWidgetCreationParams_3( + this, + Invocation.getter(#params), + ), + ) as _i2.PlatformWebViewWidgetCreationParams); + @override + _i4.Widget build(_i4.BuildContext? context) => (super.noSuchMethod( + Invocation.method( + #build, + [context], + ), + returnValue: _FakeWidget_4( + this, + Invocation.method( + #build, + [context], + ), + ), + ) as _i4.Widget); +} diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index fbb502c7cca8..ed6c546ed147 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,52 @@ +## 3.3.0 + +* Adds support to access native `WebView`. + +## 3.2.4 + +* Renames Pigeon output files. + +## 3.2.3 + +* Fixes bug that prevented the web view from being garbage collected. +* Fixes bug causing a `LateInitializationError` when a `PlatformNavigationDelegate` is not provided. + +## 3.2.2 + +* Updates example code for `use_build_context_synchronously` lint. + +## 3.2.1 + +* Updates code for stricter lint checks. + +## 3.2.0 + +* Adds support for handling file selection. See `AndroidWebViewController.setOnShowFileSelector`. +* Updates pigeon dev dependency to `4.2.14`. + +## 3.1.3 + +* Fixes crash when the Java `InstanceManager` was used after plugin was removed from the engine. + +## 3.1.2 + +* Fixes bug where an `AndroidWebViewController` couldn't be reused with a new `WebViewWidget`. + +## 3.1.1 + +* Fixes bug where a `AndroidNavigationDelegate` was required to load a request. + +## 3.1.0 + +* Adds support for selecting Hybrid Composition on versions 23+. Please use + `AndroidWebViewControllerCreationParams.displayWithHybridComposition`. + +## 3.0.0 + +* **BREAKING CHANGE** Updates platform implementation to `2.0.0` release of + `webview_flutter_platform_interface`. See + [webview_flutter](https://pub.dev/packages/webview_flutter/versions/4.0.0) for updated usage. + ## 2.10.4 * Updates code for `no_leading_underscores_for_local_identifiers` lint. diff --git a/packages/webview_flutter/webview_flutter_android/README.md b/packages/webview_flutter/webview_flutter_android/README.md index bdd9edf99eaa..d2f4d94bfed4 100644 --- a/packages/webview_flutter/webview_flutter_android/README.md +++ b/packages/webview_flutter/webview_flutter_android/README.md @@ -7,6 +7,47 @@ The Android implementation of [`webview_flutter`][1]. This package is [endorsed][2], which means you can simply use `webview_flutter` normally. This package will be automatically included in your app when you do. +## Display Mode + +This plugin supports two different platform view display modes. The default display mode is subject +to change in the future, and will not be considered a breaking change, so if you want to ensure a +specific mode, you can set it explicitly. + +### Texture Layer Hybrid Composition + +This is the current default mode for versions >=23. This is a new display mode used by most +plugins starting with Flutter 3.0. This is more performant than Hybrid Composition, but has some +limitations from using an Android [SurfaceTexture](https://developer.android.com/reference/android/graphics/SurfaceTexture). +See: +* https://github.com/flutter/flutter/issues/104889 +* https://github.com/flutter/flutter/issues/116954 + +### Hybrid Composition + +This is the current default mode for versions <23. It ensures that the WebView will display and work +as expected, at the cost of some performance. See: +* https://flutter.dev/docs/development/platform-integration/platform-views#performance + +This can be configured for versions >=23 with +`AndroidWebViewWidgetCreationParams.displayWithHybridComposition`. See https://pub.dev/packages/webview_flutter#platform-specific-features +for more details on setting platform-specific features in the main plugin. + +### External Native API + +The plugin also provides a native API accessible by the native code of Android applications or +packages. This API follows the convention of breaking changes of the Dart API, which means that any +changes to the class that are not backwards compatible will only be made with a major version change +of the plugin. Native code other than this external API does not follow breaking change conventions, +so app or plugin clients should not use any other native APIs. + +The API can be accessed by importing the native class `WebViewFlutterAndroidExternalApi`: + +Java: + +```java +import io.flutter.plugins.webviewflutter.WebViewFlutterAndroidExternalApi; +``` + ## Contributing This package uses [pigeon][3] to generate the communication layer between Flutter and the host diff --git a/packages/webview_flutter/webview_flutter_android/android/build.gradle b/packages/webview_flutter/webview_flutter_android/android/build.gradle index 7384f8d453da..6783e1c977c2 100644 --- a/packages/webview_flutter/webview_flutter_android/android/build.gradle +++ b/packages/webview_flutter/webview_flutter_android/android/build.gradle @@ -30,16 +30,14 @@ android { } lintOptions { - disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' } dependencies { implementation 'androidx.annotation:annotation:1.5.0' implementation 'androidx.webkit:webkit:1.5.0' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-inline:4.8.0' + testImplementation 'org.mockito:mockito-inline:5.1.0' testImplementation 'androidx.test:core:1.3.0' } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerFlutterApiImpl.java index 1981d8eccf12..0d4797e9a1bb 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerFlutterApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerFlutterApiImpl.java @@ -47,20 +47,6 @@ public void onDownloadStart( callback); } - /** - * Communicates to Dart that the reference to a {@link DownloadListener} was removed. - * - * @param downloadListener the instance whose reference will be removed - * @param callback reply callback with return value from Dart - */ - public void dispose(DownloadListener downloadListener, Reply callback) { - if (instanceManager.containsInstance(downloadListener)) { - dispose(getIdentifierForListener(downloadListener), callback); - } else { - callback.reply(null); - } - } - private long getIdentifierForListener(DownloadListener listener) { final Long identifier = instanceManager.getIdentifierForStrongReference(listener); if (identifier == null) { diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java index ed0c2aee4700..a9cbcbdd410a 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DownloadListenerHostApiImpl.java @@ -6,7 +6,6 @@ import android.webkit.DownloadListener; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerHostApi; /** @@ -21,11 +20,9 @@ public class DownloadListenerHostApiImpl implements DownloadListenerHostApi { /** * Implementation of {@link DownloadListener} that passes arguments of callback methods to Dart. - * - *

No messages are sent to Dart after {@link DownloadListenerImpl#release} is called. */ - public static class DownloadListenerImpl implements DownloadListener, Releasable { - @Nullable private DownloadListenerFlutterApiImpl flutterApi; + public static class DownloadListenerImpl implements DownloadListener { + private final DownloadListenerFlutterApiImpl flutterApi; /** * Creates a {@link DownloadListenerImpl} that passes arguments of callbacks methods to Dart. @@ -43,18 +40,8 @@ public void onDownloadStart( String contentDisposition, String mimetype, long contentLength) { - if (flutterApi != null) { - flutterApi.onDownloadStart( - this, url, userAgent, contentDisposition, mimetype, contentLength, reply -> {}); - } - } - - @Override - public void release() { - if (flutterApi != null) { - flutterApi.dispose(this, reply -> {}); - } - flutterApi = null; + flutterApi.onDownloadStart( + this, url, userAgent, contentDisposition, mimetype, contentLength, reply -> {}); } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FileChooserParamsFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FileChooserParamsFlutterApiImpl.java new file mode 100644 index 000000000000..679785949697 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FileChooserParamsFlutterApiImpl.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 android.os.Build; +import android.webkit.WebChromeClient; +import androidx.annotation.RequiresApi; +import io.flutter.plugin.common.BinaryMessenger; +import java.util.Arrays; + +/** + * Flutter Api implementation for {@link android.webkit.WebChromeClient.FileChooserParams}. + * + *

Passes arguments of callbacks methods from a {@link + * android.webkit.WebChromeClient.FileChooserParams} to Dart. + */ +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +public class FileChooserParamsFlutterApiImpl + extends GeneratedAndroidWebView.FileChooserParamsFlutterApi { + private final InstanceManager instanceManager; + + /** + * Creates a Flutter api that sends messages to Dart. + * + * @param binaryMessenger handles sending messages to Dart + * @param instanceManager maintains instances stored to communicate with Dart objects + */ + public FileChooserParamsFlutterApiImpl( + BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + private static GeneratedAndroidWebView.FileChooserModeEnumData toFileChooserEnumData(int mode) { + final GeneratedAndroidWebView.FileChooserModeEnumData.Builder builder = + new GeneratedAndroidWebView.FileChooserModeEnumData.Builder(); + + switch (mode) { + case WebChromeClient.FileChooserParams.MODE_OPEN: + builder.setValue(GeneratedAndroidWebView.FileChooserMode.OPEN); + break; + case WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE: + builder.setValue(GeneratedAndroidWebView.FileChooserMode.OPEN_MULTIPLE); + break; + case WebChromeClient.FileChooserParams.MODE_SAVE: + builder.setValue(GeneratedAndroidWebView.FileChooserMode.SAVE); + break; + default: + throw new IllegalArgumentException(String.format("Unsupported FileChooserMode: %d", mode)); + } + + return builder.build(); + } + + /** + * Stores the FileChooserParams instance and notifies Dart to create a new FileChooserParams + * instance that is attached to this one. + * + * @return the instanceId of the stored instance + */ + public long create(WebChromeClient.FileChooserParams instance, Reply callback) { + final long instanceId = instanceManager.addHostCreatedInstance(instance); + create( + instanceId, + instance.isCaptureEnabled(), + Arrays.asList(instance.getAcceptTypes()), + toFileChooserEnumData(instance.getMode()), + instance.getFilenameHint(), + callback); + return instanceId; + } +} 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 9a6b1c44d39d..425f6c1415bd 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 @@ -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 (v4.0.2), do not edit directly. +// Autogenerated from Pigeon (v4.2.14), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.webviewflutter; @@ -17,7 +17,7 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -25,6 +25,90 @@ @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"}) public class GeneratedAndroidWebView { + /** + * Mode of how to select files for a file chooser. + * + *

See + * https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams. + */ + public enum FileChooserMode { + /** + * Open single file and requires that the file exists before allowing the user to pick it. + * + *

See + * https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams#MODE_OPEN. + */ + OPEN(0), + /** + * Similar to [open] but allows multiple files to be selected. + * + *

See + * https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams#MODE_OPEN_MULTIPLE. + */ + OPEN_MULTIPLE(1), + /** + * Allows picking a nonexistent file and saving it. + * + *

See + * https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams#MODE_SAVE. + */ + SAVE(2); + + private final int index; + + private FileChooserMode(final int index) { + this.index = index; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class FileChooserModeEnumData { + private @NonNull FileChooserMode value; + + public @NonNull FileChooserMode getValue() { + return value; + } + + public void setValue(@NonNull FileChooserMode setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"value\" is null."); + } + this.value = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private FileChooserModeEnumData() {} + + public static final class Builder { + private @Nullable FileChooserMode value; + + public @NonNull Builder setValue(@NonNull FileChooserMode setterArg) { + this.value = setterArg; + return this; + } + + public @NonNull FileChooserModeEnumData build() { + FileChooserModeEnumData pigeonReturn = new FileChooserModeEnumData(); + pigeonReturn.setValue(value); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(1); + toListResult.add(value == null ? null : value.index); + return toListResult; + } + + static @NonNull FileChooserModeEnumData fromList(@NonNull ArrayList list) { + FileChooserModeEnumData pigeonResult = new FileChooserModeEnumData(); + Object value = list.get(0); + pigeonResult.setValue(value == null ? null : FileChooserMode.values()[(int) value]); + return pigeonResult; + } + } + /** Generated class from Pigeon that represents data sent in messages. */ public static class WebResourceRequestData { private @NonNull String url; @@ -161,30 +245,30 @@ public static final class Builder { } @NonNull - Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("url", url); - toMapResult.put("isForMainFrame", isForMainFrame); - toMapResult.put("isRedirect", isRedirect); - toMapResult.put("hasGesture", hasGesture); - toMapResult.put("method", method); - toMapResult.put("requestHeaders", requestHeaders); - return toMapResult; - } - - static @NonNull WebResourceRequestData fromMap(@NonNull Map map) { + ArrayList toList() { + ArrayList toListResult = new ArrayList(6); + toListResult.add(url); + toListResult.add(isForMainFrame); + toListResult.add(isRedirect); + toListResult.add(hasGesture); + toListResult.add(method); + toListResult.add(requestHeaders); + return toListResult; + } + + static @NonNull WebResourceRequestData fromList(@NonNull ArrayList list) { WebResourceRequestData pigeonResult = new WebResourceRequestData(); - Object url = map.get("url"); + Object url = list.get(0); pigeonResult.setUrl((String) url); - Object isForMainFrame = map.get("isForMainFrame"); + Object isForMainFrame = list.get(1); pigeonResult.setIsForMainFrame((Boolean) isForMainFrame); - Object isRedirect = map.get("isRedirect"); + Object isRedirect = list.get(2); pigeonResult.setIsRedirect((Boolean) isRedirect); - Object hasGesture = map.get("hasGesture"); + Object hasGesture = list.get(3); pigeonResult.setHasGesture((Boolean) hasGesture); - Object method = map.get("method"); + Object method = list.get(4); pigeonResult.setMethod((String) method); - Object requestHeaders = map.get("requestHeaders"); + Object requestHeaders = list.get(5); pigeonResult.setRequestHeaders((Map) requestHeaders); return pigeonResult; } @@ -245,21 +329,21 @@ public static final class Builder { } @NonNull - Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("errorCode", errorCode); - toMapResult.put("description", description); - return toMapResult; + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(errorCode); + toListResult.add(description); + return toListResult; } - static @NonNull WebResourceErrorData fromMap(@NonNull Map map) { + static @NonNull WebResourceErrorData fromList(@NonNull ArrayList list) { WebResourceErrorData pigeonResult = new WebResourceErrorData(); - Object errorCode = map.get("errorCode"); + Object errorCode = list.get(0); pigeonResult.setErrorCode( (errorCode == null) ? null : ((errorCode instanceof Integer) ? (Integer) errorCode : (Long) errorCode)); - Object description = map.get("description"); + Object description = list.get(1); pigeonResult.setDescription((String) description); return pigeonResult; } @@ -320,18 +404,18 @@ public static final class Builder { } @NonNull - Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("x", x); - toMapResult.put("y", y); - return toMapResult; + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(x); + toListResult.add(y); + return toListResult; } - static @NonNull WebViewPoint fromMap(@NonNull Map map) { + static @NonNull WebViewPoint fromList(@NonNull ArrayList list) { WebViewPoint pigeonResult = new WebViewPoint(); - Object x = map.get("x"); + Object x = list.get(0); pigeonResult.setX((x == null) ? null : ((x instanceof Integer) ? (Integer) x : (Long) x)); - Object y = map.get("y"); + Object y = list.get(1); pigeonResult.setY((y == null) ? null : ((y instanceof Integer) ? (Integer) y : (Long) y)); return pigeonResult; } @@ -342,22 +426,22 @@ public interface Result { void error(Throwable error); } - - private static class JavaObjectHostApiCodec extends StandardMessageCodec { - public static final JavaObjectHostApiCodec INSTANCE = new JavaObjectHostApiCodec(); - - private JavaObjectHostApiCodec() {} - } - - /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + /** + * Handles methods calls to the native Java Object class. + * + *

Also handles calls to remove the reference to an instance with `dispose`. + * + *

See https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html. + * + *

Generated interface from Pigeon that represents a handler of messages from Flutter. + */ public interface JavaObjectHostApi { void dispose(@NonNull Long identifier); /** The codec used by JavaObjectHostApi. */ static MessageCodec getCodec() { - return JavaObjectHostApiCodec.INSTANCE; + return new StandardMessageCodec(); } - /** * Sets up an instance of `JavaObjectHostApi` to handle messages through the `binaryMessenger`. */ @@ -369,17 +453,19 @@ static void setup(BinaryMessenger binaryMessenger, JavaObjectHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number identifierArg = (Number) args.get(0); if (identifierArg == null) { throw new NullPointerException("identifierArg unexpectedly null."); } api.dispose((identifierArg == null) ? null : identifierArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -389,14 +475,13 @@ static void setup(BinaryMessenger binaryMessenger, JavaObjectHostApi api) { } } } - - private static class JavaObjectFlutterApiCodec extends StandardMessageCodec { - public static final JavaObjectFlutterApiCodec INSTANCE = new JavaObjectFlutterApiCodec(); - - private JavaObjectFlutterApiCodec() {} - } - - /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + /** + * Handles callbacks methods for the native Java Object class. + * + *

See https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html. + * + *

Generated class from Pigeon that represents Flutter messages that can be called from Java. + */ public static class JavaObjectFlutterApi { private final BinaryMessenger binaryMessenger; @@ -407,9 +492,9 @@ public JavaObjectFlutterApi(BinaryMessenger argBinaryMessenger) { public interface Reply { void reply(T reply); } - + /** The codec used by JavaObjectFlutterApi. */ static MessageCodec getCodec() { - return JavaObjectFlutterApiCodec.INSTANCE; + return new StandardMessageCodec(); } public void dispose(@NonNull Long identifierArg, Reply callback) { @@ -417,19 +502,12 @@ public void dispose(@NonNull Long identifierArg, Reply callback) { new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.JavaObjectFlutterApi.dispose", getCodec()); channel.send( - new ArrayList(Arrays.asList(identifierArg)), + new ArrayList(Collections.singletonList(identifierArg)), channelReply -> { callback.reply(null); }); } } - - private static class CookieManagerHostApiCodec extends StandardMessageCodec { - public static final CookieManagerHostApiCodec INSTANCE = new CookieManagerHostApiCodec(); - - private CookieManagerHostApiCodec() {} - } - /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface CookieManagerHostApi { void clearCookies(Result result); @@ -438,9 +516,8 @@ public interface CookieManagerHostApi { /** The codec used by CookieManagerHostApi. */ static MessageCodec getCodec() { - return CookieManagerHostApiCodec.INSTANCE; + return new StandardMessageCodec(); } - /** * Sets up an instance of `CookieManagerHostApi` to handle messages through the * `binaryMessenger`. @@ -455,25 +532,25 @@ static void setup(BinaryMessenger binaryMessenger, CookieManagerHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { Result resultCallback = new Result() { public void success(Boolean result) { - wrapped.put("result", result); + wrapped.add(0, result); reply.reply(wrapped); } public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); } }; api.clearCookies(resultCallback); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); + ArrayList wrappedError = wrapError(exception); + reply.reply(wrappedError); } }); } else { @@ -487,9 +564,10 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; String urlArg = (String) args.get(0); if (urlArg == null) { throw new NullPointerException("urlArg unexpectedly null."); @@ -499,9 +577,10 @@ public void error(Throwable error) { throw new NullPointerException("valueArg unexpectedly null."); } api.setCookie(urlArg, valueArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -518,10 +597,10 @@ private static class WebViewHostApiCodec extends StandardMessageCodec { private WebViewHostApiCodec() {} @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { case (byte) 128: - return WebViewPoint.fromMap((Map) readValue(buffer)); + return WebViewPoint.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -529,10 +608,10 @@ protected Object readValueOfType(byte type, ByteBuffer buffer) { } @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof WebViewPoint) { stream.write(128); - writeValue(stream, ((WebViewPoint) value).toMap()); + writeValue(stream, ((WebViewPoint) value).toList()); } else { super.writeValue(stream, value); } @@ -543,8 +622,6 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { public interface WebViewHostApi { void create(@NonNull Long instanceId, @NonNull Boolean useHybridComposition); - void dispose(@NonNull Long instanceId); - void loadData( @NonNull Long instanceId, @NonNull String data, @@ -619,7 +696,6 @@ void removeJavaScriptChannel( static MessageCodec getCodec() { return WebViewHostApiCodec.INSTANCE; } - /** Sets up an instance of `WebViewHostApi` to handle messages through the `binaryMessenger`. */ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { { @@ -629,9 +705,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -643,34 +720,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { api.create( (instanceIdArg == null) ? null : instanceIdArg.longValue(), useHybridCompositionArg); - wrapped.put("result", null); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.WebViewHostApi.dispose", getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList) message; - Number instanceIdArg = (Number) args.get(0); - if (instanceIdArg == null) { - throw new NullPointerException("instanceIdArg unexpectedly null."); - } - api.dispose((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -685,9 +738,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -703,9 +757,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { dataArg, mimeTypeArg, encodingArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -722,9 +777,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -744,9 +800,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { mimeTypeArg, encodingArg, historyUrlArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -761,9 +818,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -780,9 +838,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { (instanceIdArg == null) ? null : instanceIdArg.longValue(), urlArg, headersArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -797,9 +856,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -814,9 +874,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { } api.postUrl( (instanceIdArg == null) ? null : instanceIdArg.longValue(), urlArg, dataArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -831,18 +892,20 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } String output = api.getUrl((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", output); + wrapped.add(0, output); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -857,18 +920,20 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } Boolean output = api.canGoBack((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", output); + wrapped.add(0, output); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -883,18 +948,20 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } Boolean output = api.canGoForward((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", output); + wrapped.add(0, output); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -909,17 +976,19 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } api.goBack((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -934,17 +1003,19 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } api.goForward((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -959,17 +1030,19 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } api.reload((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -984,9 +1057,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -998,9 +1072,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { api.clearCache( (instanceIdArg == null) ? null : instanceIdArg.longValue(), includeDiskFilesArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1017,9 +1092,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1031,13 +1107,13 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { Result resultCallback = new Result() { public void success(String result) { - wrapped.put("result", result); + wrapped.add(0, result); reply.reply(wrapped); } public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); } }; @@ -1046,8 +1122,8 @@ public void error(Throwable error) { javascriptStringArg, resultCallback); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); + ArrayList wrappedError = wrapError(exception); + reply.reply(wrappedError); } }); } else { @@ -1061,18 +1137,20 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } String output = api.getTitle((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", output); + wrapped.add(0, output); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1087,9 +1165,10 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1106,9 +1185,10 @@ public void error(Throwable error) { (instanceIdArg == null) ? null : instanceIdArg.longValue(), (xArg == null) ? null : xArg.longValue(), (yArg == null) ? null : yArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1123,9 +1203,10 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1142,9 +1223,10 @@ public void error(Throwable error) { (instanceIdArg == null) ? null : instanceIdArg.longValue(), (xArg == null) ? null : xArg.longValue(), (yArg == null) ? null : yArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1159,18 +1241,20 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } Long output = api.getScrollX((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", output); + wrapped.add(0, output); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1185,18 +1269,20 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } Long output = api.getScrollY((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", output); + wrapped.add(0, output); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1211,9 +1297,10 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1221,9 +1308,10 @@ public void error(Throwable error) { WebViewPoint output = api.getScrollPosition( (instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", output); + wrapped.add(0, output); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1240,17 +1328,19 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Boolean enabledArg = (Boolean) args.get(0); if (enabledArg == null) { throw new NullPointerException("enabledArg unexpectedly null."); } api.setWebContentsDebuggingEnabled(enabledArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1265,9 +1355,10 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1281,9 +1372,10 @@ public void error(Throwable error) { (webViewClientInstanceIdArg == null) ? null : webViewClientInstanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1300,9 +1392,10 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1317,9 +1410,10 @@ public void error(Throwable error) { (javaScriptChannelInstanceIdArg == null) ? null : javaScriptChannelInstanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1336,9 +1430,10 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1353,9 +1448,10 @@ public void error(Throwable error) { (javaScriptChannelInstanceIdArg == null) ? null : javaScriptChannelInstanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1372,9 +1468,10 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1383,9 +1480,10 @@ public void error(Throwable error) { api.setDownloadListener( (instanceIdArg == null) ? null : instanceIdArg.longValue(), (listenerInstanceIdArg == null) ? null : listenerInstanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1402,9 +1500,10 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1413,9 +1512,10 @@ public void error(Throwable error) { api.setWebChromeClient( (instanceIdArg == null) ? null : instanceIdArg.longValue(), (clientInstanceIdArg == null) ? null : clientInstanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1432,9 +1532,10 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1446,9 +1547,10 @@ public void error(Throwable error) { api.setBackgroundColor( (instanceIdArg == null) ? null : instanceIdArg.longValue(), (colorArg == null) ? null : colorArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1458,19 +1560,10 @@ public void error(Throwable error) { } } } - - private static class WebSettingsHostApiCodec extends StandardMessageCodec { - public static final WebSettingsHostApiCodec INSTANCE = new WebSettingsHostApiCodec(); - - private WebSettingsHostApiCodec() {} - } - /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface WebSettingsHostApi { void create(@NonNull Long instanceId, @NonNull Long webViewInstanceId); - void dispose(@NonNull Long instanceId); - void setDomStorageEnabled(@NonNull Long instanceId, @NonNull Boolean flag); void setJavaScriptCanOpenWindowsAutomatically(@NonNull Long instanceId, @NonNull Boolean flag); @@ -1497,9 +1590,8 @@ public interface WebSettingsHostApi { /** The codec used by WebSettingsHostApi. */ static MessageCodec getCodec() { - return WebSettingsHostApiCodec.INSTANCE; + return new StandardMessageCodec(); } - /** * Sets up an instance of `WebSettingsHostApi` to handle messages through the `binaryMessenger`. */ @@ -1511,9 +1603,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1525,34 +1618,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { api.create( (instanceIdArg == null) ? null : instanceIdArg.longValue(), (webViewInstanceIdArg == null) ? null : webViewInstanceIdArg.longValue()); - wrapped.put("result", null); - } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.WebSettingsHostApi.dispose", getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList) message; - Number instanceIdArg = (Number) args.get(0); - if (instanceIdArg == null) { - throw new NullPointerException("instanceIdArg unexpectedly null."); - } - api.dispose((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1569,9 +1638,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1582,9 +1652,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setDomStorageEnabled( (instanceIdArg == null) ? null : instanceIdArg.longValue(), flagArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1601,9 +1672,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1614,9 +1686,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setJavaScriptCanOpenWindowsAutomatically( (instanceIdArg == null) ? null : instanceIdArg.longValue(), flagArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1633,9 +1706,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1646,9 +1720,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setSupportMultipleWindows( (instanceIdArg == null) ? null : instanceIdArg.longValue(), supportArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1665,9 +1740,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1678,9 +1754,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setJavaScriptEnabled( (instanceIdArg == null) ? null : instanceIdArg.longValue(), flagArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1697,9 +1774,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1708,9 +1786,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { api.setUserAgentString( (instanceIdArg == null) ? null : instanceIdArg.longValue(), userAgentStringArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1727,9 +1806,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1740,9 +1820,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setMediaPlaybackRequiresUserGesture( (instanceIdArg == null) ? null : instanceIdArg.longValue(), requireArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1759,9 +1840,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1772,9 +1854,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setSupportZoom( (instanceIdArg == null) ? null : instanceIdArg.longValue(), supportArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1791,9 +1874,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1804,9 +1888,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setLoadWithOverviewMode( (instanceIdArg == null) ? null : instanceIdArg.longValue(), overviewArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1823,9 +1908,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1836,9 +1922,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setUseWideViewPort( (instanceIdArg == null) ? null : instanceIdArg.longValue(), useArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1855,9 +1942,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1868,9 +1956,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setDisplayZoomControls( (instanceIdArg == null) ? null : instanceIdArg.longValue(), enabledArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1887,9 +1976,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1900,9 +1990,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setBuiltInZoomControls( (instanceIdArg == null) ? null : instanceIdArg.longValue(), enabledArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1919,9 +2010,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1932,9 +2024,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setAllowFileAccess( (instanceIdArg == null) ? null : instanceIdArg.longValue(), enabledArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1944,23 +2037,14 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } } } - - private static class JavaScriptChannelHostApiCodec extends StandardMessageCodec { - public static final JavaScriptChannelHostApiCodec INSTANCE = - new JavaScriptChannelHostApiCodec(); - - private JavaScriptChannelHostApiCodec() {} - } - /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface JavaScriptChannelHostApi { void create(@NonNull Long instanceId, @NonNull String channelName); /** The codec used by JavaScriptChannelHostApi. */ static MessageCodec getCodec() { - return JavaScriptChannelHostApiCodec.INSTANCE; + return new StandardMessageCodec(); } - /** * Sets up an instance of `JavaScriptChannelHostApi` to handle messages through the * `binaryMessenger`. @@ -1973,9 +2057,10 @@ static void setup(BinaryMessenger binaryMessenger, JavaScriptChannelHostApi api) if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); @@ -1986,9 +2071,10 @@ static void setup(BinaryMessenger binaryMessenger, JavaScriptChannelHostApi api) } api.create( (instanceIdArg == null) ? null : instanceIdArg.longValue(), channelNameArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1998,14 +2084,6 @@ static void setup(BinaryMessenger binaryMessenger, JavaScriptChannelHostApi api) } } } - - private static class JavaScriptChannelFlutterApiCodec extends StandardMessageCodec { - public static final JavaScriptChannelFlutterApiCodec INSTANCE = - new JavaScriptChannelFlutterApiCodec(); - - private JavaScriptChannelFlutterApiCodec() {} - } - /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class JavaScriptChannelFlutterApi { private final BinaryMessenger binaryMessenger; @@ -2017,22 +2095,9 @@ public JavaScriptChannelFlutterApi(BinaryMessenger argBinaryMessenger) { public interface Reply { void reply(T reply); } - + /** The codec used by JavaScriptChannelFlutterApi. */ static MessageCodec getCodec() { - return JavaScriptChannelFlutterApiCodec.INSTANCE; - } - - public void dispose(@NonNull Long instanceIdArg, Reply callback) { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.JavaScriptChannelFlutterApi.dispose", - getCodec()); - channel.send( - new ArrayList(Arrays.asList(instanceIdArg)), - channelReply -> { - callback.reply(null); - }); + return new StandardMessageCodec(); } public void postMessage( @@ -2049,22 +2114,17 @@ public void postMessage( }); } } - - private static class WebViewClientHostApiCodec extends StandardMessageCodec { - public static final WebViewClientHostApiCodec INSTANCE = new WebViewClientHostApiCodec(); - - private WebViewClientHostApiCodec() {} - } - /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface WebViewClientHostApi { - void create(@NonNull Long instanceId, @NonNull Boolean shouldOverrideUrlLoading); + void create(@NonNull Long instanceId); + + void setSynchronousReturnValueForShouldOverrideUrlLoading( + @NonNull Long instanceId, @NonNull Boolean value); /** The codec used by WebViewClientHostApi. */ static MessageCodec getCodec() { - return WebViewClientHostApiCodec.INSTANCE; + return new StandardMessageCodec(); } - /** * Sets up an instance of `WebViewClientHostApi` to handle messages through the * `binaryMessenger`. @@ -2077,24 +2137,53 @@ static void setup(BinaryMessenger binaryMessenger, WebViewClientHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - Boolean shouldOverrideUrlLoadingArg = (Boolean) args.get(1); - if (shouldOverrideUrlLoadingArg == null) { - throw new NullPointerException( - "shouldOverrideUrlLoadingArg unexpectedly null."); + api.create((instanceIdArg == null) ? null : instanceIdArg.longValue()); + wrapped.add(0, null); + } catch (Error | RuntimeException exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebViewClientHostApi.setSynchronousReturnValueForShouldOverrideUrlLoading", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + ArrayList args = (ArrayList) message; + assert args != null; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); } - api.create( - (instanceIdArg == null) ? null : instanceIdArg.longValue(), - shouldOverrideUrlLoadingArg); - wrapped.put("result", null); + Boolean valueArg = (Boolean) args.get(1); + if (valueArg == null) { + throw new NullPointerException("valueArg unexpectedly null."); + } + api.setSynchronousReturnValueForShouldOverrideUrlLoading( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), valueArg); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -2111,13 +2200,13 @@ private static class WebViewClientFlutterApiCodec extends StandardMessageCodec { private WebViewClientFlutterApiCodec() {} @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { case (byte) 128: - return WebResourceErrorData.fromMap((Map) readValue(buffer)); + return WebResourceErrorData.fromList((ArrayList) readValue(buffer)); case (byte) 129: - return WebResourceRequestData.fromMap((Map) readValue(buffer)); + return WebResourceRequestData.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -2125,13 +2214,13 @@ protected Object readValueOfType(byte type, ByteBuffer buffer) { } @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof WebResourceErrorData) { stream.write(128); - writeValue(stream, ((WebResourceErrorData) value).toMap()); + writeValue(stream, ((WebResourceErrorData) value).toList()); } else if (value instanceof WebResourceRequestData) { stream.write(129); - writeValue(stream, ((WebResourceRequestData) value).toMap()); + writeValue(stream, ((WebResourceRequestData) value).toList()); } else { super.writeValue(stream, value); } @@ -2149,22 +2238,11 @@ public WebViewClientFlutterApi(BinaryMessenger argBinaryMessenger) { public interface Reply { void reply(T reply); } - + /** The codec used by WebViewClientFlutterApi. */ static MessageCodec getCodec() { return WebViewClientFlutterApiCodec.INSTANCE; } - public void dispose(@NonNull Long instanceIdArg, Reply callback) { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.WebViewClientFlutterApi.dispose", getCodec()); - channel.send( - new ArrayList(Arrays.asList(instanceIdArg)), - channelReply -> { - callback.reply(null); - }); - } - public void onPageStarted( @NonNull Long instanceIdArg, @NonNull Long webViewInstanceIdArg, @@ -2275,22 +2353,14 @@ public void urlLoading( }); } } - - private static class DownloadListenerHostApiCodec extends StandardMessageCodec { - public static final DownloadListenerHostApiCodec INSTANCE = new DownloadListenerHostApiCodec(); - - private DownloadListenerHostApiCodec() {} - } - /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface DownloadListenerHostApi { void create(@NonNull Long instanceId); /** The codec used by DownloadListenerHostApi. */ static MessageCodec getCodec() { - return DownloadListenerHostApiCodec.INSTANCE; + return new StandardMessageCodec(); } - /** * Sets up an instance of `DownloadListenerHostApi` to handle messages through the * `binaryMessenger`. @@ -2303,17 +2373,19 @@ static void setup(BinaryMessenger binaryMessenger, DownloadListenerHostApi api) if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } api.create((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -2323,14 +2395,6 @@ static void setup(BinaryMessenger binaryMessenger, DownloadListenerHostApi api) } } } - - private static class DownloadListenerFlutterApiCodec extends StandardMessageCodec { - public static final DownloadListenerFlutterApiCodec INSTANCE = - new DownloadListenerFlutterApiCodec(); - - private DownloadListenerFlutterApiCodec() {} - } - /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class DownloadListenerFlutterApi { private final BinaryMessenger binaryMessenger; @@ -2342,20 +2406,9 @@ public DownloadListenerFlutterApi(BinaryMessenger argBinaryMessenger) { public interface Reply { void reply(T reply); } - + /** The codec used by DownloadListenerFlutterApi. */ static MessageCodec getCodec() { - return DownloadListenerFlutterApiCodec.INSTANCE; - } - - public void dispose(@NonNull Long instanceIdArg, Reply callback) { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.DownloadListenerFlutterApi.dispose", getCodec()); - channel.send( - new ArrayList(Arrays.asList(instanceIdArg)), - channelReply -> { - callback.reply(null); - }); + return new StandardMessageCodec(); } public void onDownloadStart( @@ -2385,22 +2438,17 @@ public void onDownloadStart( }); } } - - private static class WebChromeClientHostApiCodec extends StandardMessageCodec { - public static final WebChromeClientHostApiCodec INSTANCE = new WebChromeClientHostApiCodec(); - - private WebChromeClientHostApiCodec() {} - } - /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface WebChromeClientHostApi { - void create(@NonNull Long instanceId, @NonNull Long webViewClientInstanceId); + void create(@NonNull Long instanceId); + + void setSynchronousReturnValueForOnShowFileChooser( + @NonNull Long instanceId, @NonNull Boolean value); /** The codec used by WebChromeClientHostApi. */ static MessageCodec getCodec() { - return WebChromeClientHostApiCodec.INSTANCE; + return new StandardMessageCodec(); } - /** * Sets up an instance of `WebChromeClientHostApi` to handle messages through the * `binaryMessenger`. @@ -2413,25 +2461,53 @@ static void setup(BinaryMessenger binaryMessenger, WebChromeClientHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } - Number webViewClientInstanceIdArg = (Number) args.get(1); - if (webViewClientInstanceIdArg == null) { - throw new NullPointerException("webViewClientInstanceIdArg unexpectedly null."); + api.create((instanceIdArg == null) ? null : instanceIdArg.longValue()); + wrapped.add(0, null); + } catch (Error | RuntimeException exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebChromeClientHostApi.setSynchronousReturnValueForOnShowFileChooser", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + ArrayList args = (ArrayList) message; + assert args != null; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); } - api.create( - (instanceIdArg == null) ? null : instanceIdArg.longValue(), - (webViewClientInstanceIdArg == null) - ? null - : webViewClientInstanceIdArg.longValue()); - wrapped.put("result", null); + Boolean valueArg = (Boolean) args.get(1); + if (valueArg == null) { + throw new NullPointerException("valueArg unexpectedly null."); + } + api.setSynchronousReturnValueForOnShowFileChooser( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), valueArg); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -2441,14 +2517,6 @@ static void setup(BinaryMessenger binaryMessenger, WebChromeClientHostApi api) { } } } - - private static class FlutterAssetManagerHostApiCodec extends StandardMessageCodec { - public static final FlutterAssetManagerHostApiCodec INSTANCE = - new FlutterAssetManagerHostApiCodec(); - - private FlutterAssetManagerHostApiCodec() {} - } - /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface FlutterAssetManagerHostApi { @NonNull @@ -2459,9 +2527,8 @@ public interface FlutterAssetManagerHostApi { /** The codec used by FlutterAssetManagerHostApi. */ static MessageCodec getCodec() { - return FlutterAssetManagerHostApiCodec.INSTANCE; + return new StandardMessageCodec(); } - /** * Sets up an instance of `FlutterAssetManagerHostApi` to handle messages through the * `binaryMessenger`. @@ -2474,17 +2541,19 @@ static void setup(BinaryMessenger binaryMessenger, FlutterAssetManagerHostApi ap if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; String pathArg = (String) args.get(0); if (pathArg == null) { throw new NullPointerException("pathArg unexpectedly null."); } List output = api.list(pathArg); - wrapped.put("result", output); + wrapped.add(0, output); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -2501,17 +2570,19 @@ static void setup(BinaryMessenger binaryMessenger, FlutterAssetManagerHostApi ap if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; String nameArg = (String) args.get(0); if (nameArg == null) { throw new NullPointerException("nameArg unexpectedly null."); } String output = api.getAssetFilePathByName(nameArg); - wrapped.put("result", output); + wrapped.add(0, output); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -2521,14 +2592,6 @@ static void setup(BinaryMessenger binaryMessenger, FlutterAssetManagerHostApi ap } } } - - private static class WebChromeClientFlutterApiCodec extends StandardMessageCodec { - public static final WebChromeClientFlutterApiCodec INSTANCE = - new WebChromeClientFlutterApiCodec(); - - private WebChromeClientFlutterApiCodec() {} - } - /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class WebChromeClientFlutterApi { private final BinaryMessenger binaryMessenger; @@ -2540,46 +2603,48 @@ public WebChromeClientFlutterApi(BinaryMessenger argBinaryMessenger) { public interface Reply { void reply(T reply); } - + /** The codec used by WebChromeClientFlutterApi. */ static MessageCodec getCodec() { - return WebChromeClientFlutterApiCodec.INSTANCE; + return new StandardMessageCodec(); } - public void dispose(@NonNull Long instanceIdArg, Reply callback) { + public void onProgressChanged( + @NonNull Long instanceIdArg, + @NonNull Long webViewInstanceIdArg, + @NonNull Long progressArg, + Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.WebChromeClientFlutterApi.dispose", getCodec()); + binaryMessenger, + "dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged", + getCodec()); channel.send( - new ArrayList(Arrays.asList(instanceIdArg)), + new ArrayList(Arrays.asList(instanceIdArg, webViewInstanceIdArg, progressArg)), channelReply -> { callback.reply(null); }); } - public void onProgressChanged( + public void onShowFileChooser( @NonNull Long instanceIdArg, @NonNull Long webViewInstanceIdArg, - @NonNull Long progressArg, - Reply callback) { + @NonNull Long paramsInstanceIdArg, + Reply> callback) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, - "dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged", + "dev.flutter.pigeon.WebChromeClientFlutterApi.onShowFileChooser", getCodec()); channel.send( - new ArrayList(Arrays.asList(instanceIdArg, webViewInstanceIdArg, progressArg)), + new ArrayList( + Arrays.asList(instanceIdArg, webViewInstanceIdArg, paramsInstanceIdArg)), channelReply -> { - callback.reply(null); + @SuppressWarnings("ConstantConditions") + List output = (List) channelReply; + callback.reply(output); }); } } - - private static class WebStorageHostApiCodec extends StandardMessageCodec { - public static final WebStorageHostApiCodec INSTANCE = new WebStorageHostApiCodec(); - - private WebStorageHostApiCodec() {} - } - /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface WebStorageHostApi { void create(@NonNull Long instanceId); @@ -2588,9 +2653,8 @@ public interface WebStorageHostApi { /** The codec used by WebStorageHostApi. */ static MessageCodec getCodec() { - return WebStorageHostApiCodec.INSTANCE; + return new StandardMessageCodec(); } - /** * Sets up an instance of `WebStorageHostApi` to handle messages through the `binaryMessenger`. */ @@ -2602,17 +2666,19 @@ static void setup(BinaryMessenger binaryMessenger, WebStorageHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } api.create((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -2627,17 +2693,19 @@ static void setup(BinaryMessenger binaryMessenger, WebStorageHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; + assert args != null; Number instanceIdArg = (Number) args.get(0); if (instanceIdArg == null) { throw new NullPointerException("instanceIdArg unexpectedly null."); } api.deleteAllData((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -2648,13 +2716,84 @@ static void setup(BinaryMessenger binaryMessenger, WebStorageHostApi api) { } } - private static Map wrapError(Throwable exception) { - Map errorMap = new HashMap<>(); - errorMap.put("message", exception.toString()); - errorMap.put("code", exception.getClass().getSimpleName()); - errorMap.put( - "details", + private static class FileChooserParamsFlutterApiCodec extends StandardMessageCodec { + public static final FileChooserParamsFlutterApiCodec INSTANCE = + new FileChooserParamsFlutterApiCodec(); + + private FileChooserParamsFlutterApiCodec() {} + + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return FileChooserModeEnumData.fromList((ArrayList) readValue(buffer)); + + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof FileChooserModeEnumData) { + stream.write(128); + writeValue(stream, ((FileChooserModeEnumData) value).toList()); + } else { + super.writeValue(stream, value); + } + } + } + + /** + * Handles callbacks methods for the native Java FileChooserParams class. + * + *

See + * https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams. + * + *

Generated class from Pigeon that represents Flutter messages that can be called from Java. + */ + public static class FileChooserParamsFlutterApi { + private final BinaryMessenger binaryMessenger; + + public FileChooserParamsFlutterApi(BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + public interface Reply { + void reply(T reply); + } + /** The codec used by FileChooserParamsFlutterApi. */ + static MessageCodec getCodec() { + return FileChooserParamsFlutterApiCodec.INSTANCE; + } + + public void create( + @NonNull Long instanceIdArg, + @NonNull Boolean isCaptureEnabledArg, + @NonNull List acceptTypesArg, + @NonNull FileChooserModeEnumData modeArg, + @Nullable String filenameHintArg, + Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.FileChooserParamsFlutterApi.create", getCodec()); + channel.send( + new ArrayList( + Arrays.asList( + instanceIdArg, isCaptureEnabledArg, acceptTypesArg, modeArg, filenameHintArg)), + channelReply -> { + callback.reply(null); + }); + } + } + + @NonNull + private static ArrayList wrapError(@NonNull Throwable exception) { + ArrayList errorList = new ArrayList<>(3); + errorList.add(exception.toString()); + errorList.add(exception.getClass().getSimpleName()); + errorList.add( "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); - return errorMap; + return errorList; } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java index fefd577ee9b5..55775a914c56 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java @@ -6,6 +6,7 @@ import android.os.Handler; import android.os.Looper; +import android.util.Log; import androidx.annotation.Nullable; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; @@ -35,6 +36,8 @@ public class InstanceManager { // 0 <= n < 2^16. private static final long MIN_HOST_CREATED_IDENTIFIER = 65536; private static final long CLEAR_FINALIZED_WEAK_REFERENCES_INTERVAL = 30000; + private static final String TAG = "InstanceManager"; + private static final String CLOSED_WARNING = "Method was called while the manager was closed."; /** Interface for listening when a weak reference of an instance is removed from the manager. */ public interface FinalizationListener { @@ -79,11 +82,15 @@ private InstanceManager(FinalizationListener finalizationListener) { * * @param identifier the identifier paired to an instance. * @param the expected return type. - * @return the removed instance if the manager contains the given identifier, otherwise null. + * @return the removed instance if the manager contains the given identifier, otherwise null if + * the manager doesn't contain the value or the manager is closed. */ @Nullable public T remove(long identifier) { - assertManagerIsNotClosed(); + if (isClosed()) { + Log.w(TAG, CLOSED_WARNING); + return null; + } return (T) strongInstances.remove(identifier); } @@ -95,11 +102,14 @@ public T remove(long identifier) { * * @param instance an instance that may be stored in the manager. * @return the identifier associated with `instance` if the manager contains the value, otherwise - * null. + * null if the manager doesn't contain the value or the manager is closed. */ @Nullable public Long getIdentifierForStrongReference(Object instance) { - assertManagerIsNotClosed(); + if (isClosed()) { + Log.w(TAG, CLOSED_WARNING); + return null; + } final Long identifier = identifiers.get(instance); if (identifier != null) { strongInstances.put(identifier, instance); @@ -114,11 +124,16 @@ public Long getIdentifierForStrongReference(Object instance) { * The Dart InstanceManager is considered the source of truth and has the capability to overwrite * stored pairs in response to hot restarts. * + *

If the manager is closed, the addition is ignored. + * * @param instance the instance to be stored. * @param identifier the identifier to be paired with instance. This value must be >= 0. */ public void addDartCreatedInstance(Object instance, long identifier) { - assertManagerIsNotClosed(); + if (isClosed()) { + Log.w(TAG, CLOSED_WARNING); + return; + } addInstance(instance, identifier); } @@ -126,10 +141,13 @@ public void addDartCreatedInstance(Object instance, long identifier) { * Adds a new instance that was instantiated from the host platform. * * @param instance the instance to be stored. - * @return the unique identifier stored with instance. + * @return the unique identifier stored with instance. If the manager is closed, returns -1. */ public long addHostCreatedInstance(Object instance) { - assertManagerIsNotClosed(); + if (isClosed()) { + Log.w(TAG, CLOSED_WARNING); + return -1; + } final long identifier = nextIdentifier++; addInstance(instance, identifier); return identifier; @@ -141,11 +159,14 @@ public long addHostCreatedInstance(Object instance) { * @param identifier the identifier paired to an instance. * @param the expected return type. * @return the instance associated with `identifier` if the manager contains the value, otherwise - * null. + * null if the manager doesn't contain the value or the manager is closed. */ @Nullable public T getInstance(long identifier) { - assertManagerIsNotClosed(); + if (isClosed()) { + Log.w(TAG, CLOSED_WARNING); + return null; + } final WeakReference instance = (WeakReference) weakInstances.get(identifier); if (instance != null) { return instance.get(); @@ -157,22 +178,38 @@ public T getInstance(long identifier) { * Returns whether this manager contains the given `instance`. * * @param instance the instance whose presence in this manager is to be tested. - * @return whether this manager contains the given `instance`. + * @return whether this manager contains the given `instance`. If the manager is closed, returns + * `false`. */ public boolean containsInstance(Object instance) { - assertManagerIsNotClosed(); + if (isClosed()) { + Log.w(TAG, CLOSED_WARNING); + return false; + } return identifiers.containsKey(instance); } /** * Closes the manager and releases resources. * - *

Calling a method after calling this one will throw an {@link AssertionError}. This method - * excluded. + *

Methods called after this one will be ignored and log a warning. */ public void close() { handler.removeCallbacks(this::releaseAllFinalizedInstances); isClosed = true; + identifiers.clear(); + weakInstances.clear(); + strongInstances.clear(); + weakReferencesToIdentifiers.clear(); + } + + /** + * Whether the manager has released resources and is not longer usable. + * + *

See {@link #close()}. + */ + public boolean isClosed() { + return isClosed; } private void releaseAllFinalizedInstances() { @@ -199,10 +236,4 @@ private void addInstance(Object instance, long identifier) { weakReferencesToIdentifiers.put(weakReference, identifier); strongInstances.put(identifier, instance); } - - private void assertManagerIsNotClosed() { - if (isClosed) { - throw new AssertionError("Manager has already been closed."); - } - } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaObjectHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaObjectHostApiImpl.java index 978e5232657d..9cbf65b4c613 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaObjectHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaObjectHostApiImpl.java @@ -27,6 +27,10 @@ public JavaObjectHostApiImpl(InstanceManager instanceManager) { @Override public void dispose(@NonNull Long identifier) { + final Object instance = instanceManager.getInstance(identifier); + if (instance instanceof WebViewHostApiImpl.WebViewPlatformView) { + ((WebViewHostApiImpl.WebViewPlatformView) instance).destroy(); + } instanceManager.remove(identifier); } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java index ce6f2b81ed1c..cf2c2629989a 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java @@ -8,7 +8,6 @@ import android.os.Looper; import android.webkit.JavascriptInterface; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; /** * Added as a JavaScript interface to the WebView for any JavaScript channel that the Dart code sets @@ -16,13 +15,11 @@ * *

Exposes a single method named `postMessage` to JavaScript, which sends a message to the Dart * code. - * - *

No messages are sent to Dart after {@link JavaScriptChannel#release} is called. */ -public class JavaScriptChannel implements Releasable { +public class JavaScriptChannel { private final Handler platformThreadHandler; final String javaScriptChannelName; - @Nullable private JavaScriptChannelFlutterApiImpl flutterApi; + private final JavaScriptChannelFlutterApiImpl flutterApi; /** * Creates a {@link JavaScriptChannel} that passes arguments of callback methods to Dart. @@ -46,9 +43,7 @@ public JavaScriptChannel( public void postMessage(final String message) { final Runnable postMessageRunnable = () -> { - if (flutterApi != null) { - flutterApi.postMessage(JavaScriptChannel.this, message, reply -> {}); - } + flutterApi.postMessage(JavaScriptChannel.this, message, reply -> {}); }; if (platformThreadHandler.getLooper() == Looper.myLooper()) { @@ -57,12 +52,4 @@ public void postMessage(final String message) { platformThreadHandler.post(postMessageRunnable); } } - - @Override - public void release() { - if (flutterApi != null) { - flutterApi.dispose(this, reply -> {}); - } - flutterApi = null; - } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelFlutterApiImpl.java index dbac83382a29..ca0892699638 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelFlutterApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannelFlutterApiImpl.java @@ -33,20 +33,6 @@ public void postMessage( super.postMessage(getIdentifierForJavaScriptChannel(javaScriptChannel), messageArg, callback); } - /** - * Communicates to Dart that the reference to a {@link JavaScriptChannel} was removed. - * - * @param javaScriptChannel The instance whose reference will be removed. - * @param callback Reply callback with return value from Dart. - */ - public void dispose(JavaScriptChannel javaScriptChannel, Reply callback) { - if (instanceManager.containsInstance(javaScriptChannel)) { - dispose(getIdentifierForJavaScriptChannel(javaScriptChannel), callback); - } else { - callback.reply(null); - } - } - private long getIdentifierForJavaScriptChannel(JavaScriptChannel javaScriptChannel) { final Long identifier = instanceManager.getIdentifierForStrongReference(javaScriptChannel); if (identifier == null) { diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/Releasable.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/Releasable.java deleted file mode 100644 index 9c4ed7650640..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/Releasable.java +++ /dev/null @@ -1,14 +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. - -package io.flutter.plugins.webviewflutter; - -/** - * Represents a resource, or a holder of resources, which may be released once they are no longer - * needed. - */ -interface Releasable { - /** Notify that that the reference to an object will be removed by a holder. */ - void release(); -} 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 28d63ec82dec..92f0e41905cc 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 @@ -4,10 +4,14 @@ package io.flutter.plugins.webviewflutter; +import android.os.Build; import android.webkit.WebChromeClient; import android.webkit.WebView; +import androidx.annotation.RequiresApi; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientFlutterApi; +import java.util.List; +import java.util.Objects; /** * Flutter Api implementation for {@link WebChromeClient}. @@ -15,6 +19,7 @@ *

Passes arguments of callbacks methods from a {@link WebChromeClient} to Dart. */ public class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { + private final BinaryMessenger binaryMessenger; private final InstanceManager instanceManager; /** @@ -26,6 +31,7 @@ public class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { public WebChromeClientFlutterApiImpl( BinaryMessenger binaryMessenger, InstanceManager instanceManager) { super(binaryMessenger); + this.binaryMessenger = binaryMessenger; this.instanceManager = instanceManager; } @@ -40,18 +46,25 @@ public void onProgressChanged( getIdentifierForClient(webChromeClient), webViewIdentifier, progress, callback); } - /** - * Communicates to Dart that the reference to a {@link WebChromeClient}} was removed. - * - * @param webChromeClient the instance whose reference will be removed - * @param callback reply callback with return value from Dart - */ - public void dispose(WebChromeClient webChromeClient, Reply callback) { - if (instanceManager.containsInstance(webChromeClient)) { - dispose(getIdentifierForClient(webChromeClient), callback); - } else { - callback.reply(null); + /** Passes arguments from {@link WebChromeClient#onShowFileChooser} to Dart. */ + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public void onShowFileChooser( + WebChromeClient webChromeClient, + WebView webView, + WebChromeClient.FileChooserParams fileChooserParams, + Reply> callback) { + Long paramsInstanceId = instanceManager.getIdentifierForStrongReference(fileChooserParams); + if (paramsInstanceId == null) { + final FileChooserParamsFlutterApiImpl flutterApi = + new FileChooserParamsFlutterApiImpl(binaryMessenger, instanceManager); + paramsInstanceId = flutterApi.create(fileChooserParams, reply -> {}); } + + onShowFileChooser( + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(webChromeClient)), + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(webView)), + paramsInstanceId, + callback); } private long getIdentifierForClient(WebChromeClient webChromeClient) { 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 0f50c82fa7bb..a5825c0133ec 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 @@ -4,8 +4,10 @@ package io.flutter.plugins.webviewflutter; +import android.net.Uri; import android.os.Build; import android.os.Message; +import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; import android.webkit.WebView; @@ -15,6 +17,7 @@ import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientHostApi; +import java.util.Objects; /** * Host api implementation for {@link WebChromeClient}. @@ -29,22 +32,62 @@ public class WebChromeClientHostApiImpl implements WebChromeClientHostApi { /** * Implementation of {@link WebChromeClient} that passes arguments of callback methods to Dart. */ - public static class WebChromeClientImpl extends WebChromeClient implements Releasable { - @Nullable private WebChromeClientFlutterApiImpl flutterApi; - private WebViewClient webViewClient; + public static class WebChromeClientImpl extends SecureWebChromeClient { + private final WebChromeClientFlutterApiImpl flutterApi; + private boolean returnValueForOnShowFileChooser = false; /** * Creates a {@link WebChromeClient} that passes arguments of callbacks methods to Dart. * * @param flutterApi handles sending messages to Dart - * @param webViewClient receives forwarded calls from {@link WebChromeClient#onCreateWindow} */ - public WebChromeClientImpl( - @NonNull WebChromeClientFlutterApiImpl flutterApi, WebViewClient webViewClient) { + public WebChromeClientImpl(@NonNull WebChromeClientFlutterApiImpl flutterApi) { this.flutterApi = flutterApi; - this.webViewClient = webViewClient; } + @Override + public void onProgressChanged(WebView view, int progress) { + flutterApi.onProgressChanged(this, view, (long) progress, reply -> {}); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean onShowFileChooser( + WebView webView, + ValueCallback filePathCallback, + FileChooserParams fileChooserParams) { + final boolean currentReturnValueForOnShowFileChooser = returnValueForOnShowFileChooser; + flutterApi.onShowFileChooser( + this, + webView, + fileChooserParams, + reply -> { + // The returned list of file paths can only be passed to `filePathCallback` if the + // `onShowFileChooser` method returned true. + if (currentReturnValueForOnShowFileChooser) { + final Uri[] filePaths = new Uri[reply.size()]; + for (int i = 0; i < reply.size(); i++) { + filePaths[i] = Uri.parse(reply.get(i)); + } + filePathCallback.onReceiveValue(filePaths); + } + }); + return currentReturnValueForOnShowFileChooser; + } + + /** Sets return value for {@link #onShowFileChooser}. */ + public void setReturnValueForOnShowFileChooser(boolean value) { + returnValueForOnShowFileChooser = value; + } + } + + /** + * Implementation of {@link WebChromeClient} that only allows secure urls when opening a new + * window. + */ + public static class SecureWebChromeClient extends WebChromeClient { + @Nullable private WebViewClient webViewClient; + @Override public boolean onCreateWindow( final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { @@ -67,6 +110,14 @@ public boolean onCreateWindow( @VisibleForTesting boolean onCreateWindow( final WebView view, Message resultMsg, @Nullable WebView onCreateWindowWebView) { + // WebChromeClient requires a WebViewClient because of a bug fix that makes + // calls to WebViewClient.requestLoading/WebViewClient.urlLoading when a new + // window is opened. This is to make sure a url opened by `Window.open` has + // a secure url. + if (webViewClient == null) { + return false; + } + final WebViewClient windowWebViewClient = new WebViewClient() { @RequiresApi(api = Build.VERSION_CODES.N) @@ -100,30 +151,15 @@ public boolean shouldOverrideUrlLoading(WebView windowWebView, String url) { return true; } - @Override - public void onProgressChanged(WebView view, int progress) { - if (flutterApi != null) { - flutterApi.onProgressChanged(this, view, (long) progress, reply -> {}); - } - } - /** * Set the {@link WebViewClient} that calls to {@link WebChromeClient#onCreateWindow} are passed * to. * * @param webViewClient the forwarding {@link WebViewClient} */ - public void setWebViewClient(WebViewClient webViewClient) { + public void setWebViewClient(@NonNull WebViewClient webViewClient) { this.webViewClient = webViewClient; } - - @Override - public void release() { - if (flutterApi != null) { - flutterApi.dispose(this, reply -> {}); - } - flutterApi = null; - } } /** Handles creating {@link WebChromeClient}s for a {@link WebChromeClientHostApiImpl}. */ @@ -132,12 +168,10 @@ public static class WebChromeClientCreator { * Creates a {@link DownloadListenerHostApiImpl.DownloadListenerImpl}. * * @param flutterApi handles sending messages to Dart - * @param webViewClient receives forwarded calls from {@link WebChromeClient#onCreateWindow} - * @return the created {@link DownloadListenerHostApiImpl.DownloadListenerImpl} + * @return the created {@link WebChromeClientHostApiImpl.WebChromeClientImpl} */ - public WebChromeClientImpl createWebChromeClient( - WebChromeClientFlutterApiImpl flutterApi, WebViewClient webViewClient) { - return new WebChromeClientImpl(flutterApi, webViewClient); + public WebChromeClientImpl createWebChromeClient(WebChromeClientFlutterApiImpl flutterApi) { + return new WebChromeClientImpl(flutterApi); } } @@ -158,11 +192,17 @@ public WebChromeClientHostApiImpl( } @Override - public void create(Long instanceId, Long webViewClientInstanceId) { - final WebViewClient webViewClient = - (WebViewClient) instanceManager.getInstance(webViewClientInstanceId); + public void create(Long instanceId) { final WebChromeClient webChromeClient = - webChromeClientCreator.createWebChromeClient(flutterApi, webViewClient); + webChromeClientCreator.createWebChromeClient(flutterApi); instanceManager.addDartCreatedInstance(webChromeClient, instanceId); } + + @Override + public void setSynchronousReturnValueForOnShowFileChooser( + @NonNull Long instanceId, @NonNull Boolean value) { + final WebChromeClientImpl webChromeClient = + Objects.requireNonNull(instanceManager.getInstance(instanceId)); + webChromeClient.setReturnValueForOnShowFileChooser(value); + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java index 5b6f9e78a8d5..98fd4fcfb53e 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java @@ -49,11 +49,6 @@ public void create(Long instanceId, Long webViewInstanceId) { webSettingsCreator.createWebSettings(webView), instanceId); } - @Override - public void dispose(Long instanceId) { - instanceManager.remove(instanceId); - } - @Override public void setDomStorageEnabled(Long instanceId, Boolean flag) { final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java index c23e8e79ae37..0dc0bbb82b07 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientFlutterApiImpl.java @@ -197,20 +197,6 @@ public void urlLoading( urlLoading(getIdentifierForClient(webViewClient), webViewIdentifier, urlArg, callback); } - /** - * Communicates to Dart that the reference to a {@link WebViewClient} was removed. - * - * @param webViewClient the instance whose reference will be removed - * @param callback reply callback with return value from Dart - */ - public void dispose(WebViewClient webViewClient, Reply callback) { - if (instanceManager.containsInstance(webViewClient)) { - dispose(getIdentifierForClient(webViewClient), callback); - } else { - callback.reply(null); - } - } - private long getIdentifierForClient(WebViewClient webViewClient) { final Long identifier = instanceManager.getIdentifierForStrongReference(webViewClient); if (identifier == null) { diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java index 4833ee917d34..09a34f2d4a85 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientHostApiImpl.java @@ -14,10 +14,10 @@ import android.webkit.WebView; import android.webkit.WebViewClient; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.webkit.WebResourceErrorCompat; import androidx.webkit.WebViewClientCompat; +import java.util.Objects; /** * Host api implementation for {@link WebViewClient}. @@ -29,73 +29,53 @@ public class WebViewClientHostApiImpl implements GeneratedAndroidWebView.WebView private final WebViewClientCreator webViewClientCreator; private final WebViewClientFlutterApiImpl flutterApi; - /** - * An interface implemented by a class that extends {@link WebViewClient} and {@link Releasable}. - */ - public interface ReleasableWebViewClient extends Releasable {} - /** Implementation of {@link WebViewClient} that passes arguments of callback methods to Dart. */ @RequiresApi(Build.VERSION_CODES.N) - public static class WebViewClientImpl extends WebViewClient implements ReleasableWebViewClient { - @Nullable private WebViewClientFlutterApiImpl flutterApi; - private final boolean shouldOverrideUrlLoading; + public static class WebViewClientImpl extends WebViewClient { + private final WebViewClientFlutterApiImpl flutterApi; + private boolean returnValueForShouldOverrideUrlLoading = false; /** * Creates a {@link WebViewClient} that passes arguments of callbacks methods to Dart. * * @param flutterApi handles sending messages to Dart - * @param shouldOverrideUrlLoading whether loading a url should be overridden */ - public WebViewClientImpl( - @NonNull WebViewClientFlutterApiImpl flutterApi, boolean shouldOverrideUrlLoading) { - this.shouldOverrideUrlLoading = shouldOverrideUrlLoading; + public WebViewClientImpl(@NonNull WebViewClientFlutterApiImpl flutterApi) { this.flutterApi = flutterApi; } @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { - if (flutterApi != null) { - flutterApi.onPageStarted(this, view, url, reply -> {}); - } + flutterApi.onPageStarted(this, view, url, reply -> {}); } @Override public void onPageFinished(WebView view, String url) { - if (flutterApi != null) { - flutterApi.onPageFinished(this, view, url, reply -> {}); - } + flutterApi.onPageFinished(this, view, url, reply -> {}); } @Override public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { - if (flutterApi != null) { - flutterApi.onReceivedRequestError(this, view, request, error, reply -> {}); - } + flutterApi.onReceivedRequestError(this, view, request, error, reply -> {}); } @Override public void onReceivedError( WebView view, int errorCode, String description, String failingUrl) { - if (flutterApi != null) { - flutterApi.onReceivedError( - this, view, (long) errorCode, description, failingUrl, reply -> {}); - } + flutterApi.onReceivedError( + this, view, (long) errorCode, description, failingUrl, reply -> {}); } @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - if (flutterApi != null) { - flutterApi.requestLoading(this, view, request, reply -> {}); - } - return shouldOverrideUrlLoading; + flutterApi.requestLoading(this, view, request, reply -> {}); + return returnValueForShouldOverrideUrlLoading; } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (flutterApi != null) { - flutterApi.urlLoading(this, view, url, reply -> {}); - } - return shouldOverrideUrlLoading; + flutterApi.urlLoading(this, view, url, reply -> {}); + return returnValueForShouldOverrideUrlLoading; } @Override @@ -105,11 +85,9 @@ public void onUnhandledKeyEvent(WebView view, KeyEvent event) { // truly lost. } - public void release() { - if (flutterApi != null) { - flutterApi.dispose(this, reply -> {}); - } - flutterApi = null; + /** Sets return value for {@link #shouldOverrideUrlLoading}. */ + public void setReturnValueForShouldOverrideUrlLoading(boolean value) { + returnValueForShouldOverrideUrlLoading = value; } } @@ -117,29 +95,22 @@ public void release() { * Implementation of {@link WebViewClientCompat} that passes arguments of callback methods to * Dart. */ - public static class WebViewClientCompatImpl extends WebViewClientCompat - implements ReleasableWebViewClient { - private @Nullable WebViewClientFlutterApiImpl flutterApi; - private final boolean shouldOverrideUrlLoading; - - public WebViewClientCompatImpl( - @NonNull WebViewClientFlutterApiImpl flutterApi, boolean shouldOverrideUrlLoading) { - this.shouldOverrideUrlLoading = shouldOverrideUrlLoading; + public static class WebViewClientCompatImpl extends WebViewClientCompat { + private final WebViewClientFlutterApiImpl flutterApi; + private boolean returnValueForShouldOverrideUrlLoading = false; + + public WebViewClientCompatImpl(@NonNull WebViewClientFlutterApiImpl flutterApi) { this.flutterApi = flutterApi; } @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { - if (flutterApi != null) { - flutterApi.onPageStarted(this, view, url, reply -> {}); - } + flutterApi.onPageStarted(this, view, url, reply -> {}); } @Override public void onPageFinished(WebView view, String url) { - if (flutterApi != null) { - flutterApi.onPageFinished(this, view, url, reply -> {}); - } + flutterApi.onPageFinished(this, view, url, reply -> {}); } // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is @@ -151,36 +122,28 @@ public void onReceivedError( @NonNull WebView view, @NonNull WebResourceRequest request, @NonNull WebResourceErrorCompat error) { - if (flutterApi != null) { - flutterApi.onReceivedRequestError(this, view, request, error, reply -> {}); - } + flutterApi.onReceivedRequestError(this, view, request, error, reply -> {}); } @Override public void onReceivedError( WebView view, int errorCode, String description, String failingUrl) { - if (flutterApi != null) { - flutterApi.onReceivedError( - this, view, (long) errorCode, description, failingUrl, reply -> {}); - } + flutterApi.onReceivedError( + this, view, (long) errorCode, description, failingUrl, reply -> {}); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public boolean shouldOverrideUrlLoading( @NonNull WebView view, @NonNull WebResourceRequest request) { - if (flutterApi != null) { - flutterApi.requestLoading(this, view, request, reply -> {}); - } - return shouldOverrideUrlLoading; + flutterApi.requestLoading(this, view, request, reply -> {}); + return returnValueForShouldOverrideUrlLoading; } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (flutterApi != null) { - flutterApi.urlLoading(this, view, url, reply -> {}); - } - return shouldOverrideUrlLoading; + flutterApi.urlLoading(this, view, url, reply -> {}); + return returnValueForShouldOverrideUrlLoading; } @Override @@ -190,11 +153,9 @@ public void onUnhandledKeyEvent(WebView view, KeyEvent event) { // truly lost. } - public void release() { - if (flutterApi != null) { - flutterApi.dispose(this, reply -> {}); - } - flutterApi = null; + /** Sets return value for {@link #shouldOverrideUrlLoading}. */ + public void setReturnValueForShouldOverrideUrlLoading(boolean value) { + returnValueForShouldOverrideUrlLoading = value; } } @@ -206,8 +167,7 @@ public static class WebViewClientCreator { * @param flutterApi handles sending messages to Dart * @return the created {@link WebViewClient} */ - public WebViewClient createWebViewClient( - WebViewClientFlutterApiImpl flutterApi, boolean shouldOverrideUrlLoading) { + public WebViewClient createWebViewClient(WebViewClientFlutterApiImpl flutterApi) { // WebViewClientCompat is used to get // shouldOverrideUrlLoading(WebView view, WebResourceRequest request) // invoked by the webview on older Android devices, without it pages that use iframes will @@ -217,9 +177,9 @@ public WebViewClient createWebViewClient( // to bug https://bugs.chromium.org/p/chromium/issues/detail?id=925887. Also, see // https://github.com/flutter/flutter/issues/29446. if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return new WebViewClientImpl(flutterApi, shouldOverrideUrlLoading); + return new WebViewClientImpl(flutterApi); } else { - return new WebViewClientCompatImpl(flutterApi, shouldOverrideUrlLoading); + return new WebViewClientCompatImpl(flutterApi); } } } @@ -241,9 +201,24 @@ public WebViewClientHostApiImpl( } @Override - public void create(Long instanceId, Boolean shouldOverrideUrlLoading) { - final WebViewClient webViewClient = - webViewClientCreator.createWebViewClient(flutterApi, shouldOverrideUrlLoading); + public void create(@NonNull Long instanceId) { + final WebViewClient webViewClient = webViewClientCreator.createWebViewClient(flutterApi); instanceManager.addDartCreatedInstance(webViewClient, instanceId); } + + @Override + public void setSynchronousReturnValueForShouldOverrideUrlLoading( + @NonNull Long instanceId, @NonNull Boolean value) { + final WebViewClient webViewClient = + Objects.requireNonNull(instanceManager.getInstance(instanceId)); + if (webViewClient instanceof WebViewClientCompatImpl) { + ((WebViewClientCompatImpl) webViewClient).setReturnValueForShouldOverrideUrlLoading(value); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N + && webViewClient instanceof WebViewClientImpl) { + ((WebViewClientImpl) webViewClient).setReturnValueForShouldOverrideUrlLoading(value); + } else { + throw new IllegalStateException( + "This WebViewClient doesn't support setting the returnValueForShouldOverrideUrlLoading."); + } + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterAndroidExternalApi.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterAndroidExternalApi.java new file mode 100644 index 000000000000..3819d7b26f62 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterAndroidExternalApi.java @@ -0,0 +1,50 @@ +// 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.WebView; +import androidx.annotation.Nullable; +import io.flutter.embedding.engine.FlutterEngine; + +/** + * App and package facing native API provided by the `webview_flutter_android` plugin. + * + *

This class follows the convention of breaking changes of the Dart API, which means that any + * changes to the class that are not backwards compatible will only be made with a major version + * change of the plugin. + * + *

Native code other than this external API does not follow breaking change conventions, so app + * or plugin clients should not use any other native APIs. + */ +@SuppressWarnings("unused") +public interface WebViewFlutterAndroidExternalApi { + /** + * Retrieves the {@link WebView} that is associated with `identifier`. + * + *

See the Dart method `AndroidWebViewController.webViewIdentifier` to get the identifier of an + * underlying `WebView`. + * + * @param engine the execution environment the {@link WebViewFlutterPlugin} should belong to. If + * the engine doesn't contain an attached instance of {@link WebViewFlutterPlugin}, this + * method returns null. + * @param identifier the associated identifier of the `WebView`. + * @return the `WebView` associated with `identifier` or null if a `WebView` instance associated + * with `identifier` could not be found. + */ + @Nullable + static WebView getWebView(FlutterEngine engine, long identifier) { + final WebViewFlutterPlugin webViewPlugin = + (WebViewFlutterPlugin) engine.getPlugins().get(WebViewFlutterPlugin.class); + + if (webViewPlugin != null && webViewPlugin.getInstanceManager() != null) { + final Object instance = webViewPlugin.getInstanceManager().getInstance(identifier); + if (instance instanceof WebView) { + return (WebView) instance; + } + } + + return null; + } +} 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 fe7615c664a4..04a9735e0281 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.JavaObjectHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebSettingsHostApi; @@ -32,7 +33,7 @@ *

Call {@link #registerWith} to use the stable {@code io.flutter.plugin.common} package instead. */ public class WebViewFlutterPlugin implements FlutterPlugin, ActivityAware { - private InstanceManager instanceManager; + @Nullable private InstanceManager instanceManager; private FlutterPluginBinding pluginBinding; private WebViewHostApiImpl webViewHostApi; @@ -77,15 +78,22 @@ private void setUp( Context context, View containerView, FlutterAssetManager flutterAssetManager) { - - instanceManager = InstanceManager.open(identifier -> {}); + instanceManager = + InstanceManager.open( + identifier -> + new GeneratedAndroidWebView.JavaObjectFlutterApi(binaryMessenger) + .dispose(identifier, reply -> {})); viewRegistry.registerViewFactory( "plugins.flutter.io/webview", new FlutterWebViewFactory(instanceManager)); webViewHostApi = new WebViewHostApiImpl( - instanceManager, new WebViewHostApiImpl.WebViewProxy(), context, containerView); + instanceManager, + binaryMessenger, + new WebViewHostApiImpl.WebViewProxy(), + context, + containerView); javaScriptChannelHostApi = new JavaScriptChannelHostApiImpl( instanceManager, @@ -93,6 +101,7 @@ private void setUp( new JavaScriptChannelFlutterApiImpl(binaryMessenger, instanceManager), new Handler(context.getMainLooper())); + JavaObjectHostApi.setup(binaryMessenger, new JavaObjectHostApiImpl(instanceManager)); WebViewHostApi.setup(binaryMessenger, webViewHostApi); JavaScriptChannelHostApi.setup(binaryMessenger, javaScriptChannelHostApi); WebViewClientHostApi.setup( @@ -139,7 +148,10 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { - instanceManager.close(); + if (instanceManager != null) { + instanceManager.close(); + instanceManager = null; + } } @Override diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java index 778ad611d05f..77d535b78aed 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewHostApiImpl.java @@ -14,12 +14,9 @@ import android.webkit.WebViewClient; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.platform.PlatformView; -import io.flutter.plugins.webviewflutter.DownloadListenerHostApiImpl.DownloadListenerImpl; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebViewHostApi; -import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientImpl; -import io.flutter.plugins.webviewflutter.WebViewClientHostApiImpl.ReleasableWebViewClient; -import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -33,6 +30,7 @@ public class WebViewHostApiImpl implements WebViewHostApi { private final WebViewProxy webViewProxy; // Only used with WebView using virtual displays. @Nullable private final View containerView; + private final BinaryMessenger binaryMessenger; private Context context; @@ -42,10 +40,14 @@ public static class WebViewProxy { * Creates a {@link WebViewPlatformView}. * * @param context an Activity Context to access application assets + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager mangages instances used to communicate with the corresponding objects + * in Dart * @return the created {@link WebViewPlatformView} */ - public WebViewPlatformView createWebView(Context context) { - return new WebViewPlatformView(context); + public WebViewPlatformView createWebView( + Context context, BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + return new WebViewPlatformView(context, binaryMessenger, instanceManager); } /** @@ -56,8 +58,12 @@ public WebViewPlatformView createWebView(Context context) { * @return the created {@link InputAwareWebViewPlatformView} */ public InputAwareWebViewPlatformView createInputAwareWebView( - Context context, @Nullable View containerView) { - return new InputAwareWebViewPlatformView(context, containerView); + Context context, + BinaryMessenger binaryMessenger, + InstanceManager instanceManager, + @Nullable View containerView) { + return new InputAwareWebViewPlatformView( + context, binaryMessenger, instanceManager, containerView); } /** @@ -70,51 +76,24 @@ public void setWebContentsDebuggingEnabled(boolean enabled) { } } - private static class ReleasableValue { - @Nullable private T value; - - ReleasableValue() {} - - ReleasableValue(@Nullable T value) { - this.value = value; - } - - void set(@Nullable T newValue) { - release(); - value = newValue; - } - - @Nullable - T get() { - return value; - } - - void release() { - if (value != null) { - value.release(); - } - value = null; - } - } - /** Implementation of {@link WebView} that can be used as a Flutter {@link PlatformView}s. */ - public static class WebViewPlatformView extends WebView implements PlatformView, Releasable { - private final ReleasableValue - currentWebViewClient = new ReleasableValue<>(); - private final ReleasableValue currentDownloadListener = - new ReleasableValue<>(); - private final ReleasableValue currentWebChromeClient = - new ReleasableValue<>(); - private final Map> javaScriptInterfaces = - new HashMap<>(); + public static class WebViewPlatformView extends WebView implements PlatformView { + private WebViewClient currentWebViewClient; + private WebChromeClientHostApiImpl.SecureWebChromeClient currentWebChromeClient; /** * Creates a {@link WebViewPlatformView}. * * @param context an Activity Context to access application assets. This value cannot be null. */ - public WebViewPlatformView(Context context) { + public WebViewPlatformView( + Context context, BinaryMessenger binaryMessenger, InstanceManager instanceManager) { super(context); + currentWebViewClient = new WebViewClient(); + currentWebChromeClient = new WebChromeClientHostApiImpl.SecureWebChromeClient(); + + setWebViewClient(currentWebViewClient); + setWebChromeClient(currentWebChromeClient); } @Override @@ -123,63 +102,32 @@ public View getView() { } @Override - public void dispose() { - destroy(); - } + public void dispose() {} @Override public void setWebViewClient(WebViewClient webViewClient) { super.setWebViewClient(webViewClient); - currentWebViewClient.set((ReleasableWebViewClient) webViewClient); - - final WebChromeClientImpl webChromeClient = currentWebChromeClient.get(); - if (webChromeClient != null) { - ((WebChromeClientImpl) webChromeClient).setWebViewClient(webViewClient); - } - } - - @Override - public void setDownloadListener(DownloadListener listener) { - super.setDownloadListener(listener); - currentDownloadListener.set((DownloadListenerImpl) listener); + currentWebViewClient = webViewClient; + currentWebChromeClient.setWebViewClient(webViewClient); } @Override public void setWebChromeClient(WebChromeClient client) { super.setWebChromeClient(client); - currentWebChromeClient.set((WebChromeClientImpl) client); - } - - @SuppressLint("JavascriptInterface") - @Override - public void addJavascriptInterface(Object object, String name) { - super.addJavascriptInterface(object, name); - if (object instanceof JavaScriptChannel) { - final ReleasableValue javaScriptChannel = javaScriptInterfaces.get(name); - if (javaScriptChannel != null && javaScriptChannel.get() != object) { - javaScriptChannel.release(); - } - javaScriptInterfaces.put(name, new ReleasableValue<>((JavaScriptChannel) object)); + if (!(client instanceof WebChromeClientHostApiImpl.SecureWebChromeClient)) { + throw new AssertionError("Client must be a SecureWebChromeClient."); } + currentWebChromeClient = (WebChromeClientHostApiImpl.SecureWebChromeClient) client; + currentWebChromeClient.setWebViewClient(currentWebViewClient); } + // When running unit tests, the parent `WebView` class is replaced by a stub that returns null + // for every method. This is overridden so that this returns the current WebChromeClient during + // unit tests. This should only remain overridden as long as `setWebChromeClient` is overridden. + @Nullable @Override - public void removeJavascriptInterface(@NonNull String name) { - super.removeJavascriptInterface(name); - final ReleasableValue javaScriptChannel = javaScriptInterfaces.get(name); - javaScriptChannel.release(); - javaScriptInterfaces.remove(name); - } - - @Override - public void release() { - currentWebViewClient.release(); - currentDownloadListener.release(); - currentWebChromeClient.release(); - for (ReleasableValue channel : javaScriptInterfaces.values()) { - channel.release(); - } - javaScriptInterfaces.clear(); + public WebChromeClient getWebChromeClient() { + return currentWebChromeClient; } } @@ -189,23 +137,26 @@ public void release() { */ @SuppressLint("ViewConstructor") public static class InputAwareWebViewPlatformView extends InputAwareWebView - implements PlatformView, Releasable { - private final ReleasableValue - currentWebViewClient = new ReleasableValue<>(); - private final ReleasableValue currentDownloadListener = - new ReleasableValue<>(); - private final ReleasableValue currentWebChromeClient = - new ReleasableValue<>(); - private final Map> javaScriptInterfaces = - new HashMap<>(); + implements PlatformView { + private WebViewClient currentWebViewClient; + private WebChromeClientHostApiImpl.SecureWebChromeClient currentWebChromeClient; /** * Creates a {@link InputAwareWebViewPlatformView}. * * @param context an Activity Context to access application assets. This value cannot be null. */ - public InputAwareWebViewPlatformView(Context context, View containerView) { + public InputAwareWebViewPlatformView( + Context context, + BinaryMessenger binaryMessenger, + InstanceManager instanceManager, + View containerView) { super(context, containerView); + currentWebViewClient = new WebViewClient(); + currentWebChromeClient = new WebChromeClientHostApiImpl.SecureWebChromeClient(); + + setWebViewClient(currentWebViewClient); + setWebChromeClient(currentWebChromeClient); } @Override @@ -242,56 +193,18 @@ public void onInputConnectionUnlocked() { @Override public void setWebViewClient(WebViewClient webViewClient) { super.setWebViewClient(webViewClient); - currentWebViewClient.set((ReleasableWebViewClient) webViewClient); - - final WebChromeClientImpl webChromeClient = currentWebChromeClient.get(); - if (webChromeClient != null) { - webChromeClient.setWebViewClient(webViewClient); - } - } - - @Override - public void setDownloadListener(DownloadListener listener) { - super.setDownloadListener(listener); - currentDownloadListener.set((DownloadListenerImpl) listener); + currentWebViewClient = webViewClient; + currentWebChromeClient.setWebViewClient(webViewClient); } @Override public void setWebChromeClient(WebChromeClient client) { super.setWebChromeClient(client); - currentWebChromeClient.set((WebChromeClientImpl) client); - } - - @SuppressLint("JavascriptInterface") - @Override - public void addJavascriptInterface(Object object, String name) { - super.addJavascriptInterface(object, name); - if (object instanceof JavaScriptChannel) { - final ReleasableValue javaScriptChannel = javaScriptInterfaces.get(name); - if (javaScriptChannel != null && javaScriptChannel.get() != object) { - javaScriptChannel.release(); - } - javaScriptInterfaces.put(name, new ReleasableValue<>((JavaScriptChannel) object)); - } - } - - @Override - public void removeJavascriptInterface(@NonNull String name) { - super.removeJavascriptInterface(name); - final ReleasableValue javaScriptChannel = javaScriptInterfaces.get(name); - javaScriptChannel.release(); - javaScriptInterfaces.remove(name); - } - - @Override - public void release() { - currentWebViewClient.release(); - currentDownloadListener.release(); - currentWebChromeClient.release(); - for (ReleasableValue channel : javaScriptInterfaces.values()) { - channel.release(); + if (!(client instanceof WebChromeClientHostApiImpl.SecureWebChromeClient)) { + throw new AssertionError("Client must be a SecureWebChromeClient."); } - javaScriptInterfaces.clear(); + currentWebChromeClient = (WebChromeClientHostApiImpl.SecureWebChromeClient) client; + currentWebChromeClient.setWebViewClient(currentWebViewClient); } } @@ -299,16 +212,19 @@ public void release() { * Creates a host API that handles creating {@link WebView}s and invoking its methods. * * @param instanceManager maintains instances stored to communicate with Dart objects + * @param binaryMessenger used to communicate with Dart over asynchronous messages * @param webViewProxy handles creating {@link WebView}s and calling its static methods * @param context an Activity Context to access application assets. This value cannot be null. * @param containerView parent of the webView */ public WebViewHostApiImpl( InstanceManager instanceManager, + BinaryMessenger binaryMessenger, WebViewProxy webViewProxy, Context context, @Nullable View containerView) { this.instanceManager = instanceManager; + this.binaryMessenger = binaryMessenger; this.webViewProxy = webViewProxy; this.context = context; this.containerView = containerView; @@ -332,22 +248,14 @@ public void create(Long instanceId, Boolean useHybridComposition) { final WebView webView = useHybridComposition - ? webViewProxy.createWebView(context) - : webViewProxy.createInputAwareWebView(context, containerView); + ? webViewProxy.createWebView(context, binaryMessenger, instanceManager) + : webViewProxy.createInputAwareWebView( + context, binaryMessenger, instanceManager, containerView); displayListenerProxy.onPostWebViewInitialization(displayManager); instanceManager.addDartCreatedInstance(webView, instanceId); } - @Override - public void dispose(Long instanceId) { - final WebView instance = (WebView) instanceManager.getInstance(instanceId); - if (instance != null) { - ((Releasable) instance).release(); - instanceManager.remove(instanceId); - } - } - @Override public void loadData(Long instanceId, String data, String mimeType, String encoding) { final WebView webView = (WebView) instanceManager.getInstance(instanceId); diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java index da25dace4517..caffbb9a95ef 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/DownloadListenerTest.java @@ -6,11 +6,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; -import android.webkit.DownloadListener; import io.flutter.plugins.webviewflutter.DownloadListenerHostApiImpl.DownloadListenerCreator; import io.flutter.plugins.webviewflutter.DownloadListenerHostApiImpl.DownloadListenerImpl; import org.junit.After; @@ -67,11 +64,5 @@ public void postMessage() { eq("mimetype"), eq(54L), any()); - - reset(mockFlutterApi); - downloadListener.release(); - downloadListener.onDownloadStart("", "", "", "", 23); - verify(mockFlutterApi, never()) - .onDownloadStart((DownloadListener) any(), any(), any(), any(), any(), eq(23), any()); } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FileChooserParamsTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FileChooserParamsTest.java new file mode 100644 index 000000000000..3172ea4330c8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FileChooserParamsTest.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.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.webkit.WebChromeClient.FileChooserParams; +import io.flutter.plugin.common.BinaryMessenger; +import java.util.Arrays; +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class FileChooserParamsTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public FileChooserParams mockFileChooserParams; + + @Mock public BinaryMessenger mockBinaryMessenger; + + InstanceManager instanceManager; + + @Before + public void setUp() { + instanceManager = InstanceManager.open(identifier -> {}); + } + + @After + public void tearDown() { + instanceManager.close(); + } + + @Test + public void flutterApiCreate() { + final FileChooserParamsFlutterApiImpl spyFlutterApi = + spy(new FileChooserParamsFlutterApiImpl(mockBinaryMessenger, instanceManager)); + + when(mockFileChooserParams.isCaptureEnabled()).thenReturn(true); + when(mockFileChooserParams.getAcceptTypes()).thenReturn(new String[] {"my", "list"}); + when(mockFileChooserParams.getMode()).thenReturn(FileChooserParams.MODE_OPEN_MULTIPLE); + when(mockFileChooserParams.getFilenameHint()).thenReturn("filenameHint"); + spyFlutterApi.create(mockFileChooserParams, reply -> {}); + + final long identifier = + Objects.requireNonNull( + instanceManager.getIdentifierForStrongReference(mockFileChooserParams)); + final ArgumentCaptor modeCaptor = + ArgumentCaptor.forClass(GeneratedAndroidWebView.FileChooserModeEnumData.class); + + verify(spyFlutterApi) + .create( + eq(identifier), + eq(true), + eq(Arrays.asList("my", "list")), + modeCaptor.capture(), + eq("filenameHint"), + any()); + assertEquals( + modeCaptor.getValue().getValue(), GeneratedAndroidWebView.FileChooserMode.OPEN_MULTIPLE); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/InstanceManagerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/InstanceManagerTest.java index 4731e2a4beb1..6a19c883548a 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/InstanceManagerTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/InstanceManagerTest.java @@ -5,6 +5,7 @@ package io.flutter.plugins.webviewflutter; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -59,4 +60,52 @@ public void remove() { instanceManager.close(); } + + @Test + public void removeReturnsNullWhenClosed() { + final Object object = new Object(); + final InstanceManager instanceManager = InstanceManager.open(identifier -> {}); + instanceManager.addDartCreatedInstance(object, 0); + instanceManager.close(); + + assertNull(instanceManager.remove(0)); + } + + @Test + public void getIdentifierForStrongReferenceReturnsNullWhenClosed() { + final Object object = new Object(); + final InstanceManager instanceManager = InstanceManager.open(identifier -> {}); + instanceManager.addDartCreatedInstance(object, 0); + instanceManager.close(); + + assertNull(instanceManager.getIdentifierForStrongReference(object)); + } + + @Test + public void addHostCreatedInstanceReturnsNegativeOneWhenClosed() { + final InstanceManager instanceManager = InstanceManager.open(identifier -> {}); + instanceManager.close(); + + assertEquals(instanceManager.addHostCreatedInstance(new Object()), -1L); + } + + @Test + public void getInstanceReturnsNullWhenClosed() { + final Object object = new Object(); + final InstanceManager instanceManager = InstanceManager.open(identifier -> {}); + instanceManager.addDartCreatedInstance(object, 0); + instanceManager.close(); + + assertNull(instanceManager.getInstance(0)); + } + + @Test + public void containsInstanceReturnsFalseWhenClosed() { + final Object object = new Object(); + final InstanceManager instanceManager = InstanceManager.open(identifier -> {}); + instanceManager.addDartCreatedInstance(object, 0); + instanceManager.close(); + + assertFalse(instanceManager.containsInstance(object)); + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java index 4bde211c6a4d..c9a5e64c0a3a 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/JavaScriptChannelTest.java @@ -6,8 +6,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import android.os.Handler; @@ -62,10 +60,5 @@ public void tearDown() { public void postMessage() { javaScriptChannel.postMessage("A message post."); verify(mockFlutterApi).postMessage(eq(javaScriptChannel), eq("A message post."), any()); - - reset(mockFlutterApi); - javaScriptChannel.release(); - javaScriptChannel.postMessage("a message"); - verify(mockFlutterApi, never()).postMessage((JavaScriptChannel) any(), any(), 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 03d48d17df91..e821537eda97 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 @@ -10,13 +10,11 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.net.Uri; import android.os.Message; -import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.webkit.WebView.WebViewTransport; @@ -50,21 +48,20 @@ public void setUp() { instanceManager = InstanceManager.open(identifier -> {}); instanceManager.addDartCreatedInstance(mockWebView, 0L); - instanceManager.addDartCreatedInstance(mockWebViewClient, 1L); final WebChromeClientCreator webChromeClientCreator = new WebChromeClientCreator() { @Override public WebChromeClientImpl createWebChromeClient( - WebChromeClientFlutterApiImpl flutterApi, WebViewClient webViewClient) { - webChromeClient = super.createWebChromeClient(flutterApi, webViewClient); + WebChromeClientFlutterApiImpl flutterApi) { + webChromeClient = super.createWebChromeClient(flutterApi); return webChromeClient; } }; hostApiImpl = new WebChromeClientHostApiImpl(instanceManager, webChromeClientCreator, mockFlutterApi); - hostApiImpl.create(2L, 1L); + hostApiImpl.create(2L); } @After @@ -76,11 +73,6 @@ public void tearDown() { public void onProgressChanged() { webChromeClient.onProgressChanged(mockWebView, 23); verify(mockFlutterApi).onProgressChanged(eq(webChromeClient), eq(mockWebView), eq(23L), any()); - - reset(mockFlutterApi); - webChromeClient.release(); - webChromeClient.onProgressChanged(mockWebView, 11); - verify(mockFlutterApi, never()).onProgressChanged((WebChromeClient) any(), any(), any(), any()); } @Test @@ -91,6 +83,7 @@ public void onCreateWindow() { final Message message = new Message(); message.obj = mock(WebViewTransport.class); + webChromeClient.setWebViewClient(mockWebViewClient); assertTrue(webChromeClient.onCreateWindow(mockWebView, message, mockOnCreateWindowWebView)); /// Capture the WebViewClient used with onCreateWindow WebView. diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java index 5d0cb701010e..3267291b2e99 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java @@ -8,8 +8,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -35,6 +33,8 @@ public class WebViewClientTest { @Mock public WebView mockWebView; + @Mock public WebViewClientCompatImpl mockWebViewClient; + InstanceManager instanceManager; WebViewClientHostApiImpl hostApiImpl; WebViewClientCompatImpl webViewClient; @@ -48,18 +48,15 @@ public void setUp() { final WebViewClientCreator webViewClientCreator = new WebViewClientCreator() { @Override - public WebViewClient createWebViewClient( - WebViewClientFlutterApiImpl flutterApi, boolean shouldOverrideUrlLoading) { - webViewClient = - (WebViewClientCompatImpl) - super.createWebViewClient(flutterApi, shouldOverrideUrlLoading); + public WebViewClient createWebViewClient(WebViewClientFlutterApiImpl flutterApi) { + webViewClient = (WebViewClientCompatImpl) super.createWebViewClient(flutterApi); return webViewClient; } }; hostApiImpl = new WebViewClientHostApiImpl(instanceManager, webViewClientCreator, mockFlutterApi); - hostApiImpl.create(1L, true); + hostApiImpl.create(1L); } @After @@ -72,11 +69,6 @@ public void onPageStarted() { webViewClient.onPageStarted(mockWebView, "https://www.google.com", null); verify(mockFlutterApi) .onPageStarted(eq(webViewClient), eq(mockWebView), eq("https://www.google.com"), any()); - - reset(mockFlutterApi); - webViewClient.release(); - webViewClient.onPageStarted(mockWebView, "", null); - verify(mockFlutterApi, never()).onPageStarted((WebViewClient) any(), any(), any(), any()); } @Test @@ -90,12 +82,6 @@ public void onReceivedError() { eq("description"), eq("https://www.google.com"), any()); - - reset(mockFlutterApi); - webViewClient.release(); - webViewClient.onReceivedError(mockWebView, 33, "", ""); - verify(mockFlutterApi, never()) - .onReceivedError((WebViewClient) any(), any(), any(), any(), any(), any()); } @Test @@ -103,11 +89,6 @@ public void urlLoading() { webViewClient.shouldOverrideUrlLoading(mockWebView, "https://www.google.com"); verify(mockFlutterApi) .urlLoading(eq(webViewClient), eq(mockWebView), eq("https://www.google.com"), any()); - - reset(mockFlutterApi); - webViewClient.release(); - webViewClient.shouldOverrideUrlLoading(mockWebView, ""); - verify(mockFlutterApi, never()).urlLoading((WebViewClient) any(), any(), any(), any()); } @Test @@ -125,4 +106,23 @@ public void convertWebResourceRequestWithNullHeaders() { WebViewClientFlutterApiImpl.createWebResourceRequestData(mockRequest); assertEquals(data.getRequestHeaders(), new HashMap()); } + + @Test + public void setReturnValueForShouldOverrideUrlLoading() { + final WebViewClientHostApiImpl webViewClientHostApi = + new WebViewClientHostApiImpl( + instanceManager, + new WebViewClientCreator() { + @Override + public WebViewClient createWebViewClient(WebViewClientFlutterApiImpl flutterApi) { + return mockWebViewClient; + } + }, + mockFlutterApi); + + instanceManager.addDartCreatedInstance(mockWebViewClient, 0); + webViewClientHostApi.setSynchronousReturnValueForShouldOverrideUrlLoading(0L, false); + + verify(mockWebViewClient).setReturnValueForShouldOverrideUrlLoading(false); + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewFlutterPluginTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewFlutterAndroidExternalApiTest.java similarity index 58% rename from packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewFlutterPluginTest.java rename to packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewFlutterAndroidExternalApiTest.java index 16dc6cf5de2b..0877dcaf2b06 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewFlutterPluginTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewFlutterAndroidExternalApiTest.java @@ -4,11 +4,16 @@ package io.flutter.plugins.webviewflutter; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.Context; +import android.webkit.WebView; +import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.PluginRegistry; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.platform.PlatformViewRegistry; import org.junit.Rule; @@ -17,7 +22,7 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; -public class WebViewFlutterPluginTest { +public class WebViewFlutterAndroidExternalApiTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock Context mockContext; @@ -29,7 +34,7 @@ public class WebViewFlutterPluginTest { @Mock FlutterPlugin.FlutterPluginBinding mockPluginBinding; @Test - public void getInstanceManagerAfterOnAttachedToEngine() { + public void getWebView() { final WebViewFlutterPlugin webViewFlutterPlugin = new WebViewFlutterPlugin(); when(mockPluginBinding.getApplicationContext()).thenReturn(mockContext); @@ -38,7 +43,19 @@ public void getInstanceManagerAfterOnAttachedToEngine() { webViewFlutterPlugin.onAttachedToEngine(mockPluginBinding); - assertNotNull(webViewFlutterPlugin.getInstanceManager()); + final InstanceManager instanceManager = webViewFlutterPlugin.getInstanceManager(); + assertNotNull(instanceManager); + + final WebView mockWebView = mock(WebView.class); + instanceManager.addDartCreatedInstance(mockWebView, 0); + + final PluginRegistry mockPluginRegistry = mock(PluginRegistry.class); + when(mockPluginRegistry.get(WebViewFlutterPlugin.class)).thenReturn(webViewFlutterPlugin); + + final FlutterEngine mockFlutterEngine = mock(FlutterEngine.class); + when(mockFlutterEngine.getPlugins()).thenReturn(mockPluginRegistry); + + assertEquals(WebViewFlutterAndroidExternalApi.getWebView(mockFlutterEngine, 0), mockWebView); webViewFlutterPlugin.onDetachedFromEngine(mockPluginBinding); } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java index 30bc256cd985..1721ccdce8e4 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java @@ -5,6 +5,8 @@ package io.flutter.plugins.webviewflutter; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -15,12 +17,10 @@ import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebViewClient; -import io.flutter.plugins.webviewflutter.DownloadListenerHostApiImpl.DownloadListenerImpl; -import io.flutter.plugins.webviewflutter.WebChromeClientHostApiImpl.WebChromeClientImpl; -import io.flutter.plugins.webviewflutter.WebViewClientHostApiImpl.WebViewClientImpl; -import io.flutter.plugins.webviewflutter.WebViewHostApiImpl.InputAwareWebViewPlatformView; +import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.webviewflutter.WebViewHostApiImpl.WebViewPlatformView; import java.util.HashMap; +import java.util.Objects; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -39,6 +39,8 @@ public class WebViewTest { @Mock Context mockContext; + @Mock BinaryMessenger mockBinaryMessenger; + InstanceManager testInstanceManager; WebViewHostApiImpl testHostApiImpl; @@ -46,9 +48,11 @@ public class WebViewTest { public void setUp() { testInstanceManager = InstanceManager.open(identifier -> {}); - when(mockWebViewProxy.createWebView(mockContext)).thenReturn(mockWebView); + when(mockWebViewProxy.createWebView(mockContext, mockBinaryMessenger, testInstanceManager)) + .thenReturn(mockWebView); testHostApiImpl = - new WebViewHostApiImpl(testInstanceManager, mockWebViewProxy, mockContext, null); + new WebViewHostApiImpl( + testInstanceManager, mockBinaryMessenger, mockWebViewProxy, mockContext, null); testHostApiImpl.create(0L, true); } @@ -57,112 +61,6 @@ public void tearDown() { testInstanceManager.close(); } - @Test - public void releaseWebView() { - final WebViewPlatformView webView = new WebViewPlatformView(mockContext); - - final WebViewClientImpl mockWebViewClient = mock(WebViewClientImpl.class); - final WebChromeClientImpl mockWebChromeClient = mock(WebChromeClientImpl.class); - final DownloadListenerImpl mockDownloadListener = mock(DownloadListenerImpl.class); - final JavaScriptChannel mockJavaScriptChannel = mock(JavaScriptChannel.class); - - webView.setWebViewClient(mockWebViewClient); - webView.setWebChromeClient(mockWebChromeClient); - webView.setDownloadListener(mockDownloadListener); - webView.addJavascriptInterface(mockJavaScriptChannel, "jchannel"); - - webView.release(); - - verify(mockWebViewClient).release(); - verify(mockWebChromeClient).release(); - verify(mockDownloadListener).release(); - verify(mockJavaScriptChannel).release(); - } - - @Test - public void releaseWebViewDependents() { - final WebViewPlatformView webView = new WebViewPlatformView(mockContext); - - final WebViewClientImpl mockWebViewClient = mock(WebViewClientImpl.class); - final WebChromeClientImpl mockWebChromeClient = mock(WebChromeClientImpl.class); - final DownloadListenerImpl mockDownloadListener = mock(DownloadListenerImpl.class); - final JavaScriptChannel mockJavaScriptChannel = mock(JavaScriptChannel.class); - final JavaScriptChannel mockJavaScriptChannel2 = mock(JavaScriptChannel.class); - - webView.setWebViewClient(mockWebViewClient); - webView.setWebChromeClient(mockWebChromeClient); - webView.setDownloadListener(mockDownloadListener); - webView.addJavascriptInterface(mockJavaScriptChannel, "jchannel"); - - // Release should be called on the object added above. - webView.addJavascriptInterface(mockJavaScriptChannel2, "jchannel"); - verify(mockJavaScriptChannel).release(); - - webView.setWebViewClient(null); - webView.setWebChromeClient(null); - webView.setDownloadListener(null); - webView.removeJavascriptInterface("jchannel"); - - verify(mockWebViewClient).release(); - verify(mockWebChromeClient).release(); - verify(mockDownloadListener).release(); - verify(mockJavaScriptChannel2).release(); - } - - @Test - public void releaseInputAwareWebView() { - final InputAwareWebViewPlatformView webView = - new InputAwareWebViewPlatformView(mockContext, null); - - final WebViewClientImpl mockWebViewClient = mock(WebViewClientImpl.class); - final WebChromeClientImpl mockWebChromeClient = mock(WebChromeClientImpl.class); - final DownloadListenerImpl mockDownloadListener = mock(DownloadListenerImpl.class); - final JavaScriptChannel mockJavaScriptChannel = mock(JavaScriptChannel.class); - - webView.setWebViewClient(mockWebViewClient); - webView.setWebChromeClient(mockWebChromeClient); - webView.setDownloadListener(mockDownloadListener); - webView.addJavascriptInterface(mockJavaScriptChannel, "jchannel"); - - webView.release(); - - verify(mockWebViewClient).release(); - verify(mockWebChromeClient).release(); - verify(mockDownloadListener).release(); - verify(mockJavaScriptChannel).release(); - } - - @Test - public void releaseInputAwareWebViewDependents() { - final InputAwareWebViewPlatformView webView = - new InputAwareWebViewPlatformView(mockContext, null); - - final WebViewClientImpl mockWebViewClient = mock(WebViewClientImpl.class); - final WebChromeClientImpl mockWebChromeClient = mock(WebChromeClientImpl.class); - final DownloadListenerImpl mockDownloadListener = mock(DownloadListenerImpl.class); - final JavaScriptChannel mockJavaScriptChannel = mock(JavaScriptChannel.class); - final JavaScriptChannel mockJavaScriptChannel2 = mock(JavaScriptChannel.class); - - webView.setWebViewClient(mockWebViewClient); - webView.setWebChromeClient(mockWebChromeClient); - webView.setDownloadListener(mockDownloadListener); - webView.addJavascriptInterface(mockJavaScriptChannel, "jchannel"); - - // Release should be called on the object added above. - webView.addJavascriptInterface(mockJavaScriptChannel2, "jchannel"); - verify(mockJavaScriptChannel).release(); - - webView.setWebViewClient(null); - webView.setWebChromeClient(null); - webView.setDownloadListener(null); - webView.removeJavascriptInterface("jchannel"); - - verify(mockWebViewClient).release(); - verify(mockWebChromeClient).release(); - verify(mockDownloadListener).release(); - verify(mockJavaScriptChannel2).release(); - } - @Test public void loadData() { testHostApiImpl.loadData( @@ -367,4 +265,53 @@ public void setWebChromeClient() { testHostApiImpl.setWebChromeClient(0L, 1L); verify(mockWebView).setWebChromeClient(mockWebChromeClient); } + + @Test + public void defaultWebChromeClientIsSecureWebChromeClient() { + final WebViewPlatformView webView = new WebViewPlatformView(mockContext, null, null); + assertTrue( + webView.getWebChromeClient() instanceof WebChromeClientHostApiImpl.SecureWebChromeClient); + assertFalse( + webView.getWebChromeClient() instanceof WebChromeClientHostApiImpl.WebChromeClientImpl); + } + + @Test + public void defaultWebChromeClientDoesNotAttemptToCommunicateWithDart() { + final WebViewPlatformView webView = new WebViewPlatformView(mockContext, null, null); + // This shouldn't throw an Exception. + Objects.requireNonNull(webView.getWebChromeClient()).onProgressChanged(webView, 0); + } + + @Test + public void disposeDoesNotCallDestroy() { + final boolean[] destroyCalled = {false}; + final WebViewPlatformView webView = + new WebViewPlatformView(mockContext, null, null) { + @Override + public void destroy() { + destroyCalled[0] = true; + } + }; + webView.dispose(); + + assertFalse(destroyCalled[0]); + } + + @Test + public void destroyWebViewWhenDisposedFromJavaObjectHostApi() { + final boolean[] destroyCalled = {false}; + final WebViewPlatformView webView = + new WebViewPlatformView(mockContext, null, null) { + @Override + public void destroy() { + destroyCalled[0] = true; + } + }; + + testInstanceManager.addDartCreatedInstance(webView, 0); + final JavaObjectHostApiImpl javaObjectHostApi = new JavaObjectHostApiImpl(testInstanceManager); + javaObjectHostApi.dispose(0L); + + assertTrue(destroyCalled[0]); + } } diff --git a/packages/webview_flutter/webview_flutter_android/example/android/gradle.properties b/packages/webview_flutter/webview_flutter_android/example/android/gradle.properties index 94adc3a3f97a..598d13fee446 100644 --- a/packages/webview_flutter/webview_flutter_android/example/android/gradle.properties +++ b/packages/webview_flutter/webview_flutter_android/example/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/packages/webview_flutter/webview_flutter_android/example/integration_test/legacy/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_android/example/integration_test/legacy/webview_flutter_test.dart new file mode 100644 index 000000000000..cbec6b767952 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/integration_test/legacy/webview_flutter_test.dart @@ -0,0 +1,1574 @@ +// 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. + +// This test is run using `flutter drive` by the CI (see /script/tool/README.md +// in this repository for details on driving that tooling manually), but can +// also be run using `flutter test` directly during development. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231) +// ignore: unnecessary_import +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:webview_flutter_android/src/instance_manager.dart'; +import 'package:webview_flutter_android/src/weak_reference_utils.dart'; +import 'package:webview_flutter_android/src/webview_flutter_android_legacy.dart'; +import 'package:webview_flutter_android_example/legacy/navigation_decision.dart'; +import 'package:webview_flutter_android_example/legacy/navigation_request.dart'; +import 'package:webview_flutter_android_example/legacy/web_view.dart'; +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; + +Future main() async { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + final HttpServer server = await HttpServer.bind(InternetAddress.anyIPv4, 0); + server.forEach((HttpRequest request) { + if (request.uri.path == '/hello.txt') { + request.response.writeln('Hello, world.'); + } else if (request.uri.path == '/secondary.txt') { + request.response.writeln('How are you today?'); + } else if (request.uri.path == '/headers') { + request.response.writeln('${request.headers}'); + } else if (request.uri.path == '/favicon.ico') { + request.response.statusCode = HttpStatus.notFound; + } else { + fail('unexpected request: ${request.method} ${request.uri}'); + } + request.response.close(); + }); + final String prefixUrl = 'http://${server.address.address}:${server.port}'; + final String primaryUrl = '$prefixUrl/hello.txt'; + final String secondaryUrl = '$prefixUrl/secondary.txt'; + final String headersUrl = '$prefixUrl/headers'; + + testWidgets('initialUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageFinishedCompleter = Completer(); + await tester.pumpWidget( + MaterialApp( + home: Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: pageFinishedCompleter.complete, + ), + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageFinishedCompleter.future; + + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }); + + testWidgets('loadUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = StreamController(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoads.add(url); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + + await controller.loadUrl(secondaryUrl); + await expectLater( + pageLoads.stream.firstWhere((String url) => url == secondaryUrl), + completion(secondaryUrl), + ); + }); + + testWidgets( + 'withWeakRefenceTo allows encapsulating class to be garbage collected', + (WidgetTester tester) async { + final Completer gcCompleter = Completer(); + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: gcCompleter.complete, + ); + + ClassWithCallbackClass? instance = ClassWithCallbackClass(); + instanceManager.addHostCreatedInstance(instance.callbackClass, 0); + instance = null; + + // Force garbage collection. + await IntegrationTestWidgetsFlutterBinding.instance + .watchPerformance(() async { + await tester.pumpAndSettle(); + }); + + final int gcIdentifier = await gcCompleter.future; + expect(gcIdentifier, 0); + }, timeout: const Timeout(Duration(seconds: 10))); + + testWidgets('evaluateJavascript', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String result = await controller.evaluateJavascript('1 + 1'); + expect(result, equals('2')); + }); + + testWidgets('loadUrl with headers', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageStarts = StreamController(); + final StreamController pageLoads = StreamController(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarts.add(url); + }, + onPageFinished: (String url) { + pageLoads.add(url); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final Map headers = { + 'test_header': 'flutter_test_header' + }; + await controller.loadUrl(headersUrl, headers: headers); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, headersUrl); + + await pageStarts.stream.firstWhere((String url) => url == currentUrl); + await pageLoads.stream.firstWhere((String url) => url == currentUrl); + + final String content = await controller + .runJavascriptReturningResult('document.documentElement.innerText'); + expect(content.contains('flutter_test_header'), isTrue); + }); + + testWidgets('JavascriptChannel', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final Completer channelCompleter = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + // This is the data URL for: '' + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'Echo', + onMessageReceived: (JavascriptMessage message) { + channelCompleter.complete(message.message); + }, + ), + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + expect(channelCompleter.isCompleted, isFalse); + await controller.runJavascript('Echo.postMessage("hello");'); + + await expectLater(channelCompleter.future, completion('hello')); + }); + + testWidgets('resize webview', (WidgetTester tester) async { + final Completer initialResizeCompleter = Completer(); + final Completer buttonTapResizeCompleter = Completer(); + final Completer onPageFinished = Completer(); + + bool resizeButtonTapped = false; + await tester.pumpWidget(ResizableWebView( + onResize: (_) { + if (resizeButtonTapped) { + buttonTapResizeCompleter.complete(); + } else { + initialResizeCompleter.complete(); + } + }, + onPageFinished: () => onPageFinished.complete(), + )); + await onPageFinished.future; + // Wait for a potential call to resize after page is loaded. + await initialResizeCompleter.future.timeout( + const Duration(seconds: 3), + onTimeout: () => null, + ); + + resizeButtonTapped = true; + await tester.tap(find.byKey(const ValueKey('resizeButton'))); + await tester.pumpAndSettle(); + expect(buttonTapResizeCompleter.future, completes); + }); + + testWidgets('set custom userAgent', (WidgetTester tester) async { + final Completer controllerCompleter1 = + Completer(); + final GlobalKey globalKey = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent1', + onWebViewCreated: (WebViewController controller) { + controllerCompleter1.complete(controller); + }, + ), + ), + ); + final WebViewController controller1 = await controllerCompleter1.future; + final String customUserAgent1 = await _getUserAgent(controller1); + expect(customUserAgent1, 'Custom_User_Agent1'); + // rebuild the WebView with a different user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent2', + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller1); + expect(customUserAgent2, 'Custom_User_Agent2'); + }); + + testWidgets('use default platform userAgent after webView is rebuilt', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final GlobalKey globalKey = GlobalKey(); + // Build the webView with no user agent to get the default platform user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: globalKey, + initialUrl: primaryUrl, + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String defaultPlatformUserAgent = await _getUserAgent(controller); + // rebuild the WebView with a custom user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent', + ), + ), + ); + final String customUserAgent = await _getUserAgent(controller); + expect(customUserAgent, 'Custom_User_Agent'); + // rebuilds the WebView with no user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller); + expect(customUserAgent2, defaultPlatformUserAgent); + }); + + group('Video playback policy', () { + late String videoTestBase64; + setUpAll(() async { + final ByteData videoData = + await rootBundle.load('assets/sample_video.mp4'); + final String base64VideoData = + base64Encode(Uint8List.view(videoData.buffer)); + final String videoTest = ''' + + Video auto play + + + + + + + '''; + videoTestBase64 = base64Encode(const Utf8Encoder().convert(videoTest)); + }); + + testWidgets('Auto media playback', (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(false)); + + controllerCompleter = Completer(); + pageLoaded = Completer(); + + // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + controller = await controllerCompleter.future; + await pageLoaded.future; + + isPaused = await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(true)); + }); + + testWidgets('Changes to initialMediaPlaybackPolicy are ignored', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(false)); + + pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + await controller.reload(); + + await pageLoaded.future; + + isPaused = await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(false)); + }); + + testWidgets('Video plays inline when allowsInlineMediaPlayback is true', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageLoaded = Completer(); + final Completer videoPlaying = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'VideoTestTime', + onMessageReceived: (JavascriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1 && !videoPlaying.isCompleted) { + videoPlaying.complete(null); + } + }, + ), + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + allowsInlineMediaPlayback: true, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + // Pump once to trigger the video play. + await tester.pump(); + + // Makes sure we get the correct event that indicates the video is actually playing. + await videoPlaying.future; + + final String fullScreen = + await controller.runJavascriptReturningResult('isFullScreen();'); + expect(fullScreen, _webviewBool(false)); + }); + }); + + group('Audio playback policy', () { + late String audioTestBase64; + setUpAll(() async { + final ByteData audioData = + await rootBundle.load('assets/sample_audio.ogg'); + final String base64AudioData = + base64Encode(Uint8List.view(audioData.buffer)); + final String audioTest = ''' + + Audio auto play + + + + + + + '''; + audioTestBase64 = base64Encode(const Utf8Encoder().convert(audioTest)); + }); + + testWidgets('Auto media playback', (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageStarted = Completer(); + Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(false)); + + controllerCompleter = Completer(); + pageStarted = Completer(); + pageLoaded = Completer(); + + // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + isPaused = await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(true)); + }); + + testWidgets('Changes to initialMediaPlaybackPolicy are ignored', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageStarted = Completer(); + Completer pageLoaded = Completer(); + + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(false)); + + pageStarted = Completer(); + pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + await controller.reload(); + + await pageStarted.future; + await pageLoaded.future; + + isPaused = await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(false)); + }); + }); + + testWidgets('getTitle', (WidgetTester tester) async { + const String getTitleTest = ''' + + Some title + + + + + '''; + final String getTitleTestBase64 = + base64Encode(const Utf8Encoder().convert(getTitleTest)); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + final String? title = await controller.getTitle(); + expect(title, 'Some title'); + }); + + group('Programmatic Scroll', () { + testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { + const String scrollTestPage = ''' + + + + + + +

+ + + '''; + + final String scrollTestPageBase64 = + base64Encode(const Utf8Encoder().convert(scrollTestPage)); + + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + int scrollPosX = await controller.getScrollX(); + int scrollPosY = await controller.getScrollY(); + + // Check scrollTo() + const int X_SCROLL = 123; + const int Y_SCROLL = 321; + // Get the initial position; this ensures that scrollTo is actually + // changing something, but also gives the native view's scroll position + // time to settle. + expect(scrollPosX, isNot(X_SCROLL)); + expect(scrollPosX, isNot(Y_SCROLL)); + + await controller.scrollTo(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL); + expect(scrollPosY, Y_SCROLL); + + // Check scrollBy() (on top of scrollTo()) + await controller.scrollBy(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL * 2); + expect(scrollPosY, Y_SCROLL * 2); + }); + }); + + group('SurfaceAndroidWebView', () { + setUpAll(() { + WebView.platform = SurfaceAndroidWebView(); + }); + + tearDownAll(() { + WebView.platform = AndroidWebView(); + }); + + testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { + const String scrollTestPage = ''' + + + + + + +
+ + + '''; + + final String scrollTestPageBase64 = + base64Encode(const Utf8Encoder().convert(scrollTestPage)); + + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + // Check scrollTo() + const int X_SCROLL = 123; + const int Y_SCROLL = 321; + + await controller.scrollTo(X_SCROLL, Y_SCROLL); + int scrollPosX = await controller.getScrollX(); + int scrollPosY = await controller.getScrollY(); + expect(X_SCROLL, scrollPosX); + expect(Y_SCROLL, scrollPosY); + + // Check scrollBy() (on top of scrollTo()) + await controller.scrollBy(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(X_SCROLL * 2, scrollPosX); + expect(Y_SCROLL * 2, scrollPosY); + }); + + testWidgets('inputs are scrolled into view when focused', + (WidgetTester tester) async { + const String scrollTestPage = ''' + + + + + + +
+ + + + '''; + + final String scrollTestPageBase64 = + base64Encode(const Utf8Encoder().convert(scrollTestPage)); + + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.runAsync(() async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: SizedBox( + width: 200, + height: 200, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ), + ); + await Future.delayed(const Duration(milliseconds: 20)); + await tester.pump(); + }); + + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + final String viewportRectJSON = await _runJavaScriptReturningResult( + controller, 'JSON.stringify(viewport.getBoundingClientRect())'); + final Map viewportRectRelativeToViewport = + jsonDecode(viewportRectJSON) as Map; + + num getDomRectComponent( + Map rectAsJson, String component) { + return rectAsJson[component]! as num; + } + + // Check that the input is originally outside of the viewport. + + final String initialInputClientRectJSON = + await _runJavaScriptReturningResult( + controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); + final Map initialInputClientRectRelativeToViewport = + jsonDecode(initialInputClientRectJSON) as Map; + + expect( + getDomRectComponent( + initialInputClientRectRelativeToViewport, 'bottom') <= + getDomRectComponent(viewportRectRelativeToViewport, 'bottom'), + isFalse); + + await controller.runJavascript('inputEl.focus()'); + + // Check that focusing the input brought it into view. + + final String lastInputClientRectJSON = + await _runJavaScriptReturningResult( + controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); + final Map lastInputClientRectRelativeToViewport = + jsonDecode(lastInputClientRectJSON) as Map; + + expect( + getDomRectComponent(lastInputClientRectRelativeToViewport, 'top') >= + getDomRectComponent(viewportRectRelativeToViewport, 'top'), + isTrue); + expect( + getDomRectComponent( + lastInputClientRectRelativeToViewport, 'bottom') <= + getDomRectComponent(viewportRectRelativeToViewport, 'bottom'), + isTrue); + + expect( + getDomRectComponent(lastInputClientRectRelativeToViewport, 'left') >= + getDomRectComponent(viewportRectRelativeToViewport, 'left'), + isTrue); + expect( + getDomRectComponent(lastInputClientRectRelativeToViewport, 'right') <= + getDomRectComponent(viewportRectRelativeToViewport, 'right'), + isTrue); + }); + }); + + group('NavigationDelegate', () { + const String blankPage = ''; + final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + '${base64Encode(const Utf8Encoder().convert(blankPage))}'; + + testWidgets('can allow requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller.runJavascript('location.href = "$secondaryUrl"'); + + await pageLoads.stream.first; // Wait for the next page load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }); + + testWidgets('onWebResourceError', (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://www.notawebsite..com', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + ), + ), + ); + + final WebResourceError error = await errorCompleter.future; + expect(error, isNotNull); + + expect(error.errorType, isNotNull); + expect( + error.failingUrl?.startsWith('https://www.notawebsite..com'), isTrue); + }); + + testWidgets('onWebResourceError is not called with valid url', + (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }); + + testWidgets( + 'onWebResourceError only called for main frame', + (WidgetTester tester) async { + const String iframeTest = ''' + + + + WebResourceError test + + + + + + '''; + final String iframeTestBase64 = + base64Encode(const Utf8Encoder().convert(iframeTest)); + + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,$iframeTestBase64', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }, + ); + + testWidgets('can block requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .runJavascript('location.href = "https://www.youtube.com/"'); + + // There should never be any second page load, since our new URL is + // blocked. Still wait for a potential page change for some time in order + // to give the test a chance to fail. + await pageLoads.stream.first + .timeout(const Duration(milliseconds: 500), onTimeout: () => ''); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, isNot(contains('youtube.com'))); + }); + + testWidgets('supports asynchronous decisions', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) async { + NavigationDecision decision = NavigationDecision.prevent; + decision = await Future.delayed( + const Duration(milliseconds: 10), + () => NavigationDecision.navigate); + return decision; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller.runJavascript('location.href = "$secondaryUrl"'); + + await pageLoads.stream.first; // Wait for second page to load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }); + }); + + testWidgets('launches with gestureNavigationEnabled on iOS', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: SizedBox( + width: 400, + height: 300, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + gestureNavigationEnabled: true, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }); + + testWidgets('target _blank opens in same window', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageLoaded = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller.runJavascript('window.open("$primaryUrl", "_blank")'); + await pageLoaded.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }); + + testWidgets( + 'can open new window and go back', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(); + }, + initialUrl: primaryUrl, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + expect(controller.currentUrl(), completion(primaryUrl)); + await pageLoaded.future; + pageLoaded = Completer(); + + await controller.runJavascript('window.open("$secondaryUrl")'); + await pageLoaded.future; + pageLoaded = Completer(); + expect(controller.currentUrl(), completion(secondaryUrl)); + + expect(controller.canGoBack(), completion(true)); + await controller.goBack(); + await pageLoaded.future; + await expectLater(controller.currentUrl(), completion(primaryUrl)); + }, + ); + + testWidgets( + 'JavaScript does not run in parent window', + (WidgetTester tester) async { + const String iframe = ''' + + + '''; + final String iframeTestBase64 = + base64Encode(const Utf8Encoder().convert(iframe)); + + final String openWindowTest = ''' + + + + XSS test + + + + + + '''; + final String openWindowTestBase64 = + base64Encode(const Utf8Encoder().convert(openWindowTest)); + final Completer controllerCompleter = + Completer(); + final Completer pageLoadCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + initialUrl: + 'data:text/html;charset=utf-8;base64,$openWindowTestBase64', + onPageFinished: (String url) { + pageLoadCompleter.complete(); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageLoadCompleter.future; + + final String iframeLoaded = + await controller.runJavascriptReturningResult('iframeLoaded'); + expect(iframeLoaded, 'true'); + + final String elementText = await controller.runJavascriptReturningResult( + 'document.querySelector("p") && document.querySelector("p").textContent', + ); + expect(elementText, 'null'); + }, + ); + + testWidgets( + 'clearCache should clear local storage', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + + Completer pageLoadCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (_) => pageLoadCompleter.complete(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + + await pageLoadCompleter.future; + pageLoadCompleter = Completer(); + + final WebViewController controller = await controllerCompleter.future; + await controller.runJavascript('localStorage.setItem("myCat", "Tom");'); + final String myCatItem = await controller.runJavascriptReturningResult( + 'localStorage.getItem("myCat");', + ); + expect(myCatItem, '"Tom"'); + + await controller.clearCache(); + await pageLoadCompleter.future; + + final String nullItem = await controller.runJavascriptReturningResult( + 'localStorage.getItem("myCat");', + ); + expect(nullItem, 'null'); + }, + ); +} + +// JavaScript booleans evaluate to different string values on Android and iOS. +// This utility method returns the string boolean value of the current platform. +String _webviewBool(bool value) { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return value ? '1' : '0'; + } + return value ? 'true' : 'false'; +} + +/// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. +Future _getUserAgent(WebViewController controller) async { + return _runJavaScriptReturningResult(controller, 'navigator.userAgent;'); +} + +Future _runJavaScriptReturningResult( + WebViewController controller, + String js, +) async { + return jsonDecode(await controller.runJavascriptReturningResult(js)) + as String; +} + +class ResizableWebView extends StatefulWidget { + const ResizableWebView({ + Key? key, + required this.onResize, + required this.onPageFinished, + }) : super(key: key); + + final JavascriptMessageHandler onResize; + final VoidCallback onPageFinished; + + @override + State createState() => ResizableWebViewState(); +} + +class ResizableWebViewState extends State { + double webViewWidth = 200; + double webViewHeight = 200; + + static const String resizePage = ''' + + Resize test + + + + + + '''; + + @override + Widget build(BuildContext context) { + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizePage)); + return Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: webViewWidth, + height: webViewHeight, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + javascriptChannels: { + JavascriptChannel( + name: 'Resize', + onMessageReceived: widget.onResize, + ), + }, + onPageFinished: (_) => widget.onPageFinished(), + javascriptMode: JavascriptMode.unrestricted, + ), + ), + TextButton( + key: const Key('resizeButton'), + onPressed: () { + setState(() { + webViewWidth += 100.0; + webViewHeight += 100.0; + }); + }, + child: const Text('ResizeButton'), + ), + ], + ), + ); + } +} + +class CopyableObjectWithCallback with Copyable { + CopyableObjectWithCallback(this.callback); + + final VoidCallback callback; + + @override + CopyableObjectWithCallback copy() { + return CopyableObjectWithCallback(callback); + } +} + +class ClassWithCallbackClass { + ClassWithCallbackClass() { + callbackClass = CopyableObjectWithCallback( + withWeakReferenceTo( + this, + (WeakReference weakReference) { + return () { + // Weak reference to `this` in callback. + // ignore: unnecessary_statements + weakReference; + }; + }, + ), + ); + } + + late final CopyableObjectWithCallback callbackClass; +} diff --git a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart index 69c1a46d750f..af144e55efba 100644 --- a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart @@ -13,16 +13,15 @@ import 'dart:io'; // ignore: unnecessary_import import 'dart:typed_data'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:webview_flutter_android/webview_android.dart'; -import 'package:webview_flutter_android/webview_surface_android.dart'; -import 'package:webview_flutter_android_example/navigation_decision.dart'; -import 'package:webview_flutter_android_example/navigation_request.dart'; -import 'package:webview_flutter_android_example/web_view.dart'; +import 'package:webview_flutter_android/src/android_webview.dart' as android; +import 'package:webview_flutter_android/src/android_webview_api_impls.dart'; +import 'package:webview_flutter_android/src/instance_manager.dart'; +import 'package:webview_flutter_android/src/weak_reference_utils.dart'; +import 'package:webview_flutter_android/webview_flutter_android.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; Future main() async { @@ -48,164 +47,234 @@ Future main() async { final String secondaryUrl = '$prefixUrl/secondary.txt'; final String headersUrl = '$prefixUrl/headers'; - testWidgets('initialUrl', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final Completer pageFinishedCompleter = Completer(); - await tester.pumpWidget( - MaterialApp( - home: Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageFinished: pageFinishedCompleter.complete, - ), - ), - ), - ); + testWidgets('loadRequest', (WidgetTester tester) async { + final Completer pageFinished = Completer(); + + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageFinished.complete()), + ) + ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); - final WebViewController controller = await controllerCompleter.future; - await pageFinishedCompleter.future; + await pageFinished.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); }); - testWidgets('loadUrl', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageLoads = StreamController(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageFinished: (String url) { - pageLoads.add(url); - }, - ), - ), + testWidgets( + 'withWeakRefenceTo allows encapsulating class to be garbage collected', + (WidgetTester tester) async { + final Completer gcCompleter = Completer(); + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: gcCompleter.complete, ); - final WebViewController controller = await controllerCompleter.future; - await controller.loadUrl(secondaryUrl); - await expectLater( - pageLoads.stream.firstWhere((String url) => url == secondaryUrl), - completion(secondaryUrl), - ); - }); + ClassWithCallbackClass? instance = ClassWithCallbackClass(); + instanceManager.addHostCreatedInstance(instance.callbackClass, 0); + instance = null; + + // Force garbage collection. + await IntegrationTestWidgetsFlutterBinding.instance + .watchPerformance(() async { + await tester.pumpAndSettle(); + }); - testWidgets('evaluateJavascript', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); + final int gcIdentifier = await gcCompleter.future; + expect(gcIdentifier, 0); + }, timeout: const Timeout(Duration(seconds: 10))); + + testWidgets( + 'WebView is released by garbage collection', + (WidgetTester tester) async { + final Completer webViewGCCompleter = Completer(); + + late final InstanceManager instanceManager; + instanceManager = + InstanceManager(onWeakReferenceRemoved: (int identifier) { + final Copyable instance = + instanceManager.getInstanceWithWeakReference(identifier)!; + if (instance is android.WebView && !webViewGCCompleter.isCompleted) { + webViewGCCompleter.complete(); + } + }); + + android.WebView.api = WebViewHostApiImpl( + instanceManager: instanceManager, + ); + android.WebSettings.api = WebSettingsHostApiImpl( + instanceManager: instanceManager, + ); + android.WebChromeClient.api = WebChromeClientHostApiImpl( + instanceManager: instanceManager, + ); + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + AndroidWebViewWidgetCreationParams( + instanceManager: instanceManager, + controller: PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ), + ), + ).build(context); }, - javascriptMode: JavascriptMode.unrestricted, ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - final String result = await controller.evaluateJavascript('1 + 1'); - expect(result, equals('2')); - }); + ); + await tester.pumpAndSettle(); - testWidgets('loadUrl with headers', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageStarts = StreamController(); - final StreamController pageLoads = StreamController(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (String url) { - pageStarts.add(url); - }, - onPageFinished: (String url) { - pageLoads.add(url); + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + AndroidWebViewWidgetCreationParams( + instanceManager: instanceManager, + controller: PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ), + ), + ).build(context); }, ), - ), + ); + await tester.pumpAndSettle(); + + // Force garbage collection. + await IntegrationTestWidgetsFlutterBinding.instance + .watchPerformance(() async { + await tester.pumpAndSettle(); + }); + + await tester.pumpAndSettle(); + await expectLater(webViewGCCompleter.future, completes); + + android.WebView.api = WebViewHostApiImpl(); + android.WebSettings.api = WebSettingsHostApiImpl(); + android.WebChromeClient.api = WebChromeClientHostApiImpl(); + }, + timeout: const Timeout(Duration(seconds: 10)), + ); + + testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async { + final Completer pageFinished = Completer(); + + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageFinished.complete()), + ) + ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await pageFinished.future; + + await expectLater( + controller.runJavaScriptReturningResult('1 + 1'), + completion(2), ); - final WebViewController controller = await controllerCompleter.future; + }); + + testWidgets('loadRequest with headers', (WidgetTester tester) async { final Map headers = { 'test_header': 'flutter_test_header' }; - await controller.loadUrl(headersUrl, headers: headers); - final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, headersUrl); - await pageStarts.stream.firstWhere((String url) => url == currentUrl); - await pageLoads.stream.firstWhere((String url) => url == currentUrl); + final StreamController pageLoads = StreamController(); - final String content = await controller - .runJavascriptReturningResult('document.documentElement.innerText'); + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((String url) => pageLoads.add(url)), + ) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse(headersUrl), + headers: headers, + ), + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await pageLoads.stream.firstWhere((String url) => url == headersUrl); + + final String content = await controller.runJavaScriptReturningResult( + 'document.documentElement.innerText', + ) as String; expect(content.contains('flutter_test_header'), isTrue); }); testWidgets('JavascriptChannel', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final Completer pageStarted = Completer(); - final Completer pageLoaded = Completer(); + final Completer pageFinished = Completer(); + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageFinished.complete()), + ); + final Completer channelCompleter = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - // This is the data URL for: '' - initialUrl: - 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - javascriptChannels: { - JavascriptChannel( - name: 'Echo', - onMessageReceived: (JavascriptMessage message) { - channelCompleter.complete(message.message); - }, - ), - }, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), + await controller.addJavaScriptChannel( + JavaScriptChannelParams( + name: 'Echo', + onMessageReceived: (JavaScriptMessage message) { + channelCompleter.complete(message.message); + }, ), ); - final WebViewController controller = await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - expect(channelCompleter.isCompleted, isFalse); - await controller.runJavascript('Echo.postMessage("hello");'); + await controller.loadHtmlString( + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await pageFinished.future; + await controller.runJavaScript('Echo.postMessage("hello");'); await expectLater(channelCompleter.future, completion('hello')); }); @@ -216,7 +285,7 @@ Future main() async { bool resizeButtonTapped = false; await tester.pumpWidget(ResizableWebView( - onResize: (_) { + onResize: () { if (resizeButtonTapped) { buttonTapResizeCompleter.complete(); } else { @@ -225,6 +294,7 @@ Future main() async { }, onPageFinished: () => onPageFinished.complete(), )); + await onPageFinished.future; // Wait for a potential call to resize after page is loaded. await initialResizeCompleter.future.timeout( @@ -233,98 +303,40 @@ Future main() async { ); resizeButtonTapped = true; + await tester.tap(find.byKey(const ValueKey('resizeButton'))); await tester.pumpAndSettle(); - expect(buttonTapResizeCompleter.future, completes); + + await expectLater(buttonTapResizeCompleter.future, completes); }); testWidgets('set custom userAgent', (WidgetTester tester) async { - final Completer controllerCompleter1 = - Completer(); - final GlobalKey globalKey = GlobalKey(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: globalKey, - initialUrl: 'about:blank', - javascriptMode: JavascriptMode.unrestricted, - userAgent: 'Custom_User_Agent1', - onWebViewCreated: (WebViewController controller) { - controllerCompleter1.complete(controller); - }, - ), - ), - ); - final WebViewController controller1 = await controllerCompleter1.future; - final String customUserAgent1 = await _getUserAgent(controller1); - expect(customUserAgent1, 'Custom_User_Agent1'); - // rebuild the WebView with a different user agent. - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: globalKey, - initialUrl: 'about:blank', - javascriptMode: JavascriptMode.unrestricted, - userAgent: 'Custom_User_Agent2', - ), - ), - ); + final Completer pageFinished = Completer(); + + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageFinished.complete()), + ) + ..setUserAgent('Custom_User_Agent1') + ..loadRequest(LoadRequestParams(uri: Uri.parse('about:blank'))); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); - final String customUserAgent2 = await _getUserAgent(controller1); - expect(customUserAgent2, 'Custom_User_Agent2'); - }); + await pageFinished.future; - testWidgets('use default platform userAgent after webView is rebuilt', - (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final GlobalKey globalKey = GlobalKey(); - // Build the webView with no user agent to get the default platform user agent. - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: globalKey, - initialUrl: primaryUrl, - javascriptMode: JavascriptMode.unrestricted, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - final String defaultPlatformUserAgent = await _getUserAgent(controller); - // rebuild the WebView with a custom user agent. - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: globalKey, - initialUrl: 'about:blank', - javascriptMode: JavascriptMode.unrestricted, - userAgent: 'Custom_User_Agent', - ), - ), - ); final String customUserAgent = await _getUserAgent(controller); - expect(customUserAgent, 'Custom_User_Agent'); - // rebuilds the WebView with no user agent. - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: globalKey, - initialUrl: 'about:blank', - javascriptMode: JavascriptMode.unrestricted, - ), - ), - ); - - final String customUserAgent2 = await _getUserAgent(controller); - expect(customUserAgent2, defaultPlatformUserAgent); + expect(customUserAgent, 'Custom_User_Agent1'); }); group('Video playback policy', () { @@ -335,201 +347,156 @@ Future main() async { final String base64VideoData = base64Encode(Uint8List.view(videoData.buffer)); final String videoTest = ''' - - Video auto play - - - - - - - '''; + + Video auto play + + + + + + + '''; videoTestBase64 = base64Encode(const Utf8Encoder().convert(videoTest)); }); testWidgets('Auto media playback', (WidgetTester tester) async { - Completer controllerCompleter = - Completer(); Completer pageLoaded = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - ), - ), - ); - WebViewController controller = await controllerCompleter.future; - await pageLoaded.future; - - String isPaused = - await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(false)); - - controllerCompleter = Completer(); - pageLoaded = Completer(); - - // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, + PlatformWebViewController controller = AndroidWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + AndroidNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoaded.complete()), + ) + ..setMediaPlaybackRequiresUserGesture(false) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,$videoTestBase64', + ), ), - ), - ); - - controller = await controllerCompleter.future; - await pageLoaded.future; - - isPaused = await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(true)); - }); + ); - testWidgets('Changes to initialMediaPlaybackPolicy are ignored', - (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - Completer pageLoaded = Completer(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); - final GlobalKey key = GlobalKey(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = - await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(false)); + bool isPaused = + await controller.runJavaScriptReturningResult('isPaused();') as bool; + expect(isPaused, false); pageLoaded = Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, + controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoaded.complete()), + ) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,$videoTestBase64', + ), ), - ), - ); + ); - await controller.reload(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoaded.future; - isPaused = await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(false)); + isPaused = + await controller.runJavaScriptReturningResult('isPaused();') as bool; + expect(isPaused, true); }); - testWidgets('Video plays inline when allowsInlineMediaPlayback is true', - (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); + testWidgets('Video plays inline', (WidgetTester tester) async { final Completer pageLoaded = Completer(); final Completer videoPlaying = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - javascriptChannels: { - JavascriptChannel( - name: 'VideoTestTime', - onMessageReceived: (JavascriptMessage message) { - final double currentTime = double.parse(message.message); - // Let it play for at least 1 second to make sure the related video's properties are set. - if (currentTime > 1 && !videoPlaying.isCompleted) { - videoPlaying.complete(null); - } - }, - ), - }, - onPageFinished: (String url) { - pageLoaded.complete(null); + final PlatformWebViewController controller = AndroidWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + AndroidNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoaded.complete()), + ) + ..addJavaScriptChannel( + JavaScriptChannelParams( + name: 'VideoTestTime', + onMessageReceived: (JavaScriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1 && !videoPlaying.isCompleted) { + videoPlaying.complete(null); + } }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - allowsInlineMediaPlayback: true, ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - await pageLoaded.future; + ) + ..setMediaPlaybackRequiresUserGesture(false) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,$videoTestBase64', + ), + ), + ); - // Pump once to trigger the video play. - await tester.pump(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await pageLoaded.future; // Makes sure we get the correct event that indicates the video is actually playing. await videoPlaying.future; - final String fullScreen = - await controller.runJavascriptReturningResult('isFullScreen();'); - expect(fullScreen, _webviewBool(false)); + final bool fullScreen = await controller + .runJavaScriptReturningResult('isFullScreen();') as bool; + expect(fullScreen, false); }); }); @@ -565,138 +532,71 @@ Future main() async { }); testWidgets('Auto media playback', (WidgetTester tester) async { - Completer controllerCompleter = - Completer(); - Completer pageStarted = Completer(); Completer pageLoaded = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + PlatformWebViewController controller = AndroidWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + AndroidNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoaded.complete()), + ) + ..setMediaPlaybackRequiresUserGesture(false) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,$audioTestBase64', + ), ), - ), - ); - WebViewController controller = await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - - String isPaused = - await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(false)); - - controllerCompleter = Completer(); - pageStarted = Completer(); - pageLoaded = Completer(); + ); - // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); - controller = await controllerCompleter.future; - await pageStarted.future; await pageLoaded.future; - isPaused = await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(true)); - }); + bool isPaused = + await controller.runJavaScriptReturningResult('isPaused();') as bool; + expect(isPaused, false); - testWidgets('Changes to initialMediaPlaybackPolicy are ignored', - (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - Completer pageStarted = Completer(); - Completer pageLoaded = Completer(); - - final GlobalKey key = GlobalKey(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - - String isPaused = - await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(false)); - - pageStarted = Completer(); pageLoaded = Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, + controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoaded.complete()), + ) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,$audioTestBase64', + ), ), - ), - ); + ); - await controller.reload(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); - await pageStarted.future; await pageLoaded.future; - isPaused = await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(false)); + isPaused = + await controller.runJavaScriptReturningResult('isPaused();') as bool; + expect(isPaused, true); }); }); @@ -711,33 +611,41 @@ Future main() async { '''; final String getTitleTestBase64 = base64Encode(const Utf8Encoder().convert(getTitleTest)); - final Completer pageStarted = Completer(); final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, + + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoaded.complete()), + ) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', + ), ), - ), - ); + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); - final WebViewController controller = await controllerCompleter.future; - await pageStarted.future; await pageLoaded.future; + // On at least iOS, it does not appear to be guaranteed that the native + // code has the title when the page load completes. Execute some JavaScript + // before checking the title to ensure that the page has been fully parsed + // and processed. + await controller.runJavaScript('1;'); + final String? title = await controller.getTitle(); expect(title, 'Some title'); }); @@ -769,32 +677,36 @@ Future main() async { base64Encode(const Utf8Encoder().convert(scrollTestPage)); final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - initialUrl: - 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoaded.complete()), + ) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + ), ), - ), - ); + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); - final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; await tester.pumpAndSettle(const Duration(seconds: 3)); - int scrollPosX = await controller.getScrollX(); - int scrollPosY = await controller.getScrollY(); + Offset scrollPos = await controller.getScrollPosition(); // Check scrollTo() const int X_SCROLL = 123; @@ -802,206 +714,19 @@ Future main() async { // Get the initial position; this ensures that scrollTo is actually // changing something, but also gives the native view's scroll position // time to settle. - expect(scrollPosX, isNot(X_SCROLL)); - expect(scrollPosX, isNot(Y_SCROLL)); + expect(scrollPos.dx, isNot(X_SCROLL)); + expect(scrollPos.dy, isNot(Y_SCROLL)); await controller.scrollTo(X_SCROLL, Y_SCROLL); - scrollPosX = await controller.getScrollX(); - scrollPosY = await controller.getScrollY(); - expect(scrollPosX, X_SCROLL); - expect(scrollPosY, Y_SCROLL); + scrollPos = await controller.getScrollPosition(); + expect(scrollPos.dx, X_SCROLL); + expect(scrollPos.dy, Y_SCROLL); // Check scrollBy() (on top of scrollTo()) await controller.scrollBy(X_SCROLL, Y_SCROLL); - scrollPosX = await controller.getScrollX(); - scrollPosY = await controller.getScrollY(); - expect(scrollPosX, X_SCROLL * 2); - expect(scrollPosY, Y_SCROLL * 2); - }); - }); - - group('SurfaceAndroidWebView', () { - setUpAll(() { - WebView.platform = SurfaceAndroidWebView(); - }); - - tearDownAll(() { - WebView.platform = AndroidWebView(); - }); - - testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { - const String scrollTestPage = ''' - - - - - - -
- - - '''; - - final String scrollTestPageBase64 = - base64Encode(const Utf8Encoder().convert(scrollTestPage)); - - final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - initialUrl: - 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), - ), - ); - - final WebViewController controller = await controllerCompleter.future; - await pageLoaded.future; - - await tester.pumpAndSettle(const Duration(seconds: 3)); - - // Check scrollTo() - const int X_SCROLL = 123; - const int Y_SCROLL = 321; - - await controller.scrollTo(X_SCROLL, Y_SCROLL); - int scrollPosX = await controller.getScrollX(); - int scrollPosY = await controller.getScrollY(); - expect(X_SCROLL, scrollPosX); - expect(Y_SCROLL, scrollPosY); - - // Check scrollBy() (on top of scrollTo()) - await controller.scrollBy(X_SCROLL, Y_SCROLL); - scrollPosX = await controller.getScrollX(); - scrollPosY = await controller.getScrollY(); - expect(X_SCROLL * 2, scrollPosX); - expect(Y_SCROLL * 2, scrollPosY); - }); - - testWidgets('inputs are scrolled into view when focused', - (WidgetTester tester) async { - const String scrollTestPage = ''' - - - - - - -
- - - - '''; - - final String scrollTestPageBase64 = - base64Encode(const Utf8Encoder().convert(scrollTestPage)); - - final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - - await tester.runAsync(() async { - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: SizedBox( - width: 200, - height: 200, - child: WebView( - initialUrl: - 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - javascriptMode: JavascriptMode.unrestricted, - ), - ), - ), - ); - await Future.delayed(const Duration(milliseconds: 20)); - await tester.pump(); - }); - - final WebViewController controller = await controllerCompleter.future; - await pageLoaded.future; - final String viewportRectJSON = await _runJavaScriptReturningResult( - controller, 'JSON.stringify(viewport.getBoundingClientRect())'); - final Map viewportRectRelativeToViewport = - jsonDecode(viewportRectJSON) as Map; - - // Check that the input is originally outside of the viewport. - - final String initialInputClientRectJSON = - await _runJavaScriptReturningResult( - controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); - final Map initialInputClientRectRelativeToViewport = - jsonDecode(initialInputClientRectJSON) as Map; - - expect( - initialInputClientRectRelativeToViewport['bottom'] <= - viewportRectRelativeToViewport['bottom'], - isFalse); - - await controller.runJavascript('inputEl.focus()'); - - // Check that focusing the input brought it into view. - - final String lastInputClientRectJSON = - await _runJavaScriptReturningResult( - controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); - final Map lastInputClientRectRelativeToViewport = - jsonDecode(lastInputClientRectJSON) as Map; - - expect( - lastInputClientRectRelativeToViewport['top'] >= - viewportRectRelativeToViewport['top'], - isTrue); - expect( - lastInputClientRectRelativeToViewport['bottom'] <= - viewportRectRelativeToViewport['bottom'], - isTrue); - - expect( - lastInputClientRectRelativeToViewport['left'] >= - viewportRectRelativeToViewport['left'], - isTrue); - expect( - lastInputClientRectRelativeToViewport['right'] <= - viewportRectRelativeToViewport['right'], - isTrue); + scrollPos = await controller.getScrollPosition(); + expect(scrollPos.dx, X_SCROLL * 2); + expect(scrollPos.dy, Y_SCROLL * 2); }); }); @@ -1011,35 +736,41 @@ Future main() async { '${base64Encode(const Utf8Encoder().convert(blankPage))}'; testWidgets('can allow requests', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageLoads = - StreamController.broadcast(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: blankPageEncoded, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - navigationDelegate: (NavigationRequest request) { - return (request.url.contains('youtube.com')) + Completer pageLoaded = Completer(); + + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + ) + ..setOnPageFinished((_) => pageLoaded.complete()) + ..setOnNavigationRequest((NavigationRequest navigationRequest) { + return (navigationRequest.url.contains('youtube.com')) ? NavigationDecision.prevent : NavigationDecision.navigate; - }, - onPageFinished: (String url) => pageLoads.add(url), - ), - ), - ); + }), + ) + ..loadRequest( + LoadRequestParams(uri: Uri.parse(blankPageEncoded)), + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); - await pageLoads.stream.first; // Wait for initial page load. - final WebViewController controller = await controllerCompleter.future; - await controller.runJavascript('location.href = "$secondaryUrl"'); + await pageLoaded.future; // Wait for initial page load. + + pageLoaded = Completer(); + await controller.runJavaScript('location.href = "$secondaryUrl"'); + await pageLoaded.future; // Wait for the next page load. - await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); expect(currentUrl, secondaryUrl); }); @@ -1048,25 +779,39 @@ Future main() async { final Completer errorCompleter = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'https://www.notawebsite..com', - onWebResourceError: (WebResourceError error) { + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnWebResourceError((WebResourceError error) { errorCompleter.complete(error); - }, - ), - ), - ); + }), + ) + ..loadRequest( + LoadRequestParams(uri: Uri.parse('https://www.notawebsite..com')), + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); final WebResourceError error = await errorCompleter.future; expect(error, isNotNull); expect(error.errorType, isNotNull); expect( - error.failingUrl?.startsWith('https://www.notawebsite..com'), isTrue); + (error as AndroidWebResourceError) + .failingUrl + ?.startsWith('https://www.notawebsite..com'), + isTrue, + ); }); testWidgets('onWebResourceError is not called with valid url', @@ -1075,190 +820,145 @@ Future main() async { Completer(); final Completer pageFinishCompleter = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: - 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', - onWebResourceError: (WebResourceError error) { + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + ) + ..setOnPageFinished((_) => pageFinishCompleter.complete()) + ..setOnWebResourceError((WebResourceError error) { errorCompleter.complete(error); - }, - onPageFinished: (_) => pageFinishCompleter.complete(), + }), + ) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + ), ), - ), - ); + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); expect(errorCompleter.future, doesNotComplete); await pageFinishCompleter.future; }); - testWidgets( - 'onWebResourceError only called for main frame', - (WidgetTester tester) async { - const String iframeTest = ''' - - - - WebResourceError test - - - - - - '''; - final String iframeTestBase64 = - base64Encode(const Utf8Encoder().convert(iframeTest)); - - final Completer errorCompleter = - Completer(); - final Completer pageFinishCompleter = Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: - 'data:text/html;charset=utf-8;base64,$iframeTestBase64', - onWebResourceError: (WebResourceError error) { - errorCompleter.complete(error); - }, - onPageFinished: (_) => pageFinishCompleter.complete(), - ), - ), - ); - - expect(errorCompleter.future, doesNotComplete); - await pageFinishCompleter.future; - }, - ); - testWidgets('can block requests', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageLoads = - StreamController.broadcast(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: blankPageEncoded, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - navigationDelegate: (NavigationRequest request) { - return (request.url.contains('youtube.com')) + Completer pageLoaded = Completer(); + + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + ) + ..setOnPageFinished((_) => pageLoaded.complete()) + ..setOnNavigationRequest((NavigationRequest navigationRequest) { + return (navigationRequest.url.contains('youtube.com')) ? NavigationDecision.prevent : NavigationDecision.navigate; - }, - onPageFinished: (String url) => pageLoads.add(url), - ), - ), - ); + }), + ) + ..loadRequest(LoadRequestParams(uri: Uri.parse(blankPageEncoded))); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); - await pageLoads.stream.first; // Wait for initial page load. - final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; // Wait for initial page load. + + pageLoaded = Completer(); await controller - .runJavascript('location.href = "https://www.youtube.com/"'); + .runJavaScript('location.href = "https://www.youtube.com/"'); // There should never be any second page load, since our new URL is // blocked. Still wait for a potential page change for some time in order // to give the test a chance to fail. - await pageLoads.stream.first - .timeout(const Duration(milliseconds: 500), onTimeout: () => ''); + await pageLoaded.future + .timeout(const Duration(milliseconds: 500), onTimeout: () => false); final String? currentUrl = await controller.currentUrl(); expect(currentUrl, isNot(contains('youtube.com'))); }); testWidgets('supports asynchronous decisions', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageLoads = - StreamController.broadcast(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: blankPageEncoded, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - navigationDelegate: (NavigationRequest request) async { + Completer pageLoaded = Completer(); + + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + ) + ..setOnPageFinished((_) => pageLoaded.complete()) + ..setOnNavigationRequest( + (NavigationRequest navigationRequest) async { NavigationDecision decision = NavigationDecision.prevent; decision = await Future.delayed( const Duration(milliseconds: 10), () => NavigationDecision.navigate); return decision; - }, - onPageFinished: (String url) => pageLoads.add(url), - ), - ), - ); + }), + ) + ..loadRequest(LoadRequestParams(uri: Uri.parse(blankPageEncoded))); - await pageLoads.stream.first; // Wait for initial page load. - final WebViewController controller = await controllerCompleter.future; - await controller.runJavascript('location.href = "$secondaryUrl"'); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await pageLoaded.future; // Wait for initial page load. + + pageLoaded = Completer(); + await controller.runJavaScript('location.href = "$secondaryUrl"'); + await pageLoaded.future; // Wait for second page to load. - await pageLoads.stream.first; // Wait for second page to load. final String? currentUrl = await controller.currentUrl(); expect(currentUrl, secondaryUrl); }); }); - testWidgets('launches with gestureNavigationEnabled on iOS', - (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: SizedBox( - width: 400, - height: 300, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - gestureNavigationEnabled: true, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - ), - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, primaryUrl); - }); - testWidgets('target _blank opens in same window', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); final Completer pageLoaded = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - await controller.runJavascript('window.open("$primaryUrl", "_blank")'); + + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate(PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoaded.complete())); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await controller.runJavaScript('window.open("$primaryUrl", "_blank")'); await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); @@ -1267,31 +967,30 @@ Future main() async { testWidgets( 'can open new window and go back', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); Completer pageLoaded = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (String url) { - pageLoaded.complete(); - }, - initialUrl: primaryUrl, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; + + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate(PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoaded.complete())) + ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + expect(controller.currentUrl(), completion(primaryUrl)); await pageLoaded.future; pageLoaded = Completer(); - await controller.runJavascript('window.open("$secondaryUrl")'); + await controller.runJavaScript('window.open("$secondaryUrl")'); await pageLoaded.future; pageLoaded = Completer(); expect(controller.currentUrl(), completion(secondaryUrl)); @@ -1336,105 +1035,101 @@ Future main() async { '''; final String openWindowTestBase64 = base64Encode(const Utf8Encoder().convert(openWindowTest)); - final Completer controllerCompleter = - Completer(); + final Completer pageLoadCompleter = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - initialUrl: - 'data:text/html;charset=utf-8;base64,$openWindowTestBase64', - onPageFinished: (String url) { - pageLoadCompleter.complete(); - }, + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate(PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoadCompleter.complete())) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,$openWindowTestBase64', + ), ), - ), - ); + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); - final WebViewController controller = await controllerCompleter.future; await pageLoadCompleter.future; - final String iframeLoaded = - await controller.runJavascriptReturningResult('iframeLoaded'); - expect(iframeLoaded, 'true'); + final bool iframeLoaded = + await controller.runJavaScriptReturningResult('iframeLoaded') as bool; + expect(iframeLoaded, true); - final String elementText = await controller.runJavascriptReturningResult( + final String elementText = await controller.runJavaScriptReturningResult( 'document.querySelector("p") && document.querySelector("p").textContent', - ); + ) as String; expect(elementText, 'null'); }, ); testWidgets( - 'clearCache should clear local storage', + '`AndroidWebViewController` can be reused with a new `AndroidWebViewWidget`', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - - Completer pageLoadCompleter = Completer(); + Completer pageLoaded = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (_) => pageLoadCompleter.complete(), - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - ), - ), - ); + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setPlatformNavigationDelegate(PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoaded.complete())) + ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); - await pageLoadCompleter.future; - pageLoadCompleter = Completer(); + await pageLoaded.future; - final WebViewController controller = await controllerCompleter.future; - await controller.runJavascript('localStorage.setItem("myCat", "Tom");'); - final String myCatItem = await controller.runJavascriptReturningResult( - 'localStorage.getItem("myCat");', - ); - expect(myCatItem, '"Tom"'); + await tester.pumpWidget(Container()); + await tester.pumpAndSettle(); - await controller.clearCache(); - await pageLoadCompleter.future; + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); - final String nullItem = await controller.runJavascriptReturningResult( - 'localStorage.getItem("myCat");', + pageLoaded = Completer(); + await controller.loadRequest( + LoadRequestParams(uri: Uri.parse(primaryUrl)), + ); + await expectLater( + pageLoaded.future, + completes, ); - expect(nullItem, 'null'); }, ); } -// JavaScript booleans evaluate to different string values on Android and iOS. -// This utility method returns the string boolean value of the current platform. -String _webviewBool(bool value) { - if (defaultTargetPlatform == TargetPlatform.iOS) { - return value ? '1' : '0'; - } - return value ? 'true' : 'false'; -} - /// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. -Future _getUserAgent(WebViewController controller) async { +Future _getUserAgent(PlatformWebViewController controller) async { return _runJavaScriptReturningResult(controller, 'navigator.userAgent;'); } Future _runJavaScriptReturningResult( - WebViewController controller, + PlatformWebViewController controller, String js, ) async { - return jsonDecode(await controller.runJavascriptReturningResult(js)) + return jsonDecode(await controller.runJavaScriptReturningResult(js) as String) as String; } @@ -1445,7 +1140,7 @@ class ResizableWebView extends StatefulWidget { required this.onPageFinished, }) : super(key: key); - final JavascriptMessageHandler onResize; + final VoidCallback onResize; final VoidCallback onPageFinished; @override @@ -1453,6 +1148,31 @@ class ResizableWebView extends StatefulWidget { } class ResizableWebViewState extends State { + late final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => widget.onPageFinished()), + ) + ..addJavaScriptChannel( + JavaScriptChannelParams( + name: 'Resize', + onMessageReceived: (_) { + widget.onResize(); + }, + ), + ) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,${base64Encode(const Utf8Encoder().convert(resizePage))}', + ), + ), + ); + double webViewWidth = 200; double webViewHeight = 200; @@ -1475,8 +1195,6 @@ class ResizableWebViewState extends State { @override Widget build(BuildContext context) { - final String resizeTestBase64 = - base64Encode(const Utf8Encoder().convert(resizePage)); return Directionality( textDirection: TextDirection.ltr, child: Column( @@ -1484,18 +1202,9 @@ class ResizableWebViewState extends State { SizedBox( width: webViewWidth, height: webViewHeight, - child: WebView( - initialUrl: - 'data:text/html;charset=utf-8;base64,$resizeTestBase64', - javascriptChannels: { - JavascriptChannel( - name: 'Resize', - onMessageReceived: widget.onResize, - ), - }, - onPageFinished: (_) => widget.onPageFinished(), - javascriptMode: JavascriptMode.unrestricted, - ), + child: PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context), ), TextButton( key: const Key('resizeButton'), @@ -1512,3 +1221,33 @@ class ResizableWebViewState extends State { ); } } + +class CopyableObjectWithCallback with Copyable { + CopyableObjectWithCallback(this.callback); + + final VoidCallback callback; + + @override + CopyableObjectWithCallback copy() { + return CopyableObjectWithCallback(callback); + } +} + +class ClassWithCallbackClass { + ClassWithCallbackClass() { + callbackClass = CopyableObjectWithCallback( + withWeakReferenceTo( + this, + (WeakReference weakReference) { + return () { + // Weak reference to `this` in callback. + // ignore: unnecessary_statements + weakReference; + }; + }, + ), + ); + } + + late final CopyableObjectWithCallback callbackClass; +} diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/navigation_decision.dart b/packages/webview_flutter/webview_flutter_android/example/lib/legacy/navigation_decision.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_android/example/lib/navigation_decision.dart rename to packages/webview_flutter/webview_flutter_android/example/lib/legacy/navigation_decision.dart diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/navigation_request.dart b/packages/webview_flutter/webview_flutter_android/example/lib/legacy/navigation_request.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_android/example/lib/navigation_request.dart rename to packages/webview_flutter/webview_flutter_android/example/lib/legacy/navigation_request.dart diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_android/example/lib/legacy/web_view.dart similarity index 98% rename from packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart rename to packages/webview_flutter/webview_flutter_android/example/lib/legacy/web_view.dart index 56745314d92b..b77a503c959a 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/legacy/web_view.dart @@ -8,9 +8,10 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:webview_flutter_android/webview_android.dart'; -import 'package:webview_flutter_android/webview_android_cookie_manager.dart'; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +// ignore: implementation_imports +import 'package:webview_flutter_android/src/webview_flutter_android_legacy.dart'; +// ignore: implementation_imports +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; import 'navigation_decision.dart'; import 'navigation_request.dart'; @@ -319,10 +320,10 @@ class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { required bool isForMainFrame, }) async { if (url.startsWith('https://www.youtube.com/')) { - print('blocking navigation to $url'); + debugPrint('blocking navigation to $url'); return false; } - print('allowing navigation to $url'); + debugPrint('allowing navigation to $url'); return true; } diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart index 4492e6e6e26f..75f01b457b3a 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -8,27 +8,14 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; + import 'package:flutter/material.dart'; -import 'package:flutter_driver/driver_extension.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:webview_flutter_android/webview_surface_android.dart'; +import 'package:webview_flutter_android/webview_flutter_android.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; -import 'navigation_decision.dart'; -import 'navigation_request.dart'; -import 'web_view.dart'; - -void appMain() { - enableFlutterDriverExtension(); - main(); -} - void main() { - // Configure the [WebView] to use the [SurfaceAndroidWebView] - // implementation instead of the default [AndroidWebView]. - WebView.platform = SurfaceAndroidWebView(); - - runApp(const MaterialApp(home: _WebViewExample())); + runApp(const MaterialApp(home: WebViewExample())); } const String kNavigationExamplePage = ''' @@ -46,7 +33,7 @@ The navigation delegate is set to block navigation to the youtube website. '''; -const String kExamplePage = ''' +const String kLocalExamplePage = ''' @@ -86,16 +73,70 @@ const String kTransparentBackgroundPage = ''' '''; -class _WebViewExample extends StatefulWidget { - const _WebViewExample({Key? key}) : super(key: key); +class WebViewExample extends StatefulWidget { + const WebViewExample({Key? key, this.cookieManager}) : super(key: key); + + final PlatformWebViewCookieManager? cookieManager; @override - _WebViewExampleState createState() => _WebViewExampleState(); + State createState() => _WebViewExampleState(); } -class _WebViewExampleState extends State<_WebViewExample> { - final Completer _controller = - Completer(); +class _WebViewExampleState extends State { + late final PlatformWebViewController _controller; + + @override + void initState() { + super.initState(); + + _controller = PlatformWebViewController( + AndroidWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(const Color(0x80000000)) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + ) + ..setOnProgress((int progress) { + debugPrint('WebView is loading (progress : $progress%)'); + }) + ..setOnPageStarted((String url) { + debugPrint('Page started loading: $url'); + }) + ..setOnPageFinished((String url) { + debugPrint('Page finished loading: $url'); + }) + ..setOnWebResourceError((WebResourceError error) { + debugPrint(''' +Page resource error: + code: ${error.errorCode} + description: ${error.description} + errorType: ${error.errorType} + isForMainFrame: ${error.isForMainFrame} + '''); + }) + ..setOnNavigationRequest((NavigationRequest request) { + if (request.url.startsWith('https://www.youtube.com/')) { + debugPrint('blocking navigation to ${request.url}'); + return NavigationDecision.prevent; + } + debugPrint('allowing navigation to ${request.url}'); + return NavigationDecision.navigate; + }), + ) + ..addJavaScriptChannel(JavaScriptChannelParams( + name: 'Toaster', + onMessageReceived: (JavaScriptMessage message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message.message)), + ); + }, + )) + ..loadRequest(LoadRequestParams( + uri: Uri.parse('https://flutter.dev'), + )); + } @override Widget build(BuildContext context) { @@ -105,74 +146,36 @@ class _WebViewExampleState extends State<_WebViewExample> { title: const Text('Flutter WebView example'), // This drop down menu demonstrates that Flutter widgets can be shown over the web view. actions: [ - _NavigationControls(_controller.future), - _SampleMenu(_controller.future), + NavigationControls(webViewController: _controller), + SampleMenu( + webViewController: _controller, + cookieManager: widget.cookieManager, + ), ], ), - body: WebView( - initialUrl: 'https://flutter.dev', - onWebViewCreated: (WebViewController controller) { - _controller.complete(controller); - }, - onProgress: (int progress) { - print('WebView is loading (progress : $progress%)'); - }, - navigationDelegate: (NavigationRequest request) { - if (request.url.startsWith('https://www.youtube.com/')) { - print('blocking navigation to $request}'); - return NavigationDecision.prevent; - } - print('allowing navigation to $request'); - return NavigationDecision.navigate; - }, - onPageStarted: (String url) { - print('Page started loading: $url'); - }, - onPageFinished: (String url) { - print('Page finished loading: $url'); - }, - javascriptChannels: _createJavascriptChannels(context), - javascriptMode: JavascriptMode.unrestricted, - userAgent: 'Custom_User_Agent', - backgroundColor: const Color(0x80000000), - ), + body: PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: _controller), + ).build(context), floatingActionButton: favoriteButton(), ); } Widget favoriteButton() { - return FutureBuilder( - future: _controller.future, - builder: (BuildContext context, - AsyncSnapshot controller) { - if (controller.hasData) { - return FloatingActionButton( - onPressed: () async { - final String url = (await controller.data!.currentUrl())!; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Favorited $url')), - ); - }, - child: const Icon(Icons.favorite), - ); - } - return Container(); - }); + return FloatingActionButton( + onPressed: () async { + final String? url = await _controller.currentUrl(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Favorited $url')), + ); + } + }, + child: const Icon(Icons.favorite), + ); } } -Set _createJavascriptChannels(BuildContext context) { - return { - JavascriptChannel( - name: 'Snackbar', - onMessageReceived: (JavascriptMessage message) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(message.message))); - }), - }; -} - -enum _MenuOptions { +enum MenuOptions { showUserAgent, listCookies, clearCookies, @@ -180,233 +183,251 @@ enum _MenuOptions { listCache, clearCache, navigationDelegate, - loadFlutterAsset, + doPostRequest, loadLocalFile, + loadFlutterAsset, loadHtmlString, transparentBackground, - doPostRequest, setCookie, } -class _SampleMenu extends StatelessWidget { - const _SampleMenu(this.controller); +class SampleMenu extends StatelessWidget { + SampleMenu({ + Key? key, + required this.webViewController, + PlatformWebViewCookieManager? cookieManager, + }) : cookieManager = cookieManager ?? + PlatformWebViewCookieManager( + const PlatformWebViewCookieManagerCreationParams(), + ), + super(key: key); - final Future controller; + final PlatformWebViewController webViewController; + late final PlatformWebViewCookieManager cookieManager; @override Widget build(BuildContext context) { - return FutureBuilder( - future: controller, - builder: - (BuildContext context, AsyncSnapshot controller) { - return PopupMenuButton<_MenuOptions>( - key: const ValueKey('ShowPopupMenu'), - onSelected: (_MenuOptions value) { - switch (value) { - case _MenuOptions.showUserAgent: - _onShowUserAgent(controller.data!, context); - break; - case _MenuOptions.listCookies: - _onListCookies(controller.data!, context); - break; - case _MenuOptions.clearCookies: - _onClearCookies(controller.data!, context); - break; - case _MenuOptions.addToCache: - _onAddToCache(controller.data!, context); - break; - case _MenuOptions.listCache: - _onListCache(controller.data!, context); - break; - case _MenuOptions.clearCache: - _onClearCache(controller.data!, context); - break; - case _MenuOptions.navigationDelegate: - _onNavigationDelegateExample(controller.data!, context); - break; - case _MenuOptions.loadFlutterAsset: - _onLoadFlutterAssetExample(controller.data!, context); - break; - case _MenuOptions.loadLocalFile: - _onLoadLocalFileExample(controller.data!, context); - break; - case _MenuOptions.loadHtmlString: - _onLoadHtmlStringExample(controller.data!, context); - break; - case _MenuOptions.transparentBackground: - _onTransparentBackground(controller.data!, context); - break; - case _MenuOptions.doPostRequest: - _onDoPostRequest(controller.data!, context); - break; - case _MenuOptions.setCookie: - _onSetCookie(controller.data!, context); - break; - } - }, - itemBuilder: (BuildContext context) => >[ - PopupMenuItem<_MenuOptions>( - value: _MenuOptions.showUserAgent, - enabled: controller.hasData, - child: const Text('Show user agent'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.listCookies, - child: Text('List cookies'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.clearCookies, - child: Text('Clear cookies'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.addToCache, - child: Text('Add to cache'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.listCache, - child: Text('List cache'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.clearCache, - child: Text('Clear cache'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.navigationDelegate, - child: Text('Navigation Delegate example'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.loadFlutterAsset, - child: Text('Load Flutter Asset'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.loadHtmlString, - child: Text('Load HTML string'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.loadLocalFile, - child: Text('Load local file'), - ), - const PopupMenuItem<_MenuOptions>( - key: ValueKey('ShowTransparentBackgroundExample'), - value: _MenuOptions.transparentBackground, - child: Text('Transparent background example'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.doPostRequest, - child: Text('Post Request'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.setCookie, - child: Text('Set Cookie'), - ), - ], - ); + return PopupMenuButton( + key: const ValueKey('ShowPopupMenu'), + onSelected: (MenuOptions value) { + switch (value) { + case MenuOptions.showUserAgent: + _onShowUserAgent(); + break; + case MenuOptions.listCookies: + _onListCookies(context); + break; + case MenuOptions.clearCookies: + _onClearCookies(context); + break; + case MenuOptions.addToCache: + _onAddToCache(context); + break; + case MenuOptions.listCache: + _onListCache(); + break; + case MenuOptions.clearCache: + _onClearCache(context); + break; + case MenuOptions.navigationDelegate: + _onNavigationDelegateExample(); + break; + case MenuOptions.doPostRequest: + _onDoPostRequest(); + break; + case MenuOptions.loadLocalFile: + _onLoadLocalFileExample(); + break; + case MenuOptions.loadFlutterAsset: + _onLoadFlutterAssetExample(); + break; + case MenuOptions.loadHtmlString: + _onLoadHtmlStringExample(); + break; + case MenuOptions.transparentBackground: + _onTransparentBackground(); + break; + case MenuOptions.setCookie: + _onSetCookie(); + break; + } }, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem( + value: MenuOptions.showUserAgent, + child: Text('Show user agent'), + ), + const PopupMenuItem( + value: MenuOptions.listCookies, + child: Text('List cookies'), + ), + const PopupMenuItem( + value: MenuOptions.clearCookies, + child: Text('Clear cookies'), + ), + const PopupMenuItem( + value: MenuOptions.addToCache, + child: Text('Add to cache'), + ), + const PopupMenuItem( + value: MenuOptions.listCache, + child: Text('List cache'), + ), + const PopupMenuItem( + value: MenuOptions.clearCache, + child: Text('Clear cache'), + ), + const PopupMenuItem( + value: MenuOptions.navigationDelegate, + child: Text('Navigation Delegate example'), + ), + const PopupMenuItem( + value: MenuOptions.doPostRequest, + child: Text('Post Request'), + ), + const PopupMenuItem( + value: MenuOptions.loadHtmlString, + child: Text('Load HTML string'), + ), + const PopupMenuItem( + value: MenuOptions.loadLocalFile, + child: Text('Load local file'), + ), + const PopupMenuItem( + value: MenuOptions.loadFlutterAsset, + child: Text('Load Flutter Asset'), + ), + const PopupMenuItem( + value: MenuOptions.setCookie, + child: Text('Set cookie'), + ), + const PopupMenuItem( + key: ValueKey('ShowTransparentBackgroundExample'), + value: MenuOptions.transparentBackground, + child: Text('Transparent background example'), + ), + ], ); } - Future _onShowUserAgent( - WebViewController controller, BuildContext context) async { - // Send a message with the user agent string to the Snackbar JavaScript channel we registered + Future _onShowUserAgent() { + // Send a message with the user agent string to the Toaster JavaScript channel we registered // with the WebView. - await controller.runJavascript( - 'Snackbar.postMessage("User Agent: " + navigator.userAgent);'); + return webViewController.runJavaScript( + 'Toaster.postMessage("User Agent: " + navigator.userAgent);', + ); } - Future _onListCookies( - WebViewController controller, BuildContext context) async { - final String cookies = - await controller.runJavascriptReturningResult('document.cookie'); - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Column( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Cookies:'), - _getCookieList(cookies), - ], - ), - )); + Future _onListCookies(BuildContext context) async { + final String cookies = await webViewController + .runJavaScriptReturningResult('document.cookie') as String; + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Cookies:'), + _getCookieList(cookies), + ], + ), + )); + } } - Future _onAddToCache( - WebViewController controller, BuildContext context) async { - await controller.runJavascript( - 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Added a test entry to cache.'), - )); + Future _onAddToCache(BuildContext context) async { + await webViewController.runJavaScript( + 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";', + ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Added a test entry to cache.'), + )); + } } - Future _onListCache( - WebViewController controller, BuildContext context) async { - await controller.runJavascript('caches.keys()' + Future _onListCache() { + return webViewController.runJavaScript('caches.keys()' // ignore: missing_whitespace_between_adjacent_strings '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' - '.then((caches) => Snackbar.postMessage(caches))'); + '.then((caches) => Toaster.postMessage(caches))'); } - Future _onClearCache( - WebViewController controller, BuildContext context) async { - await controller.clearCache(); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Cache cleared.'), - )); + Future _onClearCache(BuildContext context) async { + await webViewController.clearCache(); + await webViewController.clearLocalStorage(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Cache cleared.'), + )); + } } - Future _onClearCookies( - WebViewController controller, BuildContext context) async { - final bool hadCookies = await WebViewCookieManager.instance.clearCookies(); + Future _onClearCookies(BuildContext context) async { + final bool hadCookies = await cookieManager.clearCookies(); String message = 'There were cookies. Now, they are gone!'; if (!hadCookies) { message = 'There are no cookies.'; } - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(message), - )); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(message), + )); + } } - Future _onSetCookie( - WebViewController controller, BuildContext context) async { - await WebViewCookieManager.instance.setCookie( - const WebViewCookie( - name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'), + Future _onNavigationDelegateExample() { + final String contentBase64 = base64Encode( + const Utf8Encoder().convert(kNavigationExamplePage), + ); + return webViewController.loadRequest( + LoadRequestParams( + uri: Uri.parse('data:text/html;base64,$contentBase64'), + ), ); - await controller.loadUrl('https://httpbin.org/anything'); } - Future _onNavigationDelegateExample( - WebViewController controller, BuildContext context) async { - final String contentBase64 = - base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); - await controller.loadUrl('data:text/html;base64,$contentBase64'); + Future _onSetCookie() async { + await cookieManager.setCookie( + const WebViewCookie( + name: 'foo', + value: 'bar', + domain: 'httpbin.org', + path: '/anything', + ), + ); + await webViewController.loadRequest(LoadRequestParams( + uri: Uri.parse('https://httpbin.org/anything'), + )); } - Future _onLoadFlutterAssetExample( - WebViewController controller, BuildContext context) async { - await controller.loadFlutterAsset('assets/www/index.html'); + Future _onDoPostRequest() { + return webViewController.loadRequest(LoadRequestParams( + uri: Uri.parse('https://httpbin.org/post'), + method: LoadRequestMethod.post, + headers: const { + 'foo': 'bar', + 'Content-Type': 'text/plain', + }, + body: Uint8List.fromList('Test Body'.codeUnits), + )); } - Future _onLoadLocalFileExample( - WebViewController controller, BuildContext context) async { + Future _onLoadLocalFileExample() async { final String pathToIndex = await _prepareLocalFile(); + await webViewController.loadFile(pathToIndex); + } - await controller.loadFile(pathToIndex); + Future _onLoadFlutterAssetExample() { + return webViewController.loadFlutterAsset('assets/www/index.html'); } - Future _onLoadHtmlStringExample( - WebViewController controller, BuildContext context) async { - await controller.loadHtmlString(kExamplePage); + Future _onLoadHtmlStringExample() { + return webViewController.loadHtmlString(kLocalExamplePage); } - Future _onDoPostRequest( - WebViewController controller, BuildContext context) async { - final WebViewRequest request = WebViewRequest( - uri: Uri.parse('https://httpbin.org/post'), - method: WebViewRequestMethod.post, - body: Uint8List.fromList('Test Body'.codeUnits), - ); - await controller.loadRequest(request); + Future _onTransparentBackground() { + return webViewController.loadHtmlString(kTransparentBackgroundPage); } Widget _getCookieList(String cookies) { @@ -425,83 +446,59 @@ class _SampleMenu extends StatelessWidget { static Future _prepareLocalFile() async { final String tmpDir = (await getTemporaryDirectory()).path; - final File indexFile = File('$tmpDir/www/index.html'); + final File indexFile = File( + {tmpDir, 'www', 'index.html'}.join(Platform.pathSeparator)); - await Directory('$tmpDir/www').create(recursive: true); - await indexFile.writeAsString(kExamplePage); + await indexFile.create(recursive: true); + await indexFile.writeAsString(kLocalExamplePage); return indexFile.path; } - - Future _onTransparentBackground( - WebViewController controller, BuildContext context) async { - await controller.loadHtmlString(kTransparentBackgroundPage); - } } -class _NavigationControls extends StatelessWidget { - const _NavigationControls(this._webViewControllerFuture) - : assert(_webViewControllerFuture != null); +class NavigationControls extends StatelessWidget { + const NavigationControls({Key? key, required this.webViewController}) + : super(key: key); - final Future _webViewControllerFuture; + final PlatformWebViewController webViewController; @override Widget build(BuildContext context) { - return FutureBuilder( - future: _webViewControllerFuture, - builder: - (BuildContext context, AsyncSnapshot snapshot) { - final bool webViewReady = - snapshot.connectionState == ConnectionState.done; - final WebViewController? controller = snapshot.data; - - return Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_back_ios), - onPressed: !webViewReady - ? null - : () async { - if (await controller!.canGoBack()) { - await controller.goBack(); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('No back history item')), - ); - return; - } - }, - ), - IconButton( - icon: const Icon(Icons.arrow_forward_ios), - onPressed: !webViewReady - ? null - : () async { - if (await controller!.canGoForward()) { - await controller.goForward(); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('No forward history item')), - ); - return; - } - }, - ), - IconButton( - icon: const Icon(Icons.replay), - onPressed: !webViewReady - ? null - : () { - controller!.reload(); - }, - ), - ], - ); - }, + return Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: () async { + if (await webViewController.canGoBack()) { + await webViewController.goBack(); + } else { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No back history item')), + ); + } + } + }, + ), + IconButton( + icon: const Icon(Icons.arrow_forward_ios), + onPressed: () async { + if (await webViewController.canGoForward()) { + await webViewController.goForward(); + } else { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No forward history item')), + ); + } + } + }, + ), + IconButton( + icon: const Icon(Icons.replay), + onPressed: () => webViewController.reload(), + ), + ], ); } } - -/// Callback type for handling messages sent from JavaScript running in a web view. -typedef JavascriptMessageHandler = void Function(JavascriptMessage message); diff --git a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml index de6980a736cc..0fc0daf84118 100644 --- a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml @@ -4,6 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" + flutter: ">=3.0.0" dependencies: flutter: @@ -18,7 +19,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: ../ - webview_flutter_platform_interface: ^1.8.0 + webview_flutter_platform_interface: ^2.0.0 dev_dependencies: espresso: ^0.2.0 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 new file mode 100644 index 000000000000..9437e9dd3eb4 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart @@ -0,0 +1,106 @@ +// 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 'android_webview.dart' as android_webview; + +/// Handles constructing objects and calling static methods for the Android +/// WebView native library. +/// +/// This class provides dependency injection for the implementations of the +/// platform interface classes. Improving the ease of unit testing and/or +/// overriding the underlying Android WebView classes. +/// +/// By default each function calls the default constructor of the WebView class +/// it intends to return. +class AndroidWebViewProxy { + /// Constructs a [AndroidWebViewProxy]. + const AndroidWebViewProxy({ + this.createAndroidWebView = android_webview.WebView.new, + this.createAndroidWebChromeClient = android_webview.WebChromeClient.new, + this.createAndroidWebViewClient = android_webview.WebViewClient.new, + this.createFlutterAssetManager = android_webview.FlutterAssetManager.new, + this.createJavaScriptChannel = android_webview.JavaScriptChannel.new, + this.createDownloadListener = android_webview.DownloadListener.new, + }); + + /// Constructs a [android_webview.WebView]. + /// + /// Due to changes in Flutter 3.0 the [useHybridComposition] doesn't have + /// any effect and should not be exposed publicly. More info here: + /// https://github.com/flutter/flutter/issues/108106 + final android_webview.WebView Function({ + required bool useHybridComposition, + }) 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, + }) createAndroidWebChromeClient; + + /// Constructs a [android_webview.WebViewClient]. + final android_webview.WebViewClient Function({ + void Function(android_webview.WebView webView, String url)? onPageStarted, + void Function(android_webview.WebView webView, String url)? onPageFinished, + void Function( + android_webview.WebView webView, + android_webview.WebResourceRequest request, + android_webview.WebResourceError error, + )? + onReceivedRequestError, + @Deprecated('Only called on Android version < 23.') + void Function( + android_webview.WebView webView, + int errorCode, + String description, + String failingUrl, + )? + onReceivedError, + void Function( + android_webview.WebView webView, + android_webview.WebResourceRequest request, + )? + requestLoading, + void Function(android_webview.WebView webView, String url)? urlLoading, + }) createAndroidWebViewClient; + + /// Constructs a [android_webview.FlutterAssetManager]. + final android_webview.FlutterAssetManager Function() + createFlutterAssetManager; + + /// Constructs a [android_webview.JavaScriptChannel]. + final android_webview.JavaScriptChannel Function( + String channelName, { + required void Function(String) postMessage, + }) createJavaScriptChannel; + + /// Constructs a [android_webview.DownloadListener]. + final android_webview.DownloadListener Function({ + required void Function( + String url, + String userAgent, + String contentDisposition, + String mimetype, + int contentLength, + ) + onDownloadStart, + }) createDownloadListener; + + /// Enables debugging of web contents (HTML / CSS / JavaScript) loaded into any WebViews of this application. + /// + /// This flag can be enabled in order to facilitate debugging of web layouts + /// and JavaScript code running inside WebViews. Please refer to + /// [android_webview.WebView] documentation for the debugging guide. The + /// default is false. + /// + /// See [android_webview.WebView].setWebContentsDebuggingEnabled. + Future setWebContentsDebuggingEnabled(bool enabled) { + return android_webview.WebView.setWebContentsDebuggingEnabled(enabled); + } +} 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 e08703106990..1ab30a9ea1fd 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 @@ -16,10 +16,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show BinaryMessenger; import 'package:flutter/widgets.dart' show AndroidViewSurface; -import 'android_webview.pigeon.dart'; +import 'android_webview.g.dart'; import 'android_webview_api_impls.dart'; import 'instance_manager.dart'; +export 'android_webview_api_impls.dart' show FileChooserMode; + /// Root of the Java class hierarchy. /// /// See https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html. @@ -38,7 +40,9 @@ class JavaObject with Copyable { /// Global instance of [InstanceManager]. static final InstanceManager globalInstanceManager = InstanceManager( - onWeakReferenceRemoved: (_) {}, + onWeakReferenceRemoved: (int identifier) { + JavaObjectHostApiImpl().dispose(identifier); + }, ); /// Pigeon Host Api implementation for [JavaObject]. @@ -76,6 +80,10 @@ class JavaObject with Copyable { /// When a [WebView] is no longer needed [release] must be called. class WebView extends JavaObject { /// Constructs a new WebView. + /// + /// Due to changes in Flutter 3.0 the [useHybridComposition] doesn't have + /// any effect and should not be exposed publicly. More info here: + /// https://github.com/flutter/flutter/issues/108106 WebView({this.useHybridComposition = false}) : super.detached() { api.createFromInstance(this); } @@ -90,8 +98,6 @@ class WebView extends JavaObject { @visibleForTesting static WebViewHostApiImpl api = WebViewHostApiImpl(); - WebViewClient? _currentWebViewClient; - /// Whether the [WebView] will be rendered with an [AndroidViewSurface]. /// /// This implementation uses hybrid composition to render the WebView Widget. @@ -330,8 +336,6 @@ class WebView extends JavaObject { /// /// This will replace the current handler. Future setWebViewClient(WebViewClient webViewClient) { - _currentWebViewClient = webViewClient; - WebViewClient.api.createFromInstance(webViewClient); return api.setWebViewClientFromInstance(this, webViewClient); } @@ -375,11 +379,8 @@ class WebView extends JavaObject { /// Registers the interface to be used when content can not be handled by the rendering engine, and should be downloaded instead. /// /// This will replace the current handler. - Future setDownloadListener(DownloadListener? listener) async { - await Future.wait(>[ - if (listener != null) DownloadListener.api.createFromInstance(listener), - api.setDownloadListenerFromInstance(this, listener) - ]); + Future setDownloadListener(DownloadListener? listener) { + return api.setDownloadListenerFromInstance(this, listener); } /// Sets the chrome handler. @@ -387,20 +388,8 @@ class WebView extends JavaObject { /// This is an implementation of [WebChromeClient] for use in handling /// JavaScript dialogs, favicons, titles, and the progress. This will replace /// the current handler. - Future setWebChromeClient(WebChromeClient? client) async { - // WebView requires a WebViewClient because of a bug fix that makes - // calls to WebViewClient.requestLoading/WebViewClient.urlLoading when a new - // window is opened. This is to make sure a url opened by `Window.open` has - // a secure url. - assert( - _currentWebViewClient != null, - "Can't set a WebChromeClient without setting a WebViewClient first.", - ); - await Future.wait(>[ - if (client != null) - WebChromeClient.api.createFromInstance(client, _currentWebViewClient!), - api.setWebChromeClientFromInstance(this, client), - ]); + Future setWebChromeClient(WebChromeClient? client) { + return api.setWebChromeClientFromInstance(this, client); } /// Sets the background color of this WebView. @@ -408,15 +397,6 @@ class WebView extends JavaObject { return api.setBackgroundColorFromInstance(this, color.value); } - /// Releases all resources used by the [WebView]. - /// - /// Any methods called after [release] will throw an exception. - Future release() { - _currentWebViewClient = null; - WebSettings.api.disposeFromInstance(settings); - return api.disposeFromInstance(this); - } - @override WebView copy() { return WebView.detached(useHybridComposition: useHybridComposition); @@ -624,9 +604,10 @@ class JavaScriptChannel extends JavaObject { /// Constructs a [JavaScriptChannel]. JavaScriptChannel( this.channelName, { - void Function(String message)? postMessage, + required this.postMessage, }) : super.detached() { AndroidWebViewFlutterApis.instance.ensureSetUp(); + api.createFromInstance(this); } /// Constructs a [JavaScriptChannel] without creating the associated Java @@ -636,7 +617,7 @@ class JavaScriptChannel extends JavaObject { /// create copies. JavaScriptChannel.detached( this.channelName, { - void Function(String message)? postMessage, + required this.postMessage, }) : super.detached(); /// Pigeon Host Api implementation for [JavaScriptChannel]. @@ -647,7 +628,7 @@ class JavaScriptChannel extends JavaObject { final String channelName; /// Callback method when javaScript calls `postMessage` on the object instance passed. - void postMessage(String message) {} + final void Function(String message) postMessage; @override JavaScriptChannel copy() { @@ -659,26 +640,15 @@ class JavaScriptChannel extends JavaObject { class WebViewClient extends JavaObject { /// Constructs a [WebViewClient]. WebViewClient({ - this.shouldOverrideUrlLoading = true, - void Function(WebView webView, String url)? onPageStarted, - void Function(WebView webView, String url)? onPageFinished, - void Function( - WebView webView, - WebResourceRequest request, - WebResourceError error, - )? - onReceivedRequestError, - void Function( - WebView webView, - int errorCode, - String description, - String failingUrl, - )? - onReceivedError, - void Function(WebView webView, WebResourceRequest request)? requestLoading, - void Function(WebView webView, String url)? urlLoading, + this.onPageStarted, + this.onPageFinished, + this.onReceivedRequestError, + @Deprecated('Only called on Android version < 23.') this.onReceivedError, + this.requestLoading, + this.urlLoading, }) : super.detached() { AndroidWebViewFlutterApis.instance.ensureSetUp(); + api.createFromInstance(this); } /// Constructs a [WebViewClient] without creating the associated Java object. @@ -686,24 +656,12 @@ class WebViewClient extends JavaObject { /// This should only be used by subclasses created by this library or to /// create copies. WebViewClient.detached({ - this.shouldOverrideUrlLoading = true, - void Function(WebView webView, String url)? onPageStarted, - void Function(WebView webView, String url)? onPageFinished, - void Function( - WebView webView, - WebResourceRequest request, - WebResourceError error, - )? - onReceivedRequestError, - void Function( - WebView webView, - int errorCode, - String description, - String failingUrl, - )? - onReceivedError, - void Function(WebView webView, WebResourceRequest request)? requestLoading, - void Function(WebView webView, String url)? urlLoading, + this.onPageStarted, + this.onPageFinished, + this.onReceivedRequestError, + @Deprecated('Only called on Android version < 23.') this.onReceivedError, + this.requestLoading, + this.urlLoading, }) : super.detached(); /// User authentication failed on server. @@ -790,20 +748,6 @@ class WebViewClient extends JavaObject { @visibleForTesting static WebViewClientHostApiImpl api = WebViewClientHostApiImpl(); - /// Whether loading a url should be overridden. - /// - /// In Java, `shouldOverrideUrlLoading()` and `shouldOverrideRequestLoading()` - /// callbacks must synchronously return a boolean. This sets the default - /// return value. - /// - /// Setting [shouldOverrideUrlLoading] to true causes the current [WebView] to - /// abort loading the URL, while returning false causes the [WebView] to - /// continue loading the URL as usual. [requestLoading] or [urlLoading] will - /// still be called either way. - /// - /// Defaults to true. - final bool shouldOverrideUrlLoading; - /// Notify the host application that a page has started loading. /// /// This method is called once for each main frame load so a page with iframes @@ -812,7 +756,7 @@ class WebViewClient extends JavaObject { /// embedded frame changes, i.e. clicking a link whose target is an iframe, it /// will also not be called for fragment navigations (navigations to /// #fragment_id). - void onPageStarted(WebView webView, String url) {} + final void Function(WebView webView, String url)? onPageStarted; // TODO(bparrishMines): Update documentation when WebView.postVisualStateCallback is added. /// Notify the host application that a page has finished loading. @@ -820,7 +764,7 @@ class WebViewClient extends JavaObject { /// This method is called only for main frame. Receiving an [onPageFinished] /// callback does not guarantee that the next frame drawn by WebView will /// reflect the state of the DOM at this point. - void onPageFinished(WebView webView, String url) {} + final void Function(WebView webView, String url)? onPageFinished; /// Report web resource loading error to the host application. /// @@ -829,48 +773,58 @@ class WebViewClient extends JavaObject { /// be called for any resource (iframe, image, etc.), not just for the main /// page. Thus, it is recommended to perform minimum required work in this /// callback. - void onReceivedRequestError( + final void Function( WebView webView, WebResourceRequest request, WebResourceError error, - ) {} + )? onReceivedRequestError; /// Report an error to the host application. /// /// These errors are unrecoverable (i.e. the main resource is unavailable). /// The errorCode parameter corresponds to one of the error* constants. @Deprecated('Only called on Android version < 23.') - void onReceivedError( + final void Function( WebView webView, int errorCode, String description, String failingUrl, - ) {} + )? onReceivedError; - // TODO(bparrishMines): Update documentation once synchronous url handling is supported. - /// When a URL is about to be loaded in the current [WebView]. + /// When the current [WebView] wants to load a URL. /// - /// If a [WebViewClient] is not provided, by default [WebView] will ask - /// Activity Manager to choose the proper handler for the URL. If a - /// [WebViewClient] is provided, setting [shouldOverrideUrlLoading] to true - /// causes the current [WebView] to abort loading the URL, while returning - /// false causes the [WebView] to continue loading the URL as usual. - void requestLoading(WebView webView, WebResourceRequest request) {} + /// The value set by [setSynchronousReturnValueForShouldOverrideUrlLoading] + /// indicates whether the [WebView] loaded the request. + final void Function(WebView webView, WebResourceRequest request)? + requestLoading; - // TODO(bparrishMines): Update documentation once synchronous url handling is supported. - /// When a URL is about to be loaded in the current [WebView]. + /// When the current [WebView] wants to load a URL. /// - /// If a [WebViewClient] is not provided, by default [WebView] will ask - /// Activity Manager to choose the proper handler for the URL. If a - /// [WebViewClient] is provided, setting [shouldOverrideUrlLoading] to true - /// causes the current [WebView] to abort loading the URL, while returning - /// false causes the [WebView] to continue loading the URL as usual. - void urlLoading(WebView webView, String url) {} + /// The value set by [setSynchronousReturnValueForShouldOverrideUrlLoading] + /// indicates whether the [WebView] loaded the URL. + final void Function(WebView webView, String url)? urlLoading; + + /// Sets the required synchronous return value for the Java method, + /// `WebViewClient.shouldOverrideUrlLoading(...)`. + /// + /// The Java method, `WebViewClient.shouldOverrideUrlLoading(...)`, requires + /// a boolean to be returned and this method sets the returned value for all + /// calls to the Java method. + /// + /// Setting this to true causes the current [WebView] to abort loading any URL + /// received by [requestLoading] or [urlLoading], while setting this to false + /// causes the [WebView] to continue loading a URL as usual. + /// + /// Defaults to false. + Future setSynchronousReturnValueForShouldOverrideUrlLoading( + bool value, + ) { + return api.setShouldOverrideUrlLoadingReturnValueFromInstance(this, value); + } @override WebViewClient copy() { return WebViewClient.detached( - shouldOverrideUrlLoading: shouldOverrideUrlLoading, onPageStarted: onPageStarted, onPageFinished: onPageFinished, onReceivedRequestError: onReceivedRequestError, @@ -885,17 +839,9 @@ class WebViewClient extends JavaObject { /// engine for [WebView], and should be downloaded instead. class DownloadListener extends JavaObject { /// Constructs a [DownloadListener]. - DownloadListener({ - void Function( - String url, - String userAgent, - String contentDisposition, - String mimetype, - int contentLength, - )? - onDownloadStart, - }) : super.detached() { + DownloadListener({required this.onDownloadStart}) : super.detached() { AndroidWebViewFlutterApis.instance.ensureSetUp(); + api.createFromInstance(this); } /// Constructs a [DownloadListener] without creating the associated Java @@ -903,43 +849,34 @@ class DownloadListener extends JavaObject { /// /// This should only be used by subclasses created by this library or to /// create copies. - DownloadListener.detached({ - void Function( - String url, - String userAgent, - String contentDisposition, - String mimetype, - int contentLength, - )? - onDownloadStart, - }) : super.detached(); + DownloadListener.detached({required this.onDownloadStart}) : super.detached(); /// Pigeon Host Api implementation for [DownloadListener]. @visibleForTesting static DownloadListenerHostApiImpl api = DownloadListenerHostApiImpl(); /// Notify the host application that a file should be downloaded. - void onDownloadStart( + final void Function( String url, String userAgent, String contentDisposition, String mimetype, int contentLength, - ) {} + ) onDownloadStart; @override DownloadListener copy() { - return DownloadListener(onDownloadStart: onDownloadStart); + return DownloadListener.detached(onDownloadStart: onDownloadStart); } } /// Handles JavaScript dialogs, favicons, titles, and the progress for [WebView]. class WebChromeClient extends JavaObject { /// Constructs a [WebChromeClient]. - WebChromeClient({ - void Function(WebView webView, int progress)? onProgressChanged, - }) : super.detached() { + WebChromeClient({this.onProgressChanged, this.onShowFileChooser}) + : super.detached() { AndroidWebViewFlutterApis.instance.ensureSetUp(); + api.createFromInstance(this); } /// Constructs a [WebChromeClient] without creating the associated Java @@ -948,7 +885,8 @@ class WebChromeClient extends JavaObject { /// This should only be used by subclasses created by this library or to /// create copies. WebChromeClient.detached({ - void Function(WebView webView, int progress)? onProgressChanged, + this.onProgressChanged, + this.onShowFileChooser, }) : super.detached(); /// Pigeon Host Api implementation for [WebChromeClient]. @@ -956,11 +894,97 @@ class WebChromeClient extends JavaObject { static WebChromeClientHostApiImpl api = WebChromeClientHostApiImpl(); /// Notify the host application that a file should be downloaded. - void onProgressChanged(WebView webView, int progress) {} + final void Function(WebView webView, int progress)? onProgressChanged; + + /// Indicates the client should show a file chooser. + /// + /// To handle the request for a file chooser with this callback, passing true + /// to [setSynchronousReturnValueForOnShowFileChooser] is required. Otherwise, + /// the returned list of strings will be ignored and the client will use the + /// default handling of a file chooser request. + /// + /// Only invoked on Android versions 21+. + final Future> Function( + WebView webView, + FileChooserParams params, + )? onShowFileChooser; + + /// Sets the required synchronous return value for the Java method, + /// `WebChromeClient.onShowFileChooser(...)`. + /// + /// The Java method, `WebChromeClient.onShowFileChooser(...)`, requires + /// a boolean to be returned and this method sets the returned value for all + /// calls to the Java method. + /// + /// Setting this to true indicates that all file chooser requests should be + /// handled by [onShowFileChooser] and the returned list of Strings will be + /// returned to the WebView. Otherwise, the client will use the default + /// handling and the returned value in [onShowFileChooser] will be ignored. + /// + /// Requires [onShowFileChooser] to be nonnull. + /// + /// Defaults to false. + Future setSynchronousReturnValueForOnShowFileChooser( + bool value, + ) { + if (value && onShowFileChooser == null) { + throw StateError( + 'Setting this to true requires `onShowFileChooser` to be nonnull.', + ); + } + return api.setSynchronousReturnValueForOnShowFileChooserFromInstance( + this, + value, + ); + } @override WebChromeClient copy() { - return WebChromeClient.detached(onProgressChanged: onProgressChanged); + return WebChromeClient.detached( + onProgressChanged: onProgressChanged, + onShowFileChooser: onShowFileChooser, + ); + } +} + +/// Parameters received when a [WebChromeClient] should show a file chooser. +/// +/// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams. +class FileChooserParams extends JavaObject { + /// Constructs a [FileChooserParams] without creating the associated Java + /// object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies. + FileChooserParams.detached({ + required this.isCaptureEnabled, + required this.acceptTypes, + required this.filenameHint, + required this.mode, + super.binaryMessenger, + super.instanceManager, + }) : super.detached(); + + /// Preference for a live media captured value (e.g. Camera, Microphone). + final bool isCaptureEnabled; + + /// A list of acceptable MIME types. + final List acceptTypes; + + /// The file name of a default selection if specified, or null. + final String? filenameHint; + + /// Mode of how to select files for a file chooser. + final FileChooserMode mode; + + @override + FileChooserParams copy() { + return FileChooserParams.detached( + isCaptureEnabled: isCaptureEnabled, + acceptTypes: acceptTypes, + filenameHint: filenameHint, + mode: mode, + ); } } diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart similarity index 63% rename from packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart rename to packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart index e793dbb950a8..d3c306a10238 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.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 (v4.0.2), do not edit directly. +// Autogenerated from Pigeon (v4.2.14), 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 import 'dart:async'; @@ -10,6 +10,48 @@ import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; +/// Mode of how to select files for a file chooser. +/// +/// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams. +enum FileChooserMode { + /// Open single file and requires that the file exists before allowing the + /// user to pick it. + /// + /// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams#MODE_OPEN. + open, + + /// Similar to [open] but allows multiple files to be selected. + /// + /// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams#MODE_OPEN_MULTIPLE. + openMultiple, + + /// Allows picking a nonexistent file and saving it. + /// + /// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams#MODE_SAVE. + save, +} + +class FileChooserModeEnumData { + FileChooserModeEnumData({ + required this.value, + }); + + FileChooserMode value; + + Object encode() { + return [ + value.index, + ]; + } + + static FileChooserModeEnumData decode(Object result) { + result as List; + return FileChooserModeEnumData( + value: FileChooserMode.values[result[0]! as int], + ); + } +} + class WebResourceRequestData { WebResourceRequestData({ required this.url, @@ -21,33 +63,38 @@ class WebResourceRequestData { }); String url; + bool isForMainFrame; + bool? isRedirect; + bool hasGesture; + String method; + Map requestHeaders; Object encode() { - final Map pigeonMap = {}; - pigeonMap['url'] = url; - pigeonMap['isForMainFrame'] = isForMainFrame; - pigeonMap['isRedirect'] = isRedirect; - pigeonMap['hasGesture'] = hasGesture; - pigeonMap['method'] = method; - pigeonMap['requestHeaders'] = requestHeaders; - return pigeonMap; - } - - static WebResourceRequestData decode(Object message) { - final Map pigeonMap = message as Map; + return [ + url, + isForMainFrame, + isRedirect, + hasGesture, + method, + requestHeaders, + ]; + } + + static WebResourceRequestData decode(Object result) { + result as List; return WebResourceRequestData( - url: pigeonMap['url']! as String, - isForMainFrame: pigeonMap['isForMainFrame']! as bool, - isRedirect: pigeonMap['isRedirect'] as bool?, - hasGesture: pigeonMap['hasGesture']! as bool, - method: pigeonMap['method']! as String, - requestHeaders: (pigeonMap['requestHeaders'] as Map?)! - .cast(), + url: result[0]! as String, + isForMainFrame: result[1]! as bool, + isRedirect: result[2] as bool?, + hasGesture: result[3]! as bool, + method: result[4]! as String, + requestHeaders: + (result[5] as Map?)!.cast(), ); } } @@ -59,20 +106,21 @@ class WebResourceErrorData { }); int errorCode; + String description; Object encode() { - final Map pigeonMap = {}; - pigeonMap['errorCode'] = errorCode; - pigeonMap['description'] = description; - return pigeonMap; + return [ + errorCode, + description, + ]; } - static WebResourceErrorData decode(Object message) { - final Map pigeonMap = message as Map; + static WebResourceErrorData decode(Object result) { + result as List; return WebResourceErrorData( - errorCode: pigeonMap['errorCode']! as int, - description: pigeonMap['description']! as String, + errorCode: result[0]! as int, + description: result[1]! as String, ); } } @@ -84,57 +132,56 @@ class WebViewPoint { }); int x; + int y; Object encode() { - final Map pigeonMap = {}; - pigeonMap['x'] = x; - pigeonMap['y'] = y; - return pigeonMap; + return [ + x, + y, + ]; } - static WebViewPoint decode(Object message) { - final Map pigeonMap = message as Map; + static WebViewPoint decode(Object result) { + result as List; return WebViewPoint( - x: pigeonMap['x']! as int, - y: pigeonMap['y']! as int, + x: result[0]! as int, + y: result[1]! as int, ); } } -class _JavaObjectHostApiCodec extends StandardMessageCodec { - const _JavaObjectHostApiCodec(); -} - +/// Handles methods calls to the native Java Object class. +/// +/// Also handles calls to remove the reference to an instance with `dispose`. +/// +/// See https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html. class JavaObjectHostApi { /// Constructor for [JavaObjectHostApi]. 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. JavaObjectHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _JavaObjectHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future dispose(int arg_identifier) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.JavaObjectHostApi.dispose', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -142,14 +189,14 @@ class JavaObjectHostApi { } } -class _JavaObjectFlutterApiCodec extends StandardMessageCodec { - const _JavaObjectFlutterApiCodec(); -} - +/// Handles callbacks methods for the native Java Object class. +/// +/// See https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html. abstract class JavaObjectFlutterApi { - static const MessageCodec codec = _JavaObjectFlutterApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void dispose(int identifier); + static void setup(JavaObjectFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -174,47 +221,39 @@ abstract class JavaObjectFlutterApi { } } -class _CookieManagerHostApiCodec extends StandardMessageCodec { - const _CookieManagerHostApiCodec(); -} - class CookieManagerHostApi { /// Constructor for [CookieManagerHostApi]. 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. CookieManagerHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _CookieManagerHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future clearCookies() async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.CookieManagerHostApi.clearCookies', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + final List? replyList = await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as bool?)!; + return (replyList[0] as bool?)!; } } @@ -222,20 +261,18 @@ class CookieManagerHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.CookieManagerHostApi.setCookie', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_url, arg_value]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_url, arg_value]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -273,7 +310,6 @@ class WebViewHostApi { /// BinaryMessenger will be used which routes to the host platform. WebViewHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _WebViewHostApiCodec(); @@ -282,45 +318,19 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.create', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = + final List? replyList = await channel.send([arg_instanceId, arg_useHybridComposition]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } - - Future dispose(int arg_instanceId) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.dispose', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -332,21 +342,19 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.loadData', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel.send( + final List? replyList = await channel.send( [arg_instanceId, arg_data, arg_mimeType, arg_encoding]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -363,26 +371,24 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel.send([ + final List? replyList = await channel.send([ arg_instanceId, arg_baseUrl, arg_data, arg_mimeType, arg_encoding, arg_historyUrl - ]) as Map?; - if (replyMap == null) { + ]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -394,21 +400,19 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.loadUrl', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = + final List? replyList = await channel.send([arg_instanceId, arg_url, arg_headers]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -420,21 +424,18 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.postUrl', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId, arg_url, arg_data]) - as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_url, arg_data]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -445,23 +446,21 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.getUrl', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { - return (replyMap['result'] as String?); + return (replyList[0] as String?); } } @@ -469,28 +468,26 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.canGoBack', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as bool?)!; + return (replyList[0] as bool?)!; } } @@ -498,28 +495,26 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.canGoForward', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as bool?)!; + return (replyList[0] as bool?)!; } } @@ -527,20 +522,18 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.goBack', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -551,20 +544,18 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.goForward', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -575,20 +566,18 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.reload', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -599,21 +588,19 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.clearCache', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = + final List? replyList = await channel.send([arg_instanceId, arg_includeDiskFiles]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -625,24 +612,22 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.evaluateJavascript', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = + final List? replyList = await channel.send([arg_instanceId, arg_javascriptString]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { - return (replyMap['result'] as String?); + return (replyList[0] as String?); } } @@ -650,23 +635,21 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.getTitle', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { - return (replyMap['result'] as String?); + return (replyList[0] as String?); } } @@ -674,21 +657,18 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.scrollTo', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId, arg_x, arg_y]) - as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_x, arg_y]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -699,21 +679,18 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.scrollBy', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId, arg_x, arg_y]) - as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_x, arg_y]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -724,28 +701,26 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.getScrollX', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as int?)!; + return (replyList[0] as int?)!; } } @@ -753,28 +728,26 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.getScrollY', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as int?)!; + return (replyList[0] as int?)!; } } @@ -782,28 +755,26 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.getScrollPosition', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as WebViewPoint?)!; + return (replyList[0] as WebViewPoint?)!; } } @@ -812,20 +783,18 @@ class WebViewHostApi { 'dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_enabled]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_enabled]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -837,21 +806,19 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.setWebViewClient', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel + final List? replyList = await channel .send([arg_instanceId, arg_webViewClientInstanceId]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -863,21 +830,19 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel + final List? replyList = await channel .send([arg_instanceId, arg_javaScriptChannelInstanceId]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -889,21 +854,19 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel + final List? replyList = await channel .send([arg_instanceId, arg_javaScriptChannelInstanceId]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -915,21 +878,19 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.setDownloadListener', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = + final List? replyList = await channel.send([arg_instanceId, arg_listenerInstanceId]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -941,21 +902,19 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.setWebChromeClient', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = + final List? replyList = await channel.send([arg_instanceId, arg_clientInstanceId]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -966,20 +925,18 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.setBackgroundColor', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_color]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_color]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -987,64 +944,33 @@ class WebViewHostApi { } } -class _WebSettingsHostApiCodec extends StandardMessageCodec { - const _WebSettingsHostApiCodec(); -} - class WebSettingsHostApi { /// Constructor for [WebSettingsHostApi]. 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. WebSettingsHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _WebSettingsHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future create(int arg_instanceId, int arg_webViewInstanceId) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.create', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = + final List? replyList = await channel.send([arg_instanceId, arg_webViewInstanceId]) - as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } - - Future dispose(int arg_instanceId) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.dispose', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1055,20 +981,18 @@ class WebSettingsHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_flag]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_flag]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1081,20 +1005,18 @@ class WebSettingsHostApi { 'dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_flag]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_flag]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1107,20 +1029,18 @@ class WebSettingsHostApi { 'dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_support]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_support]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1131,20 +1051,18 @@ class WebSettingsHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_flag]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_flag]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1156,21 +1074,18 @@ class WebSettingsHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId, arg_userAgentString]) - as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_userAgentString]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1183,20 +1098,18 @@ class WebSettingsHostApi { 'dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_require]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_require]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1207,20 +1120,18 @@ class WebSettingsHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_support]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_support]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1232,21 +1143,18 @@ class WebSettingsHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId, arg_overview]) - as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_overview]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1257,20 +1165,18 @@ class WebSettingsHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_use]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_use]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1282,20 +1188,18 @@ class WebSettingsHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_enabled]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_enabled]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1307,20 +1211,18 @@ class WebSettingsHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_enabled]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_enabled]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1331,20 +1233,18 @@ class WebSettingsHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_enabled]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_enabled]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1352,40 +1252,32 @@ class WebSettingsHostApi { } } -class _JavaScriptChannelHostApiCodec extends StandardMessageCodec { - const _JavaScriptChannelHostApiCodec(); -} - class JavaScriptChannelHostApi { /// Constructor for [JavaScriptChannelHostApi]. 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. JavaScriptChannelHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _JavaScriptChannelHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future create(int arg_instanceId, String arg_channelName) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.JavaScriptChannelHostApi.create', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId, arg_channelName]) - as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_channelName]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1393,37 +1285,13 @@ class JavaScriptChannelHostApi { } } -class _JavaScriptChannelFlutterApiCodec extends StandardMessageCodec { - const _JavaScriptChannelFlutterApiCodec(); -} - abstract class JavaScriptChannelFlutterApi { - static const MessageCodec codec = - _JavaScriptChannelFlutterApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); - void dispose(int instanceId); void postMessage(int instanceId, String message); + static void setup(JavaScriptChannelFlutterApi? api, {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.JavaScriptChannelFlutterApi.dispose', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.JavaScriptChannelFlutterApi.dispose 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.JavaScriptChannelFlutterApi.dispose was null, expected non-null int.'); - api.dispose(arg_instanceId!); - return; - }); - } - } { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.JavaScriptChannelFlutterApi.postMessage', codec, @@ -1449,41 +1317,56 @@ abstract class JavaScriptChannelFlutterApi { } } -class _WebViewClientHostApiCodec extends StandardMessageCodec { - const _WebViewClientHostApiCodec(); -} - class WebViewClientHostApi { /// Constructor for [WebViewClientHostApi]. 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. WebViewClientHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _WebViewClientHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); - Future create( - int arg_instanceId, bool arg_shouldOverrideUrlLoading) async { + Future create(int arg_instanceId) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewClientHostApi.create', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_shouldOverrideUrlLoading]) - as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) 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 setSynchronousReturnValueForShouldOverrideUrlLoading( + int arg_instanceId, bool arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientHostApi.setSynchronousReturnValueForShouldOverrideUrlLoading', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_instanceId, arg_value]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1524,37 +1407,23 @@ class _WebViewClientFlutterApiCodec extends StandardMessageCodec { abstract class WebViewClientFlutterApi { static const MessageCodec codec = _WebViewClientFlutterApiCodec(); - void dispose(int instanceId); void onPageStarted(int instanceId, int webViewInstanceId, String url); + void onPageFinished(int instanceId, int webViewInstanceId, String url); + void onReceivedRequestError(int instanceId, int webViewInstanceId, WebResourceRequestData request, WebResourceErrorData error); + void onReceivedError(int instanceId, int webViewInstanceId, int errorCode, String description, String failingUrl); + void requestLoading( int instanceId, int webViewInstanceId, WebResourceRequestData request); + void urlLoading(int instanceId, int webViewInstanceId, String url); + static void setup(WebViewClientFlutterApi? api, {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewClientFlutterApi.dispose', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewClientFlutterApi.dispose 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.WebViewClientFlutterApi.dispose was null, expected non-null int.'); - api.dispose(arg_instanceId!); - return; - }); - } - } { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewClientFlutterApi.onPageStarted', codec, @@ -1724,39 +1593,32 @@ abstract class WebViewClientFlutterApi { } } -class _DownloadListenerHostApiCodec extends StandardMessageCodec { - const _DownloadListenerHostApiCodec(); -} - class DownloadListenerHostApi { /// Constructor for [DownloadListenerHostApi]. 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. DownloadListenerHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _DownloadListenerHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future create(int arg_instanceId) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.DownloadListenerHostApi.create', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1764,37 +1626,14 @@ class DownloadListenerHostApi { } } -class _DownloadListenerFlutterApiCodec extends StandardMessageCodec { - const _DownloadListenerFlutterApiCodec(); -} - abstract class DownloadListenerFlutterApi { - static const MessageCodec codec = _DownloadListenerFlutterApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); - void dispose(int instanceId); void onDownloadStart(int instanceId, String url, String userAgent, String contentDisposition, String mimetype, int contentLength); + static void setup(DownloadListenerFlutterApi? api, {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DownloadListenerFlutterApi.dispose', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.DownloadListenerFlutterApi.dispose 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.DownloadListenerFlutterApi.dispose was null, expected non-null int.'); - api.dispose(arg_instanceId!); - return; - }); - } - } { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.DownloadListenerFlutterApi.onDownloadStart', @@ -1834,50 +1673,61 @@ abstract class DownloadListenerFlutterApi { } } -class _WebChromeClientHostApiCodec extends StandardMessageCodec { - const _WebChromeClientHostApiCodec(); -} - class WebChromeClientHostApi { /// Constructor for [WebChromeClientHostApi]. 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. WebChromeClientHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _WebChromeClientHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); - Future create( - int arg_instanceId, int arg_webViewClientInstanceId) async { + Future create(int arg_instanceId) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebChromeClientHostApi.create', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_webViewClientInstanceId]) - as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; } } -} -class _FlutterAssetManagerHostApiCodec extends StandardMessageCodec { - const _FlutterAssetManagerHostApiCodec(); + Future setSynchronousReturnValueForOnShowFileChooser( + int arg_instanceId, bool arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebChromeClientHostApi.setSynchronousReturnValueForOnShowFileChooser', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_instanceId, arg_value]) 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; + } + } } class FlutterAssetManagerHostApi { @@ -1886,37 +1736,34 @@ class FlutterAssetManagerHostApi { /// BinaryMessenger will be used which routes to the host platform. FlutterAssetManagerHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _FlutterAssetManagerHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future> list(String arg_path) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.FlutterAssetManagerHostApi.list', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_path]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_path]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as List?)!.cast(); + return (replyList[0] as List?)!.cast(); } } @@ -1925,65 +1772,70 @@ class FlutterAssetManagerHostApi { 'dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_name]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_name]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as String?)!; + return (replyList[0] as String?)!; } } } -class _WebChromeClientFlutterApiCodec extends StandardMessageCodec { - const _WebChromeClientFlutterApiCodec(); -} - abstract class WebChromeClientFlutterApi { - static const MessageCodec codec = _WebChromeClientFlutterApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); - void dispose(int instanceId); void onProgressChanged(int instanceId, int webViewInstanceId, int progress); + + Future> onShowFileChooser( + int instanceId, int webViewInstanceId, int paramsInstanceId); + static void setup(WebChromeClientFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebChromeClientFlutterApi.dispose', codec, + 'dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged', + codec, binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.dispose was null.'); + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged 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.dispose was null, expected non-null int.'); - api.dispose(arg_instanceId!); + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged was null, expected non-null int.'); + final int? arg_webViewInstanceId = (args[1] as int?); + assert(arg_webViewInstanceId != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged was null, expected non-null int.'); + final int? arg_progress = (args[2] as int?); + assert(arg_progress != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged was null, expected non-null int.'); + api.onProgressChanged( + arg_instanceId!, arg_webViewInstanceId!, arg_progress!); return; }); } } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged', + 'dev.flutter.pigeon.WebChromeClientFlutterApi.onShowFileChooser', codec, binaryMessenger: binaryMessenger); if (api == null) { @@ -1991,59 +1843,52 @@ abstract class WebChromeClientFlutterApi { } else { channel.setMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged was null.'); + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onShowFileChooser 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.onProgressChanged was null, expected non-null int.'); + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onShowFileChooser was null, expected non-null int.'); final int? arg_webViewInstanceId = (args[1] as int?); assert(arg_webViewInstanceId != null, - 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged was null, expected non-null int.'); - final int? arg_progress = (args[2] as int?); - assert(arg_progress != null, - 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onProgressChanged was null, expected non-null int.'); - api.onProgressChanged( - arg_instanceId!, arg_webViewInstanceId!, arg_progress!); - return; + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onShowFileChooser was null, expected non-null int.'); + final int? arg_paramsInstanceId = (args[2] as int?); + assert(arg_paramsInstanceId != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onShowFileChooser was null, expected non-null int.'); + final List output = await api.onShowFileChooser( + arg_instanceId!, arg_webViewInstanceId!, arg_paramsInstanceId!); + return output; }); } } } } -class _WebStorageHostApiCodec extends StandardMessageCodec { - const _WebStorageHostApiCodec(); -} - class WebStorageHostApi { /// Constructor for [WebStorageHostApi]. 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. WebStorageHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _WebStorageHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future create(int arg_instanceId) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebStorageHostApi.create', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2054,23 +1899,92 @@ class WebStorageHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebStorageHostApi.deleteAllData', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; } } } + +class _FileChooserParamsFlutterApiCodec extends StandardMessageCodec { + const _FileChooserParamsFlutterApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is FileChooserModeEnumData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return FileChooserModeEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +/// Handles callbacks methods for the native Java FileChooserParams class. +/// +/// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams. +abstract class FileChooserParamsFlutterApi { + static const MessageCodec codec = + _FileChooserParamsFlutterApiCodec(); + + void create(int instanceId, bool isCaptureEnabled, List acceptTypes, + FileChooserModeEnumData mode, String? filenameHint); + + static void setup(FileChooserParamsFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FileChooserParamsFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.FileChooserParamsFlutterApi.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.FileChooserParamsFlutterApi.create was null, expected non-null int.'); + final bool? arg_isCaptureEnabled = (args[1] as bool?); + assert(arg_isCaptureEnabled != null, + 'Argument for dev.flutter.pigeon.FileChooserParamsFlutterApi.create was null, expected non-null bool.'); + final List? arg_acceptTypes = + (args[2] as List?)?.cast(); + assert(arg_acceptTypes != null, + 'Argument for dev.flutter.pigeon.FileChooserParamsFlutterApi.create was null, expected non-null List.'); + final FileChooserModeEnumData? arg_mode = + (args[3] as FileChooserModeEnumData?); + assert(arg_mode != null, + 'Argument for dev.flutter.pigeon.FileChooserParamsFlutterApi.create was null, expected non-null FileChooserModeEnumData.'); + final String? arg_filenameHint = (args[4] as String?); + api.create(arg_instanceId!, arg_isCaptureEnabled!, arg_acceptTypes!, + arg_mode!, arg_filenameHint); + 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 4f2c51c4a13a..127a2fa58ef8 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 @@ -10,9 +10,11 @@ import 'dart:ui'; import 'package:flutter/services.dart' show BinaryMessenger; import 'android_webview.dart'; -import 'android_webview.pigeon.dart'; +import 'android_webview.g.dart'; import 'instance_manager.dart'; +export 'android_webview.g.dart' show FileChooserMode; + /// Converts [WebResourceRequestData] to [WebResourceRequest] WebResourceRequest _toWebResourceRequest(WebResourceRequestData data) { return WebResourceRequest( @@ -42,6 +44,7 @@ class AndroidWebViewFlutterApis { WebViewClientFlutterApiImpl? webViewClientFlutterApi, WebChromeClientFlutterApiImpl? webChromeClientFlutterApi, JavaScriptChannelFlutterApiImpl? javaScriptChannelFlutterApi, + FileChooserParamsFlutterApiImpl? fileChooserParamsFlutterApi, }) { this.javaObjectFlutterApi = javaObjectFlutterApi ?? JavaObjectFlutterApiImpl(); @@ -53,6 +56,8 @@ class AndroidWebViewFlutterApis { webChromeClientFlutterApi ?? WebChromeClientFlutterApiImpl(); this.javaScriptChannelFlutterApi = javaScriptChannelFlutterApi ?? JavaScriptChannelFlutterApiImpl(); + this.fileChooserParamsFlutterApi = + fileChooserParamsFlutterApi ?? FileChooserParamsFlutterApiImpl(); } static bool _haveBeenSetUp = false; @@ -77,6 +82,9 @@ class AndroidWebViewFlutterApis { /// Flutter Api for [JavaScriptChannel]. late final JavaScriptChannelFlutterApiImpl javaScriptChannelFlutterApi; + /// Flutter Api for [FileChooserParams]. + late final FileChooserParamsFlutterApiImpl fileChooserParamsFlutterApi; + /// Ensures all the Flutter APIs have been setup to receive calls from native code. void ensureSetUp() { if (!_haveBeenSetUp) { @@ -85,6 +93,7 @@ class AndroidWebViewFlutterApis { WebViewClientFlutterApi.setup(webViewClientFlutterApi); WebChromeClientFlutterApi.setup(webChromeClientFlutterApi); JavaScriptChannelFlutterApi.setup(javaScriptChannelFlutterApi); + FileChooserParamsFlutterApi.setup(fileChooserParamsFlutterApi); _haveBeenSetUp = true; } } @@ -128,10 +137,9 @@ class JavaObjectFlutterApiImpl implements JavaObjectFlutterApi { class WebViewHostApiImpl extends WebViewHostApi { /// Constructs a [WebViewHostApiImpl]. WebViewHostApiImpl({ - BinaryMessenger? binaryMessenger, + super.binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, - super(binaryMessenger: binaryMessenger); + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; /// Maintains instances stored to communicate with java objects. final InstanceManager instanceManager; @@ -144,15 +152,6 @@ class WebViewHostApiImpl extends WebViewHostApi { ); } - /// Helper method to convert instances ids to objects. - Future disposeFromInstance(WebView instance) async { - final int? instanceId = instanceManager.getIdentifier(instance); - if (instanceId != null) { - instanceManager.remove(instanceId); - await dispose(instanceId); - } - } - /// Helper method to convert the instances ids to objects. Future loadDataFromInstance( WebView instance, @@ -351,10 +350,9 @@ class WebViewHostApiImpl extends WebViewHostApi { class WebSettingsHostApiImpl extends WebSettingsHostApi { /// Constructs a [WebSettingsHostApiImpl]. WebSettingsHostApiImpl({ - BinaryMessenger? binaryMessenger, + super.binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, - super(binaryMessenger: binaryMessenger); + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; /// Maintains instances stored to communicate with java objects. final InstanceManager instanceManager; @@ -367,15 +365,6 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { ); } - /// Helper method to convert instances ids to objects. - Future disposeFromInstance(WebSettings instance) async { - final int? instanceId = instanceManager.getIdentifier(instance); - if (instanceId != null) { - instanceManager.remove(instanceId); - return dispose(instanceId); - } - } - /// Helper method to convert instances ids to objects. Future setDomStorageEnabledFromInstance( WebSettings instance, @@ -502,10 +491,9 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { class JavaScriptChannelHostApiImpl extends JavaScriptChannelHostApi { /// Constructs a [JavaScriptChannelHostApiImpl]. JavaScriptChannelHostApiImpl({ - BinaryMessenger? binaryMessenger, + super.binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, - super(binaryMessenger: binaryMessenger); + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; /// Maintains instances stored to communicate with java objects. final InstanceManager instanceManager; @@ -531,11 +519,6 @@ class JavaScriptChannelFlutterApiImpl extends JavaScriptChannelFlutterApi { /// Maintains instances stored to communicate with java objects. final InstanceManager instanceManager; - @override - void dispose(int instanceId) { - instanceManager.remove(instanceId); - } - @override void postMessage(int instanceId, String message) { final JavaScriptChannel? instance = instanceManager @@ -552,10 +535,9 @@ class JavaScriptChannelFlutterApiImpl extends JavaScriptChannelFlutterApi { class WebViewClientHostApiImpl extends WebViewClientHostApi { /// Constructs a [WebViewClientHostApiImpl]. WebViewClientHostApiImpl({ - BinaryMessenger? binaryMessenger, + super.binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, - super(binaryMessenger: binaryMessenger); + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; /// Maintains instances stored to communicate with java objects. final InstanceManager instanceManager; @@ -564,9 +546,20 @@ class WebViewClientHostApiImpl extends WebViewClientHostApi { Future createFromInstance(WebViewClient instance) async { if (instanceManager.getIdentifier(instance) == null) { final int identifier = instanceManager.addDartCreatedInstance(instance); - return create(identifier, instance.shouldOverrideUrlLoading); + return create(identifier); } } + + /// Helper method to convert instances ids to objects. + Future setShouldOverrideUrlLoadingReturnValueFromInstance( + WebViewClient instance, + bool value, + ) { + return setSynchronousReturnValueForShouldOverrideUrlLoading( + instanceManager.getIdentifier(instance)!, + value, + ); + } } /// Flutter api implementation for [WebViewClient]. @@ -578,11 +571,6 @@ class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { /// Maintains instances stored to communicate with java objects. final InstanceManager instanceManager; - @override - void dispose(int instanceId) { - instanceManager.remove(instanceId); - } - @override void onPageFinished(int instanceId, int webViewInstanceId, String url) { final WebViewClient? instance = instanceManager @@ -597,7 +585,9 @@ class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { webViewInstance != null, 'InstanceManager does not contain an WebView with instanceId: $webViewInstanceId', ); - instance!.onPageFinished(webViewInstance!, url); + if (instance!.onPageFinished != null) { + instance.onPageFinished!(webViewInstance!, url); + } } @override @@ -614,7 +604,9 @@ class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { webViewInstance != null, 'InstanceManager does not contain an WebView with instanceId: $webViewInstanceId', ); - instance!.onPageStarted(webViewInstance!, url); + if (instance!.onPageStarted != null) { + instance.onPageStarted!(webViewInstance!, url); + } } @override @@ -638,12 +630,14 @@ class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { 'InstanceManager does not contain an WebView with instanceId: $webViewInstanceId', ); // ignore: deprecated_member_use_from_same_package - instance!.onReceivedError( - webViewInstance!, - errorCode, - description, - failingUrl, - ); + if (instance!.onReceivedError != null) { + instance.onReceivedError!( + webViewInstance!, + errorCode, + description, + failingUrl, + ); + } } @override @@ -665,11 +659,13 @@ class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { webViewInstance != null, 'InstanceManager does not contain an WebView with instanceId: $webViewInstanceId', ); - instance!.onReceivedRequestError( - webViewInstance!, - _toWebResourceRequest(request), - _toWebResourceError(error), - ); + if (instance!.onReceivedRequestError != null) { + instance.onReceivedRequestError!( + webViewInstance!, + _toWebResourceRequest(request), + _toWebResourceError(error), + ); + } } @override @@ -690,7 +686,12 @@ class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { webViewInstance != null, 'InstanceManager does not contain an WebView with instanceId: $webViewInstanceId', ); - instance!.requestLoading(webViewInstance!, _toWebResourceRequest(request)); + if (instance!.requestLoading != null) { + instance.requestLoading!( + webViewInstance!, + _toWebResourceRequest(request), + ); + } } @override @@ -711,7 +712,9 @@ class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { webViewInstance != null, 'InstanceManager does not contain an WebView with instanceId: $webViewInstanceId', ); - instance!.urlLoading(webViewInstance!, url); + if (instance!.urlLoading != null) { + instance.urlLoading!(webViewInstance!, url); + } } } @@ -719,10 +722,9 @@ class WebViewClientFlutterApiImpl extends WebViewClientFlutterApi { class DownloadListenerHostApiImpl extends DownloadListenerHostApi { /// Constructs a [DownloadListenerHostApiImpl]. DownloadListenerHostApiImpl({ - BinaryMessenger? binaryMessenger, + super.binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, - super(binaryMessenger: binaryMessenger); + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; /// Maintains instances stored to communicate with java objects. final InstanceManager instanceManager; @@ -745,11 +747,6 @@ class DownloadListenerFlutterApiImpl extends DownloadListenerFlutterApi { /// Maintains instances stored to communicate with java objects. final InstanceManager instanceManager; - @override - void dispose(int instanceId) { - instanceManager.remove(instanceId); - } - @override void onDownloadStart( int instanceId, @@ -779,27 +776,31 @@ class DownloadListenerFlutterApiImpl extends DownloadListenerFlutterApi { class WebChromeClientHostApiImpl extends WebChromeClientHostApi { /// Constructs a [WebChromeClientHostApiImpl]. WebChromeClientHostApiImpl({ - BinaryMessenger? binaryMessenger, + super.binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, - super(binaryMessenger: binaryMessenger); + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; /// Maintains instances stored to communicate with java objects. final InstanceManager instanceManager; /// Helper method to convert instances ids to objects. - Future createFromInstance( - WebChromeClient instance, - WebViewClient webViewClient, - ) async { + Future createFromInstance(WebChromeClient instance) async { if (instanceManager.getIdentifier(instance) == null) { final int identifier = instanceManager.addDartCreatedInstance(instance); - return create( - identifier, - instanceManager.getIdentifier(webViewClient)!, - ); + return create(identifier); } } + + /// Helper method to convert instances ids to objects. + Future setSynchronousReturnValueForOnShowFileChooserFromInstance( + WebChromeClient instance, + bool value, + ) { + return setSynchronousReturnValueForOnShowFileChooser( + instanceManager.getIdentifier(instance)!, + value, + ); + } } /// Flutter api implementation for [DownloadListener]. @@ -811,11 +812,6 @@ class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { /// Maintains instances stored to communicate with java objects. final InstanceManager instanceManager; - @override - void dispose(int instanceId) { - instanceManager.remove(instanceId); - } - @override void onProgressChanged(int instanceId, int webViewInstanceId, int progress) { final WebChromeClient? instance = instanceManager @@ -830,7 +826,29 @@ class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { webViewInstance != null, 'InstanceManager does not contain an WebView with instanceId: $webViewInstanceId', ); - instance!.onProgressChanged(webViewInstance!, progress); + if (instance!.onProgressChanged != null) { + instance.onProgressChanged!(webViewInstance!, progress); + } + } + + @override + Future> onShowFileChooser( + int instanceId, + int webViewInstanceId, + int paramsInstanceId, + ) { + final WebChromeClient instance = + instanceManager.getInstanceWithWeakReference(instanceId)!; + if (instance.onShowFileChooser != null) { + return instance.onShowFileChooser!( + instanceManager.getInstanceWithWeakReference(webViewInstanceId)! + as WebView, + instanceManager.getInstanceWithWeakReference(paramsInstanceId)! + as FileChooserParams, + ); + } + + return Future>.value(const []); } } @@ -838,10 +856,9 @@ class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { class WebStorageHostApiImpl extends WebStorageHostApi { /// Constructs a [WebStorageHostApiImpl]. WebStorageHostApiImpl({ - BinaryMessenger? binaryMessenger, + super.binaryMessenger, InstanceManager? instanceManager, - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, - super(binaryMessenger: binaryMessenger); + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; /// Maintains instances stored to communicate with java objects. final InstanceManager instanceManager; @@ -859,3 +876,32 @@ class WebStorageHostApiImpl extends WebStorageHostApi { return deleteAllData(instanceManager.getIdentifier(instance)!); } } + +/// Flutter api implementation for [FileChooserParams]. +class FileChooserParamsFlutterApiImpl extends FileChooserParamsFlutterApi { + /// Constructs a [FileChooserParamsFlutterApiImpl]. + FileChooserParamsFlutterApiImpl({InstanceManager? instanceManager}) + : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Maintains instances stored to communicate with java objects. + final InstanceManager instanceManager; + + @override + void create( + int instanceId, + bool isCaptureEnabled, + List acceptTypes, + FileChooserModeEnumData mode, + String? filenameHint, + ) { + instanceManager.addHostCreatedInstance( + FileChooserParams.detached( + isCaptureEnabled: isCaptureEnabled, + acceptTypes: acceptTypes.cast(), + mode: mode.value, + filenameHint: filenameHint, + ), + instanceId, + ); + } +} 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 new file mode 100644 index 000000000000..6bd3dc03746c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart @@ -0,0 +1,914 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231) +// ignore: unnecessary_import +import 'dart:async'; + +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231) +// ignore: unnecessary_import +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'android_proxy.dart'; +import 'android_webview.dart' as android_webview; +import 'instance_manager.dart'; +import 'platform_views_service_proxy.dart'; +import 'weak_reference_utils.dart'; + +/// Object specifying creation parameters for creating a [AndroidWebViewController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebViewControllerCreationParams] for +/// more information. +@immutable +class AndroidWebViewControllerCreationParams + extends PlatformWebViewControllerCreationParams { + /// Creates a new [AndroidWebViewControllerCreationParams] instance. + AndroidWebViewControllerCreationParams({ + @visibleForTesting this.androidWebViewProxy = const AndroidWebViewProxy(), + @visibleForTesting android_webview.WebStorage? androidWebStorage, + }) : androidWebStorage = + androidWebStorage ?? android_webview.WebStorage.instance, + super(); + + /// Creates a [AndroidWebViewControllerCreationParams] instance based on [PlatformWebViewControllerCreationParams]. + factory AndroidWebViewControllerCreationParams.fromPlatformWebViewControllerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebViewControllerCreationParams params, { + @visibleForTesting + AndroidWebViewProxy androidWebViewProxy = const AndroidWebViewProxy(), + @visibleForTesting android_webview.WebStorage? androidWebStorage, + }) { + return AndroidWebViewControllerCreationParams( + androidWebViewProxy: androidWebViewProxy, + androidWebStorage: + androidWebStorage ?? android_webview.WebStorage.instance, + ); + } + + /// Handles constructing objects and calling static methods for the Android WebView + /// native library. + @visibleForTesting + final AndroidWebViewProxy androidWebViewProxy; + + /// Manages the JavaScript storage APIs provided by the [android_webview.WebView]. + @visibleForTesting + final android_webview.WebStorage androidWebStorage; +} + +/// Implementation of the [PlatformWebViewController] with the Android WebView API. +class AndroidWebViewController extends PlatformWebViewController { + /// Creates a new [AndroidWebViewCookieManager]. + AndroidWebViewController(PlatformWebViewControllerCreationParams params) + : super.implementation(params is AndroidWebViewControllerCreationParams + ? params + : AndroidWebViewControllerCreationParams + .fromPlatformWebViewControllerCreationParams(params)) { + _webView.settings.setDomStorageEnabled(true); + _webView.settings.setJavaScriptCanOpenWindowsAutomatically(true); + _webView.settings.setSupportMultipleWindows(true); + _webView.settings.setLoadWithOverviewMode(true); + _webView.settings.setUseWideViewPort(true); + _webView.settings.setDisplayZoomControls(false); + _webView.settings.setBuiltInZoomControls(true); + + _webView.setWebChromeClient(_webChromeClient); + } + + AndroidWebViewControllerCreationParams get _androidWebViewParams => + params as AndroidWebViewControllerCreationParams; + + /// The native [android_webview.WebView] being controlled. + late final android_webview.WebView _webView = + _androidWebViewParams.androidWebViewProxy.createAndroidWebView( + // Due to changes in Flutter 3.0 the `useHybridComposition` doesn't have + // any effect and is purposefully not exposed publicly by the + // [AndroidWebViewController]. More info here: + // https://github.com/flutter/flutter/issues/108106 + useHybridComposition: true, + ); + + late final android_webview.WebChromeClient _webChromeClient = + _androidWebViewParams.androidWebViewProxy.createAndroidWebChromeClient( + onProgressChanged: withWeakReferenceTo(this, + (WeakReference weakReference) { + return (android_webview.WebView webView, int progress) { + if (weakReference.target?._currentNavigationDelegate?._onProgress != + null) { + weakReference + .target!._currentNavigationDelegate!._onProgress!(progress); + } + }; + }), + onShowFileChooser: withWeakReferenceTo(this, + (WeakReference weakReference) { + return (android_webview.WebView webView, + android_webview.FileChooserParams params) async { + if (weakReference.target?._onShowFileSelectorCallback != null) { + return weakReference.target!._onShowFileSelectorCallback!( + FileSelectorParams._fromFileChooserParams(params), + ); + } + return []; + }; + }), + ); + + /// The native [android_webview.FlutterAssetManager] allows managing assets. + late final android_webview.FlutterAssetManager _flutterAssetManager = + _androidWebViewParams.androidWebViewProxy.createFlutterAssetManager(); + + final Map _javaScriptChannelParams = + {}; + + AndroidNavigationDelegate? _currentNavigationDelegate; + + Future> Function(FileSelectorParams)? + _onShowFileSelectorCallback; + + /// Whether to enable the platform's webview content debugging tools. + /// + /// Defaults to false. + static Future enableDebugging( + bool enabled, { + @visibleForTesting + AndroidWebViewProxy webViewProxy = const AndroidWebViewProxy(), + }) { + return webViewProxy.setWebContentsDebuggingEnabled(enabled); + } + + /// Identifier used to retrieve the underlying native `WKWebView`. + /// + /// This is typically used by other plugins to retrieve the native `WebView` + /// from an `InstanceManager`. + /// + /// See Java method `WebViewFlutterPlugin.getWebView`. + int get webViewIdentifier => + // ignore: invalid_use_of_visible_for_testing_member + android_webview.WebView.api.instanceManager.getIdentifier(_webView)!; + + @override + Future loadFile( + String absoluteFilePath, + ) { + final String url = absoluteFilePath.startsWith('file://') + ? absoluteFilePath + : Uri.file(absoluteFilePath).toString(); + + _webView.settings.setAllowFileAccess(true); + return _webView.loadUrl(url, {}); + } + + @override + Future loadFlutterAsset( + String key, + ) async { + final String assetFilePath = + await _flutterAssetManager.getAssetFilePathByName(key); + final List pathElements = assetFilePath.split('/'); + final String fileName = pathElements.removeLast(); + final List paths = + await _flutterAssetManager.list(pathElements.join('/')); + + if (!paths.contains(fileName)) { + throw ArgumentError( + 'Asset for key "$key" not found.', + 'key', + ); + } + + return _webView.loadUrl( + Uri.file('/android_asset/$assetFilePath').toString(), + {}, + ); + } + + @override + Future loadHtmlString( + String html, { + String? baseUrl, + }) { + return _webView.loadDataWithBaseUrl( + baseUrl: baseUrl, + data: html, + mimeType: 'text/html', + ); + } + + @override + Future loadRequest( + LoadRequestParams params, + ) { + if (!params.uri.hasScheme) { + throw ArgumentError('WebViewRequest#uri is required to have a scheme.'); + } + switch (params.method) { + case LoadRequestMethod.get: + return _webView.loadUrl(params.uri.toString(), params.headers); + case LoadRequestMethod.post: + return _webView.postUrl( + params.uri.toString(), params.body ?? Uint8List(0)); + } + // The enum comes from a different package, which could get a new value at + // any time, so a fallback case is necessary. Since there is no reasonable + // default behavior, throw to alert the client that they need an updated + // version. This is deliberately outside the switch rather than a `default` + // so that the linter will flag the switch as needing an update. + // ignore: dead_code + throw UnimplementedError( + 'This version of `AndroidWebViewController` currently has no ' + 'implementation for HTTP method ${params.method.serialize()} in ' + 'loadRequest.'); + } + + @override + Future currentUrl() => _webView.getUrl(); + + @override + Future canGoBack() => _webView.canGoBack(); + + @override + Future canGoForward() => _webView.canGoForward(); + + @override + Future goBack() => _webView.goBack(); + + @override + Future goForward() => _webView.goForward(); + + @override + Future reload() => _webView.reload(); + + @override + Future clearCache() => _webView.clearCache(true); + + @override + Future clearLocalStorage() => + _androidWebViewParams.androidWebStorage.deleteAllData(); + + @override + Future setPlatformNavigationDelegate( + covariant AndroidNavigationDelegate handler) async { + _currentNavigationDelegate = handler; + handler.setOnLoadRequest(loadRequest); + _webView.setWebViewClient(handler.androidWebViewClient); + _webView.setDownloadListener(handler.androidDownloadListener); + } + + @override + Future runJavaScript(String javaScript) { + return _webView.evaluateJavascript(javaScript); + } + + @override + Future runJavaScriptReturningResult(String javaScript) async { + final String? result = await _webView.evaluateJavascript(javaScript); + + if (result == null) { + return ''; + } else if (result == 'true') { + return true; + } else if (result == 'false') { + return false; + } + + return num.tryParse(result) ?? result; + } + + @override + Future addJavaScriptChannel( + JavaScriptChannelParams javaScriptChannelParams, + ) { + final AndroidJavaScriptChannelParams androidJavaScriptParams = + javaScriptChannelParams is AndroidJavaScriptChannelParams + ? javaScriptChannelParams + : AndroidJavaScriptChannelParams.fromJavaScriptChannelParams( + javaScriptChannelParams); + + // When JavaScript channel with the same name exists make sure to remove it + // before registering the new channel. + if (_javaScriptChannelParams.containsKey(androidJavaScriptParams.name)) { + _webView + .removeJavaScriptChannel(androidJavaScriptParams._javaScriptChannel); + } + + _javaScriptChannelParams[androidJavaScriptParams.name] = + androidJavaScriptParams; + + return _webView + .addJavaScriptChannel(androidJavaScriptParams._javaScriptChannel); + } + + @override + Future removeJavaScriptChannel(String javaScriptChannelName) async { + final AndroidJavaScriptChannelParams? javaScriptChannelParams = + _javaScriptChannelParams[javaScriptChannelName]; + if (javaScriptChannelParams == null) { + return; + } + + _javaScriptChannelParams.remove(javaScriptChannelName); + return _webView + .removeJavaScriptChannel(javaScriptChannelParams._javaScriptChannel); + } + + @override + Future getTitle() => _webView.getTitle(); + + @override + Future scrollTo(int x, int y) => _webView.scrollTo(x, y); + + @override + Future scrollBy(int x, int y) => _webView.scrollBy(x, y); + + @override + Future getScrollPosition() { + return _webView.getScrollPosition(); + } + + @override + Future enableZoom(bool enabled) => + _webView.settings.setSupportZoom(enabled); + + @override + Future setBackgroundColor(Color color) => + _webView.setBackgroundColor(color); + + @override + Future setJavaScriptMode(JavaScriptMode javaScriptMode) => + _webView.settings + .setJavaScriptEnabled(javaScriptMode == JavaScriptMode.unrestricted); + + @override + Future setUserAgent(String? userAgent) => + _webView.settings.setUserAgentString(userAgent); + + /// Sets the restrictions that apply on automatic media playback. + Future setMediaPlaybackRequiresUserGesture(bool require) { + return _webView.settings.setMediaPlaybackRequiresUserGesture(require); + } + + /// Sets the callback that is invoked when the client should show a file + /// selector. + Future setOnShowFileSelector( + Future> Function(FileSelectorParams params)? + onShowFileSelector, + ) { + _onShowFileSelectorCallback = onShowFileSelector; + return _webChromeClient.setSynchronousReturnValueForOnShowFileChooser( + onShowFileSelector != null, + ); + } +} + +/// Mode of how to select files for a file chooser. +enum FileSelectorMode { + /// Open single file and requires that the file exists before allowing the + /// user to pick it. + open, + + /// Similar to [open] but allows multiple files to be selected. + openMultiple, + + /// Allows picking a nonexistent file and saving it. + save, +} + +/// Parameters received when the `WebView` should show a file selector. +@immutable +class FileSelectorParams { + /// Constructs a [FileSelectorParams]. + const FileSelectorParams({ + required this.isCaptureEnabled, + required this.acceptTypes, + this.filenameHint, + required this.mode, + }); + + factory FileSelectorParams._fromFileChooserParams( + android_webview.FileChooserParams params, + ) { + final FileSelectorMode mode; + switch (params.mode) { + case android_webview.FileChooserMode.open: + mode = FileSelectorMode.open; + break; + case android_webview.FileChooserMode.openMultiple: + mode = FileSelectorMode.openMultiple; + break; + case android_webview.FileChooserMode.save: + mode = FileSelectorMode.save; + break; + } + + return FileSelectorParams( + isCaptureEnabled: params.isCaptureEnabled, + acceptTypes: params.acceptTypes, + mode: mode, + filenameHint: params.filenameHint, + ); + } + + /// Preference for a live media captured value (e.g. Camera, Microphone). + final bool isCaptureEnabled; + + /// A list of acceptable MIME types. + final List acceptTypes; + + /// The file name of a default selection if specified, or null. + final String? filenameHint; + + /// Mode of how to select files for a file selector. + final FileSelectorMode mode; +} + +/// An implementation of [JavaScriptChannelParams] with the Android WebView API. +/// +/// See [AndroidWebViewController.addJavaScriptChannel]. +@immutable +class AndroidJavaScriptChannelParams extends JavaScriptChannelParams { + /// Constructs a [AndroidJavaScriptChannelParams]. + AndroidJavaScriptChannelParams({ + required super.name, + required super.onMessageReceived, + @visibleForTesting + AndroidWebViewProxy webViewProxy = const AndroidWebViewProxy(), + }) : assert(name.isNotEmpty), + _javaScriptChannel = webViewProxy.createJavaScriptChannel( + name, + postMessage: withWeakReferenceTo( + onMessageReceived, + (WeakReference weakReference) { + return ( + String message, + ) { + if (weakReference.target != null) { + weakReference.target!( + JavaScriptMessage(message: message), + ); + } + }; + }, + ), + ); + + /// Constructs a [AndroidJavaScriptChannelParams] using a + /// [JavaScriptChannelParams]. + AndroidJavaScriptChannelParams.fromJavaScriptChannelParams( + JavaScriptChannelParams params, { + @visibleForTesting + AndroidWebViewProxy webViewProxy = const AndroidWebViewProxy(), + }) : this( + name: params.name, + onMessageReceived: params.onMessageReceived, + webViewProxy: webViewProxy, + ); + + final android_webview.JavaScriptChannel _javaScriptChannel; +} + +/// Object specifying creation parameters for creating a [AndroidWebViewWidget]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebViewWidgetCreationParams] for +/// more information. +@immutable +class AndroidWebViewWidgetCreationParams + extends PlatformWebViewWidgetCreationParams { + /// Creates [AndroidWebWidgetCreationParams]. + AndroidWebViewWidgetCreationParams({ + super.key, + required super.controller, + super.layoutDirection, + super.gestureRecognizers, + this.displayWithHybridComposition = false, + @visibleForTesting InstanceManager? instanceManager, + @visibleForTesting + this.platformViewsServiceProxy = const PlatformViewsServiceProxy(), + }) : instanceManager = + instanceManager ?? android_webview.JavaObject.globalInstanceManager; + + /// Constructs a [WebKitWebViewWidgetCreationParams] using a + /// [PlatformWebViewWidgetCreationParams]. + AndroidWebViewWidgetCreationParams.fromPlatformWebViewWidgetCreationParams( + PlatformWebViewWidgetCreationParams params, { + bool displayWithHybridComposition = false, + @visibleForTesting InstanceManager? instanceManager, + @visibleForTesting PlatformViewsServiceProxy platformViewsServiceProxy = + const PlatformViewsServiceProxy(), + }) : this( + key: params.key, + controller: params.controller, + layoutDirection: params.layoutDirection, + gestureRecognizers: params.gestureRecognizers, + displayWithHybridComposition: displayWithHybridComposition, + instanceManager: instanceManager, + platformViewsServiceProxy: platformViewsServiceProxy, + ); + + /// Maintains instances used to communicate with the native objects they + /// represent. + /// + /// This field is exposed for testing purposes only and should not be used + /// outside of tests. + @visibleForTesting + final InstanceManager instanceManager; + + /// Proxy that provides access to the platform views service. + /// + /// This service allows creating and controlling platform-specific views. + @visibleForTesting + final PlatformViewsServiceProxy platformViewsServiceProxy; + + /// Whether the [WebView] will be displayed using the Hybrid Composition + /// PlatformView implementation. + /// + /// For most use cases, this flag should be set to false. Hybrid Composition + /// can have performance costs but doesn't have the limitation of rendering to + /// an Android SurfaceTexture. See + /// * https://flutter.dev/docs/development/platform-integration/platform-views#performance + /// * https://github.com/flutter/flutter/issues/104889 + /// * https://github.com/flutter/flutter/issues/116954 + /// + /// Defaults to false. + final bool displayWithHybridComposition; +} + +/// An implementation of [PlatformWebViewWidget] with the Android WebView API. +class AndroidWebViewWidget extends PlatformWebViewWidget { + /// Constructs a [WebKitWebViewWidget]. + AndroidWebViewWidget(PlatformWebViewWidgetCreationParams params) + : super.implementation( + params is AndroidWebViewWidgetCreationParams + ? params + : AndroidWebViewWidgetCreationParams + .fromPlatformWebViewWidgetCreationParams(params), + ); + + AndroidWebViewWidgetCreationParams get _androidParams => + params as AndroidWebViewWidgetCreationParams; + + @override + Widget build(BuildContext context) { + return PlatformViewLink( + key: _androidParams.key, + viewType: 'plugins.flutter.io/webview', + surfaceFactory: ( + BuildContext context, + PlatformViewController controller, + ) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: _androidParams.gestureRecognizers, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + return _initAndroidView( + params, + displayWithHybridComposition: + _androidParams.displayWithHybridComposition, + ) + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..create(); + }, + ); + } + + AndroidViewController _initAndroidView( + PlatformViewCreationParams params, { + required bool displayWithHybridComposition, + }) { + if (displayWithHybridComposition) { + return _androidParams.platformViewsServiceProxy.initExpensiveAndroidView( + id: params.id, + viewType: 'plugins.flutter.io/webview', + layoutDirection: _androidParams.layoutDirection, + creationParams: _androidParams.instanceManager.getIdentifier( + (_androidParams.controller as AndroidWebViewController)._webView), + creationParamsCodec: const StandardMessageCodec(), + ); + } else { + return _androidParams.platformViewsServiceProxy.initSurfaceAndroidView( + id: params.id, + viewType: 'plugins.flutter.io/webview', + layoutDirection: _androidParams.layoutDirection, + creationParams: _androidParams.instanceManager.getIdentifier( + (_androidParams.controller as AndroidWebViewController)._webView), + creationParamsCodec: const StandardMessageCodec(), + ); + } + } +} + +/// Signature for the `loadRequest` callback responsible for loading the [url] +/// after a navigation request has been approved. +typedef LoadRequestCallback = Future Function(LoadRequestParams params); + +/// Error returned in `WebView.onWebResourceError` when a web resource loading error has occurred. +@immutable +class AndroidWebResourceError extends WebResourceError { + /// Creates a new [AndroidWebResourceError]. + AndroidWebResourceError._({ + required super.errorCode, + required super.description, + super.isForMainFrame, + this.failingUrl, + }) : super( + errorType: _errorCodeToErrorType(errorCode), + ); + + /// Gets the URL for which the failing resource request was made. + final String? failingUrl; + + static WebResourceErrorType? _errorCodeToErrorType(int errorCode) { + switch (errorCode) { + case android_webview.WebViewClient.errorAuthentication: + return WebResourceErrorType.authentication; + case android_webview.WebViewClient.errorBadUrl: + return WebResourceErrorType.badUrl; + case android_webview.WebViewClient.errorConnect: + return WebResourceErrorType.connect; + case android_webview.WebViewClient.errorFailedSslHandshake: + return WebResourceErrorType.failedSslHandshake; + case android_webview.WebViewClient.errorFile: + return WebResourceErrorType.file; + case android_webview.WebViewClient.errorFileNotFound: + return WebResourceErrorType.fileNotFound; + case android_webview.WebViewClient.errorHostLookup: + return WebResourceErrorType.hostLookup; + case android_webview.WebViewClient.errorIO: + return WebResourceErrorType.io; + case android_webview.WebViewClient.errorProxyAuthentication: + return WebResourceErrorType.proxyAuthentication; + case android_webview.WebViewClient.errorRedirectLoop: + return WebResourceErrorType.redirectLoop; + case android_webview.WebViewClient.errorTimeout: + return WebResourceErrorType.timeout; + case android_webview.WebViewClient.errorTooManyRequests: + return WebResourceErrorType.tooManyRequests; + case android_webview.WebViewClient.errorUnknown: + return WebResourceErrorType.unknown; + case android_webview.WebViewClient.errorUnsafeResource: + return WebResourceErrorType.unsafeResource; + case android_webview.WebViewClient.errorUnsupportedAuthScheme: + return WebResourceErrorType.unsupportedAuthScheme; + case android_webview.WebViewClient.errorUnsupportedScheme: + return WebResourceErrorType.unsupportedScheme; + } + + throw ArgumentError( + 'Could not find a WebResourceErrorType for errorCode: $errorCode', + ); + } +} + +/// Object specifying creation parameters for creating a [AndroidNavigationDelegate]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformNavigationDelegateCreationParams] for +/// more information. +@immutable +class AndroidNavigationDelegateCreationParams + extends PlatformNavigationDelegateCreationParams { + /// Creates a new [AndroidNavigationDelegateCreationParams] instance. + const AndroidNavigationDelegateCreationParams._({ + @visibleForTesting this.androidWebViewProxy = const AndroidWebViewProxy(), + }) : super(); + + /// Creates a [AndroidNavigationDelegateCreationParams] instance based on [PlatformNavigationDelegateCreationParams]. + factory AndroidNavigationDelegateCreationParams.fromPlatformNavigationDelegateCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformNavigationDelegateCreationParams params, { + @visibleForTesting + AndroidWebViewProxy androidWebViewProxy = const AndroidWebViewProxy(), + }) { + return AndroidNavigationDelegateCreationParams._( + androidWebViewProxy: androidWebViewProxy, + ); + } + + /// Handles constructing objects and calling static methods for the Android WebView + /// native library. + @visibleForTesting + final AndroidWebViewProxy androidWebViewProxy; +} + +/// A place to register callback methods responsible to handle navigation events +/// triggered by the [android_webview.WebView]. +class AndroidNavigationDelegate extends PlatformNavigationDelegate { + /// Creates a new [AndroidNavigationDelegate]. + AndroidNavigationDelegate(PlatformNavigationDelegateCreationParams params) + : super.implementation(params is AndroidNavigationDelegateCreationParams + ? params + : AndroidNavigationDelegateCreationParams + .fromPlatformNavigationDelegateCreationParams(params)) { + final WeakReference weakThis = + WeakReference(this); + + _webViewClient = (this.params as AndroidNavigationDelegateCreationParams) + .androidWebViewProxy + .createAndroidWebViewClient( + onPageFinished: (android_webview.WebView webView, String url) { + if (weakThis.target?._onPageFinished != null) { + weakThis.target!._onPageFinished!(url); + } + }, + onPageStarted: (android_webview.WebView webView, String url) { + if (weakThis.target?._onPageStarted != null) { + weakThis.target!._onPageStarted!(url); + } + }, + onReceivedRequestError: ( + android_webview.WebView webView, + android_webview.WebResourceRequest request, + android_webview.WebResourceError error, + ) { + if (weakThis.target?._onWebResourceError != null) { + weakThis.target!._onWebResourceError!(AndroidWebResourceError._( + errorCode: error.errorCode, + description: error.description, + failingUrl: request.url, + isForMainFrame: request.isForMainFrame, + )); + } + }, + onReceivedError: ( + android_webview.WebView webView, + int errorCode, + String description, + String failingUrl, + ) { + if (weakThis.target?._onWebResourceError != null) { + weakThis.target!._onWebResourceError!(AndroidWebResourceError._( + errorCode: errorCode, + description: description, + failingUrl: failingUrl, + isForMainFrame: true, + )); + } + }, + requestLoading: ( + android_webview.WebView webView, + android_webview.WebResourceRequest request, + ) { + if (weakThis.target != null) { + weakThis.target!._handleNavigation( + request.url, + headers: request.requestHeaders, + isForMainFrame: request.isForMainFrame, + ); + } + }, + urlLoading: ( + android_webview.WebView webView, + String url, + ) { + if (weakThis.target != null) { + weakThis.target!._handleNavigation(url, isForMainFrame: true); + } + }, + ); + + _downloadListener = (this.params as AndroidNavigationDelegateCreationParams) + .androidWebViewProxy + .createDownloadListener( + onDownloadStart: ( + String url, + String userAgent, + String contentDisposition, + String mimetype, + int contentLength, + ) { + if (weakThis.target != null) { + weakThis.target?._handleNavigation(url, isForMainFrame: true); + } + }, + ); + } + + AndroidNavigationDelegateCreationParams get _androidParams => + params as AndroidNavigationDelegateCreationParams; + + late final android_webview.WebChromeClient _webChromeClient = + _androidParams.androidWebViewProxy.createAndroidWebChromeClient(); + + /// Gets the native [android_webview.WebChromeClient] that is bridged by this [AndroidNavigationDelegate]. + /// + /// Used by the [AndroidWebViewController] to set the `android_webview.WebView.setWebChromeClient`. + @Deprecated( + 'This value is not used by `AndroidWebViewController` and has no effect on the `WebView`.', + ) + android_webview.WebChromeClient get androidWebChromeClient => + _webChromeClient; + + late final android_webview.WebViewClient _webViewClient; + + /// Gets the native [android_webview.WebViewClient] that is bridged by this [AndroidNavigationDelegate]. + /// + /// Used by the [AndroidWebViewController] to set the `android_webview.WebView.setWebViewClient`. + android_webview.WebViewClient get androidWebViewClient => _webViewClient; + + late final android_webview.DownloadListener _downloadListener; + + /// Gets the native [android_webview.DownloadListener] that is bridged by this [AndroidNavigationDelegate]. + /// + /// Used by the [AndroidWebViewController] to set the `android_webview.WebView.setDownloadListener`. + android_webview.DownloadListener get androidDownloadListener => + _downloadListener; + + PageEventCallback? _onPageFinished; + PageEventCallback? _onPageStarted; + ProgressCallback? _onProgress; + WebResourceErrorCallback? _onWebResourceError; + NavigationRequestCallback? _onNavigationRequest; + LoadRequestCallback? _onLoadRequest; + + void _handleNavigation( + String url, { + required bool isForMainFrame, + Map headers = const {}, + }) { + final LoadRequestCallback? onLoadRequest = _onLoadRequest; + final NavigationRequestCallback? onNavigationRequest = _onNavigationRequest; + + if (onNavigationRequest == null || onLoadRequest == null) { + return; + } + + final FutureOr returnValue = onNavigationRequest( + NavigationRequest( + url: url, + isMainFrame: isForMainFrame, + ), + ); + + if (returnValue is NavigationDecision && + returnValue == NavigationDecision.navigate) { + onLoadRequest(LoadRequestParams( + uri: Uri.parse(url), + headers: headers, + )); + } else if (returnValue is Future) { + returnValue.then((NavigationDecision shouldLoadUrl) { + if (shouldLoadUrl == NavigationDecision.navigate) { + onLoadRequest(LoadRequestParams( + uri: Uri.parse(url), + headers: headers, + )); + } + }); + } + } + + /// Invoked when loading the url after a navigation request is approved. + Future setOnLoadRequest( + LoadRequestCallback onLoadRequest, + ) async { + _onLoadRequest = onLoadRequest; + } + + @override + Future setOnNavigationRequest( + NavigationRequestCallback onNavigationRequest, + ) async { + _onNavigationRequest = onNavigationRequest; + _webViewClient.setSynchronousReturnValueForShouldOverrideUrlLoading(true); + } + + @override + Future setOnPageStarted( + PageEventCallback onPageStarted, + ) async { + _onPageStarted = onPageStarted; + } + + @override + Future setOnPageFinished( + PageEventCallback onPageFinished, + ) async { + _onPageFinished = onPageFinished; + } + + @override + Future setOnProgress( + ProgressCallback onProgress, + ) async { + _onProgress = onProgress; + } + + @override + Future setOnWebResourceError( + WebResourceErrorCallback onWebResourceError, + ) async { + _onWebResourceError = onWebResourceError; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_cookie_manager.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_cookie_manager.dart new file mode 100644 index 000000000000..5174ca576088 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_cookie_manager.dart @@ -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. + +import 'package:flutter/foundation.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'android_webview.dart'; + +/// Object specifying creation parameters for creating a [AndroidWebViewCookieManager]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebViewCookieManagerCreationParams] for +/// more information. +@immutable +class AndroidWebViewCookieManagerCreationParams + extends PlatformWebViewCookieManagerCreationParams { + /// Creates a new [AndroidWebViewCookieManagerCreationParams] instance. + const AndroidWebViewCookieManagerCreationParams._( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformWebViewCookieManagerCreationParams params, + ) : super(); + + /// Creates a [AndroidWebViewCookieManagerCreationParams] instance based on [PlatformWebViewCookieManagerCreationParams]. + factory AndroidWebViewCookieManagerCreationParams.fromPlatformWebViewCookieManagerCreationParams( + PlatformWebViewCookieManagerCreationParams params) { + return AndroidWebViewCookieManagerCreationParams._(params); + } +} + +/// Handles all cookie operations for the Android platform. +class AndroidWebViewCookieManager extends PlatformWebViewCookieManager { + /// Creates a new [AndroidWebViewCookieManager]. + AndroidWebViewCookieManager( + PlatformWebViewCookieManagerCreationParams params, { + CookieManager? cookieManager, + }) : _cookieManager = cookieManager ?? CookieManager.instance, + super.implementation( + params is AndroidWebViewCookieManagerCreationParams + ? params + : AndroidWebViewCookieManagerCreationParams + .fromPlatformWebViewCookieManagerCreationParams(params), + ); + + final CookieManager _cookieManager; + + @override + Future clearCookies() { + return _cookieManager.clearCookies(); + } + + @override + Future setCookie(WebViewCookie cookie) { + if (!_isValidPath(cookie.path)) { + throw ArgumentError( + 'The path property for the provided cookie was not given a legal value.'); + } + return _cookieManager.setCookie( + cookie.domain, + '${Uri.encodeComponent(cookie.name)}=${Uri.encodeComponent(cookie.value)}; path=${cookie.path}', + ); + } + + bool _isValidPath(String path) { + // Permitted ranges based on RFC6265bis: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-4.1.1 + for (final int char in path.codeUnits) { + if ((char < 0x20 || char > 0x3A) && (char < 0x3C || char > 0x7E)) { + return false; + } + } + return true; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_platform.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_platform.dart new file mode 100644 index 000000000000..7997f69d7eba --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_platform.dart @@ -0,0 +1,44 @@ +// 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:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'android_webview_controller.dart'; +import 'android_webview_cookie_manager.dart'; + +/// Implementation of [WebViewPlatform] using the WebKit API. +class AndroidWebViewPlatform extends WebViewPlatform { + /// Registers this class as the default instance of [WebViewPlatform]. + static void registerWith() { + WebViewPlatform.instance = AndroidWebViewPlatform(); + } + + @override + AndroidWebViewController createPlatformWebViewController( + PlatformWebViewControllerCreationParams params, + ) { + return AndroidWebViewController(params); + } + + @override + AndroidNavigationDelegate createPlatformNavigationDelegate( + PlatformNavigationDelegateCreationParams params, + ) { + return AndroidNavigationDelegate(params); + } + + @override + AndroidWebViewWidget createPlatformWebViewWidget( + PlatformWebViewWidgetCreationParams params, + ) { + return AndroidWebViewWidget(params); + } + + @override + AndroidWebViewCookieManager createPlatformCookieManager( + PlatformWebViewCookieManagerCreationParams params, + ) { + return AndroidWebViewCookieManager(params); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart b/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android.dart similarity index 94% rename from packages/webview_flutter/webview_flutter_android/lib/webview_android.dart rename to packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android.dart index 47740247db73..cfda749fa4ab 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android.dart @@ -8,9 +8,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +// ignore: implementation_imports +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; -import 'src/android_webview.dart'; +import '../android_webview.dart'; import 'webview_android_widget.dart'; /// Builds an Android webview. diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android_cookie_manager.dart b/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_cookie_manager.dart similarity index 85% rename from packages/webview_flutter/webview_flutter_android/lib/webview_android_cookie_manager.dart rename to packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_cookie_manager.dart index 6e3f6f28d8ef..663a2076b412 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/webview_android_cookie_manager.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_cookie_manager.dart @@ -2,9 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +// ignore: implementation_imports +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; -import 'src/android_webview.dart' as android_webview; +import '../android_webview.dart' as android_webview; /// Handles all cookie operations for the current platform. class WebViewAndroidCookieManager extends WebViewCookieManagerPlatform { diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart b/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_widget.dart similarity index 67% rename from packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart rename to packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_widget.dart index 140d0da7e7f7..cd4ba820cf4c 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_widget.dart @@ -6,16 +6,18 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/widgets.dart'; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +// ignore: implementation_imports +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; -import 'src/android_webview.dart' as android_webview; +import '../android_webview.dart' as android_webview; +import '../weak_reference_utils.dart'; import 'webview_android_cookie_manager.dart'; /// Creates a [Widget] with a [android_webview.WebView]. class WebViewAndroidWidget extends StatefulWidget { /// Constructs a [WebViewAndroidWidget]. const WebViewAndroidWidget({ - Key? key, + super.key, required this.creationParams, required this.useHybridComposition, required this.callbacksHandler, @@ -25,7 +27,7 @@ class WebViewAndroidWidget extends StatefulWidget { @visibleForTesting this.flutterAssetManager = const android_webview.FlutterAssetManager(), @visibleForTesting this.webStorage, - }) : super(key: key); + }); /// Initial parameters used to setup the WebView. final CreationParams creationParams; @@ -85,12 +87,6 @@ class _WebViewAndroidWidgetState extends State { ); } - @override - void dispose() { - super.dispose(); - controller._dispose(); - } - @override Widget build(BuildContext context) { return widget.onBuildWidget(controller); @@ -127,6 +123,7 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { _setCreationParams(creationParams); webView.setDownloadListener(downloadListener); webView.setWebChromeClient(webChromeClient); + webView.setWebViewClient(webViewClient); final String? initialUrl = creationParams.initialUrl; if (initialUrl != null) { @@ -137,7 +134,61 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { final Map _javaScriptChannels = {}; - late WebViewAndroidWebViewClient _webViewClient; + late final android_webview.WebViewClient _webViewClient = withWeakReferenceTo( + this, (WeakReference weakReference) { + return webViewProxy.createWebViewClient( + onPageStarted: (_, String url) { + weakReference.target?.callbacksHandler.onPageStarted(url); + }, + onPageFinished: (_, String url) { + weakReference.target?.callbacksHandler.onPageFinished(url); + }, + onReceivedError: ( + _, + int errorCode, + String description, + String failingUrl, + ) { + weakReference.target?.callbacksHandler + .onWebResourceError(WebResourceError( + errorCode: errorCode, + description: description, + failingUrl: failingUrl, + errorType: _errorCodeToErrorType(errorCode), + )); + }, + onReceivedRequestError: ( + _, + android_webview.WebResourceRequest request, + android_webview.WebResourceError error, + ) { + if (request.isForMainFrame) { + weakReference.target?.callbacksHandler + .onWebResourceError(WebResourceError( + errorCode: error.errorCode, + description: error.description, + failingUrl: request.url, + errorType: _errorCodeToErrorType(error.errorCode), + )); + } + }, + urlLoading: (_, String url) { + weakReference.target?._handleNavigationRequest( + url: url, + isForMainFrame: true, + ); + }, + requestLoading: (_, android_webview.WebResourceRequest request) { + weakReference.target?._handleNavigationRequest( + url: request.url, + isForMainFrame: request.isForMainFrame, + ); + }, + ); + }); + + bool _hasNavigationDelegate = false; + bool _hasProgressTracking = false; /// Represents the WebView maintained by platform code. late final android_webview.WebView webView; @@ -160,20 +211,50 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { /// Receives callbacks when content should be downloaded instead. @visibleForTesting - late final WebViewAndroidDownloadListener downloadListener = - WebViewAndroidDownloadListener(loadUrl: loadUrl); + late final android_webview.DownloadListener downloadListener = + android_webview.DownloadListener( + onDownloadStart: withWeakReferenceTo( + this, + (WeakReference weakReference) { + return ( + String url, + String userAgent, + String contentDisposition, + String mimetype, + int contentLength, + ) { + weakReference.target?._handleNavigationRequest( + url: url, + isForMainFrame: true, + ); + }; + }, + ), + ); /// Handles JavaScript dialogs, favicons, titles, new windows, and the progress for [android_webview.WebView]. @visibleForTesting - late final WebViewAndroidWebChromeClient webChromeClient = - WebViewAndroidWebChromeClient(); + late final android_webview.WebChromeClient webChromeClient = + android_webview.WebChromeClient( + onProgressChanged: withWeakReferenceTo( + this, + (WeakReference weakReference) { + return (_, int progress) { + final WebViewAndroidPlatformController? controller = + weakReference.target; + if (controller != null && controller._hasProgressTracking) { + controller.callbacksHandler.onProgress(progress); + } + }; + }, + )); /// Manages the JavaScript storage APIs. final android_webview.WebStorage webStorage; /// Receive various notifications and requests for [android_webview.WebView]. @visibleForTesting - WebViewAndroidWebViewClient get webViewClient => _webViewClient; + android_webview.WebViewClient get webViewClient => _webViewClient; @override Future loadHtmlString(String html, {String? baseUrl}) { @@ -239,11 +320,17 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { case WebViewRequestMethod.post: return webView.postUrl( request.uri.toString(), request.body ?? Uint8List(0)); - default: - throw UnimplementedError( - 'This version of webview_android_widget currently has no implementation for HTTP method ${request.method.serialize()} in loadRequest.', - ); } + // The enum comes from a different package, which could get a new value at + // any time, so a fallback case is necessary. Since there is no reasonable + // default behavior, throw to alert the client that they need an updated + // version. This is deliberately outside the switch rather than a `default` + // so that the linter will flag the switch as needing an update. + // ignore: dead_code + throw UnimplementedError( + 'This version of webview_android_widget currently has no ' + 'implementation for HTTP method ${request.method.serialize()} in ' + 'loadRequest.'); } @override @@ -272,10 +359,9 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { @override Future updateSettings(WebSettings setting) async { + _hasProgressTracking = setting.hasProgressTracking ?? _hasProgressTracking; await Future.wait(>[ _setUserAgent(setting.userAgent), - if (setting.hasProgressTracking != null) - _setHasProgressTracking(setting.hasProgressTracking!), if (setting.hasNavigationDelegate != null) _setHasNavigationDelegate(setting.hasNavigationDelegate!), if (setting.javascriptMode != null) @@ -355,8 +441,6 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { @override Future getScrollY() => webView.getScrollY(); - Future _dispose() => webView.release(); - void _setCreationParams(CreationParams creationParams) { final WebSettings? webSettings = creationParams.webSettings; if (webSettings != null) { @@ -389,34 +473,11 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { .forEach(WebViewCookieManagerPlatform.instance!.setCookie); } - Future _setHasProgressTracking(bool hasProgressTracking) async { - if (hasProgressTracking) { - webChromeClient._onProgress = callbacksHandler.onProgress; - } else { - webChromeClient._onProgress = null; - } - } - Future _setHasNavigationDelegate(bool hasNavigationDelegate) { - if (hasNavigationDelegate) { - downloadListener._onNavigationRequest = - callbacksHandler.onNavigationRequest; - _webViewClient = WebViewAndroidWebViewClient.handlesNavigation( - onPageStartedCallback: callbacksHandler.onPageStarted, - onPageFinishedCallback: callbacksHandler.onPageFinished, - onWebResourceErrorCallback: callbacksHandler.onWebResourceError, - loadUrl: loadUrl, - onNavigationRequestCallback: callbacksHandler.onNavigationRequest, - ); - } else { - downloadListener._onNavigationRequest = null; - _webViewClient = WebViewAndroidWebViewClient( - onPageStartedCallback: callbacksHandler.onPageStarted, - onPageFinishedCallback: callbacksHandler.onPageFinished, - onWebResourceErrorCallback: callbacksHandler.onWebResourceError, - ); - } - return webView.setWebViewClient(_webViewClient); + _hasNavigationDelegate = hasNavigationDelegate; + return _webViewClient.setSynchronousReturnValueForShouldOverrideUrlLoading( + hasNavigationDelegate, + ); } Future _setJavaScriptMode(JavascriptMode mode) { @@ -444,114 +505,6 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { Future _setZoomEnabled(bool zoomEnabled) { return webView.settings.setSupportZoom(zoomEnabled); } -} - -/// Exposes a channel to receive calls from javaScript. -class WebViewAndroidJavaScriptChannel - extends android_webview.JavaScriptChannel { - /// Creates a [WebViewAndroidJavaScriptChannel]. - WebViewAndroidJavaScriptChannel( - String channelName, this.javascriptChannelRegistry) - : super(channelName); - - /// Manages named JavaScript channels and forwarding incoming messages on the correct channel. - final JavascriptChannelRegistry javascriptChannelRegistry; - - @override - void postMessage(String message) { - javascriptChannelRegistry.onJavascriptChannelMessage(channelName, message); - } -} - -/// Receives callbacks when content can not be handled by the rendering engine for [WebViewAndroidPlatformController], and should be downloaded instead. -/// -/// When handling navigation requests, this calls [onNavigationRequestCallback] -/// when a [android_webview.WebView] attempts to navigate to a new page. If -/// this callback return true, this calls [loadUrl]. -class WebViewAndroidDownloadListener extends android_webview.DownloadListener { - /// Creates a [WebViewAndroidDownloadListener]. - WebViewAndroidDownloadListener({required this.loadUrl}); - - // Changed by WebViewAndroidPlatformController. - FutureOr Function({ - required String url, - required bool isForMainFrame, - })? _onNavigationRequest; - - /// Callback to load a URL when a navigation request is approved. - final Future Function(String url, Map? headers) loadUrl; - - @override - void onDownloadStart( - String url, - String userAgent, - String contentDisposition, - String mimetype, - int contentLength, - ) { - if (_onNavigationRequest == null) { - return; - } - - final FutureOr returnValue = _onNavigationRequest!( - url: url, - isForMainFrame: true, - ); - - if (returnValue is bool && returnValue) { - loadUrl(url, {}); - } else { - (returnValue as Future).then((bool shouldLoadUrl) { - if (shouldLoadUrl) { - loadUrl(url, {}); - } - }); - } - } -} - -/// Receives various navigation requests and errors for [WebViewAndroidPlatformController]. -/// -/// When handling navigation requests, this calls [onNavigationRequestCallback] -/// when a [android_webview.WebView] attempts to navigate to a new page. If -/// this callback return true, this calls [loadUrl]. -class WebViewAndroidWebViewClient extends android_webview.WebViewClient { - /// Creates a [WebViewAndroidWebViewClient] that doesn't handle navigation requests. - WebViewAndroidWebViewClient({ - required this.onPageStartedCallback, - required this.onPageFinishedCallback, - required this.onWebResourceErrorCallback, - }) : loadUrl = null, - onNavigationRequestCallback = null, - super(shouldOverrideUrlLoading: false); - - /// Creates a [WebViewAndroidWebViewClient] that handles navigation requests. - WebViewAndroidWebViewClient.handlesNavigation({ - required this.onPageStartedCallback, - required this.onPageFinishedCallback, - required this.onWebResourceErrorCallback, - required this.onNavigationRequestCallback, - required this.loadUrl, - }) : super(shouldOverrideUrlLoading: true); - - /// Callback when [android_webview.WebViewClient] receives a callback from [android_webview.WebViewClient].onPageStarted. - final void Function(String url) onPageStartedCallback; - - /// Callback when [android_webview.WebViewClient] receives a callback from [android_webview.WebViewClient].onPageFinished. - final void Function(String url) onPageFinishedCallback; - - /// Callback when [android_webview.WebViewClient] receives an error callback. - void Function(WebResourceError error) onWebResourceErrorCallback; - - /// Checks whether a navigation request should be approved or disaproved. - final FutureOr Function({ - required String url, - required bool isForMainFrame, - })? onNavigationRequestCallback; - - /// Callback when a navigation request is approved. - final Future Function(String url, Map? headers)? - loadUrl; static WebResourceErrorType _errorCodeToErrorType(int errorCode) { switch (errorCode) { @@ -594,110 +547,54 @@ class WebViewAndroidWebViewClient extends android_webview.WebViewClient { ); } - /// Whether this [android_webview.WebViewClient] handles navigation requests. - bool get handlesNavigation => - loadUrl != null && onNavigationRequestCallback != null; - - @override - void onPageStarted(android_webview.WebView webView, String url) { - onPageStartedCallback(url); - } - - @override - void onPageFinished(android_webview.WebView webView, String url) { - onPageFinishedCallback(url); - } - - @override - void onReceivedError( - android_webview.WebView webView, - int errorCode, - String description, - String failingUrl, - ) { - onWebResourceErrorCallback(WebResourceError( - errorCode: errorCode, - description: description, - failingUrl: failingUrl, - errorType: _errorCodeToErrorType(errorCode), - )); - } - - @override - void onReceivedRequestError( - android_webview.WebView webView, - android_webview.WebResourceRequest request, - android_webview.WebResourceError error, - ) { - if (request.isForMainFrame) { - onWebResourceErrorCallback(WebResourceError( - errorCode: error.errorCode, - description: error.description, - failingUrl: request.url, - errorType: _errorCodeToErrorType(error.errorCode), - )); - } - } - - @override - void urlLoading(android_webview.WebView webView, String url) { - if (!handlesNavigation) { + void _handleNavigationRequest({ + required String url, + required bool isForMainFrame, + }) { + if (!_hasNavigationDelegate) { return; } - final FutureOr returnValue = onNavigationRequestCallback!( + final FutureOr returnValue = callbacksHandler.onNavigationRequest( url: url, - isForMainFrame: true, - ); - - if (returnValue is bool && returnValue) { - loadUrl!(url, {}); - } else if (returnValue is Future) { - returnValue.then((bool shouldLoadUrl) { - if (shouldLoadUrl) { - loadUrl!(url, {}); - } - }); - } - } - - @override - void requestLoading( - android_webview.WebView webView, - android_webview.WebResourceRequest request, - ) { - if (!handlesNavigation) { - return; - } - - final FutureOr returnValue = onNavigationRequestCallback!( - url: request.url, - isForMainFrame: request.isForMainFrame, + isForMainFrame: isForMainFrame, ); if (returnValue is bool && returnValue) { - loadUrl!(request.url, {}); + loadUrl(url, {}); } else if (returnValue is Future) { returnValue.then((bool shouldLoadUrl) { if (shouldLoadUrl) { - loadUrl!(request.url, {}); + loadUrl(url, {}); } }); } } } -/// Handles JavaScript dialogs, favicons, titles, and the progress for [WebViewAndroidPlatformController]. -class WebViewAndroidWebChromeClient extends android_webview.WebChromeClient { - // Changed by WebViewAndroidPlatformController. - void Function(int progress)? _onProgress; +/// Exposes a channel to receive calls from javaScript. +class WebViewAndroidJavaScriptChannel + extends android_webview.JavaScriptChannel { + /// Creates a [WebViewAndroidJavaScriptChannel]. + WebViewAndroidJavaScriptChannel( + super.channelName, + this.javascriptChannelRegistry, + ) : super( + postMessage: withWeakReferenceTo( + javascriptChannelRegistry, + (WeakReference weakReference) { + return (String message) { + weakReference.target?.onJavascriptChannelMessage( + channelName, + message, + ); + }; + }, + ), + ); - @override - void onProgressChanged(android_webview.WebView webView, int progress) { - if (_onProgress != null) { - _onProgress!(progress); - } - } + /// Manages named JavaScript channels and forwarding incoming messages on the correct channel. + final JavascriptChannelRegistry javascriptChannelRegistry; } /// Handles constructing [android_webview.WebView]s and calling static methods. @@ -713,6 +610,38 @@ class WebViewProxy { return android_webview.WebView(useHybridComposition: useHybridComposition); } + /// Constructs a [android_webview.WebViewClient]. + android_webview.WebViewClient createWebViewClient({ + void Function(android_webview.WebView webView, String url)? onPageStarted, + void Function(android_webview.WebView webView, String url)? onPageFinished, + void Function( + android_webview.WebView webView, + android_webview.WebResourceRequest request, + android_webview.WebResourceError error, + )? + onReceivedRequestError, + void Function( + android_webview.WebView webView, + int errorCode, + String description, + String failingUrl, + )? + onReceivedError, + void Function(android_webview.WebView webView, + android_webview.WebResourceRequest request)? + requestLoading, + void Function(android_webview.WebView webView, String url)? urlLoading, + }) { + return android_webview.WebViewClient( + onPageStarted: onPageStarted, + onPageFinished: onPageFinished, + onReceivedRequestError: onReceivedRequestError, + onReceivedError: onReceivedError, + requestLoading: requestLoading, + urlLoading: urlLoading, + ); + } + /// Enables debugging of web contents (HTML / CSS / JavaScript) loaded into any WebViews of this application. /// /// This flag can be enabled in order to facilitate debugging of web layouts diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart b/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_surface_android.dart similarity index 96% rename from packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart rename to packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_surface_android.dart index e89fb7d1512f..8db2fe08835f 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_surface_android.dart @@ -7,9 +7,10 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +// ignore: implementation_imports +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; -import 'src/android_webview.dart'; +import '../android_webview.dart'; import 'webview_android.dart'; import 'webview_android_widget.dart'; diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/platform_views_service_proxy.dart b/packages/webview_flutter/webview_flutter_android/lib/src/platform_views_service_proxy.dart new file mode 100644 index 000000000000..7011f335a269 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/src/platform_views_service_proxy.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:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Proxy that provides access to the platform views service. +/// +/// This service allows creating and controlling platform-specific views. +@immutable +class PlatformViewsServiceProxy { + /// Constructs a [PlatformViewsServiceProxy]. + const PlatformViewsServiceProxy(); + + /// Proxy method for [PlatformViewsService.initExpensiveAndroidView]. + ExpensiveAndroidViewController initExpensiveAndroidView({ + required int id, + required String viewType, + required TextDirection layoutDirection, + dynamic creationParams, + MessageCodec? creationParamsCodec, + VoidCallback? onFocus, + }) { + return PlatformViewsService.initExpensiveAndroidView( + id: id, + viewType: viewType, + layoutDirection: layoutDirection, + creationParams: creationParams, + creationParamsCodec: creationParamsCodec, + onFocus: onFocus, + ); + } + + /// Proxy method for [PlatformViewsService.initSurfaceAndroidView]. + SurfaceAndroidViewController initSurfaceAndroidView({ + required int id, + required String viewType, + required TextDirection layoutDirection, + dynamic creationParams, + MessageCodec? creationParamsCodec, + VoidCallback? onFocus, + }) { + return PlatformViewsService.initSurfaceAndroidView( + id: id, + viewType: viewType, + layoutDirection: layoutDirection, + creationParams: creationParams, + creationParamsCodec: creationParamsCodec, + onFocus: onFocus, + ); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/weak_reference_utils.dart b/packages/webview_flutter/webview_flutter_android/lib/src/weak_reference_utils.dart new file mode 100644 index 000000000000..fd3e3f0dc273 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/src/weak_reference_utils.dart @@ -0,0 +1,34 @@ +// 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. + +/// Helper method for creating callbacks methods with a weak reference. +/// +/// Example: +/// ``` +/// final JavascriptChannelRegistry javascriptChannelRegistry = ... +/// +/// final WKScriptMessageHandler handler = WKScriptMessageHandler( +/// didReceiveScriptMessage: withWeakRefenceTo( +/// javascriptChannelRegistry, +/// (WeakReference weakReference) { +/// return ( +/// WKUserContentController userContentController, +/// WKScriptMessage message, +/// ) { +/// weakReference.target?.onJavascriptChannelMessage( +/// message.name, +/// message.body!.toString(), +/// ); +/// }; +/// }, +/// ), +/// ); +/// ``` +S withWeakReferenceTo( + T reference, + S Function(WeakReference weakReference) onCreate, +) { + final WeakReference weakReference = WeakReference(reference); + return onCreate(weakReference); +} diff --git a/packages/shared_preferences/shared_preferences_ios/example/test_driver/integration_test.dart b/packages/webview_flutter/webview_flutter_android/lib/src/webview_flutter_android_legacy.dart similarity index 54% rename from packages/shared_preferences/shared_preferences_ios/example/test_driver/integration_test.dart rename to packages/webview_flutter/webview_flutter_android/lib/src/webview_flutter_android_legacy.dart index 4f10f2a522f3..a4f9166a07dd 100644 --- a/packages/shared_preferences/shared_preferences_ios/example/test_driver/integration_test.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/webview_flutter_android_legacy.dart @@ -2,6 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); +export 'legacy/webview_android.dart'; +export 'legacy/webview_android_cookie_manager.dart'; +export 'legacy/webview_surface_android.dart'; diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_flutter_android.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_flutter_android.dart new file mode 100644 index 000000000000..95f835ed8a1d --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_flutter_android.dart @@ -0,0 +1,9 @@ +// 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. + +library webview_flutter_android; + +export 'src/android_webview_controller.dart'; +export 'src/android_webview_cookie_manager.dart'; +export 'src/android_webview_platform.dart'; 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 f3946ed4212a..7f4d362c9273 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -6,8 +6,8 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon( PigeonOptions( - dartOut: 'lib/src/android_webview.pigeon.dart', - dartTestOut: 'test/test_android_webview.pigeon.dart', + dartOut: 'lib/src/android_webview.g.dart', + dartTestOut: 'test/test_android_webview.g.dart', dartOptions: DartOptions(copyrightHeader: [ 'Copyright 2013 The Flutter Authors. All rights reserved.', 'Use of this source code is governed by a BSD-style license that can be', @@ -26,6 +26,34 @@ import 'package:pigeon/pigeon.dart'; ), ), ) + +/// Mode of how to select files for a file chooser. +/// +/// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams. +enum FileChooserMode { + /// Open single file and requires that the file exists before allowing the + /// user to pick it. + /// + /// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams#MODE_OPEN. + open, + + /// Similar to [open] but allows multiple files to be selected. + /// + /// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams#MODE_OPEN_MULTIPLE. + openMultiple, + + /// Allows picking a nonexistent file and saving it. + /// + /// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams#MODE_SAVE. + save, +} + +// TODO(bparrishMines): Enums need be wrapped in a data class because thay can't +// be used as primitive arguments. See https://github.com/flutter/flutter/issues/87307 +class FileChooserModeEnumData { + late FileChooserMode value; +} + class WebResourceRequestData { WebResourceRequestData( this.url, @@ -88,8 +116,6 @@ abstract class CookieManagerHostApi { abstract class WebViewHostApi { void create(int instanceId, bool useHybridComposition); - void dispose(int instanceId); - void loadData( int instanceId, String data, @@ -169,8 +195,6 @@ abstract class WebViewHostApi { abstract class WebSettingsHostApi { void create(int instanceId, int webViewInstanceId); - void dispose(int instanceId); - void setDomStorageEnabled(int instanceId, bool flag); void setJavaScriptCanOpenWindowsAutomatically(int instanceId, bool flag); @@ -203,20 +227,21 @@ abstract class JavaScriptChannelHostApi { @FlutterApi() abstract class JavaScriptChannelFlutterApi { - void dispose(int instanceId); - void postMessage(int instanceId, String message); } @HostApi(dartHostTestHandler: 'TestWebViewClientHostApi') abstract class WebViewClientHostApi { - void create(int instanceId, bool shouldOverrideUrlLoading); + void create(int instanceId); + + void setSynchronousReturnValueForShouldOverrideUrlLoading( + int instanceId, + bool value, + ); } @FlutterApi() abstract class WebViewClientFlutterApi { - void dispose(int instanceId); - void onPageStarted(int instanceId, int webViewInstanceId, String url); void onPageFinished(int instanceId, int webViewInstanceId, String url); @@ -252,8 +277,6 @@ abstract class DownloadListenerHostApi { @FlutterApi() abstract class DownloadListenerFlutterApi { - void dispose(int instanceId); - void onDownloadStart( int instanceId, String url, @@ -266,7 +289,12 @@ abstract class DownloadListenerFlutterApi { @HostApi(dartHostTestHandler: 'TestWebChromeClientHostApi') abstract class WebChromeClientHostApi { - void create(int instanceId, int webViewClientInstanceId); + void create(int instanceId); + + void setSynchronousReturnValueForOnShowFileChooser( + int instanceId, + bool value, + ); } @HostApi(dartHostTestHandler: 'TestAssetManagerHostApi') @@ -278,9 +306,14 @@ abstract class FlutterAssetManagerHostApi { @FlutterApi() abstract class WebChromeClientFlutterApi { - void dispose(int instanceId); - void onProgressChanged(int instanceId, int webViewInstanceId, int progress); + + @async + List onShowFileChooser( + int instanceId, + int webViewInstanceId, + int paramsInstanceId, + ); } @HostApi(dartHostTestHandler: 'TestWebStorageHostApi') @@ -289,3 +322,17 @@ abstract class WebStorageHostApi { void deleteAllData(int instanceId); } + +/// Handles callbacks methods for the native Java FileChooserParams class. +/// +/// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams. +@FlutterApi() +abstract class FileChooserParamsFlutterApi { + void create( + int instanceId, + bool isCaptureEnabled, + List acceptTypes, + FileChooserModeEnumData mode, + String? filenameHint, + ); +} diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index e411b4e1326a..ac8971006ba2 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -2,10 +2,10 @@ name: webview_flutter_android description: A Flutter plugin that provides a WebView widget on Android. repository: https://github.com/flutter/plugins/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: 2.10.4 +version: 3.3.0 environment: - sdk: ">=2.14.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" flutter: ">=3.0.0" flutter: @@ -15,11 +15,12 @@ flutter: android: package: io.flutter.plugins.webviewflutter pluginClass: WebViewFlutterPlugin + dartPluginClass: AndroidWebViewPlatform dependencies: flutter: sdk: flutter - webview_flutter_platform_interface: ^1.8.0 + webview_flutter_platform_interface: ^2.0.0 dev_dependencies: build_runner: ^2.1.4 @@ -27,5 +28,5 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter - mockito: ^5.2.0 - pigeon: ^4.0.2 + mockito: ^5.3.2 + pigeon: ^4.2.14 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 new file mode 100644 index 000000000000..dac7c69a84f3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart @@ -0,0 +1,499 @@ +// 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_test/flutter_test.dart'; +import 'package:webview_flutter_android/src/android_proxy.dart'; +import 'package:webview_flutter_android/src/android_webview.dart' + as android_webview; +import 'package:webview_flutter_android/webview_flutter_android.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +void main() { + group('AndroidNavigationDelegate', () { + test('onPageFinished', () { + final AndroidNavigationDelegate androidNavigationDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + late final String callbackUrl; + androidNavigationDelegate + .setOnPageFinished((String url) => callbackUrl = url); + + CapturingWebViewClient.lastCreatedDelegate.onPageFinished!( + android_webview.WebView.detached(), + 'https://www.google.com', + ); + + expect(callbackUrl, 'https://www.google.com'); + }); + + test('onPageStarted', () { + final AndroidNavigationDelegate androidNavigationDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + late final String callbackUrl; + androidNavigationDelegate + .setOnPageStarted((String url) => callbackUrl = url); + + CapturingWebViewClient.lastCreatedDelegate.onPageStarted!( + android_webview.WebView.detached(), + 'https://www.google.com', + ); + + expect(callbackUrl, 'https://www.google.com'); + }); + + test('onWebResourceError from onReceivedRequestError', () { + final AndroidNavigationDelegate androidNavigationDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + late final WebResourceError callbackError; + androidNavigationDelegate.setOnWebResourceError( + (WebResourceError error) => callbackError = error); + + CapturingWebViewClient.lastCreatedDelegate.onReceivedRequestError!( + android_webview.WebView.detached(), + android_webview.WebResourceRequest( + url: 'https://www.google.com', + isForMainFrame: false, + isRedirect: true, + hasGesture: true, + method: 'GET', + requestHeaders: {'X-Mock': 'mocking'}, + ), + android_webview.WebResourceError( + errorCode: android_webview.WebViewClient.errorFileNotFound, + description: 'Page not found.', + ), + ); + + expect(callbackError.errorCode, + android_webview.WebViewClient.errorFileNotFound); + expect(callbackError.description, 'Page not found.'); + expect(callbackError.errorType, WebResourceErrorType.fileNotFound); + expect(callbackError.isForMainFrame, false); + }); + + test('onWebResourceError from onRequestError', () { + final AndroidNavigationDelegate androidNavigationDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + late final WebResourceError callbackError; + androidNavigationDelegate.setOnWebResourceError( + (WebResourceError error) => callbackError = error); + + CapturingWebViewClient.lastCreatedDelegate.onReceivedError!( + android_webview.WebView.detached(), + android_webview.WebViewClient.errorFileNotFound, + 'Page not found.', + 'https://www.google.com', + ); + + expect(callbackError.errorCode, + android_webview.WebViewClient.errorFileNotFound); + expect(callbackError.description, 'Page not found.'); + expect(callbackError.errorType, WebResourceErrorType.fileNotFound); + expect(callbackError.isForMainFrame, true); + }); + + test( + 'onNavigationRequest from requestLoading should not be called when loadUrlCallback is not specified', + () { + final AndroidNavigationDelegate androidNavigationDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + NavigationRequest? callbackNavigationRequest; + androidNavigationDelegate + .setOnNavigationRequest((NavigationRequest navigationRequest) { + callbackNavigationRequest = navigationRequest; + return NavigationDecision.prevent; + }); + + CapturingWebViewClient.lastCreatedDelegate.requestLoading!( + android_webview.WebView.detached(), + android_webview.WebResourceRequest( + url: 'https://www.google.com', + isForMainFrame: true, + isRedirect: true, + hasGesture: true, + method: 'GET', + requestHeaders: {'X-Mock': 'mocking'}, + ), + ); + + expect(callbackNavigationRequest, isNull); + }); + + test( + 'onLoadRequest from requestLoading should not be called when navigationRequestCallback is not specified', + () { + final Completer completer = Completer(); + final AndroidNavigationDelegate androidNavigationDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + androidNavigationDelegate.setOnLoadRequest((_) { + completer.complete(); + return completer.future; + }); + + CapturingWebViewClient.lastCreatedDelegate.requestLoading!( + android_webview.WebView.detached(), + android_webview.WebResourceRequest( + url: 'https://www.google.com', + isForMainFrame: true, + isRedirect: true, + hasGesture: true, + method: 'GET', + requestHeaders: {'X-Mock': 'mocking'}, + ), + ); + + expect(completer.isCompleted, false); + }); + + test( + 'onLoadRequest from requestLoading should not be called when onNavigationRequestCallback returns NavigationDecision.prevent', + () { + final Completer completer = Completer(); + final AndroidNavigationDelegate androidNavigationDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + androidNavigationDelegate.setOnLoadRequest((_) { + completer.complete(); + return completer.future; + }); + + late final NavigationRequest callbackNavigationRequest; + androidNavigationDelegate + .setOnNavigationRequest((NavigationRequest navigationRequest) { + callbackNavigationRequest = navigationRequest; + return NavigationDecision.prevent; + }); + + CapturingWebViewClient.lastCreatedDelegate.requestLoading!( + android_webview.WebView.detached(), + android_webview.WebResourceRequest( + url: 'https://www.google.com', + isForMainFrame: true, + isRedirect: true, + hasGesture: true, + method: 'GET', + requestHeaders: {'X-Mock': 'mocking'}, + ), + ); + + expect(callbackNavigationRequest.isMainFrame, true); + expect(callbackNavigationRequest.url, 'https://www.google.com'); + expect(completer.isCompleted, false); + }); + + test( + 'onLoadRequest from requestLoading should complete when onNavigationRequestCallback returns NavigationDecision.navigate', + () { + final Completer completer = Completer(); + late final LoadRequestParams loadRequestParams; + final AndroidNavigationDelegate androidNavigationDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + androidNavigationDelegate.setOnLoadRequest((LoadRequestParams params) { + loadRequestParams = params; + completer.complete(); + return completer.future; + }); + + late final NavigationRequest callbackNavigationRequest; + androidNavigationDelegate + .setOnNavigationRequest((NavigationRequest navigationRequest) { + callbackNavigationRequest = navigationRequest; + return NavigationDecision.navigate; + }); + + CapturingWebViewClient.lastCreatedDelegate.requestLoading!( + android_webview.WebView.detached(), + android_webview.WebResourceRequest( + url: 'https://www.google.com', + isForMainFrame: true, + isRedirect: true, + hasGesture: true, + method: 'GET', + requestHeaders: {'X-Mock': 'mocking'}, + ), + ); + + expect(loadRequestParams.uri.toString(), 'https://www.google.com'); + expect(loadRequestParams.headers, {'X-Mock': 'mocking'}); + expect(callbackNavigationRequest.isMainFrame, true); + expect(callbackNavigationRequest.url, 'https://www.google.com'); + expect(completer.isCompleted, true); + }); + + test( + 'onNavigationRequest from urlLoading should not be called when loadUrlCallback is not specified', + () { + final AndroidNavigationDelegate androidNavigationDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + NavigationRequest? callbackNavigationRequest; + androidNavigationDelegate + .setOnNavigationRequest((NavigationRequest navigationRequest) { + callbackNavigationRequest = navigationRequest; + return NavigationDecision.prevent; + }); + + CapturingWebViewClient.lastCreatedDelegate.urlLoading!( + android_webview.WebView.detached(), + 'https://www.google.com', + ); + + expect(callbackNavigationRequest, isNull); + }); + + test( + 'onLoadRequest from urlLoading should not be called when navigationRequestCallback is not specified', + () { + final Completer completer = Completer(); + final AndroidNavigationDelegate androidNavigationDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + androidNavigationDelegate.setOnLoadRequest((_) { + completer.complete(); + return completer.future; + }); + + CapturingWebViewClient.lastCreatedDelegate.urlLoading!( + android_webview.WebView.detached(), + 'https://www.google.com', + ); + + expect(completer.isCompleted, false); + }); + + test( + 'onLoadRequest from urlLoading should not be called when onNavigationRequestCallback returns NavigationDecision.prevent', + () { + final Completer completer = Completer(); + final AndroidNavigationDelegate androidNavigationDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + androidNavigationDelegate.setOnLoadRequest((_) { + completer.complete(); + return completer.future; + }); + + late final NavigationRequest callbackNavigationRequest; + androidNavigationDelegate + .setOnNavigationRequest((NavigationRequest navigationRequest) { + callbackNavigationRequest = navigationRequest; + return NavigationDecision.prevent; + }); + + CapturingWebViewClient.lastCreatedDelegate.urlLoading!( + android_webview.WebView.detached(), + 'https://www.google.com', + ); + + expect(callbackNavigationRequest.isMainFrame, true); + expect(callbackNavigationRequest.url, 'https://www.google.com'); + expect(completer.isCompleted, false); + }); + + test( + 'onLoadRequest from urlLoading should complete when onNavigationRequestCallback returns NavigationDecision.navigate', + () { + final Completer completer = Completer(); + late final LoadRequestParams loadRequestParams; + final AndroidNavigationDelegate androidNavigationDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + androidNavigationDelegate.setOnLoadRequest((LoadRequestParams params) { + loadRequestParams = params; + completer.complete(); + return completer.future; + }); + + late final NavigationRequest callbackNavigationRequest; + androidNavigationDelegate + .setOnNavigationRequest((NavigationRequest navigationRequest) { + callbackNavigationRequest = navigationRequest; + return NavigationDecision.navigate; + }); + + CapturingWebViewClient.lastCreatedDelegate.urlLoading!( + android_webview.WebView.detached(), + 'https://www.google.com', + ); + + expect(loadRequestParams.uri.toString(), 'https://www.google.com'); + expect(loadRequestParams.headers, {}); + expect(callbackNavigationRequest.isMainFrame, true); + expect(callbackNavigationRequest.url, 'https://www.google.com'); + expect(completer.isCompleted, true); + }); + + test('setOnNavigationRequest should override URL loading', () { + final AndroidNavigationDelegate androidNavigationDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + androidNavigationDelegate.setOnNavigationRequest( + (NavigationRequest request) => NavigationDecision.navigate, + ); + + expect( + CapturingWebViewClient.lastCreatedDelegate + .synchronousReturnValueForShouldOverrideUrlLoading, + isTrue); + }); + + test( + 'onLoadRequest from onDownloadStart should not be called when navigationRequestCallback is not specified', + () { + final Completer completer = Completer(); + final AndroidNavigationDelegate androidNavigationDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + androidNavigationDelegate.setOnLoadRequest((_) { + completer.complete(); + return completer.future; + }); + + CapturingDownloadListener.lastCreatedListener.onDownloadStart( + '', + '', + '', + '', + 0, + ); + + expect(completer.isCompleted, false); + }); + + test( + 'onLoadRequest from onDownloadStart should not be called when onNavigationRequestCallback returns NavigationDecision.prevent', + () { + final Completer completer = Completer(); + final AndroidNavigationDelegate androidNavigationDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + androidNavigationDelegate.setOnLoadRequest((_) { + completer.complete(); + return completer.future; + }); + + late final NavigationRequest callbackNavigationRequest; + androidNavigationDelegate + .setOnNavigationRequest((NavigationRequest navigationRequest) { + callbackNavigationRequest = navigationRequest; + return NavigationDecision.prevent; + }); + + CapturingDownloadListener.lastCreatedListener.onDownloadStart( + 'https://www.google.com', + '', + '', + '', + 0, + ); + + expect(callbackNavigationRequest.isMainFrame, true); + expect(callbackNavigationRequest.url, 'https://www.google.com'); + expect(completer.isCompleted, false); + }); + + test( + 'onLoadRequest from onDownloadStart should complete when onNavigationRequestCallback returns NavigationDecision.navigate', + () { + final Completer completer = Completer(); + late final LoadRequestParams loadRequestParams; + final AndroidNavigationDelegate androidNavigationDelegate = + AndroidNavigationDelegate(_buildCreationParams()); + + androidNavigationDelegate.setOnLoadRequest((LoadRequestParams params) { + loadRequestParams = params; + completer.complete(); + return completer.future; + }); + + late final NavigationRequest callbackNavigationRequest; + androidNavigationDelegate + .setOnNavigationRequest((NavigationRequest navigationRequest) { + callbackNavigationRequest = navigationRequest; + return NavigationDecision.navigate; + }); + + CapturingDownloadListener.lastCreatedListener.onDownloadStart( + 'https://www.google.com', + '', + '', + '', + 0, + ); + + expect(loadRequestParams.uri.toString(), 'https://www.google.com'); + expect(loadRequestParams.headers, {}); + expect(callbackNavigationRequest.isMainFrame, true); + expect(callbackNavigationRequest.url, 'https://www.google.com'); + expect(completer.isCompleted, true); + }); + }); +} + +AndroidNavigationDelegateCreationParams _buildCreationParams() { + return AndroidNavigationDelegateCreationParams + .fromPlatformNavigationDelegateCreationParams( + const PlatformNavigationDelegateCreationParams(), + androidWebViewProxy: const AndroidWebViewProxy( + createAndroidWebChromeClient: CapturingWebChromeClient.new, + createAndroidWebViewClient: CapturingWebViewClient.new, + createDownloadListener: CapturingDownloadListener.new, + ), + ); +} + +// Records the last created instance of itself. +class CapturingWebViewClient extends android_webview.WebViewClient { + CapturingWebViewClient({ + super.onPageFinished, + super.onPageStarted, + super.onReceivedError, + super.onReceivedRequestError, + super.requestLoading, + super.urlLoading, + }) : super.detached() { + lastCreatedDelegate = this; + } + + static CapturingWebViewClient lastCreatedDelegate = CapturingWebViewClient(); + + bool synchronousReturnValueForShouldOverrideUrlLoading = false; + + @override + Future setSynchronousReturnValueForShouldOverrideUrlLoading( + bool value) async { + synchronousReturnValueForShouldOverrideUrlLoading = value; + } +} + +// Records the last created instance of itself. +class CapturingWebChromeClient extends android_webview.WebChromeClient { + CapturingWebChromeClient({ + super.onProgressChanged, + super.onShowFileChooser, + }) : super.detached() { + lastCreatedDelegate = this; + } + static CapturingWebChromeClient lastCreatedDelegate = + CapturingWebChromeClient(); +} + +// Records the last created instance of itself. +class CapturingDownloadListener extends android_webview.DownloadListener { + CapturingDownloadListener({ + required super.onDownloadStart, + }) : super.detached() { + lastCreatedListener = this; + } + static CapturingDownloadListener lastCreatedListener = + CapturingDownloadListener(onDownloadStart: (_, __, ___, ____, _____) {}); +} 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 new file mode 100644 index 000000000000..43bab384e0cc --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart @@ -0,0 +1,1018 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231) +// ignore: unnecessary_import +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:webview_flutter_android/src/android_proxy.dart'; +import 'package:webview_flutter_android/src/android_webview.dart' + as android_webview; +import 'package:webview_flutter_android/src/android_webview_api_impls.dart'; +import 'package:webview_flutter_android/src/instance_manager.dart'; +import 'package:webview_flutter_android/src/platform_views_service_proxy.dart'; +import 'package:webview_flutter_android/webview_flutter_android.dart'; +import 'package:webview_flutter_platform_interface/src/webview_platform.dart'; + +import 'android_navigation_delegate_test.dart'; +import 'android_webview_controller_test.mocks.dart'; + +@GenerateNiceMocks(>[ + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + AndroidWebViewController createControllerWithMocks({ + android_webview.FlutterAssetManager? mockFlutterAssetManager, + android_webview.JavaScriptChannel? mockJavaScriptChannel, + android_webview.WebChromeClient Function({ + void Function(android_webview.WebView webView, int progress)? + onProgressChanged, + Future> Function( + android_webview.WebView webView, + android_webview.FileChooserParams params, + )? + onShowFileChooser, + })? + createWebChromeClient, + android_webview.WebView? mockWebView, + android_webview.WebViewClient? mockWebViewClient, + android_webview.WebStorage? mockWebStorage, + android_webview.WebSettings? mockSettings, + }) { + final android_webview.WebView nonNullMockWebView = + mockWebView ?? MockWebView(); + + final AndroidWebViewControllerCreationParams creationParams = + AndroidWebViewControllerCreationParams( + 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, + }) => + MockWebChromeClient(), + createAndroidWebView: ({required bool useHybridComposition}) => + nonNullMockWebView, + createAndroidWebViewClient: ({ + void Function(android_webview.WebView webView, String url)? + onPageFinished, + void Function(android_webview.WebView webView, String url)? + onPageStarted, + @Deprecated('Only called on Android version < 23.') + void Function( + android_webview.WebView webView, + int errorCode, + String description, + String failingUrl, + )? + onReceivedError, + void Function( + android_webview.WebView webView, + android_webview.WebResourceRequest request, + android_webview.WebResourceError error, + )? + onReceivedRequestError, + void Function( + android_webview.WebView webView, + android_webview.WebResourceRequest request, + )? + requestLoading, + void Function(android_webview.WebView webView, String url)? + urlLoading, + }) => + mockWebViewClient ?? MockWebViewClient(), + createFlutterAssetManager: () => + mockFlutterAssetManager ?? MockFlutterAssetManager(), + createJavaScriptChannel: ( + String channelName, { + required void Function(String) postMessage, + }) => + mockJavaScriptChannel ?? MockJavaScriptChannel(), + )); + + when(nonNullMockWebView.settings) + .thenReturn(mockSettings ?? MockWebSettings()); + + return AndroidWebViewController(creationParams); + } + + group('AndroidWebViewController', () { + AndroidJavaScriptChannelParams + createAndroidJavaScriptChannelParamsWithMocks({ + String? name, + MockJavaScriptChannel? mockJavaScriptChannel, + }) { + return AndroidJavaScriptChannelParams( + name: name ?? 'test', + onMessageReceived: (JavaScriptMessage message) {}, + webViewProxy: AndroidWebViewProxy( + createJavaScriptChannel: ( + String channelName, { + required void Function(String) postMessage, + }) => + mockJavaScriptChannel ?? MockJavaScriptChannel(), + )); + } + + test('loadFile without file prefix', () async { + final MockWebView mockWebView = MockWebView(); + final MockWebSettings mockWebSettings = MockWebSettings(); + createControllerWithMocks( + mockWebView: mockWebView, + mockSettings: mockWebSettings, + ); + + verify(mockWebSettings.setBuiltInZoomControls(true)).called(1); + verify(mockWebSettings.setDisplayZoomControls(false)).called(1); + verify(mockWebSettings.setDomStorageEnabled(true)).called(1); + verify(mockWebSettings.setJavaScriptCanOpenWindowsAutomatically(true)) + .called(1); + verify(mockWebSettings.setLoadWithOverviewMode(true)).called(1); + verify(mockWebSettings.setSupportMultipleWindows(true)).called(1); + verify(mockWebSettings.setUseWideViewPort(true)).called(1); + }); + + test('loadFile without file prefix', () async { + final MockWebView mockWebView = MockWebView(); + final MockWebSettings mockWebSettings = MockWebSettings(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + mockSettings: mockWebSettings, + ); + + await controller.loadFile('/path/to/file.html'); + + verify(mockWebSettings.setAllowFileAccess(true)).called(1); + verify(mockWebView.loadUrl( + 'file:///path/to/file.html', + {}, + )).called(1); + }); + + test('loadFile without file prefix and characters to be escaped', () async { + final MockWebView mockWebView = MockWebView(); + final MockWebSettings mockWebSettings = MockWebSettings(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + mockSettings: mockWebSettings, + ); + + await controller.loadFile('/path/to/?_<_>_.html'); + + verify(mockWebSettings.setAllowFileAccess(true)).called(1); + verify(mockWebView.loadUrl( + 'file:///path/to/%3F_%3C_%3E_.html', + {}, + )).called(1); + }); + + test('loadFile with file prefix', () async { + final MockWebView mockWebView = MockWebView(); + final MockWebSettings mockWebSettings = MockWebSettings(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + when(mockWebView.settings).thenReturn(mockWebSettings); + + await controller.loadFile('file:///path/to/file.html'); + + verify(mockWebSettings.setAllowFileAccess(true)).called(1); + verify(mockWebView.loadUrl( + 'file:///path/to/file.html', + {}, + )).called(1); + }); + + test('loadFlutterAsset when asset does not exists', () async { + final MockWebView mockWebView = MockWebView(); + final MockFlutterAssetManager mockAssetManager = + MockFlutterAssetManager(); + final AndroidWebViewController controller = createControllerWithMocks( + mockFlutterAssetManager: mockAssetManager, + mockWebView: mockWebView, + ); + + when(mockAssetManager.getAssetFilePathByName('mock_key')) + .thenAnswer((_) => Future.value('')); + when(mockAssetManager.list('')) + .thenAnswer((_) => Future>.value([])); + + try { + await controller.loadFlutterAsset('mock_key'); + fail('Expected an `ArgumentError`.'); + } on ArgumentError catch (e) { + expect(e.message, 'Asset for key "mock_key" not found.'); + expect(e.name, 'key'); + } on Error { + fail('Expect an `ArgumentError`.'); + } + + verify(mockAssetManager.getAssetFilePathByName('mock_key')).called(1); + verify(mockAssetManager.list('')).called(1); + verifyNever(mockWebView.loadUrl(any, any)); + }); + + test('loadFlutterAsset when asset does exists', () async { + final MockWebView mockWebView = MockWebView(); + final MockFlutterAssetManager mockAssetManager = + MockFlutterAssetManager(); + final AndroidWebViewController controller = createControllerWithMocks( + mockFlutterAssetManager: mockAssetManager, + mockWebView: mockWebView, + ); + + when(mockAssetManager.getAssetFilePathByName('mock_key')) + .thenAnswer((_) => Future.value('www/mock_file.html')); + when(mockAssetManager.list('www')).thenAnswer( + (_) => Future>.value(['mock_file.html'])); + + await controller.loadFlutterAsset('mock_key'); + + verify(mockAssetManager.getAssetFilePathByName('mock_key')).called(1); + verify(mockAssetManager.list('www')).called(1); + verify(mockWebView.loadUrl( + 'file:///android_asset/www/mock_file.html', {})); + }); + + test( + 'loadFlutterAsset when asset name contains characters that should be escaped', + () async { + final MockWebView mockWebView = MockWebView(); + final MockFlutterAssetManager mockAssetManager = + MockFlutterAssetManager(); + final AndroidWebViewController controller = createControllerWithMocks( + mockFlutterAssetManager: mockAssetManager, + mockWebView: mockWebView, + ); + + when(mockAssetManager.getAssetFilePathByName('mock_key')) + .thenAnswer((_) => Future.value('www/?_<_>_.html')); + when(mockAssetManager.list('www')).thenAnswer( + (_) => Future>.value(['?_<_>_.html'])); + + await controller.loadFlutterAsset('mock_key'); + + verify(mockAssetManager.getAssetFilePathByName('mock_key')).called(1); + verify(mockAssetManager.list('www')).called(1); + verify(mockWebView.loadUrl( + 'file:///android_asset/www/%3F_%3C_%3E_.html', {})); + }); + + test('loadHtmlString without baseUrl', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + await controller.loadHtmlString('

Hello Test!

'); + + verify(mockWebView.loadDataWithBaseUrl( + data: '

Hello Test!

', + mimeType: 'text/html', + )).called(1); + }); + + test('loadHtmlString with baseUrl', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + await controller.loadHtmlString('

Hello Test!

', + baseUrl: 'https://flutter.dev'); + + verify(mockWebView.loadDataWithBaseUrl( + data: '

Hello Test!

', + baseUrl: 'https://flutter.dev', + mimeType: 'text/html', + )).called(1); + }); + + test('loadRequest without URI scheme', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + final LoadRequestParams requestParams = LoadRequestParams( + uri: Uri.parse('flutter.dev'), + ); + + try { + await controller.loadRequest(requestParams); + fail('Expect an `ArgumentError`.'); + } on ArgumentError catch (e) { + expect(e.message, 'WebViewRequest#uri is required to have a scheme.'); + } on Error { + fail('Expect a `ArgumentError`.'); + } + + verifyNever(mockWebView.loadUrl(any, any)); + verifyNever(mockWebView.postUrl(any, any)); + }); + + test('loadRequest using the GET method', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + final LoadRequestParams requestParams = LoadRequestParams( + uri: Uri.parse('https://flutter.dev'), + headers: const {'X-Test': 'Testing'}, + ); + + await controller.loadRequest(requestParams); + + verify(mockWebView.loadUrl( + 'https://flutter.dev', + {'X-Test': 'Testing'}, + )); + verifyNever(mockWebView.postUrl(any, any)); + }); + + test('loadRequest using the POST method without body', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + final LoadRequestParams requestParams = LoadRequestParams( + uri: Uri.parse('https://flutter.dev'), + method: LoadRequestMethod.post, + headers: const {'X-Test': 'Testing'}, + ); + + await controller.loadRequest(requestParams); + + verify(mockWebView.postUrl( + 'https://flutter.dev', + Uint8List(0), + )); + verifyNever(mockWebView.loadUrl(any, any)); + }); + + test('loadRequest using the POST method with body', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + final LoadRequestParams requestParams = LoadRequestParams( + uri: Uri.parse('https://flutter.dev'), + method: LoadRequestMethod.post, + headers: const {'X-Test': 'Testing'}, + body: Uint8List.fromList('{"message": "Hello World!"}'.codeUnits), + ); + + await controller.loadRequest(requestParams); + + verify(mockWebView.postUrl( + 'https://flutter.dev', + Uint8List.fromList('{"message": "Hello World!"}'.codeUnits), + )); + verifyNever(mockWebView.loadUrl(any, any)); + }); + + test('currentUrl', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + await controller.currentUrl(); + + verify(mockWebView.getUrl()).called(1); + }); + + test('canGoBack', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + await controller.canGoBack(); + + verify(mockWebView.canGoBack()).called(1); + }); + + test('canGoForward', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + await controller.canGoForward(); + + verify(mockWebView.canGoForward()).called(1); + }); + + test('goBack', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + await controller.goBack(); + + verify(mockWebView.goBack()).called(1); + }); + + test('goForward', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + await controller.goForward(); + + verify(mockWebView.goForward()).called(1); + }); + + test('reload', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + await controller.reload(); + + verify(mockWebView.reload()).called(1); + }); + + test('clearCache', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + await controller.clearCache(); + + verify(mockWebView.clearCache(true)).called(1); + }); + + test('clearLocalStorage', () async { + final MockWebStorage mockWebStorage = MockWebStorage(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebStorage: mockWebStorage, + ); + + await controller.clearLocalStorage(); + + verify(mockWebStorage.deleteAllData()).called(1); + }); + + test('setPlatformNavigationDelegate', () async { + final MockAndroidNavigationDelegate mockNavigationDelegate = + MockAndroidNavigationDelegate(); + final MockWebView mockWebView = MockWebView(); + final MockWebChromeClient mockWebChromeClient = MockWebChromeClient(); + final MockWebViewClient mockWebViewClient = MockWebViewClient(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + when(mockNavigationDelegate.androidWebChromeClient) + .thenReturn(mockWebChromeClient); + when(mockNavigationDelegate.androidWebViewClient) + .thenReturn(mockWebViewClient); + + await controller.setPlatformNavigationDelegate(mockNavigationDelegate); + + verify(mockWebView.setWebViewClient(mockWebViewClient)); + verifyNever(mockWebView.setWebChromeClient(mockWebChromeClient)); + }); + + test('onProgress', () { + final AndroidNavigationDelegate androidNavigationDelegate = + AndroidNavigationDelegate( + AndroidNavigationDelegateCreationParams + .fromPlatformNavigationDelegateCreationParams( + const PlatformNavigationDelegateCreationParams(), + androidWebViewProxy: const AndroidWebViewProxy( + createAndroidWebViewClient: android_webview.WebViewClient.detached, + createAndroidWebChromeClient: + android_webview.WebChromeClient.detached, + createDownloadListener: android_webview.DownloadListener.detached, + ), + ), + ); + + late final int callbackProgress; + androidNavigationDelegate + .setOnProgress((int progress) => callbackProgress = progress); + + final AndroidWebViewController controller = createControllerWithMocks( + createWebChromeClient: CapturingWebChromeClient.new, + ); + controller.setPlatformNavigationDelegate(androidNavigationDelegate); + + CapturingWebChromeClient.lastCreatedDelegate.onProgressChanged!( + android_webview.WebView.detached(), + 42, + ); + + expect(callbackProgress, 42); + }); + + test('onProgress does not cause LateInitializationError', () { + // ignore: unused_local_variable + final AndroidWebViewController controller = createControllerWithMocks( + createWebChromeClient: CapturingWebChromeClient.new, + ); + + // Should not cause LateInitializationError + CapturingWebChromeClient.lastCreatedDelegate.onProgressChanged!( + android_webview.WebView.detached(), + 42, + ); + }); + + test('setOnShowFileSelector', () async { + late final Future> Function( + android_webview.WebView webView, + android_webview.FileChooserParams params, + ) onShowFileChooserCallback; + final MockWebChromeClient mockWebChromeClient = MockWebChromeClient(); + final AndroidWebViewController controller = createControllerWithMocks( + createWebChromeClient: ({ + dynamic onProgressChanged, + Future> Function( + android_webview.WebView webView, + android_webview.FileChooserParams params, + )? + onShowFileChooser, + }) { + onShowFileChooserCallback = onShowFileChooser!; + return mockWebChromeClient; + }, + ); + + late final FileSelectorParams fileSelectorParams; + await controller.setOnShowFileSelector( + (FileSelectorParams params) async { + fileSelectorParams = params; + return []; + }, + ); + + verify( + mockWebChromeClient.setSynchronousReturnValueForOnShowFileChooser(true), + ); + + onShowFileChooserCallback( + android_webview.WebView.detached(), + android_webview.FileChooserParams.detached( + isCaptureEnabled: false, + acceptTypes: ['png'], + filenameHint: 'filenameHint', + mode: android_webview.FileChooserMode.open, + ), + ); + + expect(fileSelectorParams.isCaptureEnabled, isFalse); + expect(fileSelectorParams.acceptTypes, ['png']); + expect(fileSelectorParams.filenameHint, 'filenameHint'); + expect(fileSelectorParams.mode, FileSelectorMode.open); + }); + + test('runJavaScript', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + await controller.runJavaScript('alert("This is a test.");'); + + verify(mockWebView.evaluateJavascript('alert("This is a test.");')) + .called(1); + }); + + test('runJavaScriptReturningResult with return value', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + when(mockWebView.evaluateJavascript('return "Hello" + " World!";')) + .thenAnswer((_) => Future.value('Hello World!')); + + final String message = await controller.runJavaScriptReturningResult( + 'return "Hello" + " World!";') as String; + + expect(message, 'Hello World!'); + }); + + test('runJavaScriptReturningResult returning null', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + when(mockWebView.evaluateJavascript('alert("This is a test.");')) + .thenAnswer((_) => Future.value()); + + final String message = await controller + .runJavaScriptReturningResult('alert("This is a test.");') as String; + + expect(message, ''); + }); + + test('runJavaScriptReturningResult parses num', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + when(mockWebView.evaluateJavascript('alert("This is a test.");')) + .thenAnswer((_) => Future.value('3.14')); + + final num message = await controller + .runJavaScriptReturningResult('alert("This is a test.");') as num; + + expect(message, 3.14); + }); + + test('runJavaScriptReturningResult parses true', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + when(mockWebView.evaluateJavascript('alert("This is a test.");')) + .thenAnswer((_) => Future.value('true')); + + final bool message = await controller + .runJavaScriptReturningResult('alert("This is a test.");') as bool; + + expect(message, true); + }); + + test('runJavaScriptReturningResult parses false', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + when(mockWebView.evaluateJavascript('alert("This is a test.");')) + .thenAnswer((_) => Future.value('false')); + + final bool message = await controller + .runJavaScriptReturningResult('alert("This is a test.");') as bool; + + expect(message, false); + }); + + test('addJavaScriptChannel', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + final AndroidJavaScriptChannelParams paramsWithMock = + createAndroidJavaScriptChannelParamsWithMocks(name: 'test'); + await controller.addJavaScriptChannel(paramsWithMock); + verify(mockWebView.addJavaScriptChannel( + argThat(isA()))) + .called(1); + }); + + test( + 'addJavaScriptChannel add channel with same name should remove existing channel', + () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + final AndroidJavaScriptChannelParams paramsWithMock = + createAndroidJavaScriptChannelParamsWithMocks(name: 'test'); + await controller.addJavaScriptChannel(paramsWithMock); + verify(mockWebView.addJavaScriptChannel( + argThat(isA()))) + .called(1); + + await controller.addJavaScriptChannel(paramsWithMock); + verifyInOrder([ + mockWebView.removeJavaScriptChannel( + argThat(isA())), + mockWebView.addJavaScriptChannel( + argThat(isA())), + ]); + }); + + test('removeJavaScriptChannel when channel is not registered', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + await controller.removeJavaScriptChannel('test'); + verifyNever(mockWebView.removeJavaScriptChannel(any)); + }); + + test('removeJavaScriptChannel when channel exists', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + final AndroidJavaScriptChannelParams paramsWithMock = + createAndroidJavaScriptChannelParamsWithMocks(name: 'test'); + + // Make sure channel exists before removing it. + await controller.addJavaScriptChannel(paramsWithMock); + verify(mockWebView.addJavaScriptChannel( + argThat(isA()))) + .called(1); + + await controller.removeJavaScriptChannel('test'); + verify(mockWebView.removeJavaScriptChannel( + argThat(isA()))) + .called(1); + }); + + test('getTitle', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + await controller.getTitle(); + + verify(mockWebView.getTitle()).called(1); + }); + + test('scrollTo', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + await controller.scrollTo(4, 2); + + verify(mockWebView.scrollTo(4, 2)).called(1); + }); + + test('scrollBy', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + await controller.scrollBy(4, 2); + + verify(mockWebView.scrollBy(4, 2)).called(1); + }); + + test('getScrollPosition', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + when(mockWebView.getScrollPosition()) + .thenAnswer((_) => Future.value(const Offset(4, 2))); + + final Offset position = await controller.getScrollPosition(); + + verify(mockWebView.getScrollPosition()).called(1); + expect(position.dx, 4); + expect(position.dy, 2); + }); + + test('enableDebugging', () async { + final MockAndroidWebViewProxy mockProxy = MockAndroidWebViewProxy(); + + await AndroidWebViewController.enableDebugging( + true, + webViewProxy: mockProxy, + ); + verify(mockProxy.setWebContentsDebuggingEnabled(true)).called(1); + }); + + test('enableZoom', () async { + final MockWebView mockWebView = MockWebView(); + final MockWebSettings mockSettings = MockWebSettings(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + mockSettings: mockSettings, + ); + + clearInteractions(mockWebView); + + await controller.enableZoom(true); + + verify(mockWebView.settings).called(1); + verify(mockSettings.setSupportZoom(true)).called(1); + }); + + test('setBackgroundColor', () async { + final MockWebView mockWebView = MockWebView(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + await controller.setBackgroundColor(Colors.blue); + + verify(mockWebView.setBackgroundColor(Colors.blue)).called(1); + }); + + test('setJavaScriptMode', () async { + final MockWebView mockWebView = MockWebView(); + final MockWebSettings mockSettings = MockWebSettings(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + mockSettings: mockSettings, + ); + + clearInteractions(mockWebView); + + await controller.setJavaScriptMode(JavaScriptMode.disabled); + + verify(mockWebView.settings).called(1); + verify(mockSettings.setJavaScriptEnabled(false)).called(1); + }); + + test('setUserAgent', () async { + final MockWebView mockWebView = MockWebView(); + final MockWebSettings mockSettings = MockWebSettings(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + mockSettings: mockSettings, + ); + + clearInteractions(mockWebView); + + await controller.setUserAgent('Test Framework'); + + verify(mockWebView.settings).called(1); + verify(mockSettings.setUserAgentString('Test Framework')).called(1); + }); + }); + + test('setMediaPlaybackRequiresUserGesture', () async { + final MockWebView mockWebView = MockWebView(); + final MockWebSettings mockSettings = MockWebSettings(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + mockSettings: mockSettings, + ); + + await controller.setMediaPlaybackRequiresUserGesture(true); + + verify(mockSettings.setMediaPlaybackRequiresUserGesture(true)).called(1); + }); + + test('webViewIdentifier', () { + final MockWebView mockWebView = MockWebView(); + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + instanceManager.addHostCreatedInstance(mockWebView, 0); + + android_webview.WebView.api = WebViewHostApiImpl( + instanceManager: instanceManager, + ); + + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + expect( + controller.webViewIdentifier, + 0, + ); + + android_webview.WebView.api = WebViewHostApiImpl(); + }); + + group('AndroidWebViewWidget', () { + testWidgets('Builds Android view using supplied parameters', + (WidgetTester tester) async { + final AndroidWebViewController controller = createControllerWithMocks(); + + final AndroidWebViewWidget webViewWidget = AndroidWebViewWidget( + AndroidWebViewWidgetCreationParams( + key: const Key('test_web_view'), + controller: controller, + ), + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) => webViewWidget.build(context), + )); + + expect(find.byType(PlatformViewLink), findsOneWidget); + expect(find.byKey(const Key('test_web_view')), findsOneWidget); + }); + + testWidgets('displayWithHybridComposition is false', + (WidgetTester tester) async { + final AndroidWebViewController controller = createControllerWithMocks(); + + final MockPlatformViewsServiceProxy mockPlatformViewsService = + MockPlatformViewsServiceProxy(); + + when( + mockPlatformViewsService.initSurfaceAndroidView( + id: anyNamed('id'), + viewType: anyNamed('viewType'), + layoutDirection: anyNamed('layoutDirection'), + creationParams: anyNamed('creationParams'), + creationParamsCodec: anyNamed('creationParamsCodec'), + onFocus: anyNamed('onFocus'), + ), + ).thenReturn(MockSurfaceAndroidViewController()); + + final AndroidWebViewWidget webViewWidget = AndroidWebViewWidget( + AndroidWebViewWidgetCreationParams( + key: const Key('test_web_view'), + controller: controller, + platformViewsServiceProxy: mockPlatformViewsService, + ), + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) => webViewWidget.build(context), + )); + await tester.pumpAndSettle(); + + verify( + mockPlatformViewsService.initSurfaceAndroidView( + id: anyNamed('id'), + viewType: anyNamed('viewType'), + layoutDirection: anyNamed('layoutDirection'), + creationParams: anyNamed('creationParams'), + creationParamsCodec: anyNamed('creationParamsCodec'), + onFocus: anyNamed('onFocus'), + ), + ); + }); + + testWidgets('displayWithHybridComposition is true', + (WidgetTester tester) async { + final AndroidWebViewController controller = createControllerWithMocks(); + + final MockPlatformViewsServiceProxy mockPlatformViewsService = + MockPlatformViewsServiceProxy(); + + when( + mockPlatformViewsService.initExpensiveAndroidView( + id: anyNamed('id'), + viewType: anyNamed('viewType'), + layoutDirection: anyNamed('layoutDirection'), + creationParams: anyNamed('creationParams'), + creationParamsCodec: anyNamed('creationParamsCodec'), + onFocus: anyNamed('onFocus'), + ), + ).thenReturn(MockExpensiveAndroidViewController()); + + final AndroidWebViewWidget webViewWidget = AndroidWebViewWidget( + AndroidWebViewWidgetCreationParams( + key: const Key('test_web_view'), + controller: controller, + platformViewsServiceProxy: mockPlatformViewsService, + displayWithHybridComposition: true, + ), + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) => webViewWidget.build(context), + )); + await tester.pumpAndSettle(); + + verify( + mockPlatformViewsService.initExpensiveAndroidView( + id: anyNamed('id'), + viewType: anyNamed('viewType'), + layoutDirection: anyNamed('layoutDirection'), + creationParams: anyNamed('creationParams'), + creationParamsCodec: anyNamed('creationParamsCodec'), + onFocus: anyNamed('onFocus'), + ), + ); + }); + }); +} 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 new file mode 100644 index 000000000000..01885caff54c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart @@ -0,0 +1,2248 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter_android/test/android_webview_controller_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i9; +import 'dart:typed_data' as _i14; +import 'dart:ui' as _i4; + +import 'package:flutter/foundation.dart' as _i11; +import 'package:flutter/gestures.dart' as _i12; +import 'package:flutter/material.dart' as _i13; +import 'package:flutter/services.dart' as _i7; +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_android/src/android_proxy.dart' as _i10; +import 'package:webview_flutter_android/src/android_webview.dart' as _i2; +import 'package:webview_flutter_android/src/android_webview_controller.dart' + as _i8; +import 'package:webview_flutter_android/src/instance_manager.dart' as _i5; +import 'package:webview_flutter_android/src/platform_views_service_proxy.dart' + as _i6; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' + as _i3; + +// 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 + +class _FakeWebChromeClient_0 extends _i1.SmartFake + implements _i2.WebChromeClient { + _FakeWebChromeClient_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWebViewClient_1 extends _i1.SmartFake implements _i2.WebViewClient { + _FakeWebViewClient_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeDownloadListener_2 extends _i1.SmartFake + implements _i2.DownloadListener { + _FakeDownloadListener_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlatformNavigationDelegateCreationParams_3 extends _i1.SmartFake + implements _i3.PlatformNavigationDelegateCreationParams { + _FakePlatformNavigationDelegateCreationParams_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlatformWebViewControllerCreationParams_4 extends _i1.SmartFake + implements _i3.PlatformWebViewControllerCreationParams { + _FakePlatformWebViewControllerCreationParams_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeObject_5 extends _i1.SmartFake implements Object { + _FakeObject_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeOffset_6 extends _i1.SmartFake implements _i4.Offset { + _FakeOffset_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWebView_7 extends _i1.SmartFake implements _i2.WebView { + _FakeWebView_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeFlutterAssetManager_8 extends _i1.SmartFake + implements _i2.FlutterAssetManager { + _FakeFlutterAssetManager_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeJavaScriptChannel_9 extends _i1.SmartFake + implements _i2.JavaScriptChannel { + _FakeJavaScriptChannel_9( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeInstanceManager_10 extends _i1.SmartFake + implements _i5.InstanceManager { + _FakeInstanceManager_10( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlatformViewsServiceProxy_11 extends _i1.SmartFake + implements _i6.PlatformViewsServiceProxy { + _FakePlatformViewsServiceProxy_11( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlatformWebViewController_12 extends _i1.SmartFake + implements _i3.PlatformWebViewController { + _FakePlatformWebViewController_12( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeSize_13 extends _i1.SmartFake implements _i4.Size { + _FakeSize_13( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeExpensiveAndroidViewController_14 extends _i1.SmartFake + implements _i7.ExpensiveAndroidViewController { + _FakeExpensiveAndroidViewController_14( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeSurfaceAndroidViewController_15 extends _i1.SmartFake + implements _i7.SurfaceAndroidViewController { + _FakeSurfaceAndroidViewController_15( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWebSettings_16 extends _i1.SmartFake implements _i2.WebSettings { + _FakeWebSettings_16( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWebStorage_17 extends _i1.SmartFake implements _i2.WebStorage { + _FakeWebStorage_17( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [AndroidNavigationDelegate]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAndroidNavigationDelegate extends _i1.Mock + implements _i8.AndroidNavigationDelegate { + @override + _i2.WebChromeClient get androidWebChromeClient => (super.noSuchMethod( + Invocation.getter(#androidWebChromeClient), + returnValue: _FakeWebChromeClient_0( + this, + Invocation.getter(#androidWebChromeClient), + ), + returnValueForMissingStub: _FakeWebChromeClient_0( + this, + Invocation.getter(#androidWebChromeClient), + ), + ) as _i2.WebChromeClient); + @override + _i2.WebViewClient get androidWebViewClient => (super.noSuchMethod( + Invocation.getter(#androidWebViewClient), + returnValue: _FakeWebViewClient_1( + this, + Invocation.getter(#androidWebViewClient), + ), + returnValueForMissingStub: _FakeWebViewClient_1( + this, + Invocation.getter(#androidWebViewClient), + ), + ) as _i2.WebViewClient); + @override + _i2.DownloadListener get androidDownloadListener => (super.noSuchMethod( + Invocation.getter(#androidDownloadListener), + returnValue: _FakeDownloadListener_2( + this, + Invocation.getter(#androidDownloadListener), + ), + returnValueForMissingStub: _FakeDownloadListener_2( + this, + Invocation.getter(#androidDownloadListener), + ), + ) as _i2.DownloadListener); + @override + _i3.PlatformNavigationDelegateCreationParams get params => + (super.noSuchMethod( + Invocation.getter(#params), + returnValue: _FakePlatformNavigationDelegateCreationParams_3( + this, + Invocation.getter(#params), + ), + returnValueForMissingStub: + _FakePlatformNavigationDelegateCreationParams_3( + this, + Invocation.getter(#params), + ), + ) as _i3.PlatformNavigationDelegateCreationParams); + @override + _i9.Future setOnLoadRequest(_i8.LoadRequestCallback? onLoadRequest) => + (super.noSuchMethod( + Invocation.method( + #setOnLoadRequest, + [onLoadRequest], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setOnNavigationRequest( + _i3.NavigationRequestCallback? onNavigationRequest) => + (super.noSuchMethod( + Invocation.method( + #setOnNavigationRequest, + [onNavigationRequest], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setOnPageStarted(_i3.PageEventCallback? onPageStarted) => + (super.noSuchMethod( + Invocation.method( + #setOnPageStarted, + [onPageStarted], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setOnPageFinished(_i3.PageEventCallback? onPageFinished) => + (super.noSuchMethod( + Invocation.method( + #setOnPageFinished, + [onPageFinished], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setOnProgress(_i3.ProgressCallback? onProgress) => + (super.noSuchMethod( + Invocation.method( + #setOnProgress, + [onProgress], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setOnWebResourceError( + _i3.WebResourceErrorCallback? onWebResourceError) => + (super.noSuchMethod( + Invocation.method( + #setOnWebResourceError, + [onWebResourceError], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); +} + +/// A class which mocks [AndroidWebViewController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAndroidWebViewController extends _i1.Mock + implements _i8.AndroidWebViewController { + @override + _i3.PlatformWebViewControllerCreationParams get params => (super.noSuchMethod( + Invocation.getter(#params), + returnValue: _FakePlatformWebViewControllerCreationParams_4( + this, + Invocation.getter(#params), + ), + returnValueForMissingStub: + _FakePlatformWebViewControllerCreationParams_4( + this, + Invocation.getter(#params), + ), + ) as _i3.PlatformWebViewControllerCreationParams); + @override + _i9.Future loadFile(String? absoluteFilePath) => (super.noSuchMethod( + Invocation.method( + #loadFile, + [absoluteFilePath], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future loadFlutterAsset(String? key) => (super.noSuchMethod( + Invocation.method( + #loadFlutterAsset, + [key], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future loadHtmlString( + String? html, { + String? baseUrl, + }) => + (super.noSuchMethod( + Invocation.method( + #loadHtmlString, + [html], + {#baseUrl: baseUrl}, + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future loadRequest(_i3.LoadRequestParams? params) => + (super.noSuchMethod( + Invocation.method( + #loadRequest, + [params], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future currentUrl() => (super.noSuchMethod( + Invocation.method( + #currentUrl, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future canGoBack() => (super.noSuchMethod( + Invocation.method( + #canGoBack, + [], + ), + returnValue: _i9.Future.value(false), + returnValueForMissingStub: _i9.Future.value(false), + ) as _i9.Future); + @override + _i9.Future canGoForward() => (super.noSuchMethod( + Invocation.method( + #canGoForward, + [], + ), + returnValue: _i9.Future.value(false), + returnValueForMissingStub: _i9.Future.value(false), + ) as _i9.Future); + @override + _i9.Future goBack() => (super.noSuchMethod( + Invocation.method( + #goBack, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future goForward() => (super.noSuchMethod( + Invocation.method( + #goForward, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future reload() => (super.noSuchMethod( + Invocation.method( + #reload, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future clearCache() => (super.noSuchMethod( + Invocation.method( + #clearCache, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future clearLocalStorage() => (super.noSuchMethod( + Invocation.method( + #clearLocalStorage, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setPlatformNavigationDelegate( + _i3.PlatformNavigationDelegate? handler) => + (super.noSuchMethod( + Invocation.method( + #setPlatformNavigationDelegate, + [handler], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future runJavaScript(String? javaScript) => (super.noSuchMethod( + Invocation.method( + #runJavaScript, + [javaScript], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future runJavaScriptReturningResult(String? javaScript) => + (super.noSuchMethod( + Invocation.method( + #runJavaScriptReturningResult, + [javaScript], + ), + returnValue: _i9.Future.value(_FakeObject_5( + this, + Invocation.method( + #runJavaScriptReturningResult, + [javaScript], + ), + )), + returnValueForMissingStub: _i9.Future.value(_FakeObject_5( + this, + Invocation.method( + #runJavaScriptReturningResult, + [javaScript], + ), + )), + ) as _i9.Future); + @override + _i9.Future addJavaScriptChannel( + _i3.JavaScriptChannelParams? javaScriptChannelParams) => + (super.noSuchMethod( + Invocation.method( + #addJavaScriptChannel, + [javaScriptChannelParams], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future removeJavaScriptChannel(String? javaScriptChannelName) => + (super.noSuchMethod( + Invocation.method( + #removeJavaScriptChannel, + [javaScriptChannelName], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future getTitle() => (super.noSuchMethod( + Invocation.method( + #getTitle, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future scrollTo( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollTo, + [ + x, + y, + ], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future scrollBy( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollBy, + [ + x, + y, + ], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future<_i4.Offset> getScrollPosition() => (super.noSuchMethod( + Invocation.method( + #getScrollPosition, + [], + ), + returnValue: _i9.Future<_i4.Offset>.value(_FakeOffset_6( + this, + Invocation.method( + #getScrollPosition, + [], + ), + )), + returnValueForMissingStub: _i9.Future<_i4.Offset>.value(_FakeOffset_6( + this, + Invocation.method( + #getScrollPosition, + [], + ), + )), + ) as _i9.Future<_i4.Offset>); + @override + _i9.Future enableZoom(bool? enabled) => (super.noSuchMethod( + Invocation.method( + #enableZoom, + [enabled], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setBackgroundColor(_i4.Color? color) => (super.noSuchMethod( + Invocation.method( + #setBackgroundColor, + [color], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setJavaScriptMode(_i3.JavaScriptMode? javaScriptMode) => + (super.noSuchMethod( + Invocation.method( + #setJavaScriptMode, + [javaScriptMode], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setUserAgent(String? userAgent) => (super.noSuchMethod( + Invocation.method( + #setUserAgent, + [userAgent], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setMediaPlaybackRequiresUserGesture(bool? require) => + (super.noSuchMethod( + Invocation.method( + #setMediaPlaybackRequiresUserGesture, + [require], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setOnShowFileSelector( + _i9.Future> Function(_i8.FileSelectorParams)? + onShowFileSelectorCallback) => + (super.noSuchMethod( + Invocation.method( + #setOnShowFileSelector, + [onShowFileSelectorCallback], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); +} + +/// A class which mocks [AndroidWebViewProxy]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAndroidWebViewProxy extends _i1.Mock + implements _i10.AndroidWebViewProxy { + @override + _i2.WebView Function({required bool useHybridComposition}) + get createAndroidWebView => (super.noSuchMethod( + Invocation.getter(#createAndroidWebView), + returnValue: ({required bool useHybridComposition}) => + _FakeWebView_7( + this, + Invocation.getter(#createAndroidWebView), + ), + returnValueForMissingStub: ({required bool useHybridComposition}) => + _FakeWebView_7( + this, + Invocation.getter(#createAndroidWebView), + ), + ) as _i2.WebView Function({required bool useHybridComposition})); + @override + _i2.WebChromeClient Function({ + void Function( + _i2.WebView, + int, + )? + onProgressChanged, + _i9.Future> Function( + _i2.WebView, + _i2.FileChooserParams, + )? + onShowFileChooser, + }) get createAndroidWebChromeClient => (super.noSuchMethod( + Invocation.getter(#createAndroidWebChromeClient), + returnValue: ({ + void Function( + _i2.WebView, + int, + )? + onProgressChanged, + _i9.Future> Function( + _i2.WebView, + _i2.FileChooserParams, + )? + onShowFileChooser, + }) => + _FakeWebChromeClient_0( + this, + Invocation.getter(#createAndroidWebChromeClient), + ), + returnValueForMissingStub: ({ + void Function( + _i2.WebView, + int, + )? + onProgressChanged, + _i9.Future> Function( + _i2.WebView, + _i2.FileChooserParams, + )? + onShowFileChooser, + }) => + _FakeWebChromeClient_0( + this, + Invocation.getter(#createAndroidWebChromeClient), + ), + ) as _i2.WebChromeClient Function({ + void Function( + _i2.WebView, + int, + )? + onProgressChanged, + _i9.Future> Function( + _i2.WebView, + _i2.FileChooserParams, + )? + onShowFileChooser, + })); + @override + _i2.WebViewClient Function({ + void Function( + _i2.WebView, + String, + )? + onPageFinished, + void Function( + _i2.WebView, + String, + )? + onPageStarted, + void Function( + _i2.WebView, + int, + String, + String, + )? + onReceivedError, + void Function( + _i2.WebView, + _i2.WebResourceRequest, + _i2.WebResourceError, + )? + onReceivedRequestError, + void Function( + _i2.WebView, + _i2.WebResourceRequest, + )? + requestLoading, + void Function( + _i2.WebView, + String, + )? + urlLoading, + }) get createAndroidWebViewClient => (super.noSuchMethod( + Invocation.getter(#createAndroidWebViewClient), + returnValue: ({ + void Function( + _i2.WebView, + String, + )? + onPageFinished, + void Function( + _i2.WebView, + String, + )? + onPageStarted, + void Function( + _i2.WebView, + int, + String, + String, + )? + onReceivedError, + void Function( + _i2.WebView, + _i2.WebResourceRequest, + _i2.WebResourceError, + )? + onReceivedRequestError, + void Function( + _i2.WebView, + _i2.WebResourceRequest, + )? + requestLoading, + void Function( + _i2.WebView, + String, + )? + urlLoading, + }) => + _FakeWebViewClient_1( + this, + Invocation.getter(#createAndroidWebViewClient), + ), + returnValueForMissingStub: ({ + void Function( + _i2.WebView, + String, + )? + onPageFinished, + void Function( + _i2.WebView, + String, + )? + onPageStarted, + void Function( + _i2.WebView, + int, + String, + String, + )? + onReceivedError, + void Function( + _i2.WebView, + _i2.WebResourceRequest, + _i2.WebResourceError, + )? + onReceivedRequestError, + void Function( + _i2.WebView, + _i2.WebResourceRequest, + )? + requestLoading, + void Function( + _i2.WebView, + String, + )? + urlLoading, + }) => + _FakeWebViewClient_1( + this, + Invocation.getter(#createAndroidWebViewClient), + ), + ) as _i2.WebViewClient Function({ + void Function( + _i2.WebView, + String, + )? + onPageFinished, + void Function( + _i2.WebView, + String, + )? + onPageStarted, + void Function( + _i2.WebView, + int, + String, + String, + )? + onReceivedError, + void Function( + _i2.WebView, + _i2.WebResourceRequest, + _i2.WebResourceError, + )? + onReceivedRequestError, + void Function( + _i2.WebView, + _i2.WebResourceRequest, + )? + requestLoading, + void Function( + _i2.WebView, + String, + )? + urlLoading, + })); + @override + _i2.FlutterAssetManager Function() get createFlutterAssetManager => + (super.noSuchMethod( + Invocation.getter(#createFlutterAssetManager), + returnValue: () => _FakeFlutterAssetManager_8( + this, + Invocation.getter(#createFlutterAssetManager), + ), + returnValueForMissingStub: () => _FakeFlutterAssetManager_8( + this, + Invocation.getter(#createFlutterAssetManager), + ), + ) as _i2.FlutterAssetManager Function()); + @override + _i2.JavaScriptChannel Function( + String, { + required void Function(String) postMessage, + }) get createJavaScriptChannel => (super.noSuchMethod( + Invocation.getter(#createJavaScriptChannel), + returnValue: ( + String channelName, { + required void Function(String) postMessage, + }) => + _FakeJavaScriptChannel_9( + this, + Invocation.getter(#createJavaScriptChannel), + ), + returnValueForMissingStub: ( + String channelName, { + required void Function(String) postMessage, + }) => + _FakeJavaScriptChannel_9( + this, + Invocation.getter(#createJavaScriptChannel), + ), + ) as _i2.JavaScriptChannel Function( + String, { + required void Function(String) postMessage, + })); + @override + _i2.DownloadListener Function( + {required void Function( + String, + String, + String, + String, + int, + ) + onDownloadStart}) get createDownloadListener => (super.noSuchMethod( + Invocation.getter(#createDownloadListener), + returnValue: ( + {required void Function( + String, + String, + String, + String, + int, + ) + onDownloadStart}) => + _FakeDownloadListener_2( + this, + Invocation.getter(#createDownloadListener), + ), + returnValueForMissingStub: ( + {required void Function( + String, + String, + String, + String, + int, + ) + onDownloadStart}) => + _FakeDownloadListener_2( + this, + Invocation.getter(#createDownloadListener), + ), + ) as _i2.DownloadListener Function( + {required void Function( + String, + String, + String, + String, + int, + ) + onDownloadStart})); + @override + _i9.Future setWebContentsDebuggingEnabled(bool? enabled) => + (super.noSuchMethod( + Invocation.method( + #setWebContentsDebuggingEnabled, + [enabled], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); +} + +/// A class which mocks [AndroidWebViewWidgetCreationParams]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockAndroidWebViewWidgetCreationParams extends _i1.Mock + implements _i8.AndroidWebViewWidgetCreationParams { + @override + _i5.InstanceManager get instanceManager => (super.noSuchMethod( + Invocation.getter(#instanceManager), + returnValue: _FakeInstanceManager_10( + this, + Invocation.getter(#instanceManager), + ), + returnValueForMissingStub: _FakeInstanceManager_10( + this, + Invocation.getter(#instanceManager), + ), + ) as _i5.InstanceManager); + @override + _i6.PlatformViewsServiceProxy get platformViewsServiceProxy => + (super.noSuchMethod( + Invocation.getter(#platformViewsServiceProxy), + returnValue: _FakePlatformViewsServiceProxy_11( + this, + Invocation.getter(#platformViewsServiceProxy), + ), + returnValueForMissingStub: _FakePlatformViewsServiceProxy_11( + this, + Invocation.getter(#platformViewsServiceProxy), + ), + ) as _i6.PlatformViewsServiceProxy); + @override + bool get displayWithHybridComposition => (super.noSuchMethod( + Invocation.getter(#displayWithHybridComposition), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + _i3.PlatformWebViewController get controller => (super.noSuchMethod( + Invocation.getter(#controller), + returnValue: _FakePlatformWebViewController_12( + this, + Invocation.getter(#controller), + ), + returnValueForMissingStub: _FakePlatformWebViewController_12( + this, + Invocation.getter(#controller), + ), + ) as _i3.PlatformWebViewController); + @override + _i4.TextDirection get layoutDirection => (super.noSuchMethod( + Invocation.getter(#layoutDirection), + returnValue: _i4.TextDirection.rtl, + returnValueForMissingStub: _i4.TextDirection.rtl, + ) as _i4.TextDirection); + @override + Set<_i11.Factory<_i12.OneSequenceGestureRecognizer>> get gestureRecognizers => + (super.noSuchMethod( + Invocation.getter(#gestureRecognizers), + returnValue: <_i11.Factory<_i12.OneSequenceGestureRecognizer>>{}, + returnValueForMissingStub: < + _i11.Factory<_i12.OneSequenceGestureRecognizer>>{}, + ) as Set<_i11.Factory<_i12.OneSequenceGestureRecognizer>>); +} + +/// A class which mocks [ExpensiveAndroidViewController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockExpensiveAndroidViewController extends _i1.Mock + implements _i7.ExpensiveAndroidViewController { + @override + bool get requiresViewComposition => (super.noSuchMethod( + Invocation.getter(#requiresViewComposition), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + int get viewId => (super.noSuchMethod( + Invocation.getter(#viewId), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + bool get awaitingCreation => (super.noSuchMethod( + Invocation.getter(#awaitingCreation), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + _i7.PointTransformer get pointTransformer => (super.noSuchMethod( + Invocation.getter(#pointTransformer), + returnValue: (_i4.Offset position) => _FakeOffset_6( + this, + Invocation.getter(#pointTransformer), + ), + returnValueForMissingStub: (_i4.Offset position) => _FakeOffset_6( + this, + Invocation.getter(#pointTransformer), + ), + ) as _i7.PointTransformer); + @override + set pointTransformer(_i7.PointTransformer? transformer) => super.noSuchMethod( + Invocation.setter( + #pointTransformer, + transformer, + ), + returnValueForMissingStub: null, + ); + @override + bool get isCreated => (super.noSuchMethod( + Invocation.getter(#isCreated), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + List<_i7.PlatformViewCreatedCallback> get createdCallbacks => + (super.noSuchMethod( + Invocation.getter(#createdCallbacks), + returnValue: <_i7.PlatformViewCreatedCallback>[], + returnValueForMissingStub: <_i7.PlatformViewCreatedCallback>[], + ) as List<_i7.PlatformViewCreatedCallback>); + @override + _i9.Future setOffset(_i4.Offset? off) => (super.noSuchMethod( + Invocation.method( + #setOffset, + [off], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future create({ + _i4.Size? size, + _i4.Offset? position, + }) => + (super.noSuchMethod( + Invocation.method( + #create, + [], + { + #size: size, + #position: position, + }, + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future<_i4.Size> setSize(_i4.Size? size) => (super.noSuchMethod( + Invocation.method( + #setSize, + [size], + ), + returnValue: _i9.Future<_i4.Size>.value(_FakeSize_13( + this, + Invocation.method( + #setSize, + [size], + ), + )), + returnValueForMissingStub: _i9.Future<_i4.Size>.value(_FakeSize_13( + this, + Invocation.method( + #setSize, + [size], + ), + )), + ) as _i9.Future<_i4.Size>); + @override + _i9.Future sendMotionEvent(_i7.AndroidMotionEvent? event) => + (super.noSuchMethod( + Invocation.method( + #sendMotionEvent, + [event], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + void addOnPlatformViewCreatedListener( + _i7.PlatformViewCreatedCallback? listener) => + super.noSuchMethod( + Invocation.method( + #addOnPlatformViewCreatedListener, + [listener], + ), + returnValueForMissingStub: null, + ); + @override + void removeOnPlatformViewCreatedListener( + _i7.PlatformViewCreatedCallback? listener) => + super.noSuchMethod( + Invocation.method( + #removeOnPlatformViewCreatedListener, + [listener], + ), + returnValueForMissingStub: null, + ); + @override + _i9.Future setLayoutDirection(_i4.TextDirection? layoutDirection) => + (super.noSuchMethod( + Invocation.method( + #setLayoutDirection, + [layoutDirection], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future dispatchPointerEvent(_i13.PointerEvent? event) => + (super.noSuchMethod( + Invocation.method( + #dispatchPointerEvent, + [event], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future clearFocus() => (super.noSuchMethod( + Invocation.method( + #clearFocus, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future dispose() => (super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); +} + +/// A class which mocks [FlutterAssetManager]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFlutterAssetManager extends _i1.Mock + implements _i2.FlutterAssetManager { + @override + _i9.Future> list(String? path) => (super.noSuchMethod( + Invocation.method( + #list, + [path], + ), + returnValue: _i9.Future>.value([]), + returnValueForMissingStub: _i9.Future>.value([]), + ) as _i9.Future>); + @override + _i9.Future getAssetFilePathByName(String? name) => + (super.noSuchMethod( + Invocation.method( + #getAssetFilePathByName, + [name], + ), + returnValue: _i9.Future.value(''), + returnValueForMissingStub: _i9.Future.value(''), + ) as _i9.Future); +} + +/// A class which mocks [JavaScriptChannel]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockJavaScriptChannel extends _i1.Mock implements _i2.JavaScriptChannel { + @override + String get channelName => (super.noSuchMethod( + Invocation.getter(#channelName), + returnValue: '', + returnValueForMissingStub: '', + ) as String); + @override + void Function(String) get postMessage => (super.noSuchMethod( + Invocation.getter(#postMessage), + returnValue: (String message) {}, + returnValueForMissingStub: (String message) {}, + ) as void Function(String)); + @override + _i2.JavaScriptChannel copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeJavaScriptChannel_9( + this, + Invocation.method( + #copy, + [], + ), + ), + returnValueForMissingStub: _FakeJavaScriptChannel_9( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.JavaScriptChannel); +} + +/// A class which mocks [PlatformViewsServiceProxy]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockPlatformViewsServiceProxy extends _i1.Mock + implements _i6.PlatformViewsServiceProxy { + @override + _i7.ExpensiveAndroidViewController initExpensiveAndroidView({ + required int? id, + required String? viewType, + required _i4.TextDirection? layoutDirection, + dynamic creationParams, + _i7.MessageCodec? creationParamsCodec, + _i4.VoidCallback? onFocus, + }) => + (super.noSuchMethod( + Invocation.method( + #initExpensiveAndroidView, + [], + { + #id: id, + #viewType: viewType, + #layoutDirection: layoutDirection, + #creationParams: creationParams, + #creationParamsCodec: creationParamsCodec, + #onFocus: onFocus, + }, + ), + returnValue: _FakeExpensiveAndroidViewController_14( + this, + Invocation.method( + #initExpensiveAndroidView, + [], + { + #id: id, + #viewType: viewType, + #layoutDirection: layoutDirection, + #creationParams: creationParams, + #creationParamsCodec: creationParamsCodec, + #onFocus: onFocus, + }, + ), + ), + returnValueForMissingStub: _FakeExpensiveAndroidViewController_14( + this, + Invocation.method( + #initExpensiveAndroidView, + [], + { + #id: id, + #viewType: viewType, + #layoutDirection: layoutDirection, + #creationParams: creationParams, + #creationParamsCodec: creationParamsCodec, + #onFocus: onFocus, + }, + ), + ), + ) as _i7.ExpensiveAndroidViewController); + @override + _i7.SurfaceAndroidViewController initSurfaceAndroidView({ + required int? id, + required String? viewType, + required _i4.TextDirection? layoutDirection, + dynamic creationParams, + _i7.MessageCodec? creationParamsCodec, + _i4.VoidCallback? onFocus, + }) => + (super.noSuchMethod( + Invocation.method( + #initSurfaceAndroidView, + [], + { + #id: id, + #viewType: viewType, + #layoutDirection: layoutDirection, + #creationParams: creationParams, + #creationParamsCodec: creationParamsCodec, + #onFocus: onFocus, + }, + ), + returnValue: _FakeSurfaceAndroidViewController_15( + this, + Invocation.method( + #initSurfaceAndroidView, + [], + { + #id: id, + #viewType: viewType, + #layoutDirection: layoutDirection, + #creationParams: creationParams, + #creationParamsCodec: creationParamsCodec, + #onFocus: onFocus, + }, + ), + ), + returnValueForMissingStub: _FakeSurfaceAndroidViewController_15( + this, + Invocation.method( + #initSurfaceAndroidView, + [], + { + #id: id, + #viewType: viewType, + #layoutDirection: layoutDirection, + #creationParams: creationParams, + #creationParamsCodec: creationParamsCodec, + #onFocus: onFocus, + }, + ), + ), + ) as _i7.SurfaceAndroidViewController); +} + +/// A class which mocks [SurfaceAndroidViewController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSurfaceAndroidViewController extends _i1.Mock + implements _i7.SurfaceAndroidViewController { + @override + bool get requiresViewComposition => (super.noSuchMethod( + Invocation.getter(#requiresViewComposition), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + int get viewId => (super.noSuchMethod( + Invocation.getter(#viewId), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + bool get awaitingCreation => (super.noSuchMethod( + Invocation.getter(#awaitingCreation), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + _i7.PointTransformer get pointTransformer => (super.noSuchMethod( + Invocation.getter(#pointTransformer), + returnValue: (_i4.Offset position) => _FakeOffset_6( + this, + Invocation.getter(#pointTransformer), + ), + returnValueForMissingStub: (_i4.Offset position) => _FakeOffset_6( + this, + Invocation.getter(#pointTransformer), + ), + ) as _i7.PointTransformer); + @override + set pointTransformer(_i7.PointTransformer? transformer) => super.noSuchMethod( + Invocation.setter( + #pointTransformer, + transformer, + ), + returnValueForMissingStub: null, + ); + @override + bool get isCreated => (super.noSuchMethod( + Invocation.getter(#isCreated), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + List<_i7.PlatformViewCreatedCallback> get createdCallbacks => + (super.noSuchMethod( + Invocation.getter(#createdCallbacks), + returnValue: <_i7.PlatformViewCreatedCallback>[], + returnValueForMissingStub: <_i7.PlatformViewCreatedCallback>[], + ) as List<_i7.PlatformViewCreatedCallback>); + @override + _i9.Future setOffset(_i4.Offset? off) => (super.noSuchMethod( + Invocation.method( + #setOffset, + [off], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future create({ + _i4.Size? size, + _i4.Offset? position, + }) => + (super.noSuchMethod( + Invocation.method( + #create, + [], + { + #size: size, + #position: position, + }, + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future<_i4.Size> setSize(_i4.Size? size) => (super.noSuchMethod( + Invocation.method( + #setSize, + [size], + ), + returnValue: _i9.Future<_i4.Size>.value(_FakeSize_13( + this, + Invocation.method( + #setSize, + [size], + ), + )), + returnValueForMissingStub: _i9.Future<_i4.Size>.value(_FakeSize_13( + this, + Invocation.method( + #setSize, + [size], + ), + )), + ) as _i9.Future<_i4.Size>); + @override + _i9.Future sendMotionEvent(_i7.AndroidMotionEvent? event) => + (super.noSuchMethod( + Invocation.method( + #sendMotionEvent, + [event], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + void addOnPlatformViewCreatedListener( + _i7.PlatformViewCreatedCallback? listener) => + super.noSuchMethod( + Invocation.method( + #addOnPlatformViewCreatedListener, + [listener], + ), + returnValueForMissingStub: null, + ); + @override + void removeOnPlatformViewCreatedListener( + _i7.PlatformViewCreatedCallback? listener) => + super.noSuchMethod( + Invocation.method( + #removeOnPlatformViewCreatedListener, + [listener], + ), + returnValueForMissingStub: null, + ); + @override + _i9.Future setLayoutDirection(_i4.TextDirection? layoutDirection) => + (super.noSuchMethod( + Invocation.method( + #setLayoutDirection, + [layoutDirection], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future dispatchPointerEvent(_i13.PointerEvent? event) => + (super.noSuchMethod( + Invocation.method( + #dispatchPointerEvent, + [event], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future clearFocus() => (super.noSuchMethod( + Invocation.method( + #clearFocus, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future dispose() => (super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); +} + +/// A class which mocks [WebChromeClient]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebChromeClient extends _i1.Mock implements _i2.WebChromeClient { + @override + _i9.Future setSynchronousReturnValueForOnShowFileChooser(bool? value) => + (super.noSuchMethod( + Invocation.method( + #setSynchronousReturnValueForOnShowFileChooser, + [value], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i2.WebChromeClient copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebChromeClient_0( + this, + Invocation.method( + #copy, + [], + ), + ), + returnValueForMissingStub: _FakeWebChromeClient_0( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WebChromeClient); +} + +/// A class which mocks [WebSettings]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebSettings extends _i1.Mock implements _i2.WebSettings { + @override + _i9.Future setDomStorageEnabled(bool? flag) => (super.noSuchMethod( + Invocation.method( + #setDomStorageEnabled, + [flag], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setJavaScriptCanOpenWindowsAutomatically(bool? flag) => + (super.noSuchMethod( + Invocation.method( + #setJavaScriptCanOpenWindowsAutomatically, + [flag], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setSupportMultipleWindows(bool? support) => + (super.noSuchMethod( + Invocation.method( + #setSupportMultipleWindows, + [support], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setJavaScriptEnabled(bool? flag) => (super.noSuchMethod( + Invocation.method( + #setJavaScriptEnabled, + [flag], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setUserAgentString(String? userAgentString) => + (super.noSuchMethod( + Invocation.method( + #setUserAgentString, + [userAgentString], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setMediaPlaybackRequiresUserGesture(bool? require) => + (super.noSuchMethod( + Invocation.method( + #setMediaPlaybackRequiresUserGesture, + [require], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setSupportZoom(bool? support) => (super.noSuchMethod( + Invocation.method( + #setSupportZoom, + [support], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setLoadWithOverviewMode(bool? overview) => + (super.noSuchMethod( + Invocation.method( + #setLoadWithOverviewMode, + [overview], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setUseWideViewPort(bool? use) => (super.noSuchMethod( + Invocation.method( + #setUseWideViewPort, + [use], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setDisplayZoomControls(bool? enabled) => (super.noSuchMethod( + Invocation.method( + #setDisplayZoomControls, + [enabled], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setBuiltInZoomControls(bool? enabled) => (super.noSuchMethod( + Invocation.method( + #setBuiltInZoomControls, + [enabled], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setAllowFileAccess(bool? enabled) => (super.noSuchMethod( + Invocation.method( + #setAllowFileAccess, + [enabled], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i2.WebSettings copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebSettings_16( + this, + Invocation.method( + #copy, + [], + ), + ), + returnValueForMissingStub: _FakeWebSettings_16( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WebSettings); +} + +/// A class which mocks [WebView]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebView extends _i1.Mock implements _i2.WebView { + @override + bool get useHybridComposition => (super.noSuchMethod( + Invocation.getter(#useHybridComposition), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + @override + _i2.WebSettings get settings => (super.noSuchMethod( + Invocation.getter(#settings), + returnValue: _FakeWebSettings_16( + this, + Invocation.getter(#settings), + ), + returnValueForMissingStub: _FakeWebSettings_16( + this, + Invocation.getter(#settings), + ), + ) as _i2.WebSettings); + @override + _i9.Future loadData({ + required String? data, + String? mimeType, + String? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #loadData, + [], + { + #data: data, + #mimeType: mimeType, + #encoding: encoding, + }, + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future loadDataWithBaseUrl({ + String? baseUrl, + required String? data, + String? mimeType, + String? encoding, + String? historyUrl, + }) => + (super.noSuchMethod( + Invocation.method( + #loadDataWithBaseUrl, + [], + { + #baseUrl: baseUrl, + #data: data, + #mimeType: mimeType, + #encoding: encoding, + #historyUrl: historyUrl, + }, + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future loadUrl( + String? url, + Map? headers, + ) => + (super.noSuchMethod( + Invocation.method( + #loadUrl, + [ + url, + headers, + ], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future postUrl( + String? url, + _i14.Uint8List? data, + ) => + (super.noSuchMethod( + Invocation.method( + #postUrl, + [ + url, + data, + ], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future getUrl() => (super.noSuchMethod( + Invocation.method( + #getUrl, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future canGoBack() => (super.noSuchMethod( + Invocation.method( + #canGoBack, + [], + ), + returnValue: _i9.Future.value(false), + returnValueForMissingStub: _i9.Future.value(false), + ) as _i9.Future); + @override + _i9.Future canGoForward() => (super.noSuchMethod( + Invocation.method( + #canGoForward, + [], + ), + returnValue: _i9.Future.value(false), + returnValueForMissingStub: _i9.Future.value(false), + ) as _i9.Future); + @override + _i9.Future goBack() => (super.noSuchMethod( + Invocation.method( + #goBack, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future goForward() => (super.noSuchMethod( + Invocation.method( + #goForward, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future reload() => (super.noSuchMethod( + Invocation.method( + #reload, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future clearCache(bool? includeDiskFiles) => (super.noSuchMethod( + Invocation.method( + #clearCache, + [includeDiskFiles], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future evaluateJavascript(String? javascriptString) => + (super.noSuchMethod( + Invocation.method( + #evaluateJavascript, + [javascriptString], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future getTitle() => (super.noSuchMethod( + Invocation.method( + #getTitle, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future scrollTo( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollTo, + [ + x, + y, + ], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future scrollBy( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollBy, + [ + x, + y, + ], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future getScrollX() => (super.noSuchMethod( + Invocation.method( + #getScrollX, + [], + ), + returnValue: _i9.Future.value(0), + returnValueForMissingStub: _i9.Future.value(0), + ) as _i9.Future); + @override + _i9.Future getScrollY() => (super.noSuchMethod( + Invocation.method( + #getScrollY, + [], + ), + returnValue: _i9.Future.value(0), + returnValueForMissingStub: _i9.Future.value(0), + ) as _i9.Future); + @override + _i9.Future<_i4.Offset> getScrollPosition() => (super.noSuchMethod( + Invocation.method( + #getScrollPosition, + [], + ), + returnValue: _i9.Future<_i4.Offset>.value(_FakeOffset_6( + this, + Invocation.method( + #getScrollPosition, + [], + ), + )), + returnValueForMissingStub: _i9.Future<_i4.Offset>.value(_FakeOffset_6( + this, + Invocation.method( + #getScrollPosition, + [], + ), + )), + ) as _i9.Future<_i4.Offset>); + @override + _i9.Future setWebViewClient(_i2.WebViewClient? webViewClient) => + (super.noSuchMethod( + Invocation.method( + #setWebViewClient, + [webViewClient], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future addJavaScriptChannel( + _i2.JavaScriptChannel? javaScriptChannel) => + (super.noSuchMethod( + Invocation.method( + #addJavaScriptChannel, + [javaScriptChannel], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future removeJavaScriptChannel( + _i2.JavaScriptChannel? javaScriptChannel) => + (super.noSuchMethod( + Invocation.method( + #removeJavaScriptChannel, + [javaScriptChannel], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setDownloadListener(_i2.DownloadListener? listener) => + (super.noSuchMethod( + Invocation.method( + #setDownloadListener, + [listener], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setWebChromeClient(_i2.WebChromeClient? client) => + (super.noSuchMethod( + Invocation.method( + #setWebChromeClient, + [client], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i9.Future setBackgroundColor(_i4.Color? color) => (super.noSuchMethod( + Invocation.method( + #setBackgroundColor, + [color], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i2.WebView copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebView_7( + this, + Invocation.method( + #copy, + [], + ), + ), + returnValueForMissingStub: _FakeWebView_7( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WebView); +} + +/// A class which mocks [WebViewClient]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewClient extends _i1.Mock implements _i2.WebViewClient { + @override + _i9.Future setSynchronousReturnValueForShouldOverrideUrlLoading( + bool? value) => + (super.noSuchMethod( + Invocation.method( + #setSynchronousReturnValueForShouldOverrideUrlLoading, + [value], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i2.WebViewClient copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebViewClient_1( + this, + Invocation.method( + #copy, + [], + ), + ), + returnValueForMissingStub: _FakeWebViewClient_1( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WebViewClient); +} + +/// A class which mocks [WebStorage]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebStorage extends _i1.Mock implements _i2.WebStorage { + @override + _i9.Future deleteAllData() => (super.noSuchMethod( + Invocation.method( + #deleteAllData, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override + _i2.WebStorage copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebStorage_17( + this, + Invocation.method( + #copy, + [], + ), + ), + returnValueForMissingStub: _FakeWebStorage_17( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WebStorage); +} + +/// A class which mocks [InstanceManager]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockInstanceManager extends _i1.Mock implements _i5.InstanceManager { + @override + void Function(int) get onWeakReferenceRemoved => (super.noSuchMethod( + Invocation.getter(#onWeakReferenceRemoved), + returnValue: (int __p0) {}, + returnValueForMissingStub: (int __p0) {}, + ) as void Function(int)); + @override + set onWeakReferenceRemoved(void Function(int)? _onWeakReferenceRemoved) => + super.noSuchMethod( + Invocation.setter( + #onWeakReferenceRemoved, + _onWeakReferenceRemoved, + ), + returnValueForMissingStub: null, + ); + @override + int addDartCreatedInstance(_i5.Copyable? instance) => (super.noSuchMethod( + Invocation.method( + #addDartCreatedInstance, + [instance], + ), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + int? removeWeakReference(_i5.Copyable? instance) => (super.noSuchMethod( + Invocation.method( + #removeWeakReference, + [instance], + ), + returnValueForMissingStub: null, + ) as int?); + @override + T? remove(int? identifier) => (super.noSuchMethod( + Invocation.method( + #remove, + [identifier], + ), + returnValueForMissingStub: null, + ) as T?); + @override + T? getInstanceWithWeakReference(int? identifier) => + (super.noSuchMethod( + Invocation.method( + #getInstanceWithWeakReference, + [identifier], + ), + returnValueForMissingStub: null, + ) as T?); + @override + int? getIdentifier(_i5.Copyable? instance) => (super.noSuchMethod( + Invocation.method( + #getIdentifier, + [instance], + ), + returnValueForMissingStub: null, + ) as int?); + @override + void addHostCreatedInstance( + _i5.Copyable? instance, + int? identifier, + ) => + super.noSuchMethod( + Invocation.method( + #addHostCreatedInstance, + [ + instance, + identifier, + ], + ), + returnValueForMissingStub: null, + ); + @override + bool containsIdentifier(int? identifier) => (super.noSuchMethod( + Invocation.method( + #containsIdentifier, + [identifier], + ), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); +} diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.dart new file mode 100644 index 000000000000..9e7422fba88a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.dart @@ -0,0 +1,79 @@ +// 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:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:webview_flutter_android/src/android_webview.dart' + as android_webview; +import 'package:webview_flutter_android/webview_flutter_android.dart'; +import 'package:webview_flutter_platform_interface/src/webview_platform.dart'; + +import 'android_webview_cookie_manager_test.mocks.dart'; + +@GenerateMocks([android_webview.CookieManager]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('clearCookies should call android_webview.clearCookies', () async { + final android_webview.CookieManager mockCookieManager = MockCookieManager(); + + when(mockCookieManager.clearCookies()) + .thenAnswer((_) => Future.value(true)); + + final AndroidWebViewCookieManagerCreationParams params = + AndroidWebViewCookieManagerCreationParams + .fromPlatformWebViewCookieManagerCreationParams( + const PlatformWebViewCookieManagerCreationParams()); + + final bool hasClearedCookies = await AndroidWebViewCookieManager(params, + cookieManager: mockCookieManager) + .clearCookies(); + + expect(hasClearedCookies, true); + verify(mockCookieManager.clearCookies()); + }); + + test('setCookie should throw ArgumentError for cookie with invalid path', () { + final AndroidWebViewCookieManagerCreationParams params = + AndroidWebViewCookieManagerCreationParams + .fromPlatformWebViewCookieManagerCreationParams( + const PlatformWebViewCookieManagerCreationParams()); + + final AndroidWebViewCookieManager androidCookieManager = + AndroidWebViewCookieManager(params, cookieManager: MockCookieManager()); + + expect( + () => androidCookieManager.setCookie(const WebViewCookie( + name: 'foo', + value: 'bar', + domain: 'flutter.dev', + path: 'invalid;path', + )), + throwsA(const TypeMatcher()), + ); + }); + + test( + 'setCookie should call android_webview.csetCookie with properly formatted cookie value', + () { + final android_webview.CookieManager mockCookieManager = MockCookieManager(); + final AndroidWebViewCookieManagerCreationParams params = + AndroidWebViewCookieManagerCreationParams + .fromPlatformWebViewCookieManagerCreationParams( + const PlatformWebViewCookieManagerCreationParams()); + + AndroidWebViewCookieManager(params, cookieManager: mockCookieManager) + .setCookie(const WebViewCookie( + name: 'foo&', + value: 'bar@', + domain: 'flutter.dev', + )); + + verify(mockCookieManager.setCookie( + 'flutter.dev', + 'foo%26=bar%40; path=/', + )); + }); +} diff --git a/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart similarity index 52% rename from packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.mocks.dart rename to packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart index 308aba4fa1b0..07321805a559 100644 --- a/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart @@ -1,7 +1,8 @@ -// Mocks generated by Mockito 5.2.0 from annotations -// in webview_flutter_android/test/webview_android_cookie_manager_test.dart. +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter_android/test/android_webview_cookie_manager_test.dart. // Do not manually edit this file. +// ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i3; import 'package:mockito/mockito.dart' as _i1; @@ -16,6 +17,7 @@ import 'package:webview_flutter_android/src/android_webview.dart' as _i2; // 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 [CookieManager]. /// @@ -26,12 +28,27 @@ class MockCookieManager extends _i1.Mock implements _i2.CookieManager { } @override - _i3.Future setCookie(String? url, String? value) => - (super.noSuchMethod(Invocation.method(#setCookie, [url, value]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); + _i3.Future setCookie( + String? url, + String? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setCookie, + [ + url, + value, + ], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); @override - _i3.Future clearCookies() => - (super.noSuchMethod(Invocation.method(#clearCookies, []), - returnValue: Future.value(false)) as _i3.Future); + _i3.Future clearCookies() => (super.noSuchMethod( + Invocation.method( + #clearCookies, + [], + ), + returnValue: _i3.Future.value(false), + ) as _i3.Future); } 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 f3ec4bd0cb9f..236d87da44eb 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 @@ -6,12 +6,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:webview_flutter_android/src/android_webview.dart'; -import 'package:webview_flutter_android/src/android_webview.pigeon.dart'; +import 'package:webview_flutter_android/src/android_webview.g.dart'; import 'package:webview_flutter_android/src/android_webview_api_impls.dart'; import 'package:webview_flutter_android/src/instance_manager.dart'; import 'android_webview_test.mocks.dart'; -import 'test_android_webview.pigeon.dart'; +import 'test_android_webview.g.dart'; @GenerateMocks([ CookieManagerHostApi, @@ -269,7 +269,7 @@ void main() { final WebViewClient mockWebViewClient = MockWebViewClient(); when(mockWebViewClient.copy()).thenReturn(MockWebViewClient()); - when(mockWebViewClient.shouldOverrideUrlLoading).thenReturn(false); + instanceManager.addDartCreatedInstance(mockWebViewClient); webView.setWebViewClient(mockWebViewClient); final int webViewClientInstanceId = @@ -334,6 +334,7 @@ void main() { final DownloadListener mockDownloadListener = MockDownloadListener(); when(mockDownloadListener.copy()).thenReturn(MockDownloadListener()); + instanceManager.addDartCreatedInstance(mockDownloadListener); webView.setDownloadListener(mockDownloadListener); final int downloadListenerInstanceId = @@ -345,16 +346,6 @@ void main() { }); test('setWebChromeClient', () { - // Setting a WebChromeClient requires setting a WebViewClient first. - TestWebViewClientHostApi.setup(MockTestWebViewClientHostApi()); - WebViewClient.api = WebViewClientHostApiImpl( - instanceManager: instanceManager, - ); - final WebViewClient mockWebViewClient = MockWebViewClient(); - when(mockWebViewClient.copy()).thenReturn(MockWebViewClient()); - when(mockWebViewClient.shouldOverrideUrlLoading).thenReturn(false); - webView.setWebViewClient(mockWebViewClient); - TestWebChromeClientHostApi.setup(MockTestWebChromeClientHostApi()); WebChromeClient.api = WebChromeClientHostApiImpl( instanceManager: instanceManager, @@ -362,6 +353,7 @@ void main() { final WebChromeClient mockWebChromeClient = MockWebChromeClient(); when(mockWebChromeClient.copy()).thenReturn(MockWebChromeClient()); + instanceManager.addDartCreatedInstance(mockWebChromeClient); webView.setWebChromeClient(mockWebChromeClient); final int webChromeClientInstanceId = @@ -372,21 +364,6 @@ void main() { )); }); - test('release', () { - final MockTestWebSettingsHostApi mockWebSettingsPlatformHostApi = - MockTestWebSettingsHostApi(); - TestWebSettingsHostApi.setup(mockWebSettingsPlatformHostApi); - - WebSettings.api = - WebSettingsHostApiImpl(instanceManager: instanceManager); - final int webSettingsInstanceId = - instanceManager.getIdentifier(webView.settings)!; - - webView.release(); - verify(mockWebSettingsPlatformHostApi.dispose(webSettingsInstanceId)); - verify(mockPlatformHostApi.dispose(webViewInstanceId)); - }); - test('copy', () { expect(webView.copy(), isA()); }); @@ -544,16 +521,22 @@ void main() { }); test('postMessage', () { + late final String result; + when(mockJavaScriptChannel.postMessage).thenReturn((String message) { + result = message; + }); + flutterApi.postMessage( mockJavaScriptChannelInstanceId, 'Hello, World!', ); - verify(mockJavaScriptChannel.postMessage('Hello, World!')); + + expect(result, 'Hello, World!'); }); test('copy', () { expect( - JavaScriptChannel.detached('channel').copy(), + JavaScriptChannel.detached('channel', postMessage: (_) {}).copy(), isA(), ); }); @@ -588,30 +571,51 @@ void main() { }); test('onPageStarted', () { + late final List result; + when(mockWebViewClient.onPageStarted).thenReturn( + (WebView webView, String url) { + result = [webView, url]; + }, + ); + flutterApi.onPageStarted( mockWebViewClientInstanceId, mockWebViewInstanceId, 'https://www.google.com', ); - verify(mockWebViewClient.onPageStarted( - mockWebView, - 'https://www.google.com', - )); + + expect(result, [mockWebView, 'https://www.google.com']); }); test('onPageFinished', () { + late final List result; + when(mockWebViewClient.onPageFinished).thenReturn( + (WebView webView, String url) { + result = [webView, url]; + }, + ); + flutterApi.onPageFinished( mockWebViewClientInstanceId, mockWebViewInstanceId, 'https://www.google.com', ); - verify(mockWebViewClient.onPageFinished( - mockWebView, - 'https://www.google.com', - )); + + expect(result, [mockWebView, 'https://www.google.com']); }); test('onReceivedRequestError', () { + late final List result; + when(mockWebViewClient.onReceivedRequestError).thenReturn( + ( + WebView webView, + WebResourceRequest request, + WebResourceError error, + ) { + result = [webView, request, error]; + }, + ); + flutterApi.onReceivedRequestError( mockWebViewClientInstanceId, mockWebViewInstanceId, @@ -626,14 +630,25 @@ void main() { WebResourceErrorData(errorCode: 34, description: 'error description'), ); - verify(mockWebViewClient.onReceivedRequestError( - mockWebView, - argThat(isNotNull), - argThat(isNotNull), - )); + expect( + result, + containsAllInOrder([mockWebView, isNotNull, isNotNull]), + ); }); test('onReceivedError', () { + late final List result; + when(mockWebViewClient.onReceivedError).thenReturn( + ( + WebView webView, + int errorCode, + String description, + String failingUrl, + ) { + result = [webView, errorCode, description, failingUrl]; + }, + ); + flutterApi.onReceivedError( mockWebViewClientInstanceId, mockWebViewInstanceId, @@ -642,15 +657,22 @@ void main() { 'https://www.google.com', ); - verify(mockWebViewClient.onReceivedError( - mockWebView, - 14, - 'desc', - 'https://www.google.com', - )); + expect( + result, + containsAllInOrder( + [mockWebView, 14, 'desc', 'https://www.google.com'], + ), + ); }); test('requestLoading', () { + late final List result; + when(mockWebViewClient.requestLoading).thenReturn( + (WebView webView, WebResourceRequest request) { + result = [webView, request]; + }, + ); + flutterApi.requestLoading( mockWebViewClientInstanceId, mockWebViewInstanceId, @@ -664,20 +686,27 @@ void main() { ), ); - verify(mockWebViewClient.requestLoading( - mockWebView, - argThat(isNotNull), - )); + expect( + result, + containsAllInOrder([mockWebView, isNotNull]), + ); }); test('urlLoading', () { + late final List result; + when(mockWebViewClient.urlLoading).thenReturn( + (WebView webView, String url) { + result = [webView, url]; + }, + ); + flutterApi.urlLoading(mockWebViewClientInstanceId, mockWebViewInstanceId, 'https://www.google.com'); - verify(mockWebViewClient.urlLoading( - mockWebView, - 'https://www.google.com', - )); + expect( + result, + containsAllInOrder([mockWebView, 'https://www.google.com']), + ); }); test('copy', () { @@ -705,7 +734,26 @@ void main() { instanceManager.addDartCreatedInstance(mockDownloadListener); }); - test('onPageStarted', () { + test('onDownloadStart', () { + late final List result; + when(mockDownloadListener.onDownloadStart).thenReturn( + ( + String url, + String userAgent, + String contentDisposition, + String mimetype, + int contentLength, + ) { + result = [ + url, + userAgent, + contentDisposition, + mimetype, + contentLength, + ]; + }, + ); + flutterApi.onDownloadStart( mockDownloadListenerInstanceId, 'url', @@ -714,17 +762,26 @@ void main() { 'mimetype', 45, ); - verify(mockDownloadListener.onDownloadStart( - 'url', - 'userAgent', - 'contentDescription', - 'mimetype', - 45, - )); + + expect( + result, + containsAllInOrder([ + 'url', + 'userAgent', + 'contentDescription', + 'mimetype', + 45, + ]), + ); }); test('copy', () { - expect(DownloadListener.detached().copy(), isA()); + expect( + DownloadListener.detached( + onDownloadStart: (_, __, ____, _____, ______) {}, + ).copy(), + isA(), + ); }); }); @@ -757,19 +814,137 @@ void main() { instanceManager.addDartCreatedInstance(mockWebView); }); - test('onPageStarted', () { + test('onProgressChanged', () { + late final List result; + when(mockWebChromeClient.onProgressChanged).thenReturn( + (WebView webView, int progress) { + result = [webView, progress]; + }, + ); + flutterApi.onProgressChanged( mockWebChromeClientInstanceId, mockWebViewInstanceId, 76, ); - verify(mockWebChromeClient.onProgressChanged(mockWebView, 76)); + + expect(result, containsAllInOrder([mockWebView, 76])); + }); + + test('onShowFileChooser', () async { + late final List result; + when(mockWebChromeClient.onShowFileChooser).thenReturn( + (WebView webView, FileChooserParams params) { + result = [webView, params]; + return Future>.value(['fileOne', 'fileTwo']); + }, + ); + + final FileChooserParams params = FileChooserParams.detached( + isCaptureEnabled: false, + acceptTypes: [], + filenameHint: 'filenameHint', + mode: FileChooserMode.open, + ); + + instanceManager.addHostCreatedInstance(params, 3); + + await expectLater( + flutterApi.onShowFileChooser( + mockWebChromeClientInstanceId, + mockWebViewInstanceId, + 3, + ), + completion(['fileOne', 'fileTwo']), + ); + expect(result[0], mockWebView); + expect(result[1], params); + }); + + test('setSynchronousReturnValueForOnShowFileChooser', () { + final MockTestWebChromeClientHostApi mockHostApi = + MockTestWebChromeClientHostApi(); + TestWebChromeClientHostApi.setup(mockHostApi); + + WebChromeClient.api = + WebChromeClientHostApiImpl(instanceManager: instanceManager); + + final WebChromeClient webChromeClient = WebChromeClient.detached(); + instanceManager.addHostCreatedInstance(webChromeClient, 2); + + webChromeClient.setSynchronousReturnValueForOnShowFileChooser(false); + + verify( + mockHostApi.setSynchronousReturnValueForOnShowFileChooser(2, false), + ); + }); + + test( + 'setSynchronousReturnValueForOnShowFileChooser throws StateError when onShowFileChooser is null', + () { + final MockTestWebChromeClientHostApi mockHostApi = + MockTestWebChromeClientHostApi(); + TestWebChromeClientHostApi.setup(mockHostApi); + + WebChromeClient.api = + WebChromeClientHostApiImpl(instanceManager: instanceManager); + + final WebChromeClient clientWithNullCallback = + WebChromeClient.detached(); + instanceManager.addHostCreatedInstance(clientWithNullCallback, 2); + + expect( + () => clientWithNullCallback + .setSynchronousReturnValueForOnShowFileChooser(true), + throwsStateError, + ); + + final WebChromeClient clientWithNonnullCallback = + WebChromeClient.detached( + onShowFileChooser: (_, __) async => [], + ); + instanceManager.addHostCreatedInstance(clientWithNonnullCallback, 3); + + clientWithNonnullCallback + .setSynchronousReturnValueForOnShowFileChooser(true); + + verify( + mockHostApi.setSynchronousReturnValueForOnShowFileChooser(3, true), + ); }); 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'); + }); + }); }); group('CookieManager', () { 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 116ac834f1d6..0b5afbaf5b13 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 @@ -1,16 +1,17 @@ -// Mocks generated by Mockito 5.2.0 from annotations +// Mocks generated by Mockito 5.3.2 from annotations // in webview_flutter_android/test/android_webview_test.dart. // Do not manually edit this file. +// ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; import 'dart:typed_data' as _i7; import 'dart:ui' as _i4; import 'package:mockito/mockito.dart' as _i1; import 'package:webview_flutter_android/src/android_webview.dart' as _i2; -import 'package:webview_flutter_android/src/android_webview.pigeon.dart' as _i3; +import 'package:webview_flutter_android/src/android_webview.g.dart' as _i3; -import 'test_android_webview.pigeon.dart' as _i6; +import 'test_android_webview.g.dart' as _i6; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -21,24 +22,90 @@ import 'test_android_webview.pigeon.dart' as _i6; // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class -class _FakeDownloadListener_0 extends _i1.Fake - implements _i2.DownloadListener {} +class _FakeDownloadListener_0 extends _i1.SmartFake + implements _i2.DownloadListener { + _FakeDownloadListener_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeJavaScriptChannel_1 extends _i1.Fake - implements _i2.JavaScriptChannel {} +class _FakeJavaScriptChannel_1 extends _i1.SmartFake + implements _i2.JavaScriptChannel { + _FakeJavaScriptChannel_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeWebViewPoint_2 extends _i1.Fake implements _i3.WebViewPoint {} +class _FakeWebViewPoint_2 extends _i1.SmartFake implements _i3.WebViewPoint { + _FakeWebViewPoint_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeWebChromeClient_3 extends _i1.Fake implements _i2.WebChromeClient {} +class _FakeWebChromeClient_3 extends _i1.SmartFake + implements _i2.WebChromeClient { + _FakeWebChromeClient_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeWebSettings_4 extends _i1.Fake implements _i2.WebSettings {} +class _FakeWebSettings_4 extends _i1.SmartFake implements _i2.WebSettings { + _FakeWebSettings_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeOffset_5 extends _i1.Fake implements _i4.Offset {} +class _FakeOffset_5 extends _i1.SmartFake implements _i4.Offset { + _FakeOffset_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeWebView_6 extends _i1.Fake implements _i2.WebView {} +class _FakeWebView_6 extends _i1.SmartFake implements _i2.WebView { + _FakeWebView_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} -class _FakeWebViewClient_7 extends _i1.Fake implements _i2.WebViewClient {} +class _FakeWebViewClient_7 extends _i1.SmartFake implements _i2.WebViewClient { + _FakeWebViewClient_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} /// A class which mocks [CookieManagerHostApi]. /// @@ -50,14 +117,29 @@ class MockCookieManagerHostApi extends _i1.Mock } @override - _i5.Future clearCookies() => - (super.noSuchMethod(Invocation.method(#clearCookies, []), - returnValue: Future.value(false)) as _i5.Future); - @override - _i5.Future setCookie(String? arg_url, String? arg_value) => - (super.noSuchMethod(Invocation.method(#setCookie, [arg_url, arg_value]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + _i5.Future clearCookies() => (super.noSuchMethod( + Invocation.method( + #clearCookies, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future setCookie( + String? arg_url, + String? arg_value, + ) => + (super.noSuchMethod( + Invocation.method( + #setCookie, + [ + arg_url, + arg_value, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [DownloadListener]. @@ -69,16 +151,42 @@ class MockDownloadListener extends _i1.Mock implements _i2.DownloadListener { } @override - void onDownloadStart(String? url, String? userAgent, - String? contentDisposition, String? mimetype, int? contentLength) => - super.noSuchMethod( - Invocation.method(#onDownloadStart, - [url, userAgent, contentDisposition, mimetype, contentLength]), - returnValueForMissingStub: null); - @override - _i2.DownloadListener copy() => - (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeDownloadListener_0()) as _i2.DownloadListener); + void Function( + String, + String, + String, + String, + int, + ) get onDownloadStart => (super.noSuchMethod( + Invocation.getter(#onDownloadStart), + returnValue: ( + String url, + String userAgent, + String contentDisposition, + String mimetype, + int contentLength, + ) {}, + ) as void Function( + String, + String, + String, + String, + int, + )); + @override + _i2.DownloadListener copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeDownloadListener_0( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.DownloadListener); } /// A class which mocks [JavaScriptChannel]. @@ -90,17 +198,29 @@ class MockJavaScriptChannel extends _i1.Mock implements _i2.JavaScriptChannel { } @override - String get channelName => - (super.noSuchMethod(Invocation.getter(#channelName), returnValue: '') - as String); - @override - void postMessage(String? message) => - super.noSuchMethod(Invocation.method(#postMessage, [message]), - returnValueForMissingStub: null); - @override - _i2.JavaScriptChannel copy() => - (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeJavaScriptChannel_1()) as _i2.JavaScriptChannel); + String get channelName => (super.noSuchMethod( + Invocation.getter(#channelName), + returnValue: '', + ) as String); + @override + void Function(String) get postMessage => (super.noSuchMethod( + Invocation.getter(#postMessage), + returnValue: (String message) {}, + ) as void Function(String)); + @override + _i2.JavaScriptChannel copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeJavaScriptChannel_1( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.JavaScriptChannel); } /// A class which mocks [TestDownloadListenerHostApi]. @@ -113,9 +233,13 @@ class MockTestDownloadListenerHostApi extends _i1.Mock } @override - void create(int? instanceId) => - super.noSuchMethod(Invocation.method(#create, [instanceId]), - returnValueForMissingStub: null); + void create(int? instanceId) => super.noSuchMethod( + Invocation.method( + #create, + [instanceId], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestJavaObjectHostApi]. @@ -128,9 +252,13 @@ class MockTestJavaObjectHostApi extends _i1.Mock } @override - void dispose(int? identifier) => - super.noSuchMethod(Invocation.method(#dispose, [identifier]), - returnValueForMissingStub: null); + void dispose(int? identifier) => super.noSuchMethod( + Invocation.method( + #dispose, + [identifier], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestJavaScriptChannelHostApi]. @@ -143,9 +271,20 @@ class MockTestJavaScriptChannelHostApi extends _i1.Mock } @override - void create(int? instanceId, String? channelName) => - super.noSuchMethod(Invocation.method(#create, [instanceId, channelName]), - returnValueForMissingStub: null); + void create( + int? instanceId, + String? channelName, + ) => + super.noSuchMethod( + Invocation.method( + #create, + [ + instanceId, + channelName, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestWebChromeClientHostApi]. @@ -158,10 +297,28 @@ class MockTestWebChromeClientHostApi extends _i1.Mock } @override - void create(int? instanceId, int? webViewClientInstanceId) => + void create(int? instanceId) => super.noSuchMethod( + Invocation.method( + #create, + [instanceId], + ), + returnValueForMissingStub: null, + ); + @override + void setSynchronousReturnValueForOnShowFileChooser( + int? instanceId, + bool? value, + ) => super.noSuchMethod( - Invocation.method(#create, [instanceId, webViewClientInstanceId]), - returnValueForMissingStub: null); + Invocation.method( + #setSynchronousReturnValueForOnShowFileChooser, + [ + instanceId, + value, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestWebSettingsHostApi]. @@ -174,70 +331,200 @@ class MockTestWebSettingsHostApi extends _i1.Mock } @override - void create(int? instanceId, int? webViewInstanceId) => super.noSuchMethod( - Invocation.method(#create, [instanceId, webViewInstanceId]), - returnValueForMissingStub: null); - @override - void dispose(int? instanceId) => - super.noSuchMethod(Invocation.method(#dispose, [instanceId]), - returnValueForMissingStub: null); - @override - void setDomStorageEnabled(int? instanceId, bool? flag) => super.noSuchMethod( - Invocation.method(#setDomStorageEnabled, [instanceId, flag]), - returnValueForMissingStub: null); - @override - void setJavaScriptCanOpenWindowsAutomatically(int? instanceId, bool? flag) => + void create( + int? instanceId, + int? webViewInstanceId, + ) => super.noSuchMethod( - Invocation.method( - #setJavaScriptCanOpenWindowsAutomatically, [instanceId, flag]), - returnValueForMissingStub: null); - @override - void setSupportMultipleWindows(int? instanceId, bool? support) => + Invocation.method( + #create, + [ + instanceId, + webViewInstanceId, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setDomStorageEnabled( + int? instanceId, + bool? flag, + ) => super.noSuchMethod( - Invocation.method(#setSupportMultipleWindows, [instanceId, support]), - returnValueForMissingStub: null); - @override - void setJavaScriptEnabled(int? instanceId, bool? flag) => super.noSuchMethod( - Invocation.method(#setJavaScriptEnabled, [instanceId, flag]), - returnValueForMissingStub: null); - @override - void setUserAgentString(int? instanceId, String? userAgentString) => + Invocation.method( + #setDomStorageEnabled, + [ + instanceId, + flag, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setJavaScriptCanOpenWindowsAutomatically( + int? instanceId, + bool? flag, + ) => super.noSuchMethod( - Invocation.method(#setUserAgentString, [instanceId, userAgentString]), - returnValueForMissingStub: null); - @override - void setMediaPlaybackRequiresUserGesture(int? instanceId, bool? require) => + Invocation.method( + #setJavaScriptCanOpenWindowsAutomatically, + [ + instanceId, + flag, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setSupportMultipleWindows( + int? instanceId, + bool? support, + ) => super.noSuchMethod( - Invocation.method( - #setMediaPlaybackRequiresUserGesture, [instanceId, require]), - returnValueForMissingStub: null); - @override - void setSupportZoom(int? instanceId, bool? support) => super.noSuchMethod( - Invocation.method(#setSupportZoom, [instanceId, support]), - returnValueForMissingStub: null); - @override - void setLoadWithOverviewMode(int? instanceId, bool? overview) => + Invocation.method( + #setSupportMultipleWindows, + [ + instanceId, + support, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setJavaScriptEnabled( + int? instanceId, + bool? flag, + ) => super.noSuchMethod( - Invocation.method(#setLoadWithOverviewMode, [instanceId, overview]), - returnValueForMissingStub: null); - @override - void setUseWideViewPort(int? instanceId, bool? use) => super.noSuchMethod( - Invocation.method(#setUseWideViewPort, [instanceId, use]), - returnValueForMissingStub: null); - @override - void setDisplayZoomControls(int? instanceId, bool? enabled) => + Invocation.method( + #setJavaScriptEnabled, + [ + instanceId, + flag, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setUserAgentString( + int? instanceId, + String? userAgentString, + ) => super.noSuchMethod( - Invocation.method(#setDisplayZoomControls, [instanceId, enabled]), - returnValueForMissingStub: null); - @override - void setBuiltInZoomControls(int? instanceId, bool? enabled) => + Invocation.method( + #setUserAgentString, + [ + instanceId, + userAgentString, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setMediaPlaybackRequiresUserGesture( + int? instanceId, + bool? require, + ) => super.noSuchMethod( - Invocation.method(#setBuiltInZoomControls, [instanceId, enabled]), - returnValueForMissingStub: null); - @override - void setAllowFileAccess(int? instanceId, bool? enabled) => super.noSuchMethod( - Invocation.method(#setAllowFileAccess, [instanceId, enabled]), - returnValueForMissingStub: null); + Invocation.method( + #setMediaPlaybackRequiresUserGesture, + [ + instanceId, + require, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setSupportZoom( + int? instanceId, + bool? support, + ) => + super.noSuchMethod( + Invocation.method( + #setSupportZoom, + [ + instanceId, + support, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setLoadWithOverviewMode( + int? instanceId, + bool? overview, + ) => + super.noSuchMethod( + Invocation.method( + #setLoadWithOverviewMode, + [ + instanceId, + overview, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setUseWideViewPort( + int? instanceId, + bool? use, + ) => + super.noSuchMethod( + Invocation.method( + #setUseWideViewPort, + [ + instanceId, + use, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setDisplayZoomControls( + int? instanceId, + bool? enabled, + ) => + super.noSuchMethod( + Invocation.method( + #setDisplayZoomControls, + [ + instanceId, + enabled, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setBuiltInZoomControls( + int? instanceId, + bool? enabled, + ) => + super.noSuchMethod( + Invocation.method( + #setBuiltInZoomControls, + [ + instanceId, + enabled, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setAllowFileAccess( + int? instanceId, + bool? enabled, + ) => + super.noSuchMethod( + Invocation.method( + #setAllowFileAccess, + [ + instanceId, + enabled, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestWebStorageHostApi]. @@ -250,13 +537,21 @@ class MockTestWebStorageHostApi extends _i1.Mock } @override - void create(int? instanceId) => - super.noSuchMethod(Invocation.method(#create, [instanceId]), - returnValueForMissingStub: null); - @override - void deleteAllData(int? instanceId) => - super.noSuchMethod(Invocation.method(#deleteAllData, [instanceId]), - returnValueForMissingStub: null); + void create(int? instanceId) => super.noSuchMethod( + Invocation.method( + #create, + [instanceId], + ), + returnValueForMissingStub: null, + ); + @override + void deleteAllData(int? instanceId) => super.noSuchMethod( + Invocation.method( + #deleteAllData, + [instanceId], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestWebViewClientHostApi]. @@ -269,10 +564,28 @@ class MockTestWebViewClientHostApi extends _i1.Mock } @override - void create(int? instanceId, bool? shouldOverrideUrlLoading) => + void create(int? instanceId) => super.noSuchMethod( + Invocation.method( + #create, + [instanceId], + ), + returnValueForMissingStub: null, + ); + @override + void setSynchronousReturnValueForShouldOverrideUrlLoading( + int? instanceId, + bool? value, + ) => super.noSuchMethod( - Invocation.method(#create, [instanceId, shouldOverrideUrlLoading]), - returnValueForMissingStub: null); + Invocation.method( + #setSynchronousReturnValueForShouldOverrideUrlLoading, + [ + instanceId, + value, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestWebViewHostApi]. @@ -285,135 +598,338 @@ class MockTestWebViewHostApi extends _i1.Mock } @override - void create(int? instanceId, bool? useHybridComposition) => + void create( + int? instanceId, + bool? useHybridComposition, + ) => super.noSuchMethod( - Invocation.method(#create, [instanceId, useHybridComposition]), - returnValueForMissingStub: null); - @override - void dispose(int? instanceId) => - super.noSuchMethod(Invocation.method(#dispose, [instanceId]), - returnValueForMissingStub: null); + Invocation.method( + #create, + [ + instanceId, + useHybridComposition, + ], + ), + returnValueForMissingStub: null, + ); @override void loadData( - int? instanceId, String? data, String? mimeType, String? encoding) => + int? instanceId, + String? data, + String? mimeType, + String? encoding, + ) => super.noSuchMethod( - Invocation.method(#loadData, [instanceId, data, mimeType, encoding]), - returnValueForMissingStub: null); - @override - void loadDataWithBaseUrl(int? instanceId, String? baseUrl, String? data, - String? mimeType, String? encoding, String? historyUrl) => + Invocation.method( + #loadData, + [ + instanceId, + data, + mimeType, + encoding, + ], + ), + returnValueForMissingStub: null, + ); + @override + void loadDataWithBaseUrl( + int? instanceId, + String? baseUrl, + String? data, + String? mimeType, + String? encoding, + String? historyUrl, + ) => super.noSuchMethod( - Invocation.method(#loadDataWithBaseUrl, - [instanceId, baseUrl, data, mimeType, encoding, historyUrl]), - returnValueForMissingStub: null); - @override - void loadUrl(int? instanceId, String? url, Map? headers) => + Invocation.method( + #loadDataWithBaseUrl, + [ + instanceId, + baseUrl, + data, + mimeType, + encoding, + historyUrl, + ], + ), + returnValueForMissingStub: null, + ); + @override + void loadUrl( + int? instanceId, + String? url, + Map? headers, + ) => super.noSuchMethod( - Invocation.method(#loadUrl, [instanceId, url, headers]), - returnValueForMissingStub: null); - @override - void postUrl(int? instanceId, String? url, _i7.Uint8List? data) => - super.noSuchMethod(Invocation.method(#postUrl, [instanceId, url, data]), - returnValueForMissingStub: null); - @override - String? getUrl(int? instanceId) => - (super.noSuchMethod(Invocation.method(#getUrl, [instanceId])) as String?); - @override - bool canGoBack(int? instanceId) => - (super.noSuchMethod(Invocation.method(#canGoBack, [instanceId]), - returnValue: false) as bool); - @override - bool canGoForward(int? instanceId) => - (super.noSuchMethod(Invocation.method(#canGoForward, [instanceId]), - returnValue: false) as bool); - @override - void goBack(int? instanceId) => - super.noSuchMethod(Invocation.method(#goBack, [instanceId]), - returnValueForMissingStub: null); - @override - void goForward(int? instanceId) => - super.noSuchMethod(Invocation.method(#goForward, [instanceId]), - returnValueForMissingStub: null); - @override - void reload(int? instanceId) => - super.noSuchMethod(Invocation.method(#reload, [instanceId]), - returnValueForMissingStub: null); - @override - void clearCache(int? instanceId, bool? includeDiskFiles) => + Invocation.method( + #loadUrl, + [ + instanceId, + url, + headers, + ], + ), + returnValueForMissingStub: null, + ); + @override + void postUrl( + int? instanceId, + String? url, + _i7.Uint8List? data, + ) => + super.noSuchMethod( + Invocation.method( + #postUrl, + [ + instanceId, + url, + data, + ], + ), + returnValueForMissingStub: null, + ); + @override + String? getUrl(int? instanceId) => (super.noSuchMethod(Invocation.method( + #getUrl, + [instanceId], + )) as String?); + @override + bool canGoBack(int? instanceId) => (super.noSuchMethod( + Invocation.method( + #canGoBack, + [instanceId], + ), + returnValue: false, + ) as bool); + @override + bool canGoForward(int? instanceId) => (super.noSuchMethod( + Invocation.method( + #canGoForward, + [instanceId], + ), + returnValue: false, + ) as bool); + @override + void goBack(int? instanceId) => super.noSuchMethod( + Invocation.method( + #goBack, + [instanceId], + ), + returnValueForMissingStub: null, + ); + @override + void goForward(int? instanceId) => super.noSuchMethod( + Invocation.method( + #goForward, + [instanceId], + ), + returnValueForMissingStub: null, + ); + @override + void reload(int? instanceId) => super.noSuchMethod( + Invocation.method( + #reload, + [instanceId], + ), + returnValueForMissingStub: null, + ); + @override + void clearCache( + int? instanceId, + bool? includeDiskFiles, + ) => super.noSuchMethod( - Invocation.method(#clearCache, [instanceId, includeDiskFiles]), - returnValueForMissingStub: null); + Invocation.method( + #clearCache, + [ + instanceId, + includeDiskFiles, + ], + ), + returnValueForMissingStub: null, + ); @override _i5.Future evaluateJavascript( - int? instanceId, String? javascriptString) => + int? instanceId, + String? javascriptString, + ) => (super.noSuchMethod( + Invocation.method( + #evaluateJavascript, + [ + instanceId, + javascriptString, + ], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + String? getTitle(int? instanceId) => (super.noSuchMethod(Invocation.method( + #getTitle, + [instanceId], + )) as String?); + @override + void scrollTo( + int? instanceId, + int? x, + int? y, + ) => + super.noSuchMethod( + Invocation.method( + #scrollTo, + [ + instanceId, + x, + y, + ], + ), + returnValueForMissingStub: null, + ); + @override + void scrollBy( + int? instanceId, + int? x, + int? y, + ) => + super.noSuchMethod( + Invocation.method( + #scrollBy, + [ + instanceId, + x, + y, + ], + ), + returnValueForMissingStub: null, + ); + @override + int getScrollX(int? instanceId) => (super.noSuchMethod( + Invocation.method( + #getScrollX, + [instanceId], + ), + returnValue: 0, + ) as int); + @override + int getScrollY(int? instanceId) => (super.noSuchMethod( + Invocation.method( + #getScrollY, + [instanceId], + ), + returnValue: 0, + ) as int); + @override + _i3.WebViewPoint getScrollPosition(int? instanceId) => (super.noSuchMethod( + Invocation.method( + #getScrollPosition, + [instanceId], + ), + returnValue: _FakeWebViewPoint_2( + this, Invocation.method( - #evaluateJavascript, [instanceId, javascriptString]), - returnValue: Future.value()) as _i5.Future); - @override - String? getTitle(int? instanceId) => - (super.noSuchMethod(Invocation.method(#getTitle, [instanceId])) - as String?); - @override - void scrollTo(int? instanceId, int? x, int? y) => - super.noSuchMethod(Invocation.method(#scrollTo, [instanceId, x, y]), - returnValueForMissingStub: null); - @override - void scrollBy(int? instanceId, int? x, int? y) => - super.noSuchMethod(Invocation.method(#scrollBy, [instanceId, x, y]), - returnValueForMissingStub: null); - @override - int getScrollX(int? instanceId) => - (super.noSuchMethod(Invocation.method(#getScrollX, [instanceId]), - returnValue: 0) as int); - @override - int getScrollY(int? instanceId) => - (super.noSuchMethod(Invocation.method(#getScrollY, [instanceId]), - returnValue: 0) as int); - @override - _i3.WebViewPoint getScrollPosition(int? instanceId) => - (super.noSuchMethod(Invocation.method(#getScrollPosition, [instanceId]), - returnValue: _FakeWebViewPoint_2()) as _i3.WebViewPoint); + #getScrollPosition, + [instanceId], + ), + ), + ) as _i3.WebViewPoint); @override void setWebContentsDebuggingEnabled(bool? enabled) => super.noSuchMethod( - Invocation.method(#setWebContentsDebuggingEnabled, [enabled]), - returnValueForMissingStub: null); - @override - void setWebViewClient(int? instanceId, int? webViewClientInstanceId) => + Invocation.method( + #setWebContentsDebuggingEnabled, + [enabled], + ), + returnValueForMissingStub: null, + ); + @override + void setWebViewClient( + int? instanceId, + int? webViewClientInstanceId, + ) => super.noSuchMethod( - Invocation.method( - #setWebViewClient, [instanceId, webViewClientInstanceId]), - returnValueForMissingStub: null); + Invocation.method( + #setWebViewClient, + [ + instanceId, + webViewClientInstanceId, + ], + ), + returnValueForMissingStub: null, + ); @override void addJavaScriptChannel( - int? instanceId, int? javaScriptChannelInstanceId) => + int? instanceId, + int? javaScriptChannelInstanceId, + ) => super.noSuchMethod( - Invocation.method( - #addJavaScriptChannel, [instanceId, javaScriptChannelInstanceId]), - returnValueForMissingStub: null); + Invocation.method( + #addJavaScriptChannel, + [ + instanceId, + javaScriptChannelInstanceId, + ], + ), + returnValueForMissingStub: null, + ); @override void removeJavaScriptChannel( - int? instanceId, int? javaScriptChannelInstanceId) => + int? instanceId, + int? javaScriptChannelInstanceId, + ) => super.noSuchMethod( - Invocation.method(#removeJavaScriptChannel, - [instanceId, javaScriptChannelInstanceId]), - returnValueForMissingStub: null); - @override - void setDownloadListener(int? instanceId, int? listenerInstanceId) => + Invocation.method( + #removeJavaScriptChannel, + [ + instanceId, + javaScriptChannelInstanceId, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setDownloadListener( + int? instanceId, + int? listenerInstanceId, + ) => super.noSuchMethod( - Invocation.method( - #setDownloadListener, [instanceId, listenerInstanceId]), - returnValueForMissingStub: null); - @override - void setWebChromeClient(int? instanceId, int? clientInstanceId) => + Invocation.method( + #setDownloadListener, + [ + instanceId, + listenerInstanceId, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setWebChromeClient( + int? instanceId, + int? clientInstanceId, + ) => super.noSuchMethod( - Invocation.method( - #setWebChromeClient, [instanceId, clientInstanceId]), - returnValueForMissingStub: null); - @override - void setBackgroundColor(int? instanceId, int? color) => super.noSuchMethod( - Invocation.method(#setBackgroundColor, [instanceId, color]), - returnValueForMissingStub: null); + Invocation.method( + #setWebChromeClient, + [ + instanceId, + clientInstanceId, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setBackgroundColor( + int? instanceId, + int? color, + ) => + super.noSuchMethod( + Invocation.method( + #setBackgroundColor, + [ + instanceId, + color, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestAssetManagerHostApi]. @@ -426,13 +942,21 @@ class MockTestAssetManagerHostApi extends _i1.Mock } @override - List list(String? path) => - (super.noSuchMethod(Invocation.method(#list, [path]), - returnValue: []) as List); - @override - String getAssetFilePathByName(String? name) => - (super.noSuchMethod(Invocation.method(#getAssetFilePathByName, [name]), - returnValue: '') as String); + List list(String? path) => (super.noSuchMethod( + Invocation.method( + #list, + [path], + ), + returnValue: [], + ) as List); + @override + String getAssetFilePathByName(String? name) => (super.noSuchMethod( + Invocation.method( + #getAssetFilePathByName, + [name], + ), + returnValue: '', + ) as String); } /// A class which mocks [WebChromeClient]. @@ -444,13 +968,29 @@ class MockWebChromeClient extends _i1.Mock implements _i2.WebChromeClient { } @override - void onProgressChanged(_i2.WebView? webView, int? progress) => super - .noSuchMethod(Invocation.method(#onProgressChanged, [webView, progress]), - returnValueForMissingStub: null); - @override - _i2.WebChromeClient copy() => - (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWebChromeClient_3()) as _i2.WebChromeClient); + _i5.Future setSynchronousReturnValueForOnShowFileChooser(bool? value) => + (super.noSuchMethod( + Invocation.method( + #setSynchronousReturnValueForOnShowFileChooser, + [value], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i2.WebChromeClient copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebChromeClient_3( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WebChromeClient); } /// A class which mocks [WebView]. @@ -462,153 +1002,306 @@ class MockWebView extends _i1.Mock implements _i2.WebView { } @override - bool get useHybridComposition => - (super.noSuchMethod(Invocation.getter(#useHybridComposition), - returnValue: false) as bool); - @override - _i2.WebSettings get settings => - (super.noSuchMethod(Invocation.getter(#settings), - returnValue: _FakeWebSettings_4()) as _i2.WebSettings); - @override - _i5.Future loadData( - {String? data, String? mimeType, String? encoding}) => + bool get useHybridComposition => (super.noSuchMethod( + Invocation.getter(#useHybridComposition), + returnValue: false, + ) as bool); + @override + _i2.WebSettings get settings => (super.noSuchMethod( + Invocation.getter(#settings), + returnValue: _FakeWebSettings_4( + this, + Invocation.getter(#settings), + ), + ) as _i2.WebSettings); + @override + _i5.Future loadData({ + required String? data, + String? mimeType, + String? encoding, + }) => (super.noSuchMethod( - Invocation.method(#loadData, [], - {#data: data, #mimeType: mimeType, #encoding: encoding}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future loadDataWithBaseUrl( - {String? baseUrl, - String? data, - String? mimeType, - String? encoding, - String? historyUrl}) => + Invocation.method( + #loadData, + [], + { + #data: data, + #mimeType: mimeType, + #encoding: encoding, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future loadDataWithBaseUrl({ + String? baseUrl, + required String? data, + String? mimeType, + String? encoding, + String? historyUrl, + }) => (super.noSuchMethod( - Invocation.method(#loadDataWithBaseUrl, [], { + Invocation.method( + #loadDataWithBaseUrl, + [], + { #baseUrl: baseUrl, #data: data, #mimeType: mimeType, #encoding: encoding, - #historyUrl: historyUrl - }), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future loadUrl(String? url, Map? headers) => - (super.noSuchMethod(Invocation.method(#loadUrl, [url, headers]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future postUrl(String? url, _i7.Uint8List? data) => - (super.noSuchMethod(Invocation.method(#postUrl, [url, data]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future getUrl() => - (super.noSuchMethod(Invocation.method(#getUrl, []), - returnValue: Future.value()) as _i5.Future); - @override - _i5.Future canGoBack() => - (super.noSuchMethod(Invocation.method(#canGoBack, []), - returnValue: Future.value(false)) as _i5.Future); - @override - _i5.Future canGoForward() => - (super.noSuchMethod(Invocation.method(#canGoForward, []), - returnValue: Future.value(false)) as _i5.Future); - @override - _i5.Future goBack() => - (super.noSuchMethod(Invocation.method(#goBack, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future goForward() => - (super.noSuchMethod(Invocation.method(#goForward, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future reload() => - (super.noSuchMethod(Invocation.method(#reload, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future clearCache(bool? includeDiskFiles) => - (super.noSuchMethod(Invocation.method(#clearCache, [includeDiskFiles]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future evaluateJavascript(String? javascriptString) => (super - .noSuchMethod(Invocation.method(#evaluateJavascript, [javascriptString]), - returnValue: Future.value()) as _i5.Future); - @override - _i5.Future getTitle() => - (super.noSuchMethod(Invocation.method(#getTitle, []), - returnValue: Future.value()) as _i5.Future); - @override - _i5.Future scrollTo(int? x, int? y) => - (super.noSuchMethod(Invocation.method(#scrollTo, [x, y]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future scrollBy(int? x, int? y) => - (super.noSuchMethod(Invocation.method(#scrollBy, [x, y]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future getScrollX() => - (super.noSuchMethod(Invocation.method(#getScrollX, []), - returnValue: Future.value(0)) as _i5.Future); - @override - _i5.Future getScrollY() => - (super.noSuchMethod(Invocation.method(#getScrollY, []), - returnValue: Future.value(0)) as _i5.Future); - @override - _i5.Future<_i4.Offset> getScrollPosition() => - (super.noSuchMethod(Invocation.method(#getScrollPosition, []), - returnValue: Future<_i4.Offset>.value(_FakeOffset_5())) - as _i5.Future<_i4.Offset>); + #historyUrl: historyUrl, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future loadUrl( + String? url, + Map? headers, + ) => + (super.noSuchMethod( + Invocation.method( + #loadUrl, + [ + url, + headers, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future postUrl( + String? url, + _i7.Uint8List? data, + ) => + (super.noSuchMethod( + Invocation.method( + #postUrl, + [ + url, + data, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getUrl() => (super.noSuchMethod( + Invocation.method( + #getUrl, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future canGoBack() => (super.noSuchMethod( + Invocation.method( + #canGoBack, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future canGoForward() => (super.noSuchMethod( + Invocation.method( + #canGoForward, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future goBack() => (super.noSuchMethod( + Invocation.method( + #goBack, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future goForward() => (super.noSuchMethod( + Invocation.method( + #goForward, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future reload() => (super.noSuchMethod( + Invocation.method( + #reload, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future clearCache(bool? includeDiskFiles) => (super.noSuchMethod( + Invocation.method( + #clearCache, + [includeDiskFiles], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future evaluateJavascript(String? javascriptString) => + (super.noSuchMethod( + Invocation.method( + #evaluateJavascript, + [javascriptString], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getTitle() => (super.noSuchMethod( + Invocation.method( + #getTitle, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future scrollTo( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollTo, + [ + x, + y, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future scrollBy( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollBy, + [ + x, + y, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getScrollX() => (super.noSuchMethod( + Invocation.method( + #getScrollX, + [], + ), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + @override + _i5.Future getScrollY() => (super.noSuchMethod( + Invocation.method( + #getScrollY, + [], + ), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + @override + _i5.Future<_i4.Offset> getScrollPosition() => (super.noSuchMethod( + Invocation.method( + #getScrollPosition, + [], + ), + returnValue: _i5.Future<_i4.Offset>.value(_FakeOffset_5( + this, + Invocation.method( + #getScrollPosition, + [], + ), + )), + ) as _i5.Future<_i4.Offset>); @override _i5.Future setWebViewClient(_i2.WebViewClient? webViewClient) => - (super.noSuchMethod(Invocation.method(#setWebViewClient, [webViewClient]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + (super.noSuchMethod( + Invocation.method( + #setWebViewClient, + [webViewClient], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future addJavaScriptChannel( _i2.JavaScriptChannel? javaScriptChannel) => (super.noSuchMethod( - Invocation.method(#addJavaScriptChannel, [javaScriptChannel]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + Invocation.method( + #addJavaScriptChannel, + [javaScriptChannel], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future removeJavaScriptChannel( _i2.JavaScriptChannel? javaScriptChannel) => (super.noSuchMethod( - Invocation.method(#removeJavaScriptChannel, [javaScriptChannel]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + Invocation.method( + #removeJavaScriptChannel, + [javaScriptChannel], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future setDownloadListener(_i2.DownloadListener? listener) => - (super.noSuchMethod(Invocation.method(#setDownloadListener, [listener]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + (super.noSuchMethod( + Invocation.method( + #setDownloadListener, + [listener], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future setWebChromeClient(_i2.WebChromeClient? client) => - (super.noSuchMethod(Invocation.method(#setWebChromeClient, [client]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setBackgroundColor(_i4.Color? color) => - (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future release() => - (super.noSuchMethod(Invocation.method(#release, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i2.WebView copy() => (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWebView_6()) as _i2.WebView); + (super.noSuchMethod( + Invocation.method( + #setWebChromeClient, + [client], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setBackgroundColor(_i4.Color? color) => (super.noSuchMethod( + Invocation.method( + #setBackgroundColor, + [color], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i2.WebView copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebView_6( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WebView); } /// A class which mocks [WebViewClient]. @@ -620,39 +1313,28 @@ class MockWebViewClient extends _i1.Mock implements _i2.WebViewClient { } @override - bool get shouldOverrideUrlLoading => - (super.noSuchMethod(Invocation.getter(#shouldOverrideUrlLoading), - returnValue: false) as bool); - @override - void onPageStarted(_i2.WebView? webView, String? url) => - super.noSuchMethod(Invocation.method(#onPageStarted, [webView, url]), - returnValueForMissingStub: null); - @override - void onPageFinished(_i2.WebView? webView, String? url) => - super.noSuchMethod(Invocation.method(#onPageFinished, [webView, url]), - returnValueForMissingStub: null); - @override - void onReceivedRequestError(_i2.WebView? webView, - _i2.WebResourceRequest? request, _i2.WebResourceError? error) => - super.noSuchMethod( - Invocation.method(#onReceivedRequestError, [webView, request, error]), - returnValueForMissingStub: null); - @override - void onReceivedError(_i2.WebView? webView, int? errorCode, - String? description, String? failingUrl) => - super.noSuchMethod( + _i5.Future setSynchronousReturnValueForShouldOverrideUrlLoading( + bool? value) => + (super.noSuchMethod( + Invocation.method( + #setSynchronousReturnValueForShouldOverrideUrlLoading, + [value], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i2.WebViewClient copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebViewClient_7( + this, Invocation.method( - #onReceivedError, [webView, errorCode, description, failingUrl]), - returnValueForMissingStub: null); - @override - void requestLoading(_i2.WebView? webView, _i2.WebResourceRequest? request) => - super.noSuchMethod(Invocation.method(#requestLoading, [webView, request]), - returnValueForMissingStub: null); - @override - void urlLoading(_i2.WebView? webView, String? url) => - super.noSuchMethod(Invocation.method(#urlLoading, [webView, url]), - returnValueForMissingStub: null); - @override - _i2.WebViewClient copy() => (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWebViewClient_7()) as _i2.WebViewClient); + #copy, + [], + ), + ), + ) as _i2.WebViewClient); } diff --git a/packages/webview_flutter/webview_flutter_android/test/surface_android_test.dart b/packages/webview_flutter/webview_flutter_android/test/legacy/surface_android_test.dart similarity index 74% rename from packages/webview_flutter/webview_flutter_android/test/surface_android_test.dart rename to packages/webview_flutter/webview_flutter_android/test/legacy/surface_android_test.dart index 63e752b419e6..d022ab282c92 100644 --- a/packages/webview_flutter/webview_flutter_android/test/surface_android_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/legacy/surface_android_test.dart @@ -7,8 +7,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:webview_flutter_android/webview_surface_android.dart'; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_android/src/legacy/webview_surface_android.dart'; +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -17,21 +17,30 @@ void main() { late List log; setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform_views, (MethodCall call) async { log.add(call); if (call.method == 'resize') { + final Map arguments = + (call.arguments as Map) + .cast(); return { - 'width': call.arguments['width'], - 'height': call.arguments['height'], + 'width': arguments['width'], + 'height': arguments['height'], }; } + return null; }, ); }); tearDownAll(() { - SystemChannels.platform_views.setMockMethodCallHandler(null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform_views, null); }); setUp(() { @@ -113,3 +122,9 @@ class TestWebViewPlatformCallbacksHandler @override void onWebResourceError(WebResourceError error) {} } + +/// 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/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.dart b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.dart similarity index 89% rename from packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.dart rename to packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.dart index 4f274ff4499f..e4cd61634864 100644 --- a/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.dart @@ -7,8 +7,8 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:webview_flutter_android/src/android_webview.dart' as android_webview; -import 'package:webview_flutter_android/webview_android_cookie_manager.dart'; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_android/src/legacy/webview_android_cookie_manager.dart'; +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; import 'webview_android_cookie_manager_test.mocks.dart'; diff --git a/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.mocks.dart new file mode 100644 index 000000000000..85aed145bfbe --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_cookie_manager_test.mocks.dart @@ -0,0 +1,54 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter_android/test/legacy/webview_android_cookie_manager_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_android/src/android_webview.dart' as _i2; + +// 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 [CookieManager]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCookieManager extends _i1.Mock implements _i2.CookieManager { + MockCookieManager() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future setCookie( + String? url, + String? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setCookie, + [ + url, + value, + ], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future clearCookies() => (super.noSuchMethod( + Invocation.method( + #clearCookies, + [], + ), + returnValue: _i3.Future.value(false), + ) as _i3.Future); +} diff --git a/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.dart similarity index 74% rename from packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart rename to packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.dart index 73076d9141a7..44cc18510909 100644 --- a/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/widgets.dart'; @@ -13,11 +12,11 @@ import 'package:webview_flutter_android/src/android_webview.dart' as android_webview; import 'package:webview_flutter_android/src/android_webview_api_impls.dart'; import 'package:webview_flutter_android/src/instance_manager.dart'; -import 'package:webview_flutter_android/webview_android_widget.dart'; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_android/src/legacy/webview_android_widget.dart'; +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; -import 'android_webview_test.mocks.dart' show MockTestWebViewHostApi; -import 'test_android_webview.pigeon.dart'; +import '../android_webview_test.mocks.dart' show MockTestWebViewHostApi; +import '../test_android_webview.g.dart'; import 'webview_android_widget_test.mocks.dart'; @GenerateMocks([ @@ -26,10 +25,10 @@ import 'webview_android_widget_test.mocks.dart'; android_webview.WebStorage, android_webview.WebView, android_webview.WebResourceRequest, - WebViewAndroidDownloadListener, + android_webview.DownloadListener, WebViewAndroidJavaScriptChannel, - WebViewAndroidWebChromeClient, - WebViewAndroidWebViewClient, + android_webview.WebChromeClient, + android_webview.WebViewClient, JavascriptChannelRegistry, WebViewPlatformCallbacksHandler, WebViewProxy, @@ -45,9 +44,9 @@ void main() { late MockWebViewProxy mockWebViewProxy; late MockWebViewPlatformCallbacksHandler mockCallbacksHandler; - late WebViewAndroidWebViewClient webViewClient; - late WebViewAndroidDownloadListener downloadListener; - late WebViewAndroidWebChromeClient webChromeClient; + late MockWebViewClient mockWebViewClient; + late android_webview.DownloadListener downloadListener; + late android_webview.WebChromeClient webChromeClient; late MockJavascriptChannelRegistry mockJavascriptChannelRegistry; @@ -58,12 +57,21 @@ void main() { mockWebView = MockWebView(); mockWebSettings = MockWebSettings(); mockWebStorage = MockWebStorage(); + mockWebViewClient = MockWebViewClient(); when(mockWebView.settings).thenReturn(mockWebSettings); mockWebViewProxy = MockWebViewProxy(); when(mockWebViewProxy.createWebView( useHybridComposition: anyNamed('useHybridComposition'), )).thenReturn(mockWebView); + when(mockWebViewProxy.createWebViewClient( + onPageStarted: anyNamed('onPageStarted'), + onPageFinished: anyNamed('onPageFinished'), + onReceivedError: anyNamed('onReceivedError'), + onReceivedRequestError: anyNamed('onReceivedRequestError'), + requestLoading: anyNamed('requestLoading'), + urlLoading: anyNamed('urlLoading'), + )).thenReturn(mockWebViewClient); mockCallbacksHandler = MockWebViewPlatformCallbacksHandler(); mockJavascriptChannelRegistry = MockJavascriptChannelRegistry(); @@ -97,7 +105,7 @@ void main() { }, )); - webViewClient = testController.webViewClient; + mockWebViewClient = testController.webViewClient as MockWebViewClient; downloadListener = testController.downloadListener; webChromeClient = testController.webChromeClient; } @@ -114,9 +122,9 @@ void main() { verify(mockWebSettings.setBuiltInZoomControls(true)); verifyInOrder(>[ - mockWebView.setWebViewClient(webViewClient), mockWebView.setDownloadListener(downloadListener), mockWebView.setWebChromeClient(webChromeClient), + mockWebView.setWebViewClient(mockWebViewClient), ]); }); @@ -202,8 +210,10 @@ void main() { ), ); - final List javaScriptChannels = - verify(mockWebView.addJavaScriptChannel(captureAny)).captured; + final List javaScriptChannels = + verify(mockWebView.addJavaScriptChannel(captureAny)) + .captured + .cast(); expect(javaScriptChannels[0].channelName, 'a'); expect(javaScriptChannels[1].channelName, 'b'); }); @@ -225,6 +235,16 @@ void main() { }); testWidgets('hasNavigationDelegate', (WidgetTester tester) async { + final MockWebViewClient mockWebViewClient = MockWebViewClient(); + when(mockWebViewProxy.createWebViewClient( + onPageStarted: anyNamed('onPageStarted'), + onPageFinished: anyNamed('onPageFinished'), + onReceivedError: anyNamed('onReceivedError'), + onReceivedRequestError: anyNamed('onReceivedRequestError'), + requestLoading: anyNamed('requestLoading'), + urlLoading: anyNamed('urlLoading'), + )).thenReturn(mockWebViewClient); + await buildWidget( tester, creationParams: CreationParams( @@ -235,8 +255,10 @@ void main() { ), ); - expect(testController.webViewClient.handlesNavigation, isTrue); - expect(testController.webViewClient.shouldOverrideUrlLoading, isTrue); + verify( + mockWebViewClient + .setSynchronousReturnValueForShouldOverrideUrlLoading(true), + ); }); testWidgets('debuggingEnabled true', (WidgetTester tester) async { @@ -445,7 +467,7 @@ void main() { await buildWidget(tester); expect( - () async => await testController.loadRequest( + () async => testController.loadRequest( WebViewRequest( uri: Uri.parse('www.google.com'), method: WebViewRequestMethod.get, @@ -636,8 +658,10 @@ void main() { await buildWidget(tester); await testController.addJavascriptChannels({'c', 'd'}); - final List javaScriptChannels = - verify(mockWebView.addJavaScriptChannel(captureAny)).captured; + final List javaScriptChannels = + verify(mockWebView.addJavaScriptChannel(captureAny)) + .captured + .cast(); expect(javaScriptChannels[0].channelName, 'c'); expect(javaScriptChannels[1].channelName, 'd'); }); @@ -647,8 +671,10 @@ void main() { await testController.addJavascriptChannels({'c', 'd'}); await testController.removeJavascriptChannels({'c', 'd'}); - final List javaScriptChannels = - verify(mockWebView.removeJavaScriptChannel(captureAny)).captured; + final List javaScriptChannels = + verify(mockWebView.removeJavaScriptChannel(captureAny)) + .captured + .cast(); expect(javaScriptChannels[0].channelName, 'c'); expect(javaScriptChannels[1].channelName, 'd'); }); @@ -693,20 +719,53 @@ void main() { group('WebViewPlatformCallbacksHandler', () { testWidgets('onPageStarted', (WidgetTester tester) async { await buildWidget(tester); - webViewClient.onPageStarted(mockWebView, 'https://google.com'); + final void Function(android_webview.WebView, String) onPageStarted = + verify(mockWebViewProxy.createWebViewClient( + onPageStarted: captureAnyNamed('onPageStarted'), + onPageFinished: anyNamed('onPageFinished'), + onReceivedError: anyNamed('onReceivedError'), + onReceivedRequestError: anyNamed('onReceivedRequestError'), + requestLoading: anyNamed('requestLoading'), + urlLoading: anyNamed('urlLoading'), + )).captured.single as Function(android_webview.WebView, String); + + onPageStarted(mockWebView, 'https://google.com'); verify(mockCallbacksHandler.onPageStarted('https://google.com')); }); testWidgets('onPageFinished', (WidgetTester tester) async { await buildWidget(tester); - webViewClient.onPageFinished(mockWebView, 'https://google.com'); + + final void Function(android_webview.WebView, String) onPageFinished = + verify(mockWebViewProxy.createWebViewClient( + onPageStarted: anyNamed('onPageStarted'), + onPageFinished: captureAnyNamed('onPageFinished'), + onReceivedError: anyNamed('onReceivedError'), + onReceivedRequestError: anyNamed('onReceivedRequestError'), + requestLoading: anyNamed('requestLoading'), + urlLoading: anyNamed('urlLoading'), + )).captured.single as Function(android_webview.WebView, String); + + onPageFinished(mockWebView, 'https://google.com'); verify(mockCallbacksHandler.onPageFinished('https://google.com')); }); testWidgets('onWebResourceError from onReceivedError', (WidgetTester tester) async { await buildWidget(tester); - webViewClient.onReceivedError( + + final void Function(android_webview.WebView, int, String, String) + onReceivedError = verify(mockWebViewProxy.createWebViewClient( + onPageStarted: anyNamed('onPageStarted'), + onPageFinished: anyNamed('onPageFinished'), + onReceivedError: captureAnyNamed('onReceivedError'), + onReceivedRequestError: anyNamed('onReceivedRequestError'), + requestLoading: anyNamed('requestLoading'), + urlLoading: anyNamed('urlLoading'), + )).captured.single as Function( + android_webview.WebView, int, String, String); + + onReceivedError( mockWebView, android_webview.WebViewClient.errorAuthentication, 'description', @@ -727,7 +786,25 @@ void main() { testWidgets('onWebResourceError from onReceivedRequestError', (WidgetTester tester) async { await buildWidget(tester); - webViewClient.onReceivedRequestError( + + final void Function( + android_webview.WebView, + android_webview.WebResourceRequest, + android_webview.WebResourceError, + ) onReceivedRequestError = verify(mockWebViewProxy.createWebViewClient( + onPageStarted: anyNamed('onPageStarted'), + onPageFinished: anyNamed('onPageFinished'), + onReceivedError: anyNamed('onReceivedError'), + onReceivedRequestError: captureAnyNamed('onReceivedRequestError'), + requestLoading: anyNamed('requestLoading'), + urlLoading: anyNamed('urlLoading'), + )).captured.single as Function( + android_webview.WebView, + android_webview.WebResourceRequest, + android_webview.WebResourceError, + ); + + onReceivedRequestError( mockWebView, android_webview.WebResourceRequest( url: 'https://google.com', @@ -762,7 +839,17 @@ void main() { url: 'https://google.com', )).thenReturn(true); - webViewClient.urlLoading(mockWebView, 'https://google.com'); + final void Function(android_webview.WebView, String) urlLoading = + verify(mockWebViewProxy.createWebViewClient( + onPageStarted: anyNamed('onPageStarted'), + onPageFinished: anyNamed('onPageFinished'), + onReceivedError: anyNamed('onReceivedError'), + onReceivedRequestError: anyNamed('onReceivedRequestError'), + requestLoading: anyNamed('requestLoading'), + urlLoading: captureAnyNamed('urlLoading'), + )).captured.single as Function(android_webview.WebView, String); + + urlLoading(mockWebView, 'https://google.com'); verify(mockCallbacksHandler.onNavigationRequest( url: 'https://google.com', isForMainFrame: true, @@ -778,7 +865,22 @@ void main() { url: 'https://google.com', )).thenReturn(true); - webViewClient.requestLoading( + final void Function( + android_webview.WebView, + android_webview.WebResourceRequest, + ) requestLoading = verify(mockWebViewProxy.createWebViewClient( + onPageStarted: anyNamed('onPageStarted'), + onPageFinished: anyNamed('onPageFinished'), + onReceivedError: anyNamed('onReceivedError'), + onReceivedRequestError: anyNamed('onReceivedRequestError'), + requestLoading: captureAnyNamed('requestLoading'), + urlLoading: anyNamed('urlLoading'), + )).captured.single as Function( + android_webview.WebView, + android_webview.WebResourceRequest, + ); + + requestLoading( mockWebView, android_webview.WebResourceRequest( url: 'https://google.com', @@ -843,192 +945,4 @@ void main() { verify(mockPlatformHostApi.setWebContentsDebuggingEnabled(false)); }); }); - - group('WebViewAndroidWebViewClient', () { - test( - 'urlLoading should call loadUrl when onNavigationRequestCallback returns true', - () { - final Completer completer = Completer(); - final WebViewAndroidWebViewClient webViewClient = - WebViewAndroidWebViewClient.handlesNavigation( - onPageStartedCallback: (_) {}, - onPageFinishedCallback: (_) {}, - onWebResourceErrorCallback: (_) {}, - onNavigationRequestCallback: ({ - required bool isForMainFrame, - required String url, - }) => - true, - loadUrl: (String url, Map? headers) async { - completer.complete(); - }); - - webViewClient.urlLoading(MockWebView(), 'https://flutter.dev'); - expect(completer.isCompleted, isTrue); - }); - - test( - 'urlLoading should call loadUrl when onNavigationRequestCallback returns a Future true', - () async { - final Completer completer = Completer(); - final WebViewAndroidWebViewClient webViewClient = - WebViewAndroidWebViewClient.handlesNavigation( - onPageStartedCallback: (_) {}, - onPageFinishedCallback: (_) {}, - onWebResourceErrorCallback: (_) {}, - onNavigationRequestCallback: ({ - required bool isForMainFrame, - required String url, - }) => - Future.value(true), - loadUrl: (String url, Map? headers) async { - completer.complete(); - }); - - webViewClient.urlLoading(MockWebView(), 'https://flutter.dev'); - expect(completer.future, completes); - }); - - test( - 'urlLoading should not call laodUrl when onNavigationRequestCallback returns false', - () async { - final WebViewAndroidWebViewClient webViewClient = - WebViewAndroidWebViewClient.handlesNavigation( - onPageStartedCallback: (_) {}, - onPageFinishedCallback: (_) {}, - onWebResourceErrorCallback: (_) {}, - onNavigationRequestCallback: ({ - required bool isForMainFrame, - required String url, - }) => - false, - loadUrl: (String url, Map? headers) async { - fail( - 'loadUrl should not be called if onNavigationRequestCallback returns false.'); - }); - - webViewClient.urlLoading(MockWebView(), 'https://flutter.dev'); - }); - - test( - 'urlLoading should not call loadUrl when onNavigationRequestCallback returns a Future false', - () { - final WebViewAndroidWebViewClient webViewClient = - WebViewAndroidWebViewClient.handlesNavigation( - onPageStartedCallback: (_) {}, - onPageFinishedCallback: (_) {}, - onWebResourceErrorCallback: (_) {}, - onNavigationRequestCallback: ({ - required bool isForMainFrame, - required String url, - }) => - Future.value(false), - loadUrl: (String url, Map? headers) async { - fail( - 'loadUrl should not be called if onNavigationRequestCallback returns false.'); - }); - - webViewClient.urlLoading(MockWebView(), 'https://flutter.dev'); - }); - - test( - 'requestLoading should call loadUrl when onNavigationRequestCallback returns true', - () { - final Completer completer = Completer(); - final MockWebResourceRequest mockRequest = MockWebResourceRequest(); - when(mockRequest.isForMainFrame).thenReturn(true); - when(mockRequest.url).thenReturn('https://flutter.dev'); - final WebViewAndroidWebViewClient webViewClient = - WebViewAndroidWebViewClient.handlesNavigation( - onPageStartedCallback: (_) {}, - onPageFinishedCallback: (_) {}, - onWebResourceErrorCallback: (_) {}, - onNavigationRequestCallback: ({ - required bool isForMainFrame, - required String url, - }) => - true, - loadUrl: (String url, Map? headers) async { - expect(url, 'https://flutter.dev'); - completer.complete(); - }); - - webViewClient.requestLoading(MockWebView(), mockRequest); - expect(completer.isCompleted, isTrue); - }); - - test( - 'requestLoading should call loadUrl when onNavigationRequestCallback returns a Future true', - () async { - final Completer completer = Completer(); - final MockWebResourceRequest mockRequest = MockWebResourceRequest(); - when(mockRequest.isForMainFrame).thenReturn(true); - when(mockRequest.url).thenReturn('https://flutter.dev'); - final WebViewAndroidWebViewClient webViewClient = - WebViewAndroidWebViewClient.handlesNavigation( - onPageStartedCallback: (_) {}, - onPageFinishedCallback: (_) {}, - onWebResourceErrorCallback: (_) {}, - onNavigationRequestCallback: ({ - required bool isForMainFrame, - required String url, - }) => - Future.value(true), - loadUrl: (String url, Map? headers) async { - expect(url, 'https://flutter.dev'); - completer.complete(); - }); - - webViewClient.requestLoading(MockWebView(), mockRequest); - expect(completer.future, completes); - }); - - test( - 'requestLoading should not call loadUrl when onNavigationRequestCallback returns false', - () { - final MockWebResourceRequest mockRequest = MockWebResourceRequest(); - when(mockRequest.isForMainFrame).thenReturn(true); - when(mockRequest.url).thenReturn('https://flutter.dev'); - final WebViewAndroidWebViewClient webViewClient = - WebViewAndroidWebViewClient.handlesNavigation( - onPageStartedCallback: (_) {}, - onPageFinishedCallback: (_) {}, - onWebResourceErrorCallback: (_) {}, - onNavigationRequestCallback: ({ - required bool isForMainFrame, - required String url, - }) => - false, - loadUrl: (String url, Map? headers) { - fail( - 'loadUrl should not be called if onNavigationRequestCallback returns false.'); - }); - - webViewClient.requestLoading(MockWebView(), mockRequest); - }); - - test( - 'requestLoading should not call loadUrl when onNavigationRequestCallback returns a Future false', - () { - final MockWebResourceRequest mockRequest = MockWebResourceRequest(); - when(mockRequest.isForMainFrame).thenReturn(true); - when(mockRequest.url).thenReturn('https://flutter.dev'); - final WebViewAndroidWebViewClient webViewClient = - WebViewAndroidWebViewClient.handlesNavigation( - onPageStartedCallback: (_) {}, - onPageFinishedCallback: (_) {}, - onWebResourceErrorCallback: (_) {}, - onNavigationRequestCallback: ({ - required bool isForMainFrame, - required String url, - }) => - Future.value(false), - loadUrl: (String url, Map? headers) { - fail( - 'loadUrl should not be called if onNavigationRequestCallback returns false.'); - }); - - webViewClient.requestLoading(MockWebView(), mockRequest); - }); - }); } diff --git a/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.mocks.dart new file mode 100644 index 000000000000..03489ce5c1e0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.mocks.dart @@ -0,0 +1,1026 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter_android/test/legacy/webview_android_widget_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; +import 'dart:typed_data' as _i6; +import 'dart:ui' as _i3; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_android/src/android_webview.dart' as _i2; +import 'package:webview_flutter_android/src/legacy/webview_android_widget.dart' + as _i7; +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart' + as _i4; + +// 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 + +class _FakeWebSettings_0 extends _i1.SmartFake implements _i2.WebSettings { + _FakeWebSettings_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWebStorage_1 extends _i1.SmartFake implements _i2.WebStorage { + _FakeWebStorage_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeOffset_2 extends _i1.SmartFake implements _i3.Offset { + _FakeOffset_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWebView_3 extends _i1.SmartFake implements _i2.WebView { + _FakeWebView_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeDownloadListener_4 extends _i1.SmartFake + implements _i2.DownloadListener { + _FakeDownloadListener_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeJavascriptChannelRegistry_5 extends _i1.SmartFake + implements _i4.JavascriptChannelRegistry { + _FakeJavascriptChannelRegistry_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeJavaScriptChannel_6 extends _i1.SmartFake + implements _i2.JavaScriptChannel { + _FakeJavaScriptChannel_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWebChromeClient_7 extends _i1.SmartFake + implements _i2.WebChromeClient { + _FakeWebChromeClient_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWebViewClient_8 extends _i1.SmartFake implements _i2.WebViewClient { + _FakeWebViewClient_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [FlutterAssetManager]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFlutterAssetManager extends _i1.Mock + implements _i2.FlutterAssetManager { + MockFlutterAssetManager() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future> list(String? path) => (super.noSuchMethod( + Invocation.method( + #list, + [path], + ), + returnValue: _i5.Future>.value([]), + ) as _i5.Future>); + @override + _i5.Future getAssetFilePathByName(String? name) => + (super.noSuchMethod( + Invocation.method( + #getAssetFilePathByName, + [name], + ), + returnValue: _i5.Future.value(''), + ) as _i5.Future); +} + +/// A class which mocks [WebSettings]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebSettings extends _i1.Mock implements _i2.WebSettings { + MockWebSettings() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future setDomStorageEnabled(bool? flag) => (super.noSuchMethod( + Invocation.method( + #setDomStorageEnabled, + [flag], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setJavaScriptCanOpenWindowsAutomatically(bool? flag) => + (super.noSuchMethod( + Invocation.method( + #setJavaScriptCanOpenWindowsAutomatically, + [flag], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setSupportMultipleWindows(bool? support) => + (super.noSuchMethod( + Invocation.method( + #setSupportMultipleWindows, + [support], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setJavaScriptEnabled(bool? flag) => (super.noSuchMethod( + Invocation.method( + #setJavaScriptEnabled, + [flag], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setUserAgentString(String? userAgentString) => + (super.noSuchMethod( + Invocation.method( + #setUserAgentString, + [userAgentString], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setMediaPlaybackRequiresUserGesture(bool? require) => + (super.noSuchMethod( + Invocation.method( + #setMediaPlaybackRequiresUserGesture, + [require], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setSupportZoom(bool? support) => (super.noSuchMethod( + Invocation.method( + #setSupportZoom, + [support], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setLoadWithOverviewMode(bool? overview) => + (super.noSuchMethod( + Invocation.method( + #setLoadWithOverviewMode, + [overview], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setUseWideViewPort(bool? use) => (super.noSuchMethod( + Invocation.method( + #setUseWideViewPort, + [use], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setDisplayZoomControls(bool? enabled) => (super.noSuchMethod( + Invocation.method( + #setDisplayZoomControls, + [enabled], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setBuiltInZoomControls(bool? enabled) => (super.noSuchMethod( + Invocation.method( + #setBuiltInZoomControls, + [enabled], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setAllowFileAccess(bool? enabled) => (super.noSuchMethod( + Invocation.method( + #setAllowFileAccess, + [enabled], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i2.WebSettings copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebSettings_0( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WebSettings); +} + +/// A class which mocks [WebStorage]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebStorage extends _i1.Mock implements _i2.WebStorage { + MockWebStorage() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future deleteAllData() => (super.noSuchMethod( + Invocation.method( + #deleteAllData, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i2.WebStorage copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebStorage_1( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WebStorage); +} + +/// A class which mocks [WebView]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebView extends _i1.Mock implements _i2.WebView { + MockWebView() { + _i1.throwOnMissingStub(this); + } + + @override + bool get useHybridComposition => (super.noSuchMethod( + Invocation.getter(#useHybridComposition), + returnValue: false, + ) as bool); + @override + _i2.WebSettings get settings => (super.noSuchMethod( + Invocation.getter(#settings), + returnValue: _FakeWebSettings_0( + this, + Invocation.getter(#settings), + ), + ) as _i2.WebSettings); + @override + _i5.Future loadData({ + required String? data, + String? mimeType, + String? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #loadData, + [], + { + #data: data, + #mimeType: mimeType, + #encoding: encoding, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future loadDataWithBaseUrl({ + String? baseUrl, + required String? data, + String? mimeType, + String? encoding, + String? historyUrl, + }) => + (super.noSuchMethod( + Invocation.method( + #loadDataWithBaseUrl, + [], + { + #baseUrl: baseUrl, + #data: data, + #mimeType: mimeType, + #encoding: encoding, + #historyUrl: historyUrl, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future loadUrl( + String? url, + Map? headers, + ) => + (super.noSuchMethod( + Invocation.method( + #loadUrl, + [ + url, + headers, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future postUrl( + String? url, + _i6.Uint8List? data, + ) => + (super.noSuchMethod( + Invocation.method( + #postUrl, + [ + url, + data, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getUrl() => (super.noSuchMethod( + Invocation.method( + #getUrl, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future canGoBack() => (super.noSuchMethod( + Invocation.method( + #canGoBack, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future canGoForward() => (super.noSuchMethod( + Invocation.method( + #canGoForward, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future goBack() => (super.noSuchMethod( + Invocation.method( + #goBack, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future goForward() => (super.noSuchMethod( + Invocation.method( + #goForward, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future reload() => (super.noSuchMethod( + Invocation.method( + #reload, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future clearCache(bool? includeDiskFiles) => (super.noSuchMethod( + Invocation.method( + #clearCache, + [includeDiskFiles], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future evaluateJavascript(String? javascriptString) => + (super.noSuchMethod( + Invocation.method( + #evaluateJavascript, + [javascriptString], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getTitle() => (super.noSuchMethod( + Invocation.method( + #getTitle, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future scrollTo( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollTo, + [ + x, + y, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future scrollBy( + int? x, + int? y, + ) => + (super.noSuchMethod( + Invocation.method( + #scrollBy, + [ + x, + y, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getScrollX() => (super.noSuchMethod( + Invocation.method( + #getScrollX, + [], + ), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + @override + _i5.Future getScrollY() => (super.noSuchMethod( + Invocation.method( + #getScrollY, + [], + ), + returnValue: _i5.Future.value(0), + ) as _i5.Future); + @override + _i5.Future<_i3.Offset> getScrollPosition() => (super.noSuchMethod( + Invocation.method( + #getScrollPosition, + [], + ), + returnValue: _i5.Future<_i3.Offset>.value(_FakeOffset_2( + this, + Invocation.method( + #getScrollPosition, + [], + ), + )), + ) as _i5.Future<_i3.Offset>); + @override + _i5.Future setWebViewClient(_i2.WebViewClient? webViewClient) => + (super.noSuchMethod( + Invocation.method( + #setWebViewClient, + [webViewClient], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future addJavaScriptChannel( + _i2.JavaScriptChannel? javaScriptChannel) => + (super.noSuchMethod( + Invocation.method( + #addJavaScriptChannel, + [javaScriptChannel], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeJavaScriptChannel( + _i2.JavaScriptChannel? javaScriptChannel) => + (super.noSuchMethod( + Invocation.method( + #removeJavaScriptChannel, + [javaScriptChannel], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setDownloadListener(_i2.DownloadListener? listener) => + (super.noSuchMethod( + Invocation.method( + #setDownloadListener, + [listener], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setWebChromeClient(_i2.WebChromeClient? client) => + (super.noSuchMethod( + Invocation.method( + #setWebChromeClient, + [client], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setBackgroundColor(_i3.Color? color) => (super.noSuchMethod( + Invocation.method( + #setBackgroundColor, + [color], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i2.WebView copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebView_3( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WebView); +} + +/// A class which mocks [WebResourceRequest]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebResourceRequest extends _i1.Mock + implements _i2.WebResourceRequest { + MockWebResourceRequest() { + _i1.throwOnMissingStub(this); + } + + @override + String get url => (super.noSuchMethod( + Invocation.getter(#url), + returnValue: '', + ) as String); + @override + bool get isForMainFrame => (super.noSuchMethod( + Invocation.getter(#isForMainFrame), + returnValue: false, + ) as bool); + @override + bool get hasGesture => (super.noSuchMethod( + Invocation.getter(#hasGesture), + returnValue: false, + ) as bool); + @override + String get method => (super.noSuchMethod( + Invocation.getter(#method), + returnValue: '', + ) as String); + @override + Map get requestHeaders => (super.noSuchMethod( + Invocation.getter(#requestHeaders), + returnValue: {}, + ) as Map); +} + +/// A class which mocks [DownloadListener]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDownloadListener extends _i1.Mock implements _i2.DownloadListener { + MockDownloadListener() { + _i1.throwOnMissingStub(this); + } + + @override + void Function( + String, + String, + String, + String, + int, + ) get onDownloadStart => (super.noSuchMethod( + Invocation.getter(#onDownloadStart), + returnValue: ( + String url, + String userAgent, + String contentDisposition, + String mimetype, + int contentLength, + ) {}, + ) as void Function( + String, + String, + String, + String, + int, + )); + @override + _i2.DownloadListener copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeDownloadListener_4( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.DownloadListener); +} + +/// A class which mocks [WebViewAndroidJavaScriptChannel]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewAndroidJavaScriptChannel extends _i1.Mock + implements _i7.WebViewAndroidJavaScriptChannel { + MockWebViewAndroidJavaScriptChannel() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.JavascriptChannelRegistry get javascriptChannelRegistry => + (super.noSuchMethod( + Invocation.getter(#javascriptChannelRegistry), + returnValue: _FakeJavascriptChannelRegistry_5( + this, + Invocation.getter(#javascriptChannelRegistry), + ), + ) as _i4.JavascriptChannelRegistry); + @override + String get channelName => (super.noSuchMethod( + Invocation.getter(#channelName), + returnValue: '', + ) as String); + @override + void Function(String) get postMessage => (super.noSuchMethod( + Invocation.getter(#postMessage), + returnValue: (String message) {}, + ) as void Function(String)); + @override + _i2.JavaScriptChannel copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeJavaScriptChannel_6( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.JavaScriptChannel); +} + +/// A class which mocks [WebChromeClient]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebChromeClient extends _i1.Mock implements _i2.WebChromeClient { + MockWebChromeClient() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future setSynchronousReturnValueForOnShowFileChooser(bool? value) => + (super.noSuchMethod( + Invocation.method( + #setSynchronousReturnValueForOnShowFileChooser, + [value], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i2.WebChromeClient copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebChromeClient_7( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WebChromeClient); +} + +/// A class which mocks [WebViewClient]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewClient extends _i1.Mock implements _i2.WebViewClient { + MockWebViewClient() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future setSynchronousReturnValueForShouldOverrideUrlLoading( + bool? value) => + (super.noSuchMethod( + Invocation.method( + #setSynchronousReturnValueForShouldOverrideUrlLoading, + [value], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i2.WebViewClient copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWebViewClient_8( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WebViewClient); +} + +/// A class which mocks [JavascriptChannelRegistry]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockJavascriptChannelRegistry extends _i1.Mock + implements _i4.JavascriptChannelRegistry { + MockJavascriptChannelRegistry() { + _i1.throwOnMissingStub(this); + } + + @override + Map get channels => (super.noSuchMethod( + Invocation.getter(#channels), + returnValue: {}, + ) as Map); + @override + void onJavascriptChannelMessage( + String? channel, + String? message, + ) => + super.noSuchMethod( + Invocation.method( + #onJavascriptChannelMessage, + [ + channel, + message, + ], + ), + returnValueForMissingStub: null, + ); + @override + void updateJavascriptChannelsFromSet(Set<_i4.JavascriptChannel>? channels) => + super.noSuchMethod( + Invocation.method( + #updateJavascriptChannelsFromSet, + [channels], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [WebViewPlatformCallbacksHandler]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewPlatformCallbacksHandler extends _i1.Mock + implements _i4.WebViewPlatformCallbacksHandler { + MockWebViewPlatformCallbacksHandler() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.FutureOr onNavigationRequest({ + required String? url, + required bool? isForMainFrame, + }) => + (super.noSuchMethod( + Invocation.method( + #onNavigationRequest, + [], + { + #url: url, + #isForMainFrame: isForMainFrame, + }, + ), + returnValue: _i5.Future.value(false), + ) as _i5.FutureOr); + @override + void onPageStarted(String? url) => super.noSuchMethod( + Invocation.method( + #onPageStarted, + [url], + ), + returnValueForMissingStub: null, + ); + @override + void onPageFinished(String? url) => super.noSuchMethod( + Invocation.method( + #onPageFinished, + [url], + ), + returnValueForMissingStub: null, + ); + @override + void onProgress(int? progress) => super.noSuchMethod( + Invocation.method( + #onProgress, + [progress], + ), + returnValueForMissingStub: null, + ); + @override + void onWebResourceError(_i4.WebResourceError? error) => super.noSuchMethod( + Invocation.method( + #onWebResourceError, + [error], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [WebViewProxy]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewProxy extends _i1.Mock implements _i7.WebViewProxy { + MockWebViewProxy() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WebView createWebView({required bool? useHybridComposition}) => + (super.noSuchMethod( + Invocation.method( + #createWebView, + [], + {#useHybridComposition: useHybridComposition}, + ), + returnValue: _FakeWebView_3( + this, + Invocation.method( + #createWebView, + [], + {#useHybridComposition: useHybridComposition}, + ), + ), + ) as _i2.WebView); + @override + _i2.WebViewClient createWebViewClient({ + void Function( + _i2.WebView, + String, + )? + onPageStarted, + void Function( + _i2.WebView, + String, + )? + onPageFinished, + void Function( + _i2.WebView, + _i2.WebResourceRequest, + _i2.WebResourceError, + )? + onReceivedRequestError, + void Function( + _i2.WebView, + int, + String, + String, + )? + onReceivedError, + void Function( + _i2.WebView, + _i2.WebResourceRequest, + )? + requestLoading, + void Function( + _i2.WebView, + String, + )? + urlLoading, + }) => + (super.noSuchMethod( + Invocation.method( + #createWebViewClient, + [], + { + #onPageStarted: onPageStarted, + #onPageFinished: onPageFinished, + #onReceivedRequestError: onReceivedRequestError, + #onReceivedError: onReceivedError, + #requestLoading: requestLoading, + #urlLoading: urlLoading, + }, + ), + returnValue: _FakeWebViewClient_8( + this, + Invocation.method( + #createWebViewClient, + [], + { + #onPageStarted: onPageStarted, + #onPageFinished: onPageFinished, + #onReceivedRequestError: onReceivedRequestError, + #onReceivedError: onReceivedError, + #requestLoading: requestLoading, + #urlLoading: urlLoading, + }, + ), + ), + ) as _i2.WebViewClient); + @override + _i5.Future setWebContentsDebuggingEnabled(bool? enabled) => + (super.noSuchMethod( + Invocation.method( + #setWebContentsDebuggingEnabled, + [enabled], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} diff --git a/packages/webview_flutter/webview_flutter_android/test/test_android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart similarity index 91% rename from packages/webview_flutter/webview_flutter_android/test/test_android_webview.pigeon.dart rename to packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart index afc80ab53b41..56ba79a66622 100644 --- a/packages/webview_flutter/webview_flutter_android/test/test_android_webview.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.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 (v4.0.2), do not edit directly. +// Autogenerated from Pigeon (v4.2.14), 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 @@ -11,16 +11,18 @@ import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:webview_flutter_android/src/android_webview.pigeon.dart'; - -class _TestJavaObjectHostApiCodec extends StandardMessageCodec { - const _TestJavaObjectHostApiCodec(); -} +import 'package:webview_flutter_android/src/android_webview.g.dart'; +/// Handles methods calls to the native Java Object class. +/// +/// Also handles calls to remove the reference to an instance with `dispose`. +/// +/// See https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html. abstract class TestJavaObjectHostApi { - static const MessageCodec codec = _TestJavaObjectHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void dispose(int identifier); + static void setup(TestJavaObjectHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -38,7 +40,7 @@ abstract class TestJavaObjectHostApi { assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.JavaObjectHostApi.dispose was null, expected non-null int.'); api.dispose(arg_identifier!); - return {}; + return []; }); } } @@ -73,34 +75,59 @@ abstract class TestWebViewHostApi { static const MessageCodec codec = _TestWebViewHostApiCodec(); void create(int instanceId, bool useHybridComposition); - void dispose(int instanceId); + void loadData( int instanceId, String data, String? mimeType, String? encoding); + void loadDataWithBaseUrl(int instanceId, String? baseUrl, String data, String? mimeType, String? encoding, String? historyUrl); + void loadUrl(int instanceId, String url, Map headers); + void postUrl(int instanceId, String url, Uint8List data); + String? getUrl(int instanceId); + bool canGoBack(int instanceId); + bool canGoForward(int instanceId); + void goBack(int instanceId); + void goForward(int instanceId); + void reload(int instanceId); + void clearCache(int instanceId, bool includeDiskFiles); + Future evaluateJavascript(int instanceId, String javascriptString); + String? getTitle(int instanceId); + void scrollTo(int instanceId, int x, int y); + void scrollBy(int instanceId, int x, int y); + int getScrollX(int instanceId); + int getScrollY(int instanceId); + WebViewPoint getScrollPosition(int instanceId); + void setWebContentsDebuggingEnabled(bool enabled); + void setWebViewClient(int instanceId, int webViewClientInstanceId); + void addJavaScriptChannel(int instanceId, int javaScriptChannelInstanceId); + void removeJavaScriptChannel(int instanceId, int javaScriptChannelInstanceId); + void setDownloadListener(int instanceId, int? listenerInstanceId); + void setWebChromeClient(int instanceId, int? clientInstanceId); + void setBackgroundColor(int instanceId, int color); + static void setup(TestWebViewHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -121,26 +148,7 @@ abstract class TestWebViewHostApi { assert(arg_useHybridComposition != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.create was null, expected non-null bool.'); api.create(arg_instanceId!, arg_useHybridComposition!); - return {}; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebViewHostApi.dispose', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebViewHostApi.dispose 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.WebViewHostApi.dispose was null, expected non-null int.'); - api.dispose(arg_instanceId!); - return {}; + return []; }); } } @@ -164,7 +172,7 @@ abstract class TestWebViewHostApi { final String? arg_mimeType = (args[2] as String?); final String? arg_encoding = (args[3] as String?); api.loadData(arg_instanceId!, arg_data!, arg_mimeType, arg_encoding); - return {}; + return []; }); } } @@ -191,7 +199,7 @@ abstract class TestWebViewHostApi { final String? arg_historyUrl = (args[5] as String?); api.loadDataWithBaseUrl(arg_instanceId!, arg_baseUrl, arg_data!, arg_mimeType, arg_encoding, arg_historyUrl); - return {}; + return []; }); } } @@ -217,7 +225,7 @@ abstract class TestWebViewHostApi { assert(arg_headers != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.loadUrl was null, expected non-null Map.'); api.loadUrl(arg_instanceId!, arg_url!, arg_headers!); - return {}; + return []; }); } } @@ -242,7 +250,7 @@ abstract class TestWebViewHostApi { assert(arg_data != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.postUrl was null, expected non-null Uint8List.'); api.postUrl(arg_instanceId!, arg_url!, arg_data!); - return {}; + return []; }); } } @@ -261,7 +269,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getUrl was null, expected non-null int.'); final String? output = api.getUrl(arg_instanceId!); - return {'result': output}; + return [output]; }); } } @@ -280,7 +288,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.canGoBack was null, expected non-null int.'); final bool output = api.canGoBack(arg_instanceId!); - return {'result': output}; + return [output]; }); } } @@ -299,7 +307,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.canGoForward was null, expected non-null int.'); final bool output = api.canGoForward(arg_instanceId!); - return {'result': output}; + return [output]; }); } } @@ -318,7 +326,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.goBack was null, expected non-null int.'); api.goBack(arg_instanceId!); - return {}; + return []; }); } } @@ -337,7 +345,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.goForward was null, expected non-null int.'); api.goForward(arg_instanceId!); - return {}; + return []; }); } } @@ -356,7 +364,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.reload was null, expected non-null int.'); api.reload(arg_instanceId!); - return {}; + return []; }); } } @@ -378,7 +386,7 @@ abstract class TestWebViewHostApi { assert(arg_includeDiskFiles != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.clearCache was null, expected non-null bool.'); api.clearCache(arg_instanceId!, arg_includeDiskFiles!); - return {}; + return []; }); } } @@ -401,7 +409,7 @@ abstract class TestWebViewHostApi { 'Argument for dev.flutter.pigeon.WebViewHostApi.evaluateJavascript was null, expected non-null String.'); final String? output = await api.evaluateJavascript( arg_instanceId!, arg_javascriptString!); - return {'result': output}; + return [output]; }); } } @@ -420,7 +428,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getTitle was null, expected non-null int.'); final String? output = api.getTitle(arg_instanceId!); - return {'result': output}; + return [output]; }); } } @@ -445,7 +453,7 @@ abstract class TestWebViewHostApi { assert(arg_y != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollTo was null, expected non-null int.'); api.scrollTo(arg_instanceId!, arg_x!, arg_y!); - return {}; + return []; }); } } @@ -470,7 +478,7 @@ abstract class TestWebViewHostApi { assert(arg_y != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollBy was null, expected non-null int.'); api.scrollBy(arg_instanceId!, arg_x!, arg_y!); - return {}; + return []; }); } } @@ -489,7 +497,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getScrollX was null, expected non-null int.'); final int output = api.getScrollX(arg_instanceId!); - return {'result': output}; + return [output]; }); } } @@ -508,7 +516,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getScrollY was null, expected non-null int.'); final int output = api.getScrollY(arg_instanceId!); - return {'result': output}; + return [output]; }); } } @@ -527,7 +535,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getScrollPosition was null, expected non-null int.'); final WebViewPoint output = api.getScrollPosition(arg_instanceId!); - return {'result': output}; + return [output]; }); } } @@ -547,7 +555,7 @@ abstract class TestWebViewHostApi { assert(arg_enabled != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled was null, expected non-null bool.'); api.setWebContentsDebuggingEnabled(arg_enabled!); - return {}; + return []; }); } } @@ -569,7 +577,7 @@ abstract class TestWebViewHostApi { assert(arg_webViewClientInstanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebViewClient was null, expected non-null int.'); api.setWebViewClient(arg_instanceId!, arg_webViewClientInstanceId!); - return {}; + return []; }); } } @@ -592,7 +600,7 @@ abstract class TestWebViewHostApi { 'Argument for dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel was null, expected non-null int.'); api.addJavaScriptChannel( arg_instanceId!, arg_javaScriptChannelInstanceId!); - return {}; + return []; }); } } @@ -615,7 +623,7 @@ abstract class TestWebViewHostApi { 'Argument for dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel was null, expected non-null int.'); api.removeJavaScriptChannel( arg_instanceId!, arg_javaScriptChannelInstanceId!); - return {}; + return []; }); } } @@ -635,7 +643,7 @@ abstract class TestWebViewHostApi { 'Argument for dev.flutter.pigeon.WebViewHostApi.setDownloadListener was null, expected non-null int.'); final int? arg_listenerInstanceId = (args[1] as int?); api.setDownloadListener(arg_instanceId!, arg_listenerInstanceId); - return {}; + return []; }); } } @@ -655,7 +663,7 @@ abstract class TestWebViewHostApi { 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebChromeClient was null, expected non-null int.'); final int? arg_clientInstanceId = (args[1] as int?); api.setWebChromeClient(arg_instanceId!, arg_clientInstanceId); - return {}; + return []; }); } } @@ -677,34 +685,42 @@ abstract class TestWebViewHostApi { assert(arg_color != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setBackgroundColor was null, expected non-null int.'); api.setBackgroundColor(arg_instanceId!, arg_color!); - return {}; + return []; }); } } } } -class _TestWebSettingsHostApiCodec extends StandardMessageCodec { - const _TestWebSettingsHostApiCodec(); -} - abstract class TestWebSettingsHostApi { - static const MessageCodec codec = _TestWebSettingsHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void create(int instanceId, int webViewInstanceId); - void dispose(int instanceId); + void setDomStorageEnabled(int instanceId, bool flag); + void setJavaScriptCanOpenWindowsAutomatically(int instanceId, bool flag); + void setSupportMultipleWindows(int instanceId, bool support); + void setJavaScriptEnabled(int instanceId, bool flag); + void setUserAgentString(int instanceId, String? userAgentString); + void setMediaPlaybackRequiresUserGesture(int instanceId, bool require); + void setSupportZoom(int instanceId, bool support); + void setLoadWithOverviewMode(int instanceId, bool overview); + void setUseWideViewPort(int instanceId, bool use); + void setDisplayZoomControls(int instanceId, bool enabled); + void setBuiltInZoomControls(int instanceId, bool enabled); + void setAllowFileAccess(int instanceId, bool enabled); + static void setup(TestWebSettingsHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -725,26 +741,7 @@ abstract class TestWebSettingsHostApi { assert(arg_webViewInstanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.create was null, expected non-null int.'); api.create(arg_instanceId!, arg_webViewInstanceId!); - return {}; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WebSettingsHostApi.dispose', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.WebSettingsHostApi.dispose 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.WebSettingsHostApi.dispose was null, expected non-null int.'); - api.dispose(arg_instanceId!); - return {}; + return []; }); } } @@ -766,7 +763,7 @@ abstract class TestWebSettingsHostApi { assert(arg_flag != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled was null, expected non-null bool.'); api.setDomStorageEnabled(arg_instanceId!, arg_flag!); - return {}; + return []; }); } } @@ -790,7 +787,7 @@ abstract class TestWebSettingsHostApi { 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically was null, expected non-null bool.'); api.setJavaScriptCanOpenWindowsAutomatically( arg_instanceId!, arg_flag!); - return {}; + return []; }); } } @@ -813,7 +810,7 @@ abstract class TestWebSettingsHostApi { assert(arg_support != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows was null, expected non-null bool.'); api.setSupportMultipleWindows(arg_instanceId!, arg_support!); - return {}; + return []; }); } } @@ -835,7 +832,7 @@ abstract class TestWebSettingsHostApi { assert(arg_flag != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled was null, expected non-null bool.'); api.setJavaScriptEnabled(arg_instanceId!, arg_flag!); - return {}; + return []; }); } } @@ -855,7 +852,7 @@ abstract class TestWebSettingsHostApi { 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString was null, expected non-null int.'); final String? arg_userAgentString = (args[1] as String?); api.setUserAgentString(arg_instanceId!, arg_userAgentString); - return {}; + return []; }); } } @@ -879,7 +876,7 @@ abstract class TestWebSettingsHostApi { 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture was null, expected non-null bool.'); api.setMediaPlaybackRequiresUserGesture( arg_instanceId!, arg_require!); - return {}; + return []; }); } } @@ -901,7 +898,7 @@ abstract class TestWebSettingsHostApi { assert(arg_support != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom was null, expected non-null bool.'); api.setSupportZoom(arg_instanceId!, arg_support!); - return {}; + return []; }); } } @@ -924,7 +921,7 @@ abstract class TestWebSettingsHostApi { assert(arg_overview != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode was null, expected non-null bool.'); api.setLoadWithOverviewMode(arg_instanceId!, arg_overview!); - return {}; + return []; }); } } @@ -946,7 +943,7 @@ abstract class TestWebSettingsHostApi { assert(arg_use != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort was null, expected non-null bool.'); api.setUseWideViewPort(arg_instanceId!, arg_use!); - return {}; + return []; }); } } @@ -968,7 +965,7 @@ abstract class TestWebSettingsHostApi { assert(arg_enabled != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls was null, expected non-null bool.'); api.setDisplayZoomControls(arg_instanceId!, arg_enabled!); - return {}; + return []; }); } } @@ -990,7 +987,7 @@ abstract class TestWebSettingsHostApi { assert(arg_enabled != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls was null, expected non-null bool.'); api.setBuiltInZoomControls(arg_instanceId!, arg_enabled!); - return {}; + return []; }); } } @@ -1012,22 +1009,18 @@ abstract class TestWebSettingsHostApi { assert(arg_enabled != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess was null, expected non-null bool.'); api.setAllowFileAccess(arg_instanceId!, arg_enabled!); - return {}; + return []; }); } } } } -class _TestJavaScriptChannelHostApiCodec extends StandardMessageCodec { - const _TestJavaScriptChannelHostApiCodec(); -} - abstract class TestJavaScriptChannelHostApi { - static const MessageCodec codec = - _TestJavaScriptChannelHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void create(int instanceId, String channelName); + static void setup(TestJavaScriptChannelHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1048,21 +1041,21 @@ abstract class TestJavaScriptChannelHostApi { assert(arg_channelName != null, 'Argument for dev.flutter.pigeon.JavaScriptChannelHostApi.create was null, expected non-null String.'); api.create(arg_instanceId!, arg_channelName!); - return {}; + return []; }); } } } } -class _TestWebViewClientHostApiCodec extends StandardMessageCodec { - const _TestWebViewClientHostApiCodec(); -} - abstract class TestWebViewClientHostApi { - static const MessageCodec codec = _TestWebViewClientHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); + + void create(int instanceId); + + void setSynchronousReturnValueForShouldOverrideUrlLoading( + int instanceId, bool value); - void create(int instanceId, bool shouldOverrideUrlLoading); static void setup(TestWebViewClientHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1079,26 +1072,43 @@ abstract class TestWebViewClientHostApi { final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientHostApi.create was null, expected non-null int.'); - final bool? arg_shouldOverrideUrlLoading = (args[1] as bool?); - assert(arg_shouldOverrideUrlLoading != null, - 'Argument for dev.flutter.pigeon.WebViewClientHostApi.create was null, expected non-null bool.'); - api.create(arg_instanceId!, arg_shouldOverrideUrlLoading!); - return {}; + api.create(arg_instanceId!); + return []; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebViewClientHostApi.setSynchronousReturnValueForShouldOverrideUrlLoading', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebViewClientHostApi.setSynchronousReturnValueForShouldOverrideUrlLoading 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.WebViewClientHostApi.setSynchronousReturnValueForShouldOverrideUrlLoading was null, expected non-null int.'); + final bool? arg_value = (args[1] as bool?); + assert(arg_value != null, + 'Argument for dev.flutter.pigeon.WebViewClientHostApi.setSynchronousReturnValueForShouldOverrideUrlLoading was null, expected non-null bool.'); + api.setSynchronousReturnValueForShouldOverrideUrlLoading( + arg_instanceId!, arg_value!); + return []; }); } } } } -class _TestDownloadListenerHostApiCodec extends StandardMessageCodec { - const _TestDownloadListenerHostApiCodec(); -} - abstract class TestDownloadListenerHostApi { - static const MessageCodec codec = - _TestDownloadListenerHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void create(int instanceId); + static void setup(TestDownloadListenerHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1116,21 +1126,21 @@ abstract class TestDownloadListenerHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.DownloadListenerHostApi.create was null, expected non-null int.'); api.create(arg_instanceId!); - return {}; + return []; }); } } } } -class _TestWebChromeClientHostApiCodec extends StandardMessageCodec { - const _TestWebChromeClientHostApiCodec(); -} - abstract class TestWebChromeClientHostApi { - static const MessageCodec codec = _TestWebChromeClientHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); + + void create(int instanceId); + + void setSynchronousReturnValueForOnShowFileChooser( + int instanceId, bool value); - void create(int instanceId, int webViewClientInstanceId); static void setup(TestWebChromeClientHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1147,26 +1157,45 @@ abstract class TestWebChromeClientHostApi { final int? arg_instanceId = (args[0] as int?); assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.create was null, expected non-null int.'); - final int? arg_webViewClientInstanceId = (args[1] as int?); - assert(arg_webViewClientInstanceId != null, - 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.create was null, expected non-null int.'); - api.create(arg_instanceId!, arg_webViewClientInstanceId!); - return {}; + api.create(arg_instanceId!); + return []; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebChromeClientHostApi.setSynchronousReturnValueForOnShowFileChooser', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.setSynchronousReturnValueForOnShowFileChooser 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.WebChromeClientHostApi.setSynchronousReturnValueForOnShowFileChooser was null, expected non-null int.'); + final bool? arg_value = (args[1] as bool?); + assert(arg_value != null, + 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.setSynchronousReturnValueForOnShowFileChooser was null, expected non-null bool.'); + api.setSynchronousReturnValueForOnShowFileChooser( + arg_instanceId!, arg_value!); + return []; }); } } } } -class _TestAssetManagerHostApiCodec extends StandardMessageCodec { - const _TestAssetManagerHostApiCodec(); -} - abstract class TestAssetManagerHostApi { - static const MessageCodec codec = _TestAssetManagerHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); List list(String path); + String getAssetFilePathByName(String name); + static void setup(TestAssetManagerHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1184,7 +1213,7 @@ abstract class TestAssetManagerHostApi { assert(arg_path != null, 'Argument for dev.flutter.pigeon.FlutterAssetManagerHostApi.list was null, expected non-null String.'); final List output = api.list(arg_path!); - return {'result': output}; + return [output]; }); } } @@ -1204,22 +1233,20 @@ abstract class TestAssetManagerHostApi { assert(arg_name != null, 'Argument for dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName was null, expected non-null String.'); final String output = api.getAssetFilePathByName(arg_name!); - return {'result': output}; + return [output]; }); } } } } -class _TestWebStorageHostApiCodec extends StandardMessageCodec { - const _TestWebStorageHostApiCodec(); -} - abstract class TestWebStorageHostApi { - static const MessageCodec codec = _TestWebStorageHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void create(int instanceId); + void deleteAllData(int instanceId); + static void setup(TestWebStorageHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1237,7 +1264,7 @@ abstract class TestWebStorageHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebStorageHostApi.create was null, expected non-null int.'); api.create(arg_instanceId!); - return {}; + return []; }); } } @@ -1256,7 +1283,7 @@ abstract class TestWebStorageHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebStorageHostApi.deleteAllData was null, expected non-null int.'); api.deleteAllData(arg_instanceId!); - return {}; + return []; }); } } diff --git a/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart deleted file mode 100644 index acaff1c0af8b..000000000000 --- a/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart +++ /dev/null @@ -1,572 +0,0 @@ -// Mocks generated by Mockito 5.2.0 from annotations -// in webview_flutter_android/test/webview_android_widget_test.dart. -// Do not manually edit this file. - -import 'dart:async' as _i5; -import 'dart:typed_data' as _i6; -import 'dart:ui' as _i3; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_android/src/android_webview.dart' as _i2; -import 'package:webview_flutter_android/webview_android_widget.dart' as _i7; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' - as _i4; - -// 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 - -class _FakeWebSettings_0 extends _i1.Fake implements _i2.WebSettings {} - -class _FakeWebStorage_1 extends _i1.Fake implements _i2.WebStorage {} - -class _FakeOffset_2 extends _i1.Fake implements _i3.Offset {} - -class _FakeWebView_3 extends _i1.Fake implements _i2.WebView {} - -class _FakeDownloadListener_4 extends _i1.Fake - implements _i2.DownloadListener {} - -class _FakeJavascriptChannelRegistry_5 extends _i1.Fake - implements _i4.JavascriptChannelRegistry {} - -class _FakeJavaScriptChannel_6 extends _i1.Fake - implements _i2.JavaScriptChannel {} - -class _FakeWebChromeClient_7 extends _i1.Fake implements _i2.WebChromeClient {} - -class _FakeWebViewClient_8 extends _i1.Fake implements _i2.WebViewClient {} - -/// A class which mocks [FlutterAssetManager]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockFlutterAssetManager extends _i1.Mock - implements _i2.FlutterAssetManager { - MockFlutterAssetManager() { - _i1.throwOnMissingStub(this); - } - - @override - _i5.Future> list(String? path) => - (super.noSuchMethod(Invocation.method(#list, [path]), - returnValue: Future>.value([])) - as _i5.Future>); - @override - _i5.Future getAssetFilePathByName(String? name) => - (super.noSuchMethod(Invocation.method(#getAssetFilePathByName, [name]), - returnValue: Future.value('')) as _i5.Future); -} - -/// A class which mocks [WebSettings]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockWebSettings extends _i1.Mock implements _i2.WebSettings { - MockWebSettings() { - _i1.throwOnMissingStub(this); - } - - @override - _i5.Future setDomStorageEnabled(bool? flag) => - (super.noSuchMethod(Invocation.method(#setDomStorageEnabled, [flag]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setJavaScriptCanOpenWindowsAutomatically(bool? flag) => - (super.noSuchMethod( - Invocation.method(#setJavaScriptCanOpenWindowsAutomatically, [flag]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setSupportMultipleWindows(bool? support) => (super - .noSuchMethod(Invocation.method(#setSupportMultipleWindows, [support]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setJavaScriptEnabled(bool? flag) => - (super.noSuchMethod(Invocation.method(#setJavaScriptEnabled, [flag]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setUserAgentString(String? userAgentString) => (super - .noSuchMethod(Invocation.method(#setUserAgentString, [userAgentString]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setMediaPlaybackRequiresUserGesture(bool? require) => - (super.noSuchMethod( - Invocation.method(#setMediaPlaybackRequiresUserGesture, [require]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setSupportZoom(bool? support) => - (super.noSuchMethod(Invocation.method(#setSupportZoom, [support]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setLoadWithOverviewMode(bool? overview) => (super - .noSuchMethod(Invocation.method(#setLoadWithOverviewMode, [overview]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setUseWideViewPort(bool? use) => - (super.noSuchMethod(Invocation.method(#setUseWideViewPort, [use]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setDisplayZoomControls(bool? enabled) => - (super.noSuchMethod(Invocation.method(#setDisplayZoomControls, [enabled]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setBuiltInZoomControls(bool? enabled) => - (super.noSuchMethod(Invocation.method(#setBuiltInZoomControls, [enabled]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setAllowFileAccess(bool? enabled) => - (super.noSuchMethod(Invocation.method(#setAllowFileAccess, [enabled]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i2.WebSettings copy() => (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWebSettings_0()) as _i2.WebSettings); -} - -/// A class which mocks [WebStorage]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockWebStorage extends _i1.Mock implements _i2.WebStorage { - MockWebStorage() { - _i1.throwOnMissingStub(this); - } - - @override - _i5.Future deleteAllData() => - (super.noSuchMethod(Invocation.method(#deleteAllData, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i2.WebStorage copy() => (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWebStorage_1()) as _i2.WebStorage); -} - -/// A class which mocks [WebView]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockWebView extends _i1.Mock implements _i2.WebView { - MockWebView() { - _i1.throwOnMissingStub(this); - } - - @override - bool get useHybridComposition => - (super.noSuchMethod(Invocation.getter(#useHybridComposition), - returnValue: false) as bool); - @override - _i2.WebSettings get settings => - (super.noSuchMethod(Invocation.getter(#settings), - returnValue: _FakeWebSettings_0()) as _i2.WebSettings); - @override - _i5.Future loadData( - {String? data, String? mimeType, String? encoding}) => - (super.noSuchMethod( - Invocation.method(#loadData, [], - {#data: data, #mimeType: mimeType, #encoding: encoding}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future loadDataWithBaseUrl( - {String? baseUrl, - String? data, - String? mimeType, - String? encoding, - String? historyUrl}) => - (super.noSuchMethod( - Invocation.method(#loadDataWithBaseUrl, [], { - #baseUrl: baseUrl, - #data: data, - #mimeType: mimeType, - #encoding: encoding, - #historyUrl: historyUrl - }), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future loadUrl(String? url, Map? headers) => - (super.noSuchMethod(Invocation.method(#loadUrl, [url, headers]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future postUrl(String? url, _i6.Uint8List? data) => - (super.noSuchMethod(Invocation.method(#postUrl, [url, data]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future getUrl() => - (super.noSuchMethod(Invocation.method(#getUrl, []), - returnValue: Future.value()) as _i5.Future); - @override - _i5.Future canGoBack() => - (super.noSuchMethod(Invocation.method(#canGoBack, []), - returnValue: Future.value(false)) as _i5.Future); - @override - _i5.Future canGoForward() => - (super.noSuchMethod(Invocation.method(#canGoForward, []), - returnValue: Future.value(false)) as _i5.Future); - @override - _i5.Future goBack() => - (super.noSuchMethod(Invocation.method(#goBack, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future goForward() => - (super.noSuchMethod(Invocation.method(#goForward, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future reload() => - (super.noSuchMethod(Invocation.method(#reload, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future clearCache(bool? includeDiskFiles) => - (super.noSuchMethod(Invocation.method(#clearCache, [includeDiskFiles]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future evaluateJavascript(String? javascriptString) => (super - .noSuchMethod(Invocation.method(#evaluateJavascript, [javascriptString]), - returnValue: Future.value()) as _i5.Future); - @override - _i5.Future getTitle() => - (super.noSuchMethod(Invocation.method(#getTitle, []), - returnValue: Future.value()) as _i5.Future); - @override - _i5.Future scrollTo(int? x, int? y) => - (super.noSuchMethod(Invocation.method(#scrollTo, [x, y]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future scrollBy(int? x, int? y) => - (super.noSuchMethod(Invocation.method(#scrollBy, [x, y]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future getScrollX() => - (super.noSuchMethod(Invocation.method(#getScrollX, []), - returnValue: Future.value(0)) as _i5.Future); - @override - _i5.Future getScrollY() => - (super.noSuchMethod(Invocation.method(#getScrollY, []), - returnValue: Future.value(0)) as _i5.Future); - @override - _i5.Future<_i3.Offset> getScrollPosition() => - (super.noSuchMethod(Invocation.method(#getScrollPosition, []), - returnValue: Future<_i3.Offset>.value(_FakeOffset_2())) - as _i5.Future<_i3.Offset>); - @override - _i5.Future setWebViewClient(_i2.WebViewClient? webViewClient) => - (super.noSuchMethod(Invocation.method(#setWebViewClient, [webViewClient]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future addJavaScriptChannel( - _i2.JavaScriptChannel? javaScriptChannel) => - (super.noSuchMethod( - Invocation.method(#addJavaScriptChannel, [javaScriptChannel]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeJavaScriptChannel( - _i2.JavaScriptChannel? javaScriptChannel) => - (super.noSuchMethod( - Invocation.method(#removeJavaScriptChannel, [javaScriptChannel]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setDownloadListener(_i2.DownloadListener? listener) => - (super.noSuchMethod(Invocation.method(#setDownloadListener, [listener]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setWebChromeClient(_i2.WebChromeClient? client) => - (super.noSuchMethod(Invocation.method(#setWebChromeClient, [client]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setBackgroundColor(_i3.Color? color) => - (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future release() => - (super.noSuchMethod(Invocation.method(#release, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i2.WebView copy() => (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWebView_3()) as _i2.WebView); -} - -/// A class which mocks [WebResourceRequest]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockWebResourceRequest extends _i1.Mock - implements _i2.WebResourceRequest { - MockWebResourceRequest() { - _i1.throwOnMissingStub(this); - } - - @override - String get url => - (super.noSuchMethod(Invocation.getter(#url), returnValue: '') as String); - @override - bool get isForMainFrame => (super - .noSuchMethod(Invocation.getter(#isForMainFrame), returnValue: false) - as bool); - @override - bool get hasGesture => - (super.noSuchMethod(Invocation.getter(#hasGesture), returnValue: false) - as bool); - @override - String get method => - (super.noSuchMethod(Invocation.getter(#method), returnValue: '') - as String); - @override - Map get requestHeaders => - (super.noSuchMethod(Invocation.getter(#requestHeaders), - returnValue: {}) as Map); -} - -/// A class which mocks [WebViewAndroidDownloadListener]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockWebViewAndroidDownloadListener extends _i1.Mock - implements _i7.WebViewAndroidDownloadListener { - MockWebViewAndroidDownloadListener() { - _i1.throwOnMissingStub(this); - } - - @override - _i5.Future Function(String, Map?) get loadUrl => - (super.noSuchMethod(Invocation.getter(#loadUrl), - returnValue: (String url, Map? headers) => - Future.value()) as _i5.Future Function( - String, Map?)); - @override - void onDownloadStart(String? url, String? userAgent, - String? contentDisposition, String? mimetype, int? contentLength) => - super.noSuchMethod( - Invocation.method(#onDownloadStart, - [url, userAgent, contentDisposition, mimetype, contentLength]), - returnValueForMissingStub: null); - @override - _i2.DownloadListener copy() => - (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeDownloadListener_4()) as _i2.DownloadListener); -} - -/// A class which mocks [WebViewAndroidJavaScriptChannel]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockWebViewAndroidJavaScriptChannel extends _i1.Mock - implements _i7.WebViewAndroidJavaScriptChannel { - MockWebViewAndroidJavaScriptChannel() { - _i1.throwOnMissingStub(this); - } - - @override - _i4.JavascriptChannelRegistry get javascriptChannelRegistry => - (super.noSuchMethod(Invocation.getter(#javascriptChannelRegistry), - returnValue: _FakeJavascriptChannelRegistry_5()) - as _i4.JavascriptChannelRegistry); - @override - String get channelName => - (super.noSuchMethod(Invocation.getter(#channelName), returnValue: '') - as String); - @override - void postMessage(String? message) => - super.noSuchMethod(Invocation.method(#postMessage, [message]), - returnValueForMissingStub: null); - @override - _i2.JavaScriptChannel copy() => - (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeJavaScriptChannel_6()) as _i2.JavaScriptChannel); -} - -/// A class which mocks [WebViewAndroidWebChromeClient]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockWebViewAndroidWebChromeClient extends _i1.Mock - implements _i7.WebViewAndroidWebChromeClient { - MockWebViewAndroidWebChromeClient() { - _i1.throwOnMissingStub(this); - } - - @override - void onProgressChanged(_i2.WebView? webView, int? progress) => super - .noSuchMethod(Invocation.method(#onProgressChanged, [webView, progress]), - returnValueForMissingStub: null); - @override - _i2.WebChromeClient copy() => - (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWebChromeClient_7()) as _i2.WebChromeClient); -} - -/// A class which mocks [WebViewAndroidWebViewClient]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockWebViewAndroidWebViewClient extends _i1.Mock - implements _i7.WebViewAndroidWebViewClient { - MockWebViewAndroidWebViewClient() { - _i1.throwOnMissingStub(this); - } - - @override - void Function(String) get onPageStartedCallback => - (super.noSuchMethod(Invocation.getter(#onPageStartedCallback), - returnValue: (String url) {}) as void Function(String)); - @override - void Function(String) get onPageFinishedCallback => - (super.noSuchMethod(Invocation.getter(#onPageFinishedCallback), - returnValue: (String url) {}) as void Function(String)); - @override - void Function(_i4.WebResourceError) get onWebResourceErrorCallback => - (super.noSuchMethod(Invocation.getter(#onWebResourceErrorCallback), - returnValue: (_i4.WebResourceError error) {}) - as void Function(_i4.WebResourceError)); - @override - set onWebResourceErrorCallback( - void Function(_i4.WebResourceError)? _onWebResourceErrorCallback) => - super.noSuchMethod( - Invocation.setter( - #onWebResourceErrorCallback, _onWebResourceErrorCallback), - returnValueForMissingStub: null); - @override - bool get handlesNavigation => - (super.noSuchMethod(Invocation.getter(#handlesNavigation), - returnValue: false) as bool); - @override - bool get shouldOverrideUrlLoading => - (super.noSuchMethod(Invocation.getter(#shouldOverrideUrlLoading), - returnValue: false) as bool); - @override - void onPageStarted(_i2.WebView? webView, String? url) => - super.noSuchMethod(Invocation.method(#onPageStarted, [webView, url]), - returnValueForMissingStub: null); - @override - void onPageFinished(_i2.WebView? webView, String? url) => - super.noSuchMethod(Invocation.method(#onPageFinished, [webView, url]), - returnValueForMissingStub: null); - @override - void onReceivedError(_i2.WebView? webView, int? errorCode, - String? description, String? failingUrl) => - super.noSuchMethod( - Invocation.method( - #onReceivedError, [webView, errorCode, description, failingUrl]), - returnValueForMissingStub: null); - @override - void onReceivedRequestError(_i2.WebView? webView, - _i2.WebResourceRequest? request, _i2.WebResourceError? error) => - super.noSuchMethod( - Invocation.method(#onReceivedRequestError, [webView, request, error]), - returnValueForMissingStub: null); - @override - void urlLoading(_i2.WebView? webView, String? url) => - super.noSuchMethod(Invocation.method(#urlLoading, [webView, url]), - returnValueForMissingStub: null); - @override - void requestLoading(_i2.WebView? webView, _i2.WebResourceRequest? request) => - super.noSuchMethod(Invocation.method(#requestLoading, [webView, request]), - returnValueForMissingStub: null); - @override - _i2.WebViewClient copy() => (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWebViewClient_8()) as _i2.WebViewClient); -} - -/// A class which mocks [JavascriptChannelRegistry]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockJavascriptChannelRegistry extends _i1.Mock - implements _i4.JavascriptChannelRegistry { - MockJavascriptChannelRegistry() { - _i1.throwOnMissingStub(this); - } - - @override - Map get channels => - (super.noSuchMethod(Invocation.getter(#channels), - returnValue: {}) - as Map); - @override - void onJavascriptChannelMessage(String? channel, String? message) => - super.noSuchMethod( - Invocation.method(#onJavascriptChannelMessage, [channel, message]), - returnValueForMissingStub: null); - @override - void updateJavascriptChannelsFromSet(Set<_i4.JavascriptChannel>? channels) => - super.noSuchMethod( - Invocation.method(#updateJavascriptChannelsFromSet, [channels]), - returnValueForMissingStub: null); -} - -/// A class which mocks [WebViewPlatformCallbacksHandler]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockWebViewPlatformCallbacksHandler extends _i1.Mock - implements _i4.WebViewPlatformCallbacksHandler { - MockWebViewPlatformCallbacksHandler() { - _i1.throwOnMissingStub(this); - } - - @override - _i5.FutureOr onNavigationRequest({String? url, bool? isForMainFrame}) => - (super.noSuchMethod( - Invocation.method(#onNavigationRequest, [], - {#url: url, #isForMainFrame: isForMainFrame}), - returnValue: Future.value(false)) as _i5.FutureOr); - @override - void onPageStarted(String? url) => - super.noSuchMethod(Invocation.method(#onPageStarted, [url]), - returnValueForMissingStub: null); - @override - void onPageFinished(String? url) => - super.noSuchMethod(Invocation.method(#onPageFinished, [url]), - returnValueForMissingStub: null); - @override - void onProgress(int? progress) => - super.noSuchMethod(Invocation.method(#onProgress, [progress]), - returnValueForMissingStub: null); - @override - void onWebResourceError(_i4.WebResourceError? error) => - super.noSuchMethod(Invocation.method(#onWebResourceError, [error]), - returnValueForMissingStub: null); -} - -/// A class which mocks [WebViewProxy]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockWebViewProxy extends _i1.Mock implements _i7.WebViewProxy { - MockWebViewProxy() { - _i1.throwOnMissingStub(this); - } - - @override - _i2.WebView createWebView({bool? useHybridComposition}) => - (super.noSuchMethod( - Invocation.method(#createWebView, [], - {#useHybridComposition: useHybridComposition}), - returnValue: _FakeWebView_3()) as _i2.WebView); - @override - _i5.Future setWebContentsDebuggingEnabled(bool? enabled) => - (super.noSuchMethod( - Invocation.method(#setWebContentsDebuggingEnabled, [enabled]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); -} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md index b7050e4f4db3..5c33fdbcea59 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,18 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 2.0.1 + +* Improves error message when a platform interface class is used before `WebViewPlatform.instance` has been set. + +## 2.0.0 + +* **Breaking Change**: Releases new interface. See [documentation](https://pub.dev/documentation/webview_flutter_platform_interface/2.0.0/) and [design doc](https://flutter.dev/go/webview_flutter_4_interface) + for more details. +* **Breaking Change**: Removes MethodChannel implementation of interface. All platform + implementations will now need to create their own by implementing `WebViewPlatform`. + ## 1.9.5 * Updates code for `no_leading_underscores_for_local_identifiers` lint. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/README.md b/packages/webview_flutter/webview_flutter_platform_interface/README.md index 31e57ab61597..10160b3cd132 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/README.md +++ b/packages/webview_flutter/webview_flutter_platform_interface/README.md @@ -9,10 +9,10 @@ same interface. # Usage To implement a new platform-specific implementation of `webview_flutter`, extend -[`WebviewPlatform`](lib/src/platform_interface/webview_platform.dart) with an implementation that performs the +[`WebviewPlatform`](lib/src/webview_platform.dart) with an implementation that performs the platform-specific behavior, and when you register your plugin, set the default `WebviewPlatform` by calling -`WebviewPlatform.setInstance(MyPlatformWebview())`. +`WebviewPlatform.instance = MyPlatformWebview()`. # Note on breaking changes diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/javascript_channel_registry.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/platform_interface/javascript_channel_registry.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/javascript_channel_registry.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/platform_interface/javascript_channel_registry.dart diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/platform_interface.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/platform_interface/platform_interface.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/platform_interface.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/platform_interface/platform_interface.dart diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_cookie_manager.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/platform_interface/webview_cookie_manager.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_cookie_manager.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/platform_interface/webview_cookie_manager.dart diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/platform_interface/webview_platform.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/platform_interface/webview_platform.dart diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_callbacks_handler.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/platform_interface/webview_platform_callbacks_handler.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_callbacks_handler.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/platform_interface/webview_platform_callbacks_handler.dart diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/platform_interface/webview_platform_controller.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/platform_interface/webview_platform_controller.dart diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/auto_media_playback_policy.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/auto_media_playback_policy.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/auto_media_playback_policy.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/auto_media_playback_policy.dart diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/creation_params.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/creation_params.dart diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/javascript_channel.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/javascript_channel.dart diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/javascript_message.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/javascript_message.dart new file mode 100644 index 000000000000..8d080452c54a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/javascript_message.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. + +/// A message that was sent by JavaScript code running in a [WebView]. +class JavascriptMessage { + /// Constructs a JavaScript message object. + /// + /// The `message` parameter must not be null. + const JavascriptMessage(this.message) : assert(message != null); + + /// The contents of the message that was sent by the JavaScript code. + final String message; +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/javascript_mode.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/javascript_mode.dart similarity index 94% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/javascript_mode.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/javascript_mode.dart index bcbebff8bb1a..53d049175907 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/javascript_mode.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/javascript_mode.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. /// Describes the state of JavaScript support in a given web view. -enum JavaScriptMode { +enum JavascriptMode { /// JavaScript execution is disabled. disabled, diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/types.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/types.dart similarity index 51% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/types.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/types.dart index 05504fffd211..f2bcf19f42fd 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/types.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/types.dart @@ -2,12 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'auto_media_playback_policy.dart'; +export 'creation_params.dart'; +export 'javascript_channel.dart'; export 'javascript_message.dart'; export 'javascript_mode.dart'; -export 'load_request_params.dart'; -export 'platform_navigation_delegate_creation_params.dart'; -export 'platform_webview_controller_creation_params.dart'; -export 'platform_webview_cookie_manager_creation_params.dart'; -export 'platform_webview_widget_creation_params.dart'; export 'web_resource_error.dart'; +export 'web_resource_error_type.dart'; +export 'web_settings.dart'; export 'webview_cookie.dart'; +export 'webview_request.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/web_resource_error.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/web_resource_error.dart new file mode 100644 index 000000000000..b61671f0ac45 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/web_resource_error.dart @@ -0,0 +1,57 @@ +// 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 'web_resource_error_type.dart'; + +/// Error returned in `WebView.onWebResourceError` when a web resource loading error has occurred. +class WebResourceError { + /// Creates a new [WebResourceError] + /// + /// A user should not need to instantiate this class, but will receive one in + /// [WebResourceErrorCallback]. + WebResourceError({ + required this.errorCode, + required this.description, + this.domain, + this.errorType, + this.failingUrl, + }) : assert(errorCode != null), + assert(description != null); + + /// Raw code of the error from the respective platform. + /// + /// On Android, the error code will be a constant from a + /// [WebViewClient](https://developer.android.com/reference/android/webkit/WebViewClient#summary) and + /// will have a corresponding [errorType]. + /// + /// On iOS, the error code will be a constant from `NSError.code` in + /// Objective-C. See + /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html + /// for more information on error handling on iOS. Some possible error codes + /// can be found at https://developer.apple.com/documentation/webkit/wkerrorcode?language=objc. + final int errorCode; + + /// The domain of where to find the error code. + /// + /// This field is only available on iOS and represents a "domain" from where + /// the [errorCode] is from. This value is taken directly from an `NSError` + /// in Objective-C. See + /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html + /// for more information on error handling on iOS. + final String? domain; + + /// Description of the error that can be used to communicate the problem to the user. + final String description; + + /// The type this error can be categorized as. + /// + /// This will never be `null` on Android, but can be `null` on iOS. + final WebResourceErrorType? errorType; + + /// Gets the URL for which the resource request was made. + /// + /// This value is not provided on iOS. Alternatively, you can keep track of + /// the last values provided to [WebViewPlatformController.loadUrl]. + final String? failingUrl; +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error_type.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/web_resource_error_type.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error_type.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/web_resource_error_type.dart diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/web_settings.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/web_settings.dart diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/webview_cookie.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/webview_cookie.dart similarity index 63% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/webview_cookie.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/webview_cookie.dart index 7f56a312049f..406c510afd4b 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/webview_cookie.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/webview_cookie.dart @@ -2,18 +2,15 @@ // 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'; - -/// A cookie that can be set globally for all web views using [WebViewCookieManagerPlatform]. -@immutable +/// A cookie that can be set globally for all web views +/// using [WebViewCookieManagerPlatform]. class WebViewCookie { - /// Creates a new [WebViewCookieDelegate] - const WebViewCookie({ - required this.name, - required this.value, - required this.domain, - this.path = '/', - }); + /// Constructs a new [WebViewCookie]. + const WebViewCookie( + {required this.name, + required this.value, + required this.domain, + this.path = '/'}); /// The cookie-name of the cookie. /// @@ -33,9 +30,20 @@ class WebViewCookie { /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-4.1.1 final String domain; - /// The path-value of the cookie, set to `/` by default. + /// The path-value of the cookie. + /// Is set to `/` in the constructor by default. /// /// Its value should match "path-value" in RFC6265bis: /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-4.1.1 final String path; + + /// Serializes the [WebViewCookie] to a Map. + Map toJson() { + return { + 'name': name, + 'value': value, + 'domain': domain, + 'path': path + }; + } } diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/webview_request.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/webview_request.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/webview_request.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/legacy/types/webview_request.dart diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart deleted file mode 100644 index 0e98ea08fd16..000000000000 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart +++ /dev/null @@ -1,297 +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:async'; - -import 'package:flutter/services.dart'; - -import '../platform_interface/platform_interface.dart'; -import '../types/types.dart'; - -/// A [WebViewPlatformController] that uses a method channel to control the webview. -class MethodChannelWebViewPlatform implements WebViewPlatformController { - /// Constructs an instance that will listen for webviews broadcasting to the - /// given [id], using the given [WebViewPlatformCallbacksHandler]. - MethodChannelWebViewPlatform( - int id, - this._platformCallbacksHandler, - this._javascriptChannelRegistry, - ) : assert(_platformCallbacksHandler != null), - _channel = MethodChannel('plugins.flutter.io/webview_$id') { - _channel.setMethodCallHandler(_onMethodCall); - } - - final JavascriptChannelRegistry _javascriptChannelRegistry; - - final WebViewPlatformCallbacksHandler _platformCallbacksHandler; - - final MethodChannel _channel; - - static const MethodChannel _cookieManagerChannel = - MethodChannel('plugins.flutter.io/cookie_manager'); - - Future _onMethodCall(MethodCall call) async { - switch (call.method) { - case 'javascriptChannelMessage': - final String channel = call.arguments['channel']! as String; - final String message = call.arguments['message']! as String; - _javascriptChannelRegistry.onJavascriptChannelMessage(channel, message); - return true; - case 'navigationRequest': - return await _platformCallbacksHandler.onNavigationRequest( - url: call.arguments['url']! as String, - isForMainFrame: call.arguments['isForMainFrame']! as bool, - ); - case 'onPageFinished': - _platformCallbacksHandler - .onPageFinished(call.arguments['url']! as String); - return null; - case 'onProgress': - _platformCallbacksHandler.onProgress(call.arguments['progress'] as int); - return null; - case 'onPageStarted': - _platformCallbacksHandler - .onPageStarted(call.arguments['url']! as String); - return null; - case 'onWebResourceError': - _platformCallbacksHandler.onWebResourceError( - WebResourceError( - errorCode: call.arguments['errorCode']! as int, - description: call.arguments['description']! as String, - // iOS doesn't support `failingUrl`. - failingUrl: call.arguments['failingUrl'] as String?, - domain: call.arguments['domain'] as String?, - errorType: call.arguments['errorType'] == null - ? null - : WebResourceErrorType.values.firstWhere( - (WebResourceErrorType type) { - return type.toString() == - '$WebResourceErrorType.${call.arguments['errorType']}'; - }, - ), - ), - ); - return null; - } - - throw MissingPluginException( - '${call.method} was invoked but has no handler', - ); - } - - @override - Future loadFile(String absoluteFilePath) async { - assert(absoluteFilePath != null); - - try { - return await _channel.invokeMethod('loadFile', absoluteFilePath); - } on PlatformException catch (ex) { - if (ex.code == 'loadFile_failed') { - throw ArgumentError(ex.message); - } - - rethrow; - } - } - - @override - Future loadFlutterAsset(String key) async { - assert(key.isNotEmpty); - - try { - return await _channel.invokeMethod('loadFlutterAsset', key); - } on PlatformException catch (ex) { - if (ex.code == 'loadFlutterAsset_invalidKey') { - throw ArgumentError(ex.message); - } - - rethrow; - } - } - - @override - Future loadHtmlString( - String html, { - String? baseUrl, - }) async { - assert(html != null); - return _channel.invokeMethod('loadHtmlString', { - 'html': html, - 'baseUrl': baseUrl, - }); - } - - @override - Future loadUrl( - String url, - Map? headers, - ) async { - assert(url != null); - return _channel.invokeMethod('loadUrl', { - 'url': url, - 'headers': headers, - }); - } - - @override - Future loadRequest(WebViewRequest request) async { - assert(request != null); - return _channel.invokeMethod('loadRequest', { - 'request': request.toJson(), - }); - } - - @override - Future currentUrl() => _channel.invokeMethod('currentUrl'); - - @override - Future canGoBack() => - _channel.invokeMethod('canGoBack').then((bool? result) => result!); - - @override - Future canGoForward() => _channel - .invokeMethod('canGoForward') - .then((bool? result) => result!); - - @override - Future goBack() => _channel.invokeMethod('goBack'); - - @override - Future goForward() => _channel.invokeMethod('goForward'); - - @override - Future reload() => _channel.invokeMethod('reload'); - - @override - Future clearCache() => _channel.invokeMethod('clearCache'); - - @override - Future updateSettings(WebSettings settings) async { - final Map updatesMap = _webSettingsToMap(settings); - if (updatesMap.isNotEmpty) { - await _channel.invokeMethod('updateSettings', updatesMap); - } - } - - @override - Future evaluateJavascript(String javascript) { - return _channel - .invokeMethod('evaluateJavascript', javascript) - .then((String? result) => result!); - } - - @override - Future runJavascript(String javascript) async { - await _channel.invokeMethod('runJavascript', javascript); - } - - @override - Future runJavascriptReturningResult(String javascript) { - return _channel - .invokeMethod('runJavascriptReturningResult', javascript) - .then((String? result) => result!); - } - - @override - Future addJavascriptChannels(Set javascriptChannelNames) { - return _channel.invokeMethod( - 'addJavascriptChannels', javascriptChannelNames.toList()); - } - - @override - Future removeJavascriptChannels(Set javascriptChannelNames) { - return _channel.invokeMethod( - 'removeJavascriptChannels', javascriptChannelNames.toList()); - } - - @override - Future getTitle() => _channel.invokeMethod('getTitle'); - - @override - Future scrollTo(int x, int y) { - return _channel.invokeMethod('scrollTo', { - 'x': x, - 'y': y, - }); - } - - @override - Future scrollBy(int x, int y) { - return _channel.invokeMethod('scrollBy', { - 'x': x, - 'y': y, - }); - } - - @override - Future getScrollX() => - _channel.invokeMethod('getScrollX').then((int? result) => result!); - - @override - Future getScrollY() => - _channel.invokeMethod('getScrollY').then((int? result) => result!); - - /// Method channel implementation for [WebViewPlatform.clearCookies]. - static Future clearCookies() { - return _cookieManagerChannel - .invokeMethod('clearCookies') - .then((dynamic result) => result! as bool); - } - - /// Method channel implementation for [WebViewPlatform.setCookie]. - static Future setCookie(WebViewCookie cookie) { - return _cookieManagerChannel.invokeMethod( - 'setCookie', cookie.toJson()); - } - - static Map _webSettingsToMap(WebSettings? settings) { - final Map map = {}; - void addIfNonNull(String key, dynamic value) { - if (value == null) { - return; - } - map[key] = value; - } - - void addSettingIfPresent(String key, WebSetting setting) { - if (!setting.isPresent) { - return; - } - map[key] = setting.value; - } - - addIfNonNull('jsMode', settings!.javascriptMode?.index); - addIfNonNull('hasNavigationDelegate', settings.hasNavigationDelegate); - addIfNonNull('hasProgressTracking', settings.hasProgressTracking); - addIfNonNull('debuggingEnabled', settings.debuggingEnabled); - addIfNonNull('gestureNavigationEnabled', settings.gestureNavigationEnabled); - addIfNonNull( - 'allowsInlineMediaPlayback', settings.allowsInlineMediaPlayback); - addSettingIfPresent('userAgent', settings.userAgent); - addIfNonNull('zoomEnabled', settings.zoomEnabled); - return map; - } - - /// Converts a [CreationParams] object to a map as expected by `platform_views` channel. - /// - /// This is used for the `creationParams` argument of the platform views created by - /// [AndroidWebViewBuilder] and [CupertinoWebViewBuilder]. - static Map creationParamsToMap( - CreationParams creationParams, { - bool usesHybridComposition = false, - }) { - return { - 'initialUrl': creationParams.initialUrl, - 'settings': _webSettingsToMap(creationParams.webSettings), - 'javascriptChannelNames': creationParams.javascriptChannelNames.toList(), - 'userAgent': creationParams.userAgent, - 'autoMediaPlaybackPolicy': creationParams.autoMediaPlaybackPolicy.index, - 'usesHybridComposition': usesHybridComposition, - 'backgroundColor': creationParams.backgroundColor?.value, - 'cookies': creationParams.cookies - .map((WebViewCookie cookie) => cookie.toJson()) - .toList() - }; - } -} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_navigation_delegate.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_navigation_delegate.dart similarity index 71% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_navigation_delegate.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_navigation_delegate.dart index a66f1defdf60..ec7af71eea51 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_navigation_delegate.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_navigation_delegate.dart @@ -9,6 +9,19 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'webview_platform.dart'; +/// Signature for callbacks that report a pending navigation request. +typedef NavigationRequestCallback = FutureOr Function( + NavigationRequest navigationRequest); + +/// Signature for callbacks that report page events triggered by the native web view. +typedef PageEventCallback = void Function(String url); + +/// Signature for callbacks that report loading progress of a page. +typedef ProgressCallback = void Function(int progress); + +/// Signature for callbacks that report a resource loading error. +typedef WebResourceErrorCallback = void Function(WebResourceError error); + /// An interface defining navigation events that occur on the native platform. /// /// The [PlatformWebViewController] is notifying this delegate on events that @@ -18,6 +31,13 @@ abstract class PlatformNavigationDelegate extends PlatformInterface { /// Creates a new [PlatformNavigationDelegate] factory PlatformNavigationDelegate( PlatformNavigationDelegateCreationParams params) { + assert( + WebViewPlatform.instance != null, + 'A platform implementation for `webview_flutter` has not been set. Please ' + 'ensure that an implementation of `WebViewPlatform` has been set to ' + '`WebViewPlatform.instance` before use. For unit testing, ' + '`WebViewPlatform.instance` can be set with your own test implementation.', + ); final PlatformNavigationDelegate callbackDelegate = WebViewPlatform.instance!.createPlatformNavigationDelegate(params); PlatformInterface.verify(callbackDelegate, _token); @@ -40,8 +60,7 @@ abstract class PlatformNavigationDelegate extends PlatformInterface { /// /// See [PlatformWebViewController.setPlatformNavigationDelegate]. Future setOnNavigationRequest( - FutureOr Function({required String url, required bool isForMainFrame}) - onNavigationRequest, + NavigationRequestCallback onNavigationRequest, ) { throw UnimplementedError( 'setOnNavigationRequest is not implemented on the current platform.'); @@ -51,7 +70,7 @@ abstract class PlatformNavigationDelegate extends PlatformInterface { /// /// See [PlatformWebViewController.setPlatformNavigationDelegate]. Future setOnPageStarted( - void Function(String url) onPageStarted, + PageEventCallback onPageStarted, ) { throw UnimplementedError( 'setOnPageStarted is not implemented on the current platform.'); @@ -61,7 +80,7 @@ abstract class PlatformNavigationDelegate extends PlatformInterface { /// /// See [PlatformWebViewController.setPlatformNavigationDelegate]. Future setOnPageFinished( - void Function(String url) onPageFinished, + PageEventCallback onPageFinished, ) { throw UnimplementedError( 'setOnPageFinished is not implemented on the current platform.'); @@ -71,7 +90,7 @@ abstract class PlatformNavigationDelegate extends PlatformInterface { /// /// See [PlatformWebViewController.setPlatformNavigationDelegate]. Future setOnProgress( - void Function(int progress) onProgress, + ProgressCallback onProgress, ) { throw UnimplementedError( 'setOnProgress is not implemented on the current platform.'); @@ -81,7 +100,7 @@ abstract class PlatformNavigationDelegate extends PlatformInterface { /// /// See [PlatformWebViewController.setPlatformNavigationDelegate]. Future setOnWebResourceError( - void Function(WebResourceError error) onWebResourceError, + WebResourceErrorCallback onWebResourceError, ) { throw UnimplementedError( 'setOnWebResourceError is not implemented on the current platform.'); diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_webview_controller.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_controller.dart similarity index 92% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_webview_controller.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_controller.dart index 3585ec8b1886..bdeaa977d3dd 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_controller.dart @@ -2,13 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:math'; -import 'dart:ui'; - import 'package:flutter/foundation.dart'; +import 'package:flutter/painting.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'platform_navigation_delegate.dart'; +import '../../src/platform_navigation_delegate.dart'; import 'webview_platform.dart'; /// Interface for a platform implementation of a web view controller. @@ -23,6 +21,13 @@ abstract class PlatformWebViewController extends PlatformInterface { /// Creates a new [PlatformWebViewController] factory PlatformWebViewController( PlatformWebViewControllerCreationParams params) { + assert( + WebViewPlatform.instance != null, + 'A platform implementation for `webview_flutter` has not been set. Please ' + 'ensure that an implementation of `WebViewPlatform` has been set to ' + '`WebViewPlatform.instance` before use. For unit testing, ' + '`WebViewPlatform.instance` can be set with your own test implementation.', + ); final PlatformWebViewController webViewControllerDelegate = WebViewPlatform.instance!.createPlatformWebViewController(params); PlatformInterface.verify(webViewControllerDelegate, _token); @@ -178,7 +183,7 @@ abstract class PlatformWebViewController extends PlatformInterface { /// The Future completes with an error if a JavaScript error occurred, or if the /// type the given expression evaluates to is unsupported. Unsupported values include /// certain non-primitive types on iOS, as well as `undefined` or `null` on iOS 14+. - Future runJavaScriptReturningResult(String javaScript) { + Future runJavaScriptReturningResult(String javaScript) { throw UnimplementedError( 'runJavaScriptReturningResult is not implemented on the current platform'); } @@ -226,24 +231,12 @@ abstract class PlatformWebViewController extends PlatformInterface { /// Return the current scroll position of this view. /// /// Scroll position is measured from the top left. - Future> getScrollPosition() { + Future getScrollPosition() { throw UnimplementedError( 'getScrollPosition is not implemented on the current platform'); } - /// Whether to enable the platform's webview content debugging tools. - Future enableDebugging(bool enabled) { - throw UnimplementedError( - 'enableDebugging is not implemented on the current platform'); - } - - /// Whether to allow swipe based navigation on supported platforms. - Future enableGestureNavigation(bool enabled) { - throw UnimplementedError( - 'enableGestureNavigation is not implemented on the current platform'); - } - - /// Whhether to support zooming using its on-screen zoom controls and gestures. + /// Whether to support zooming using its on-screen zoom controls and gestures. Future enableZoom(bool enabled) { throw UnimplementedError( 'enableZoom is not implemented on the current platform'); @@ -269,9 +262,10 @@ abstract class PlatformWebViewController extends PlatformInterface { } /// Describes the parameters necessary for registering a JavaScript channel. +@immutable class JavaScriptChannelParams { /// Creates a new [JavaScriptChannelParams] object. - JavaScriptChannelParams({ + const JavaScriptChannelParams({ required this.name, required this.onMessageReceived, }); diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_webview_cookie_manager.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_cookie_manager.dart similarity index 86% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_webview_cookie_manager.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_cookie_manager.dart index 9e981c9022c6..a6740670e5c3 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_webview_cookie_manager.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_cookie_manager.dart @@ -19,6 +19,13 @@ abstract class PlatformWebViewCookieManager extends PlatformInterface { /// Creates a new [PlatformWebViewCookieManager] factory PlatformWebViewCookieManager( PlatformWebViewCookieManagerCreationParams params) { + assert( + WebViewPlatform.instance != null, + 'A platform implementation for `webview_flutter` has not been set. Please ' + 'ensure that an implementation of `WebViewPlatform` has been set to ' + '`WebViewPlatform.instance` before use. For unit testing, ' + '`WebViewPlatform.instance` can be set with your own test implementation.', + ); final PlatformWebViewCookieManager cookieManagerDelegate = WebViewPlatform.instance!.createPlatformCookieManager(params); PlatformInterface.verify(cookieManagerDelegate, _token); diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_webview_widget.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_widget.dart similarity index 79% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_webview_widget.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_widget.dart index 40334c650b3a..2e49c80d0a9c 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/platform_webview_widget.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_widget.dart @@ -11,6 +11,13 @@ import 'webview_platform.dart'; abstract class PlatformWebViewWidget extends PlatformInterface { /// Creates a new [PlatformWebViewWidget] factory PlatformWebViewWidget(PlatformWebViewWidgetCreationParams params) { + assert( + WebViewPlatform.instance != null, + 'A platform implementation for `webview_flutter` has not been set. Please ' + 'ensure that an implementation of `WebViewPlatform` has been set to ' + '`WebViewPlatform.instance` before use. For unit testing, ' + '`WebViewPlatform.instance` can be set with your own test implementation.', + ); final PlatformWebViewWidget webViewWidgetDelegate = WebViewPlatform.instance!.createPlatformWebViewWidget(params); PlatformInterface.verify(webViewWidgetDelegate, _token); diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_message.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_message.dart index 8d080452c54a..b37661a045a9 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_message.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_message.dart @@ -2,12 +2,49 @@ // 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'; + /// A message that was sent by JavaScript code running in a [WebView]. -class JavascriptMessage { - /// Constructs a JavaScript message object. - /// - /// The `message` parameter must not be null. - const JavascriptMessage(this.message) : assert(message != null); +/// +/// Platform specific implementations can add additional fields by extending +/// this class and providing a factory method that takes the +/// [JavaScriptMessage] as a parameter. +/// +/// {@tool sample} +/// This example demonstrates how to extend the [JavaScriptMessage] to +/// provide additional platform specific parameters. +/// +/// When extending [JavaScriptMessage] additional parameters should always +/// accept `null` or have a default value to prevent breaking changes. +/// +/// ```dart +/// @immutable +/// class WKWebViewScriptMessage extends JavaScriptMessage { +/// WKWebViewScriptMessage._( +/// JavaScriptMessage javaScriptMessage, +/// this.extraData, +/// ) : super(javaScriptMessage.message); +/// +/// factory WKWebViewScriptMessage.fromJavaScripMessage( +/// JavaScriptMessage javaScripMessage, { +/// String? extraData, +/// }) { +/// return WKWebViewScriptMessage._( +/// javaScriptMessage, +/// extraData: extraData, +/// ); +/// } +/// +/// final String? extraData; +/// } +/// ``` +/// {@end-tool} +@immutable +class JavaScriptMessage { + /// Creates a new JavaScript message object. + const JavaScriptMessage({ + required this.message, + }); /// The contents of the message that was sent by the JavaScript code. final String message; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_mode.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_mode.dart index 53d049175907..bcbebff8bb1a 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_mode.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_mode.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. /// Describes the state of JavaScript support in a given web view. -enum JavascriptMode { +enum JavaScriptMode { /// JavaScript execution is disabled. disabled, diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/load_request_params.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/load_request_params.dart similarity index 94% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/load_request_params.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/load_request_params.dart index a0d1c8821798..ad934d6747b7 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/load_request_params.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/load_request_params.dart @@ -72,8 +72,8 @@ class LoadRequestParams { /// Used by the platform implementation to create a new [LoadRequestParams]. const LoadRequestParams({ required this.uri, - required this.method, - required this.headers, + this.method = LoadRequestMethod.get, + this.headers = const {}, this.body, }); @@ -81,6 +81,8 @@ class LoadRequestParams { final Uri uri; /// HTTP method used to make the request. + /// + /// Defaults to [LoadRequestMethod.get]. final LoadRequestMethod method; /// Headers for the request. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_decision.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/navigation_decision.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_decision.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/navigation_decision.dart diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/navigation_request.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/navigation_request.dart new file mode 100644 index 000000000000..ee3f1f910f9d --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/navigation_request.dart @@ -0,0 +1,18 @@ +// 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. + +/// Defines the parameters of the pending navigation callback. +class NavigationRequest { + /// Creates a [NavigationRequest]. + const NavigationRequest({ + required this.url, + required this.isMainFrame, + }); + + /// The URL of the pending navigation request. + final String url; + + /// Indicates whether the request was made in the web site's main frame or a subframe. + final bool isMainFrame; +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_navigation_delegate_creation_params.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/platform_navigation_delegate_creation_params.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_navigation_delegate_creation_params.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/platform_navigation_delegate_creation_params.dart diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_webview_controller_creation_params.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/platform_webview_controller_creation_params.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_webview_controller_creation_params.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/platform_webview_controller_creation_params.dart diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_webview_cookie_manager_creation_params.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/platform_webview_cookie_manager_creation_params.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_webview_cookie_manager_creation_params.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/platform_webview_cookie_manager_creation_params.dart diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_webview_widget_creation_params.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/platform_webview_widget_creation_params.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/platform_webview_widget_creation_params.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/platform_webview_widget_creation_params.dart diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart index f2bcf19f42fd..4df8800c83e1 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart @@ -2,13 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -export 'auto_media_playback_policy.dart'; -export 'creation_params.dart'; -export 'javascript_channel.dart'; export 'javascript_message.dart'; export 'javascript_mode.dart'; +export 'load_request_params.dart'; +export 'navigation_decision.dart'; +export 'navigation_request.dart'; +export 'platform_navigation_delegate_creation_params.dart'; +export 'platform_webview_controller_creation_params.dart'; +export 'platform_webview_cookie_manager_creation_params.dart'; +export 'platform_webview_widget_creation_params.dart'; export 'web_resource_error.dart'; -export 'web_resource_error_type.dart'; -export 'web_settings.dart'; export 'webview_cookie.dart'; -export 'webview_request.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error.dart index b61671f0ac45..e2522da859f7 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error.dart @@ -2,56 +2,122 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'web_resource_error_type.dart'; +import 'package:flutter/foundation.dart'; + +/// Possible error type categorizations used by [WebResourceError]. +enum WebResourceErrorType { + /// User authentication failed on server. + authentication, + + /// Malformed URL. + badUrl, + + /// Failed to connect to the server. + connect, + + /// Failed to perform SSL handshake. + failedSslHandshake, + + /// Generic file error. + file, + + /// File not found. + fileNotFound, + + /// Server or proxy hostname lookup failed. + hostLookup, + + /// Failed to read or write to the server. + io, + + /// User authentication failed on proxy. + proxyAuthentication, + + /// Too many redirects. + redirectLoop, + + /// Connection timed out. + timeout, + + /// Too many requests during this load. + tooManyRequests, + + /// Generic error. + unknown, + + /// Resource load was canceled by Safe Browsing. + unsafeResource, + + /// Unsupported authentication scheme (not basic or digest). + unsupportedAuthScheme, + + /// Unsupported URI scheme. + unsupportedScheme, + + /// The web content process was terminated. + webContentProcessTerminated, + + /// The web view was invalidated. + webViewInvalidated, + + /// A JavaScript exception occurred. + javaScriptExceptionOccurred, + + /// The result of JavaScript execution could not be returned. + javaScriptResultTypeIsUnsupported, +} /// Error returned in `WebView.onWebResourceError` when a web resource loading error has occurred. +/// +/// Platform specific implementations can add additional fields by extending +/// this class. +/// +/// {@tool sample} +/// This example demonstrates how to extend the [WebResourceError] to +/// provide additional platform specific parameters. +/// +/// When extending [WebResourceError] additional parameters should always +/// accept `null` or have a default value to prevent breaking changes. +/// +/// ```dart +/// class IOSWebResourceError extends WebResourceError { +/// IOSWebResourceError._(WebResourceError error, {required this.domain}) +/// : super( +/// errorCode: error.errorCode, +/// description: error.description, +/// errorType: error.errorType, +/// ); +/// +/// factory IOSWebResourceError.fromWebResourceError( +/// WebResourceError error, { +/// required String? domain, +/// }) { +/// return IOSWebResourceError._(error, domain: domain); +/// } +/// +/// final String? domain; +/// } +/// ``` +/// {@end-tool} +@immutable class WebResourceError { - /// Creates a new [WebResourceError] - /// - /// A user should not need to instantiate this class, but will receive one in - /// [WebResourceErrorCallback]. - WebResourceError({ + /// Used by the platform implementation to create a new [WebResourceError]. + const WebResourceError({ required this.errorCode, required this.description, - this.domain, this.errorType, - this.failingUrl, - }) : assert(errorCode != null), - assert(description != null); + this.isForMainFrame, + }); /// Raw code of the error from the respective platform. - /// - /// On Android, the error code will be a constant from a - /// [WebViewClient](https://developer.android.com/reference/android/webkit/WebViewClient#summary) and - /// will have a corresponding [errorType]. - /// - /// On iOS, the error code will be a constant from `NSError.code` in - /// Objective-C. See - /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html - /// for more information on error handling on iOS. Some possible error codes - /// can be found at https://developer.apple.com/documentation/webkit/wkerrorcode?language=objc. final int errorCode; - /// The domain of where to find the error code. - /// - /// This field is only available on iOS and represents a "domain" from where - /// the [errorCode] is from. This value is taken directly from an `NSError` - /// in Objective-C. See - /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html - /// for more information on error handling on iOS. - final String? domain; - /// Description of the error that can be used to communicate the problem to the user. final String description; /// The type this error can be categorized as. - /// - /// This will never be `null` on Android, but can be `null` on iOS. final WebResourceErrorType? errorType; - /// Gets the URL for which the resource request was made. - /// - /// This value is not provided on iOS. Alternatively, you can keep track of - /// the last values provided to [WebViewPlatformController.loadUrl]. - final String? failingUrl; + /// Whether the error originated from the main frame. + final bool? isForMainFrame; } diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/webview_cookie.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/webview_cookie.dart index 406c510afd4b..7f56a312049f 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/webview_cookie.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/webview_cookie.dart @@ -2,15 +2,18 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -/// A cookie that can be set globally for all web views -/// using [WebViewCookieManagerPlatform]. +import 'package:flutter/foundation.dart'; + +/// A cookie that can be set globally for all web views using [WebViewCookieManagerPlatform]. +@immutable class WebViewCookie { - /// Constructs a new [WebViewCookie]. - const WebViewCookie( - {required this.name, - required this.value, - required this.domain, - this.path = '/'}); + /// Creates a new [WebViewCookieDelegate] + const WebViewCookie({ + required this.name, + required this.value, + required this.domain, + this.path = '/', + }); /// The cookie-name of the cookie. /// @@ -30,20 +33,9 @@ class WebViewCookie { /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-4.1.1 final String domain; - /// The path-value of the cookie. - /// Is set to `/` in the constructor by default. + /// The path-value of the cookie, set to `/` by default. /// /// Its value should match "path-value" in RFC6265bis: /// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-4.1.1 final String path; - - /// Serializes the [WebViewCookie] to a Map. - Map toJson() { - return { - 'name': name, - 'value': value, - 'domain': domain, - 'path': path - }; - } } diff --git a/packages/shared_preferences/shared_preferences_ios/ios/Classes/FLTSharedPreferencesPlugin.h b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/webview_flutter_platform_interface_legacy.dart similarity index 62% rename from packages/shared_preferences/shared_preferences_ios/ios/Classes/FLTSharedPreferencesPlugin.h rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/webview_flutter_platform_interface_legacy.dart index d2d04aee3b64..1964e7089d2d 100644 --- a/packages/shared_preferences/shared_preferences_ios/ios/Classes/FLTSharedPreferencesPlugin.h +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/webview_flutter_platform_interface_legacy.dart @@ -2,7 +2,5 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#import - -@interface FLTSharedPreferencesPlugin : NSObject -@end +export 'legacy/platform_interface/platform_interface.dart'; +export 'legacy/types/types.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/webview_platform.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/webview_platform.dart similarity index 98% rename from packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/webview_platform.dart rename to packages/webview_flutter/webview_flutter_platform_interface/lib/src/webview_platform.dart index c5c5dffc6a22..e91396243ea5 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/webview_platform.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/webview_platform.dart @@ -4,7 +4,7 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'platform_navigation_delegate.dart'; +import '../../src/platform_navigation_delegate.dart'; import 'platform_webview_controller.dart'; import 'platform_webview_cookie_manager.dart'; import 'platform_webview_widget.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/javascript_message.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/javascript_message.dart deleted file mode 100644 index b37661a045a9..000000000000 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/javascript_message.dart +++ /dev/null @@ -1,51 +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 'package:flutter/foundation.dart'; - -/// A message that was sent by JavaScript code running in a [WebView]. -/// -/// Platform specific implementations can add additional fields by extending -/// this class and providing a factory method that takes the -/// [JavaScriptMessage] as a parameter. -/// -/// {@tool sample} -/// This example demonstrates how to extend the [JavaScriptMessage] to -/// provide additional platform specific parameters. -/// -/// When extending [JavaScriptMessage] additional parameters should always -/// accept `null` or have a default value to prevent breaking changes. -/// -/// ```dart -/// @immutable -/// class WKWebViewScriptMessage extends JavaScriptMessage { -/// WKWebViewScriptMessage._( -/// JavaScriptMessage javaScriptMessage, -/// this.extraData, -/// ) : super(javaScriptMessage.message); -/// -/// factory WKWebViewScriptMessage.fromJavaScripMessage( -/// JavaScriptMessage javaScripMessage, { -/// String? extraData, -/// }) { -/// return WKWebViewScriptMessage._( -/// javaScriptMessage, -/// extraData: extraData, -/// ); -/// } -/// -/// final String? extraData; -/// } -/// ``` -/// {@end-tool} -@immutable -class JavaScriptMessage { - /// Creates a new JavaScript message object. - const JavaScriptMessage({ - required this.message, - }); - - /// The contents of the message that was sent by the JavaScript code. - final String message; -} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/web_resource_error.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/web_resource_error.dart deleted file mode 100644 index 465799472912..000000000000 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/src/types/web_resource_error.dart +++ /dev/null @@ -1,119 +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 'package:flutter/foundation.dart'; - -/// Possible error type categorizations used by [WebResourceError]. -enum WebResourceErrorType { - /// User authentication failed on server. - authentication, - - /// Malformed URL. - badUrl, - - /// Failed to connect to the server. - connect, - - /// Failed to perform SSL handshake. - failedSslHandshake, - - /// Generic file error. - file, - - /// File not found. - fileNotFound, - - /// Server or proxy hostname lookup failed. - hostLookup, - - /// Failed to read or write to the server. - io, - - /// User authentication failed on proxy. - proxyAuthentication, - - /// Too many redirects. - redirectLoop, - - /// Connection timed out. - timeout, - - /// Too many requests during this load. - tooManyRequests, - - /// Generic error. - unknown, - - /// Resource load was canceled by Safe Browsing. - unsafeResource, - - /// Unsupported authentication scheme (not basic or digest). - unsupportedAuthScheme, - - /// Unsupported URI scheme. - unsupportedScheme, - - /// The web content process was terminated. - webContentProcessTerminated, - - /// The web view was invalidated. - webViewInvalidated, - - /// A JavaScript exception occurred. - javaScriptExceptionOccurred, - - /// The result of JavaScript execution could not be returned. - javaScriptResultTypeIsUnsupported, -} - -/// Error returned in `WebView.onWebResourceError` when a web resource loading error has occurred. -/// -/// Platform specific implementations can add additional fields by extending -/// this class. -/// -/// {@tool sample} -/// This example demonstrates how to extend the [WebResourceError] to -/// provide additional platform specific parameters. -/// -/// When extending [WebResourceError] additional parameters should always -/// accept `null` or have a default value to prevent breaking changes. -/// -/// ```dart -/// class IOSWebResourceError extends WebResourceError { -/// IOSWebResourceError._(WebResourceError error, {required this.domain}) -/// : super( -/// errorCode: error.errorCode, -/// description: error.description, -/// errorType: error.errorType, -/// ); -/// -/// factory IOSWebResourceError.fromWebResourceError( -/// WebResourceError error, { -/// required String? domain, -/// }) { -/// return IOSWebResourceError._(error, domain: domain); -/// } -/// -/// final String? domain; -/// } -/// ``` -/// {@end-tool} -@immutable -class WebResourceError { - /// Used by the platform implementation to create a new [WebResourceError]. - const WebResourceError({ - required this.errorCode, - required this.description, - this.errorType, - }); - - /// Raw code of the error from the respective platform. - final int errorCode; - - /// Description of the error that can be used to communicate the problem to the user. - final String description; - - /// The type this error can be categorized as. - final WebResourceErrorType? errorType; -} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/webview_flutter_platform_interface.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/webview_flutter_platform_interface.dart deleted file mode 100644 index d14fec163327..000000000000 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/v4/webview_flutter_platform_interface.dart +++ /dev/null @@ -1,10 +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. - -export 'src/platform_navigation_delegate.dart'; -export 'src/platform_webview_controller.dart'; -export 'src/platform_webview_cookie_manager.dart'; -export 'src/platform_webview_widget.dart'; -export 'src/types/types.dart'; -export 'src/webview_platform.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/webview_flutter_platform_interface.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/webview_flutter_platform_interface.dart index aa41c8285975..d14fec163327 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/webview_flutter_platform_interface.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/webview_flutter_platform_interface.dart @@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -export 'src/method_channel/webview_method_channel.dart'; -export 'src/platform_interface/platform_interface.dart'; +export 'src/platform_navigation_delegate.dart'; +export 'src/platform_webview_controller.dart'; +export 'src/platform_webview_cookie_manager.dart'; +export 'src/platform_webview_widget.dart'; export 'src/types/types.dart'; +export 'src/webview_platform.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml index 8f60592b852d..627b6098c302 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml @@ -4,11 +4,11 @@ repository: https://github.com/flutter/plugins/tree/main/packages/webview_flutte issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview_flutter%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: 1.9.5 +version: 2.0.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/legacy/platform_interface/javascript_channel_registry_test.dart similarity index 94% rename from packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart rename to packages/webview_flutter/webview_flutter_platform_interface/test/legacy/platform_interface/javascript_channel_registry_test.dart index aec568e92b79..c9d27c601985 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/legacy/platform_interface/javascript_channel_registry_test.dart @@ -3,8 +3,7 @@ // found in the LICENSE file. import 'package:flutter_test/flutter_test.dart'; -import 'package:webview_flutter_platform_interface/src/platform_interface/javascript_channel_registry.dart'; -import 'package:webview_flutter_platform_interface/src/types/types.dart'; +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; void main() { final Map log = {}; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/webview_cookie_manager_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/legacy/platform_interface/webview_cookie_manager_test.dart similarity index 81% rename from packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/webview_cookie_manager_test.dart rename to packages/webview_flutter/webview_flutter_platform_interface/test/legacy/platform_interface/webview_cookie_manager_test.dart index e0aae2146abc..a9faea52e407 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/webview_cookie_manager_test.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/legacy/platform_interface/webview_cookie_manager_test.dart @@ -3,8 +3,7 @@ // found in the LICENSE file. import 'package:flutter_test/flutter_test.dart'; -import 'package:webview_flutter_platform_interface/src/platform_interface/platform_interface.dart'; -import 'package:webview_flutter_platform_interface/src/types/webview_cookie.dart'; +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; void main() { WebViewCookieManagerPlatform? cookieManager; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/javascript_channel_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/legacy/types/javascript_channel_test.dart similarity index 93% rename from packages/webview_flutter/webview_flutter_platform_interface/test/src/types/javascript_channel_test.dart rename to packages/webview_flutter/webview_flutter_platform_interface/test/legacy/types/javascript_channel_test.dart index 8d7177150b7c..ecb9c3fbed10 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/javascript_channel_test.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/legacy/types/javascript_channel_test.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'package:flutter_test/flutter_test.dart'; -import 'package:webview_flutter_platform_interface/src/types/javascript_channel.dart'; +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; void main() { final List validChars = diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/webview_cookie_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/legacy/types/webview_cookie_test.dart similarity index 87% rename from packages/webview_flutter/webview_flutter_platform_interface/test/src/types/webview_cookie_test.dart rename to packages/webview_flutter/webview_flutter_platform_interface/test/legacy/types/webview_cookie_test.dart index f058b8649b96..f1702f4ad1c0 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/webview_cookie_test.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/legacy/types/webview_cookie_test.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'package:flutter_test/flutter_test.dart'; -import 'package:webview_flutter_platform_interface/src/types/types.dart'; +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; void main() { test('WebViewCookie should serialize correctly', () { diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/webview_request_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/legacy/types/webview_request_test.dart similarity index 93% rename from packages/webview_flutter/webview_flutter_platform_interface/test/src/types/webview_request_test.dart rename to packages/webview_flutter/webview_flutter_platform_interface/test/legacy/types/webview_request_test.dart index 6e1a4d7b4d56..fff1a9b19878 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/webview_request_test.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/legacy/types/webview_request_test.dart @@ -4,7 +4,7 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; -import 'package:webview_flutter_platform_interface/src/types/types.dart'; +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; void main() { test('WebViewRequestMethod should serialize correctly', () { diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/platform_navigation_delegate_test.dart similarity index 95% rename from packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_navigation_delegate_test.dart rename to packages/webview_flutter/webview_flutter_platform_interface/test/platform_navigation_delegate_test.dart index dd4a26c4faf9..5e9aa2e12437 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_navigation_delegate_test.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/platform_navigation_delegate_test.dart @@ -5,8 +5,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:webview_flutter_platform_interface/v4/src/platform_navigation_delegate.dart'; -import 'package:webview_flutter_platform_interface/v4/src/webview_platform.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'webview_platform_test.mocks.dart'; @@ -61,7 +60,7 @@ void main() { expect( () => callbackDelegate.setOnNavigationRequest( - ({required bool isForMainFrame, required String url}) => true), + (NavigationRequest navigationRequest) => NavigationDecision.navigate), throwsUnimplementedError, ); }); diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.dart similarity index 92% rename from packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_controller_test.dart rename to packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.dart index 32374fb04484..6710f34895b7 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.dart @@ -7,9 +7,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:webview_flutter_platform_interface/v4/src/platform_navigation_delegate.dart'; -import 'package:webview_flutter_platform_interface/v4/src/platform_webview_controller.dart'; -import 'package:webview_flutter_platform_interface/v4/src/webview_platform.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'platform_navigation_delegate_test.dart'; import 'webview_platform_test.mocks.dart'; @@ -357,34 +355,6 @@ void main() { ); }); - test( - // ignore: lines_longer_than_80_chars - 'Default implementation of enableDebugging should throw unimplemented error', - () { - final PlatformWebViewController controller = - ExtendsPlatformWebViewController( - const PlatformWebViewControllerCreationParams()); - - expect( - () => controller.enableDebugging(true), - throwsUnimplementedError, - ); - }); - - test( - // ignore: lines_longer_than_80_chars - 'Default implementation of enableGestureNavigation should throw unimplemented error', - () { - final PlatformWebViewController controller = - ExtendsPlatformWebViewController( - const PlatformWebViewControllerCreationParams()); - - expect( - () => controller.enableGestureNavigation(true), - throwsUnimplementedError, - ); - }); - test( // ignore: lines_longer_than_80_chars 'Default implementation of enableZoom should throw unimplemented error', diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.mocks.dart new file mode 100644 index 000000000000..db142fe6a782 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.mocks.dart @@ -0,0 +1,106 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter_platform_interface/test/platform_webview_controller_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i4; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_platform_interface/src/platform_navigation_delegate.dart' + as _i3; +import 'package:webview_flutter_platform_interface/src/webview_platform.dart' + as _i2; + +// 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 + +class _FakePlatformNavigationDelegateCreationParams_0 extends _i1.SmartFake + implements _i2.PlatformNavigationDelegateCreationParams { + _FakePlatformNavigationDelegateCreationParams_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [PlatformNavigationDelegate]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPlatformNavigationDelegate extends _i1.Mock + implements _i3.PlatformNavigationDelegate { + MockPlatformNavigationDelegate() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.PlatformNavigationDelegateCreationParams get params => + (super.noSuchMethod( + Invocation.getter(#params), + returnValue: _FakePlatformNavigationDelegateCreationParams_0( + this, + Invocation.getter(#params), + ), + ) as _i2.PlatformNavigationDelegateCreationParams); + @override + _i4.Future setOnNavigationRequest( + _i3.NavigationRequestCallback? onNavigationRequest) => + (super.noSuchMethod( + Invocation.method( + #setOnNavigationRequest, + [onNavigationRequest], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future setOnPageStarted(_i3.PageEventCallback? onPageStarted) => + (super.noSuchMethod( + Invocation.method( + #setOnPageStarted, + [onPageStarted], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future setOnPageFinished(_i3.PageEventCallback? onPageFinished) => + (super.noSuchMethod( + Invocation.method( + #setOnPageFinished, + [onPageFinished], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future setOnProgress(_i3.ProgressCallback? onProgress) => + (super.noSuchMethod( + Invocation.method( + #setOnProgress, + [onProgress], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future setOnWebResourceError( + _i3.WebResourceErrorCallback? onWebResourceError) => + (super.noSuchMethod( + Invocation.method( + #setOnWebResourceError, + [onWebResourceError], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_widget_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_widget_test.dart similarity index 92% rename from packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_widget_test.dart rename to packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_widget_test.dart index ede16c162413..652f326cf20e 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_widget_test.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_widget_test.dart @@ -6,9 +6,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:webview_flutter_platform_interface/v4/src/platform_webview_controller.dart'; -import 'package:webview_flutter_platform_interface/v4/src/platform_webview_widget.dart'; -import 'package:webview_flutter_platform_interface/v4/src/webview_platform.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'webview_platform_test.mocks.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart deleted file mode 100644 index ea9eb92452ba..000000000000 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart +++ /dev/null @@ -1,746 +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. - -// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316) -// ignore: unnecessary_import -import 'dart:typed_data'; - -// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316) -// ignore: unnecessary_import -import 'package:flutter/cupertino.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('Tests on `plugin.flutter.io/webview_` channel', () { - const int channelId = 1; - const MethodChannel channel = - MethodChannel('plugins.flutter.io/webview_$channelId'); - final WebViewPlatformCallbacksHandler callbacksHandler = - MockWebViewPlatformCallbacksHandler(); - final JavascriptChannelRegistry javascriptChannelRegistry = - MockJavascriptChannelRegistry(); - - final List log = []; - channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - - switch (methodCall.method) { - case 'currentUrl': - return 'https://test.url'; - case 'canGoBack': - case 'canGoForward': - return true; - case 'loadFile': - if (methodCall.arguments == 'invalid file') { - throw PlatformException( - code: 'loadFile_failed', message: 'Failed loading file.'); - } else if (methodCall.arguments == 'some error') { - throw PlatformException( - code: 'some_error', - message: 'Some error occurred.', - ); - } - return null; - case 'loadFlutterAsset': - if (methodCall.arguments == 'invalid key') { - throw PlatformException( - code: 'loadFlutterAsset_invalidKey', - message: 'Failed loading asset.', - ); - } else if (methodCall.arguments == 'some error') { - throw PlatformException( - code: 'some_error', - message: 'Some error occurred.', - ); - } - return null; - case 'runJavascriptReturningResult': - case 'evaluateJavascript': - return methodCall.arguments as String; - case 'getScrollX': - return 10; - case 'getScrollY': - return 20; - } - - // Return null explicitly instead of relying on the implicit null - // returned by the method channel if no return statement is specified. - return null; - }); - - final MethodChannelWebViewPlatform webViewPlatform = - MethodChannelWebViewPlatform( - channelId, - callbacksHandler, - javascriptChannelRegistry, - ); - - tearDown(() { - log.clear(); - }); - - test('loadFile', () async { - await webViewPlatform.loadFile( - '/folder/asset.html', - ); - - expect( - log, - [ - isMethodCall( - 'loadFile', - arguments: '/folder/asset.html', - ), - ], - ); - }); - - test('loadFile with invalid file', () async { - expect( - () => webViewPlatform.loadFile('invalid file'), - throwsA( - isA().having( - (ArgumentError error) => error.message, - 'message', - 'Failed loading file.', - ), - ), - ); - }); - - test('loadFile with some error.', () async { - expect( - () => webViewPlatform.loadFile('some error'), - throwsA( - isA().having( - (PlatformException error) => error.message, - 'message', - 'Some error occurred.', - ), - ), - ); - }); - - test('loadFlutterAsset', () async { - await webViewPlatform.loadFlutterAsset( - 'folder/asset.html', - ); - - expect( - log, - [ - isMethodCall( - 'loadFlutterAsset', - arguments: 'folder/asset.html', - ), - ], - ); - }); - - test('loadFlutterAsset with empty key', () async { - expect(() => webViewPlatform.loadFlutterAsset(''), throwsAssertionError); - }); - - test('loadFlutterAsset with invalid key', () async { - expect( - () => webViewPlatform.loadFlutterAsset('invalid key'), - throwsA( - isA().having( - (ArgumentError error) => error.message, - 'message', - 'Failed loading asset.', - ), - ), - ); - }); - - test('loadFlutterAsset with some error.', () async { - expect( - () => webViewPlatform.loadFlutterAsset('some error'), - throwsA( - isA().having( - (PlatformException error) => error.message, - 'message', - 'Some error occurred.', - ), - ), - ); - }); - - test('loadHtmlString without base URL', () async { - await webViewPlatform.loadHtmlString( - 'Test HTML string', - ); - - expect( - log, - [ - isMethodCall( - 'loadHtmlString', - arguments: { - 'html': 'Test HTML string', - 'baseUrl': null, - }, - ), - ], - ); - }); - - test('loadHtmlString without base URL', () async { - await webViewPlatform.loadHtmlString( - 'Test HTML string', - baseUrl: 'https://flutter.dev', - ); - - expect( - log, - [ - isMethodCall( - 'loadHtmlString', - arguments: { - 'html': 'Test HTML string', - 'baseUrl': 'https://flutter.dev', - }, - ), - ], - ); - }); - - test('loadUrl with headers', () async { - await webViewPlatform.loadUrl( - 'https://test.url', - const { - 'Content-Type': 'text/plain', - 'Accept': 'text/html', - }, - ); - - expect( - log, - [ - isMethodCall( - 'loadUrl', - arguments: { - 'url': 'https://test.url', - 'headers': { - 'Content-Type': 'text/plain', - 'Accept': 'text/html', - }, - }, - ), - ], - ); - }); - - test('loadUrl without headers', () async { - await webViewPlatform.loadUrl( - 'https://test.url', - null, - ); - - expect( - log, - [ - isMethodCall( - 'loadUrl', - arguments: { - 'url': 'https://test.url', - 'headers': null, - }, - ), - ], - ); - }); - - test('loadRequest', () async { - await webViewPlatform.loadRequest(WebViewRequest( - uri: Uri.parse('https://test.url'), - method: WebViewRequestMethod.get, - )); - - expect( - log, - [ - isMethodCall( - 'loadRequest', - arguments: { - 'request': { - 'uri': 'https://test.url', - 'method': 'get', - 'headers': {}, - 'body': null, - } - }, - ), - ], - ); - }); - - test('loadRequest with optional parameters', () async { - await webViewPlatform.loadRequest(WebViewRequest( - uri: Uri.parse('https://test.url'), - method: WebViewRequestMethod.get, - headers: {'foo': 'bar'}, - body: Uint8List.fromList('hello world'.codeUnits), - )); - - expect( - log, - [ - isMethodCall( - 'loadRequest', - arguments: { - 'request': { - 'uri': 'https://test.url', - 'method': 'get', - 'headers': {'foo': 'bar'}, - 'body': 'hello world'.codeUnits, - } - }, - ), - ], - ); - }); - - test('currentUrl', () async { - final String? currentUrl = await webViewPlatform.currentUrl(); - - expect(currentUrl, 'https://test.url'); - expect( - log, - [ - isMethodCall( - 'currentUrl', - arguments: null, - ), - ], - ); - }); - - test('canGoBack', () async { - final bool canGoBack = await webViewPlatform.canGoBack(); - - expect(canGoBack, true); - expect( - log, - [ - isMethodCall( - 'canGoBack', - arguments: null, - ), - ], - ); - }); - - test('canGoForward', () async { - final bool canGoForward = await webViewPlatform.canGoForward(); - - expect(canGoForward, true); - expect( - log, - [ - isMethodCall( - 'canGoForward', - arguments: null, - ), - ], - ); - }); - - test('goBack', () async { - await webViewPlatform.goBack(); - - expect( - log, - [ - isMethodCall( - 'goBack', - arguments: null, - ), - ], - ); - }); - - test('goForward', () async { - await webViewPlatform.goForward(); - - expect( - log, - [ - isMethodCall( - 'goForward', - arguments: null, - ), - ], - ); - }); - - test('reload', () async { - await webViewPlatform.reload(); - - expect( - log, - [ - isMethodCall( - 'reload', - arguments: null, - ), - ], - ); - }); - - test('clearCache', () async { - await webViewPlatform.clearCache(); - - expect( - log, - [ - isMethodCall( - 'clearCache', - arguments: null, - ), - ], - ); - }); - - test('updateSettings', () async { - final WebSettings settings = - WebSettings(userAgent: const WebSetting.of('Dart Test')); - await webViewPlatform.updateSettings(settings); - - expect( - log, - [ - isMethodCall( - 'updateSettings', - arguments: { - 'userAgent': 'Dart Test', - }, - ), - ], - ); - }); - - test('updateSettings all parameters', () async { - final WebSettings settings = WebSettings( - userAgent: const WebSetting.of('Dart Test'), - javascriptMode: JavascriptMode.disabled, - hasNavigationDelegate: true, - hasProgressTracking: true, - debuggingEnabled: true, - gestureNavigationEnabled: true, - allowsInlineMediaPlayback: true, - zoomEnabled: false, - ); - await webViewPlatform.updateSettings(settings); - - expect( - log, - [ - isMethodCall( - 'updateSettings', - arguments: { - 'userAgent': 'Dart Test', - 'jsMode': 0, - 'hasNavigationDelegate': true, - 'hasProgressTracking': true, - 'debuggingEnabled': true, - 'gestureNavigationEnabled': true, - 'allowsInlineMediaPlayback': true, - 'zoomEnabled': false, - }, - ), - ], - ); - }); - - test('updateSettings without settings', () async { - final WebSettings settings = - WebSettings(userAgent: const WebSetting.absent()); - await webViewPlatform.updateSettings(settings); - - expect( - log.isEmpty, - true, - ); - }); - - test('evaluateJavascript', () async { - final String evaluateJavascript = - await webViewPlatform.evaluateJavascript( - 'This simulates some JavaScript code.', - ); - - expect('This simulates some JavaScript code.', evaluateJavascript); - expect( - log, - [ - isMethodCall( - 'evaluateJavascript', - arguments: 'This simulates some JavaScript code.', - ), - ], - ); - }); - - test('runJavascript', () async { - await webViewPlatform.runJavascript( - 'This simulates some JavaScript code.', - ); - - expect( - log, - [ - isMethodCall( - 'runJavascript', - arguments: 'This simulates some JavaScript code.', - ), - ], - ); - }); - - test('runJavascriptReturningResult', () async { - final String evaluateJavascript = - await webViewPlatform.runJavascriptReturningResult( - 'This simulates some JavaScript code.', - ); - - expect('This simulates some JavaScript code.', evaluateJavascript); - expect( - log, - [ - isMethodCall( - 'runJavascriptReturningResult', - arguments: 'This simulates some JavaScript code.', - ), - ], - ); - }); - - test('addJavascriptChannels', () async { - final Set channels = {'channel one', 'channel two'}; - await webViewPlatform.addJavascriptChannels(channels); - - expect(log, [ - isMethodCall( - 'addJavascriptChannels', - arguments: [ - 'channel one', - 'channel two', - ], - ), - ]); - }); - - test('addJavascriptChannels without channels', () async { - final Set channels = {}; - await webViewPlatform.addJavascriptChannels(channels); - - expect(log, [ - isMethodCall( - 'addJavascriptChannels', - arguments: [], - ), - ]); - }); - - test('removeJavascriptChannels', () async { - final Set channels = {'channel one', 'channel two'}; - await webViewPlatform.removeJavascriptChannels(channels); - - expect(log, [ - isMethodCall( - 'removeJavascriptChannels', - arguments: [ - 'channel one', - 'channel two', - ], - ), - ]); - }); - - test('removeJavascriptChannels without channels', () async { - final Set channels = {}; - await webViewPlatform.removeJavascriptChannels(channels); - - expect(log, [ - isMethodCall( - 'removeJavascriptChannels', - arguments: [], - ), - ]); - }); - - test('getTitle', () async { - final String? title = await webViewPlatform.getTitle(); - - expect(title, null); - expect( - log, - [ - isMethodCall('getTitle', arguments: null), - ], - ); - }); - - test('scrollTo', () async { - await webViewPlatform.scrollTo(10, 20); - - expect( - log, - [ - isMethodCall( - 'scrollTo', - arguments: { - 'x': 10, - 'y': 20, - }, - ), - ], - ); - }); - - test('scrollBy', () async { - await webViewPlatform.scrollBy(10, 20); - - expect( - log, - [ - isMethodCall( - 'scrollBy', - arguments: { - 'x': 10, - 'y': 20, - }, - ), - ], - ); - }); - - test('getScrollX', () async { - final int x = await webViewPlatform.getScrollX(); - - expect(x, 10); - expect( - log, - [ - isMethodCall( - 'getScrollX', - arguments: null, - ), - ], - ); - }); - - test('getScrollY', () async { - final int y = await webViewPlatform.getScrollY(); - - expect(y, 20); - expect( - log, - [ - isMethodCall( - 'getScrollY', - arguments: null, - ), - ], - ); - }); - - test('backgroundColor is null by default', () { - final CreationParams creationParams = CreationParams( - webSettings: WebSettings( - userAgent: const WebSetting.of('Dart Test'), - ), - ); - final Map creationParamsMap = - MethodChannelWebViewPlatform.creationParamsToMap(creationParams); - - expect(creationParamsMap['backgroundColor'], null); - }); - - test('backgroundColor is converted to an int', () { - const Color whiteColor = Color(0xFFFFFFFF); - final CreationParams creationParams = CreationParams( - backgroundColor: whiteColor, - webSettings: WebSettings( - userAgent: const WebSetting.of('Dart Test'), - ), - ); - final Map creationParamsMap = - MethodChannelWebViewPlatform.creationParamsToMap(creationParams); - - expect(creationParamsMap['backgroundColor'], whiteColor.value); - }); - }); - - group('Tests on `plugins.flutter.io/cookie_manager` channel', () { - const MethodChannel cookieChannel = - MethodChannel('plugins.flutter.io/cookie_manager'); - - final List log = []; - cookieChannel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - - if (methodCall.method == 'clearCookies') { - return true; - } - - // Return null explicitly instead of relying on the implicit null - // returned by the method channel if no return statement is specified. - return null; - }); - - tearDown(() { - log.clear(); - }); - - test('clearCookies', () async { - final bool clearCookies = - await MethodChannelWebViewPlatform.clearCookies(); - - expect(clearCookies, true); - expect( - log, - [ - isMethodCall( - 'clearCookies', - arguments: null, - ), - ], - ); - }); - - test('setCookie', () async { - await MethodChannelWebViewPlatform.setCookie(const WebViewCookie( - name: 'foo', value: 'bar', domain: 'flutter.dev')); - - expect( - log, - [ - isMethodCall( - 'setCookie', - arguments: { - 'name': 'foo', - 'value': 'bar', - 'domain': 'flutter.dev', - 'path': '/', - }, - ), - ], - ); - }); - }); -} - -class MockWebViewPlatformCallbacksHandler extends Mock - implements WebViewPlatformCallbacksHandler {} - -class MockJavascriptChannelRegistry extends Mock - implements JavascriptChannelRegistry {} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_controller_test.mocks.dart deleted file mode 100644 index 47e67379f124..000000000000 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/platform_webview_controller_test.mocks.dart +++ /dev/null @@ -1,72 +0,0 @@ -// Mocks generated by Mockito 5.0.16 from annotations -// in webview_flutter_platform_interface/test/src/v4/platform_webview_controller_test.dart. -// Do not manually edit this file. - -import 'dart:async' as _i4; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_platform_interface/v4/src/platform_navigation_delegate.dart' - as _i3; -import 'package:webview_flutter_platform_interface/v4/src/webview_platform.dart' - as _i2; - -// 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 - -class _FakePlatformNavigationDelegateCreationParams_0 extends _i1.Fake - implements _i2.PlatformNavigationDelegateCreationParams {} - -/// A class which mocks [PlatformNavigationDelegate]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockPlatformNavigationDelegate extends _i1.Mock - implements _i3.PlatformNavigationDelegate { - MockPlatformNavigationDelegate() { - _i1.throwOnMissingStub(this); - } - - @override - _i2.PlatformNavigationDelegateCreationParams get params => - (super.noSuchMethod(Invocation.getter(#params), - returnValue: _FakePlatformNavigationDelegateCreationParams_0()) - as _i2.PlatformNavigationDelegateCreationParams); - @override - _i4.Future setOnNavigationRequest( - _i4.FutureOr Function({bool isForMainFrame, String url})? - onNavigationRequest) => - (super.noSuchMethod( - Invocation.method(#setOnNavigationRequest, [onNavigationRequest]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future setOnPageStarted(void Function(String)? onPageStarted) => - (super.noSuchMethod(Invocation.method(#setOnPageStarted, [onPageStarted]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future setOnPageFinished(void Function(String)? onPageFinished) => - (super.noSuchMethod( - Invocation.method(#setOnPageFinished, [onPageFinished]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future setOnProgress(void Function(int)? onProgress) => - (super.noSuchMethod(Invocation.method(#setOnProgress, [onProgress]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - _i4.Future setOnWebResourceError( - void Function(_i2.WebResourceError)? onWebResourceError) => - (super.noSuchMethod( - Invocation.method(#setOnWebResourceError, [onWebResourceError]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); - @override - String toString() => super.toString(); -} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/webview_platform_test.mocks.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/webview_platform_test.mocks.dart deleted file mode 100644 index 5ce007579473..000000000000 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/webview_platform_test.mocks.dart +++ /dev/null @@ -1,78 +0,0 @@ -// Mocks generated by Mockito 5.0.16 from annotations -// in webview_flutter_platform_interface/test/src/v4/webview_platform_test.dart. -// Do not manually edit this file. - -import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_platform_interface/v4/src/platform_navigation_delegate.dart' - as _i3; -import 'package:webview_flutter_platform_interface/v4/src/platform_webview_controller.dart' - as _i4; -import 'package:webview_flutter_platform_interface/v4/src/platform_webview_cookie_manager.dart' - as _i2; -import 'package:webview_flutter_platform_interface/v4/src/platform_webview_widget.dart' - as _i5; -import 'package:webview_flutter_platform_interface/v4/src/types/types.dart' - as _i7; -import 'package:webview_flutter_platform_interface/v4/src/webview_platform.dart' - as _i6; - -// 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 - -class _FakePlatformWebViewCookieManager_0 extends _i1.Fake - implements _i2.PlatformWebViewCookieManager {} - -class _FakePlatformNavigationDelegate_1 extends _i1.Fake - implements _i3.PlatformNavigationDelegate {} - -class _FakePlatformWebViewController_2 extends _i1.Fake - implements _i4.PlatformWebViewController {} - -class _FakePlatformWebViewWidget_3 extends _i1.Fake - implements _i5.PlatformWebViewWidget {} - -/// A class which mocks [WebViewPlatform]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockWebViewPlatform extends _i1.Mock implements _i6.WebViewPlatform { - MockWebViewPlatform() { - _i1.throwOnMissingStub(this); - } - - @override - _i2.PlatformWebViewCookieManager createPlatformCookieManager( - _i7.PlatformWebViewCookieManagerCreationParams? params) => - (super.noSuchMethod( - Invocation.method(#createPlatformCookieManager, [params]), - returnValue: _FakePlatformWebViewCookieManager_0()) - as _i2.PlatformWebViewCookieManager); - @override - _i3.PlatformNavigationDelegate createPlatformNavigationDelegate( - _i7.PlatformNavigationDelegateCreationParams? params) => - (super.noSuchMethod( - Invocation.method(#createPlatformNavigationDelegate, [params]), - returnValue: _FakePlatformNavigationDelegate_1()) - as _i3.PlatformNavigationDelegate); - @override - _i4.PlatformWebViewController createPlatformWebViewController( - _i7.PlatformWebViewControllerCreationParams? params) => - (super.noSuchMethod( - Invocation.method(#createPlatformWebViewController, [params]), - returnValue: _FakePlatformWebViewController_2()) - as _i4.PlatformWebViewController); - @override - _i5.PlatformWebViewWidget createPlatformWebViewWidget( - _i7.PlatformWebViewWidgetCreationParams? params) => - (super.noSuchMethod( - Invocation.method(#createPlatformWebViewWidget, [params]), - returnValue: _FakePlatformWebViewWidget_3()) - as _i5.PlatformWebViewWidget); - @override - String toString() => super.toString(); -} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/webview_platform_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/webview_platform_test.dart similarity index 59% rename from packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/webview_platform_test.dart rename to packages/webview_flutter/webview_flutter_platform_interface/test/webview_platform_test.dart index 4ab6d587b879..ec24dd7f5fa2 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/src/v4/webview_platform_test.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/webview_platform_test.dart @@ -6,8 +6,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:webview_flutter_platform_interface/v4/src/platform_webview_controller.dart'; -import 'package:webview_flutter_platform_interface/v4/src/webview_platform.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'webview_platform_test.mocks.dart'; @@ -19,6 +18,69 @@ void main() { expect(WebViewPlatform.instance, isNull); }); + // This test can only run while `WebViewPlatform.instance` is still null. + test( + 'Interface classes throw assertion error when `WebViewPlatform.instance` is null', + () { + expect( + () => PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + ), + throwsA(isA().having( + (AssertionError error) => error.message, + 'message', + 'A platform implementation for `webview_flutter` has not been set. Please ' + 'ensure that an implementation of `WebViewPlatform` has been set to ' + '`WebViewPlatform.instance` before use. For unit testing, ' + '`WebViewPlatform.instance` can be set with your own test implementation.', + )), + ); + + expect( + () => PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ), + throwsA(isA().having( + (AssertionError error) => error.message, + 'message', + 'A platform implementation for `webview_flutter` has not been set. Please ' + 'ensure that an implementation of `WebViewPlatform` has been set to ' + '`WebViewPlatform.instance` before use. For unit testing, ' + '`WebViewPlatform.instance` can be set with your own test implementation.', + )), + ); + + expect( + () => PlatformWebViewCookieManager( + const PlatformWebViewCookieManagerCreationParams(), + ), + throwsA(isA().having( + (AssertionError error) => error.message, + 'message', + 'A platform implementation for `webview_flutter` has not been set. Please ' + 'ensure that an implementation of `WebViewPlatform` has been set to ' + '`WebViewPlatform.instance` before use. For unit testing, ' + '`WebViewPlatform.instance` can be set with your own test implementation.', + )), + ); + + expect( + () => PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams( + controller: MockWebViewControllerDelegate(), + ), + ), + throwsA(isA().having( + (AssertionError error) => error.message, + 'message', + 'A platform implementation for `webview_flutter` has not been set. Please ' + 'ensure that an implementation of `WebViewPlatform` has been set to ' + '`WebViewPlatform.instance` before use. For unit testing, ' + '`WebViewPlatform.instance` can be set with your own test implementation.', + )), + ); + }); + test('Cannot be implemented with `implements`', () { expect(() { WebViewPlatform.instance = ImplementsWebViewPlatform(); diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/webview_platform_test.mocks.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/webview_platform_test.mocks.dart new file mode 100644 index 000000000000..d613cddccd54 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/webview_platform_test.mocks.dart @@ -0,0 +1,146 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter_platform_interface/test/webview_platform_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_platform_interface/src/platform_navigation_delegate.dart' + as _i3; +import 'package:webview_flutter_platform_interface/src/platform_webview_controller.dart' + as _i4; +import 'package:webview_flutter_platform_interface/src/platform_webview_cookie_manager.dart' + as _i2; +import 'package:webview_flutter_platform_interface/src/platform_webview_widget.dart' + as _i5; +import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i7; +import 'package:webview_flutter_platform_interface/src/webview_platform.dart' + as _i6; + +// 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 + +class _FakePlatformWebViewCookieManager_0 extends _i1.SmartFake + implements _i2.PlatformWebViewCookieManager { + _FakePlatformWebViewCookieManager_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlatformNavigationDelegate_1 extends _i1.SmartFake + implements _i3.PlatformNavigationDelegate { + _FakePlatformNavigationDelegate_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlatformWebViewController_2 extends _i1.SmartFake + implements _i4.PlatformWebViewController { + _FakePlatformWebViewController_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakePlatformWebViewWidget_3 extends _i1.SmartFake + implements _i5.PlatformWebViewWidget { + _FakePlatformWebViewWidget_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [WebViewPlatform]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewPlatform extends _i1.Mock implements _i6.WebViewPlatform { + MockWebViewPlatform() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.PlatformWebViewCookieManager createPlatformCookieManager( + _i7.PlatformWebViewCookieManagerCreationParams? params) => + (super.noSuchMethod( + Invocation.method( + #createPlatformCookieManager, + [params], + ), + returnValue: _FakePlatformWebViewCookieManager_0( + this, + Invocation.method( + #createPlatformCookieManager, + [params], + ), + ), + ) as _i2.PlatformWebViewCookieManager); + @override + _i3.PlatformNavigationDelegate createPlatformNavigationDelegate( + _i7.PlatformNavigationDelegateCreationParams? params) => + (super.noSuchMethod( + Invocation.method( + #createPlatformNavigationDelegate, + [params], + ), + returnValue: _FakePlatformNavigationDelegate_1( + this, + Invocation.method( + #createPlatformNavigationDelegate, + [params], + ), + ), + ) as _i3.PlatformNavigationDelegate); + @override + _i4.PlatformWebViewController createPlatformWebViewController( + _i7.PlatformWebViewControllerCreationParams? params) => + (super.noSuchMethod( + Invocation.method( + #createPlatformWebViewController, + [params], + ), + returnValue: _FakePlatformWebViewController_2( + this, + Invocation.method( + #createPlatformWebViewController, + [params], + ), + ), + ) as _i4.PlatformWebViewController); + @override + _i5.PlatformWebViewWidget createPlatformWebViewWidget( + _i7.PlatformWebViewWidgetCreationParams? params) => + (super.noSuchMethod( + Invocation.method( + #createPlatformWebViewWidget, + [params], + ), + returnValue: _FakePlatformWebViewWidget_3( + this, + Invocation.method( + #createPlatformWebViewWidget, + [params], + ), + ), + ) as _i5.PlatformWebViewWidget); +} diff --git a/packages/webview_flutter/webview_flutter_web/CHANGELOG.md b/packages/webview_flutter/webview_flutter_web/CHANGELOG.md index 82be36f7d094..3ada124fe7ce 100644 --- a/packages/webview_flutter/webview_flutter_web/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_web/CHANGELOG.md @@ -1,5 +1,22 @@ -## NEXT +## 0.2.2 +* Updates `WebWebViewController.loadRequest` to only set the src of the iFrame + when `LoadRequestParams.headers` and `LoadRequestParams.body` are empty and is + using the HTTP GET request method. [#118573](https://github.com/flutter/flutter/issues/118573). +* Parses the `content-type` header of XHR responses to extract the correct + MIME-type and charset. [#118090](https://github.com/flutter/flutter/issues/118090). +* Sets `width` and `height` of widget the way the Engine wants, to remove distracting + warnings from the development console. +* Updates minimum Flutter version to 3.0. + +## 0.2.1 + +* Adds auto registration of the `WebViewPlatform` implementation. + +## 0.2.0 + +* **BREAKING CHANGE** Updates platform implementation to `2.0.0` release of + `webview_flutter_platform_interface`. See README for updated usage. * Updates minimum Flutter version to 2.10. ## 0.1.0+4 diff --git a/packages/webview_flutter/webview_flutter_web/README.md b/packages/webview_flutter/webview_flutter_web/README.md index a7711ee171e2..03bb6a89052e 100644 --- a/packages/webview_flutter/webview_flutter_web/README.md +++ b/packages/webview_flutter/webview_flutter_web/README.md @@ -5,10 +5,8 @@ This is an implementation of the [`webview_flutter`](https://pub.dev/packages/we It is currently severely limited and doesn't implement most of the available functionality. The following functionality is currently available: -- `loadUrl` (Without headers) -- `requestUrl` -- `loadHTMLString` (Without `baseUrl`) -- Setting the `initialUrl` through `CreationParams`. +- `loadRequest` +- `loadHtmlString` (Without `baseUrl`) Nothing else is currently supported. @@ -20,59 +18,22 @@ yet, so it currently requires extra setup to use: * [Add this package](https://pub.dev/packages/webview_flutter_web/install) as an explicit dependency of your project, in addition to depending on `webview_flutter`. -* Register `WebWebViewPlatform` as the `WebView.platform` before creating a - `WebView`. See below for examples. -Once those steps below are complete, the APIs from `webview_flutter` listed +Once the step above is complete, the APIs from `webview_flutter` listed above can be used as normal on web. -### Registering the implementation +## Tests -Before creating a `WebView` (for instance, at the start of `main`), you will -need to register the web implementation. +Tests are contained in the `test` directory. You can run all tests from the root +of the package with the following command: -#### Web-only project example - -```dart -... -import 'package:webview_flutter/webview_flutter.dart'; -import 'package:webview_flutter_web/webview_flutter_web.dart'; - -main() { - WebView.platform = WebWebViewPlatform(); - ... -``` - -#### Multi-platform project example - -If your project supports platforms other than web, you will need to use a -conditional import to avoid directly including `webview_flutter_web.dart` on -non-web platforms. For example: - -`register_web_webview.dart`: -```dart -import 'package:webview_flutter/webview_flutter.dart'; -import 'package:webview_flutter_web/webview_flutter_web.dart'; - -void registerWebViewWebImplementation() { - WebView.platform = WebWebViewPlatform(); -} -``` - -`register_web_webview_stub.dart`: -```dart -void registerWebViewWebImplementation() { - // No-op. -} +```bash +$ flutter test --platform chrome ``` -`main.dart`: -```dart -... -import 'register_web_webview_stub.dart' - if (dart.library.html) 'register_web.dart'; +This package uses `package:mockito` in some tests. Mock files can be updated +from the root of the package like so: -main() { - registerWebViewWebImplementation(); - ... +```bash +$ flutter pub run build_runner build --delete-conflicting-outputs ``` diff --git a/packages/webview_flutter/webview_flutter_web/example/integration_test/legacy/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_web/example/integration_test/legacy/webview_flutter_test.dart new file mode 100644 index 000000000000..db27f7ab5d8d --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/example/integration_test/legacy/webview_flutter_test.dart @@ -0,0 +1,72 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:html' as html; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:webview_flutter_web_example/legacy/web_view.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // URLs to navigate to in tests. These need to be URLs that we are confident will + // always be accessible, and won't do redirection. (E.g., just + // 'https://www.google.com/' will sometimes redirect traffic that looks + // like it's coming from a bot, which is true of these tests). + const String primaryUrl = 'https://flutter.dev/'; + const String secondaryUrl = 'https://www.google.com/robots.txt'; + + testWidgets('initialUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + await controllerCompleter.future; + + // Assert an iframe has been rendered to the DOM with the correct src attribute. + final html.IFrameElement? element = + html.document.querySelector('iframe') as html.IFrameElement?; + expect(element, isNotNull); + expect(element!.src, primaryUrl); + }); + + testWidgets('loadUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller.loadUrl(secondaryUrl); + + // Assert an iframe has been rendered to the DOM with the correct src attribute. + final html.IFrameElement? element = + html.document.querySelector('iframe') as html.IFrameElement?; + expect(element, isNotNull); + expect(element!.src, secondaryUrl); + }); +} diff --git a/packages/webview_flutter/webview_flutter_web/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_web/example/integration_test/webview_flutter_test.dart index 232ecdd302b7..f71d2d3c2bac 100644 --- a/packages/webview_flutter/webview_flutter_web/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_web/example/integration_test/webview_flutter_test.dart @@ -2,41 +2,52 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; import 'dart:html' as html; +import 'dart:io'; + +// FIX (dit): Remove these integration tests, or make them run. They currently never fail. +// (They won't run because they use `dart:io`. If you remove all `dart:io` bits from +// this file, they start failing with `fail()`, for example.) import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:webview_flutter_web_example/web_view.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_web/webview_flutter_web.dart'; -void main() { +Future main() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - // URLs to navigate to in tests. These need to be URLs that we are confident will - // always be accessible, and won't do redirection. (E.g., just - // 'https://www.google.com/' will sometimes redirect traffic that looks - // like it's coming from a bot, which is true of these tests). - const String primaryUrl = 'https://flutter.dev/'; - const String secondaryUrl = 'https://www.google.com/robots.txt'; + final HttpServer server = await HttpServer.bind(InternetAddress.anyIPv4, 0); + server.forEach((HttpRequest request) { + if (request.uri.path == '/hello.txt') { + request.response.writeln('Hello, world.'); + } else { + fail('unexpected request: ${request.method} ${request.uri}'); + } + request.response.close(); + }); + final String prefixUrl = 'http://${server.address.address}:${server.port}'; + final String primaryUrl = '$prefixUrl/hello.txt'; + + testWidgets('loadRequest', (WidgetTester tester) async { + final WebWebViewController controller = + WebWebViewController(const PlatformWebViewControllerCreationParams()) + ..loadRequest( + LoadRequestParams(uri: Uri.parse(primaryUrl)), + ); - testWidgets('initialUrl', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - ), + child: Builder(builder: (BuildContext context) { + return WebWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }), ), ); - await controllerCompleter.future; // Assert an iframe has been rendered to the DOM with the correct src attribute. final html.IFrameElement? element = @@ -45,28 +56,31 @@ void main() { expect(element!.src, primaryUrl); }); - testWidgets('loadUrl', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); + testWidgets('loadHtmlString', (WidgetTester tester) async { + final WebWebViewController controller = + WebWebViewController(const PlatformWebViewControllerCreationParams()) + ..loadHtmlString( + 'data:text/html;charset=utf-8,${Uri.encodeFull('test html')}', + ); + await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - ), + child: Builder(builder: (BuildContext context) { + return WebWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }), ), ); - final WebViewController controller = await controllerCompleter.future; - await controller.loadUrl(secondaryUrl); // Assert an iframe has been rendered to the DOM with the correct src attribute. final html.IFrameElement? element = html.document.querySelector('iframe') as html.IFrameElement?; expect(element, isNotNull); - expect(element!.src, secondaryUrl); + expect( + element!.src, + 'data:text/html;charset=utf-8,data:text/html;charset=utf-8,test%2520html', + ); }); } diff --git a/packages/webview_flutter/webview_flutter_web/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_web/example/lib/legacy/web_view.dart similarity index 98% rename from packages/webview_flutter/webview_flutter_web/example/lib/web_view.dart rename to packages/webview_flutter/webview_flutter_web/example/lib/legacy/web_view.dart index ffd3367d33f4..b9b8ce23537b 100644 --- a/packages/webview_flutter/webview_flutter_web/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_web/example/lib/legacy/web_view.dart @@ -5,8 +5,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; -import 'package:webview_flutter_web/webview_flutter_web.dart'; +// ignore: implementation_imports +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; +// ignore: implementation_imports +import 'package:webview_flutter_web/src/webview_flutter_web_legacy.dart'; /// Optional callback invoked when a web view is first created. [controller] is /// the [WebViewController] for the created web view. diff --git a/packages/webview_flutter/webview_flutter_web/example/lib/main.dart b/packages/webview_flutter/webview_flutter_web/example/lib/main.dart index c183625be634..ca268a28e47b 100644 --- a/packages/webview_flutter/webview_flutter_web/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_web/example/lib/main.dart @@ -8,10 +8,10 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; - -import 'web_view.dart'; +import 'package:webview_flutter_web/webview_flutter_web.dart'; void main() { + WebViewPlatform.instance = WebWebViewPlatform(); runApp(const MaterialApp(home: _WebViewExample())); } @@ -23,8 +23,13 @@ class _WebViewExample extends StatefulWidget { } class _WebViewExampleState extends State<_WebViewExample> { - final Completer _controller = - Completer(); + final PlatformWebViewController _controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + )..loadRequest( + LoadRequestParams( + uri: Uri.parse('https://flutter.dev'), + ), + ); @override Widget build(BuildContext context) { @@ -32,15 +37,12 @@ class _WebViewExampleState extends State<_WebViewExample> { appBar: AppBar( title: const Text('Flutter WebView example'), actions: [ - _SampleMenu(_controller.future), + _SampleMenu(_controller), ], ), - body: WebView( - initialUrl: 'https://flutter.dev', - onWebViewCreated: (WebViewController controller) { - _controller.complete(controller); - }, - ), + body: PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: _controller), + ).build(context), ); } } @@ -52,41 +54,37 @@ enum _MenuOptions { class _SampleMenu extends StatelessWidget { const _SampleMenu(this.controller); - final Future controller; + final PlatformWebViewController controller; @override Widget build(BuildContext context) { - return FutureBuilder( - future: controller, - builder: - (BuildContext context, AsyncSnapshot controller) { - return PopupMenuButton<_MenuOptions>( - onSelected: (_MenuOptions value) { - switch (value) { - case _MenuOptions.doPostRequest: - _onDoPostRequest(controller.data!, context); - break; - } - }, - itemBuilder: (BuildContext context) => >[ - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.doPostRequest, - child: Text('Post Request'), - ), - ], - ); + return PopupMenuButton<_MenuOptions>( + onSelected: (_MenuOptions value) { + switch (value) { + case _MenuOptions.doPostRequest: + _onDoPostRequest(controller); + break; + } }, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.doPostRequest, + child: Text('Post Request'), + ), + ], ); } - Future _onDoPostRequest( - WebViewController controller, BuildContext context) async { - final WebViewRequest request = WebViewRequest( + Future _onDoPostRequest(PlatformWebViewController controller) async { + final LoadRequestParams params = LoadRequestParams( uri: Uri.parse('https://httpbin.org/post'), - method: WebViewRequestMethod.post, - headers: {'foo': 'bar', 'Content-Type': 'text/plain'}, + method: LoadRequestMethod.post, + headers: const { + 'foo': 'bar', + 'Content-Type': 'text/plain' + }, body: Uint8List.fromList('Test Body'.codeUnits), ); - await controller.loadRequest(request); + await controller.loadRequest(params); } } diff --git a/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml index e2e0796e7ea3..4685135acdf1 100644 --- a/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml @@ -4,13 +4,14 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" + flutter: ">=3.0.0" dependencies: flutter: sdk: flutter flutter_web_plugins: sdk: flutter - webview_flutter_platform_interface: ^1.8.0 + webview_flutter_platform_interface: ^2.0.0 webview_flutter_web: # When depending on this package from a real application you should use: # webview_flutter_web: ^x.y.z diff --git a/packages/webview_flutter/webview_flutter_web/example/run_test.sh b/packages/webview_flutter/webview_flutter_web/example/run_test.sh new file mode 100755 index 000000000000..aa52974f310e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/example/run_test.sh @@ -0,0 +1,22 @@ +#!/usr/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. + +if pgrep -lf chromedriver > /dev/null; then + echo "chromedriver is running." + + if [ $# -eq 0 ]; then + echo "No target specified, running all tests..." + find integration_test/ -iname *_test.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target='{}' + else + echo "Running test target: $1..." + set -x + flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target=$1 + fi + + else + echo "chromedriver is not running." + echo "Please, check the README.md for instructions on how to use run_test.sh" +fi + diff --git a/packages/webview_flutter/webview_flutter_web/lib/src/content_type.dart b/packages/webview_flutter/webview_flutter_web/lib/src/content_type.dart new file mode 100644 index 000000000000..0aa18ce2318a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/lib/src/content_type.dart @@ -0,0 +1,48 @@ +// 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. + +/// Class to represent a content-type header value. +class ContentType { + /// Creates a [ContentType] instance by parsing a "content-type" response [header]. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type + /// See: https://httpwg.org/specs/rfc9110.html#media.type + ContentType.parse(String header) { + final Iterable chunks = + header.split(';').map((String e) => e.trim().toLowerCase()); + + for (final String chunk in chunks) { + if (!chunk.contains('=')) { + _mimeType = chunk; + } else { + final List bits = + chunk.split('=').map((String e) => e.trim()).toList(); + assert(bits.length == 2); + switch (bits[0]) { + case 'charset': + _charset = bits[1]; + break; + case 'boundary': + _boundary = bits[1]; + break; + default: + throw StateError('Unable to parse "$chunk" in content-type.'); + } + } + } + } + + String? _mimeType; + String? _charset; + String? _boundary; + + /// The MIME-type of the resource or the data. + String? get mimeType => _mimeType; + + /// The character encoding standard. + String? get charset => _charset; + + /// The separation boundary for multipart entities. + String? get boundary => _boundary; +} diff --git a/packages/webview_flutter/webview_flutter_web/lib/src/http_request_factory.dart b/packages/webview_flutter/webview_flutter_web/lib/src/http_request_factory.dart new file mode 100644 index 000000000000..4bd92f0db1db --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/lib/src/http_request_factory.dart @@ -0,0 +1,81 @@ +// 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:html'; + +/// Factory class for creating [HttpRequest] instances. +class HttpRequestFactory { + /// Creates a [HttpRequestFactory]. + const HttpRequestFactory(); + + /// Creates and sends a URL request for the specified [url]. + /// + /// By default `request` will perform an HTTP GET request, but a different + /// method (`POST`, `PUT`, `DELETE`, etc) can be used by specifying the + /// [method] parameter. (See also [HttpRequest.postFormData] for `POST` + /// requests only. + /// + /// The Future is completed when the response is available. + /// + /// If specified, `sendData` will send data in the form of a [ByteBuffer], + /// [Blob], [Document], [String], or [FormData] along with the HttpRequest. + /// + /// If specified, [responseType] sets the desired response format for the + /// request. By default it is [String], but can also be 'arraybuffer', 'blob', + /// 'document', 'json', or 'text'. See also [HttpRequest.responseType] + /// for more information. + /// + /// The [withCredentials] parameter specified that credentials such as a cookie + /// (already) set in the header or + /// [authorization headers](http://tools.ietf.org/html/rfc1945#section-10.2) + /// should be specified for the request. Details to keep in mind when using + /// credentials: + /// + /// /// Using credentials is only useful for cross-origin requests. + /// /// The `Access-Control-Allow-Origin` header of `url` cannot contain a wildcard (///). + /// /// The `Access-Control-Allow-Credentials` header of `url` must be set to true. + /// /// If `Access-Control-Expose-Headers` has not been set to true, only a subset of all the response headers will be returned when calling [getAllResponseHeaders]. + /// + /// The following is equivalent to the [getString] sample above: + /// + /// var name = Uri.encodeQueryComponent('John'); + /// var id = Uri.encodeQueryComponent('42'); + /// HttpRequest.request('users.json?name=$name&id=$id') + /// .then((HttpRequest resp) { + /// // Do something with the response. + /// }); + /// + /// Here's an example of submitting an entire form with [FormData]. + /// + /// var myForm = querySelector('form#myForm'); + /// var data = new FormData(myForm); + /// HttpRequest.request('/submit', method: 'POST', sendData: data) + /// .then((HttpRequest resp) { + /// // Do something with the response. + /// }); + /// + /// Note that requests for file:// URIs are only supported by Chrome extensions + /// with appropriate permissions in their manifest. Requests to file:// URIs + /// will also never fail- the Future will always complete successfully, even + /// when the file cannot be found. + /// + /// See also: [authorization headers](http://en.wikipedia.org/wiki/Basic_access_authentication). + Future request(String url, + {String? method, + bool? withCredentials, + String? responseType, + String? mimeType, + Map? requestHeaders, + dynamic sendData, + void Function(ProgressEvent e)? onProgress}) { + return HttpRequest.request(url, + method: method, + withCredentials: withCredentials, + responseType: responseType, + mimeType: mimeType, + requestHeaders: requestHeaders, + sendData: sendData, + onProgress: onProgress); + } +} diff --git a/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui.dart b/packages/webview_flutter/webview_flutter_web/lib/src/shims/dart_ui.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui.dart rename to packages/webview_flutter/webview_flutter_web/lib/src/shims/dart_ui.dart diff --git a/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui_fake.dart b/packages/webview_flutter/webview_flutter_web/lib/src/shims/dart_ui_fake.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui_fake.dart rename to packages/webview_flutter/webview_flutter_web/lib/src/shims/dart_ui_fake.dart diff --git a/packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui_real.dart b/packages/webview_flutter/webview_flutter_web/lib/src/shims/dart_ui_real.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_web/lib/shims/dart_ui_real.dart rename to packages/webview_flutter/webview_flutter_web/lib/src/shims/dart_ui_real.dart diff --git a/packages/webview_flutter/webview_flutter_web/lib/src/web_webview_controller.dart b/packages/webview_flutter/webview_flutter_web/lib/src/web_webview_controller.dart new file mode 100644 index 000000000000..52f93f911e40 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/lib/src/web_webview_controller.dart @@ -0,0 +1,134 @@ +// 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:convert'; +import 'dart:html' as html; + +import 'package:flutter/cupertino.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'content_type.dart'; +import 'http_request_factory.dart'; +import 'shims/dart_ui.dart' as ui; + +/// An implementation of [PlatformWebViewControllerCreationParams] using Flutter +/// for Web API. +@immutable +class WebWebViewControllerCreationParams + extends PlatformWebViewControllerCreationParams { + /// Creates a new [AndroidWebViewControllerCreationParams] instance. + WebWebViewControllerCreationParams({ + @visibleForTesting this.httpRequestFactory = const HttpRequestFactory(), + }) : super(); + + /// Creates a [WebWebViewControllerCreationParams] instance based on [PlatformWebViewControllerCreationParams]. + WebWebViewControllerCreationParams.fromPlatformWebViewControllerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebViewControllerCreationParams params, { + @visibleForTesting + HttpRequestFactory httpRequestFactory = const HttpRequestFactory(), + }) : this(httpRequestFactory: httpRequestFactory); + + static int _nextIFrameId = 0; + + /// Handles creating and sending URL requests. + final HttpRequestFactory httpRequestFactory; + + /// The underlying element used as the WebView. + @visibleForTesting + final html.IFrameElement iFrame = html.IFrameElement() + ..id = 'webView${_nextIFrameId++}' + ..style.width = '100%' + ..style.height = '100%' + ..style.border = 'none'; +} + +/// An implementation of [PlatformWebViewController] using Flutter for Web API. +class WebWebViewController extends PlatformWebViewController { + /// Constructs a [WebWebViewController]. + WebWebViewController(PlatformWebViewControllerCreationParams params) + : super.implementation(params is WebWebViewControllerCreationParams + ? params + : WebWebViewControllerCreationParams + .fromPlatformWebViewControllerCreationParams(params)); + + WebWebViewControllerCreationParams get _webWebViewParams => + params as WebWebViewControllerCreationParams; + + @override + Future loadHtmlString(String html, {String? baseUrl}) async { + // ignore: unsafe_html + _webWebViewParams.iFrame.src = Uri.dataFromString( + html, + mimeType: 'text/html', + encoding: utf8, + ).toString(); + } + + @override + Future loadRequest(LoadRequestParams params) async { + if (!params.uri.hasScheme) { + throw ArgumentError( + 'LoadRequestParams#uri is required to have a scheme.'); + } + + if (params.headers.isEmpty && + (params.body == null || params.body!.isEmpty) && + params.method == LoadRequestMethod.get) { + // ignore: unsafe_html + _webWebViewParams.iFrame.src = params.uri.toString(); + } else { + await _updateIFrameFromXhr(params); + } + } + + /// Performs an AJAX request defined by [params]. + Future _updateIFrameFromXhr(LoadRequestParams params) async { + final html.HttpRequest httpReq = + await _webWebViewParams.httpRequestFactory.request( + params.uri.toString(), + method: params.method.serialize(), + requestHeaders: params.headers, + sendData: params.body, + ); + + final String header = + httpReq.getResponseHeader('content-type') ?? 'text/html'; + final ContentType contentType = ContentType.parse(header); + final Encoding encoding = Encoding.getByName(contentType.charset) ?? utf8; + + // ignore: unsafe_html + _webWebViewParams.iFrame.src = Uri.dataFromString( + httpReq.responseText ?? '', + mimeType: contentType.mimeType, + encoding: encoding, + ).toString(); + } +} + +/// An implementation of [PlatformWebViewWidget] using Flutter the for Web API. +class WebWebViewWidget extends PlatformWebViewWidget { + /// Constructs a [WebWebViewWidget]. + WebWebViewWidget(PlatformWebViewWidgetCreationParams params) + : super.implementation(params) { + final WebWebViewController controller = + params.controller as WebWebViewController; + ui.platformViewRegistry.registerViewFactory( + controller._webWebViewParams.iFrame.id, + (int viewId) => controller._webWebViewParams.iFrame, + ); + } + + @override + Widget build(BuildContext context) { + return HtmlElementView( + key: params.key, + viewType: (params.controller as WebWebViewController) + ._webWebViewParams + .iFrame + .id, + ); + } +} diff --git a/packages/webview_flutter/webview_flutter_web/lib/src/web_webview_platform.dart b/packages/webview_flutter/webview_flutter_web/lib/src/web_webview_platform.dart new file mode 100644 index 000000000000..a5afc2bc4189 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/lib/src/web_webview_platform.dart @@ -0,0 +1,30 @@ +// 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_web_plugins/flutter_web_plugins.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'web_webview_controller.dart'; + +/// An implementation of [WebViewPlatform] using Flutter for Web API. +class WebWebViewPlatform extends WebViewPlatform { + @override + PlatformWebViewController createPlatformWebViewController( + PlatformWebViewControllerCreationParams params, + ) { + return WebWebViewController(params); + } + + @override + PlatformWebViewWidget createPlatformWebViewWidget( + PlatformWebViewWidgetCreationParams params, + ) { + return WebWebViewWidget(params); + } + + /// Gets called when the plugin is registered. + static void registerWith(Registrar registrar) { + WebViewPlatform.instance = WebWebViewPlatform(); + } +} diff --git a/packages/webview_flutter/webview_flutter_web/lib/src/webview_flutter_web_legacy.dart b/packages/webview_flutter/webview_flutter_web/lib/src/webview_flutter_web_legacy.dart new file mode 100644 index 000000000000..ebf3c799947e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/lib/src/webview_flutter_web_legacy.dart @@ -0,0 +1,220 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:html'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +// ignore: implementation_imports +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; +import 'http_request_factory.dart'; +import 'shims/dart_ui.dart' as ui; + +/// Builds an iframe based WebView. +/// +/// This is used as the default implementation for [WebView.platform] on web. +class WebWebViewPlatform implements WebViewPlatform { + /// Constructs a new instance of [WebWebViewPlatform]. + WebWebViewPlatform() { + ui.platformViewRegistry.registerViewFactory( + 'webview-iframe', + (int viewId) => IFrameElement() + ..id = 'webview-$viewId' + ..width = '100%' + ..height = '100%' + ..style.border = 'none'); + } + + @override + Widget build({ + required BuildContext context, + required CreationParams creationParams, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + required JavascriptChannelRegistry? javascriptChannelRegistry, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + }) { + return HtmlElementView( + viewType: 'webview-iframe', + onPlatformViewCreated: (int viewId) { + if (onWebViewPlatformCreated == null) { + return; + } + final IFrameElement element = + document.getElementById('webview-$viewId')! as IFrameElement; + if (creationParams.initialUrl != null) { + // ignore: unsafe_html + element.src = creationParams.initialUrl; + } + onWebViewPlatformCreated(WebWebViewPlatformController( + element, + )); + }, + ); + } + + @override + Future clearCookies() async => false; + + /// Gets called when the plugin is registered. + static void registerWith(Registrar registrar) {} +} + +/// Implementation of [WebViewPlatformController] for web. +class WebWebViewPlatformController implements WebViewPlatformController { + /// Constructs a [WebWebViewPlatformController]. + WebWebViewPlatformController(this._element); + + final IFrameElement _element; + HttpRequestFactory _httpRequestFactory = const HttpRequestFactory(); + + /// Setter for setting the HttpRequestFactory, for testing purposes. + @visibleForTesting + // ignore: avoid_setters_without_getters + set httpRequestFactory(HttpRequestFactory factory) { + _httpRequestFactory = factory; + } + + @override + Future addJavascriptChannels(Set javascriptChannelNames) { + throw UnimplementedError(); + } + + @override + Future canGoBack() { + throw UnimplementedError(); + } + + @override + Future canGoForward() { + throw UnimplementedError(); + } + + @override + Future clearCache() { + throw UnimplementedError(); + } + + @override + Future currentUrl() { + throw UnimplementedError(); + } + + @override + Future evaluateJavascript(String javascript) { + throw UnimplementedError(); + } + + @override + Future getScrollX() { + throw UnimplementedError(); + } + + @override + Future getScrollY() { + throw UnimplementedError(); + } + + @override + Future getTitle() { + throw UnimplementedError(); + } + + @override + Future goBack() { + throw UnimplementedError(); + } + + @override + Future goForward() { + throw UnimplementedError(); + } + + @override + Future loadUrl(String url, Map? headers) async { + // ignore: unsafe_html + _element.src = url; + } + + @override + Future reload() { + throw UnimplementedError(); + } + + @override + Future removeJavascriptChannels(Set javascriptChannelNames) { + throw UnimplementedError(); + } + + @override + Future runJavascript(String javascript) { + throw UnimplementedError(); + } + + @override + Future runJavascriptReturningResult(String javascript) { + throw UnimplementedError(); + } + + @override + Future scrollBy(int x, int y) { + throw UnimplementedError(); + } + + @override + Future scrollTo(int x, int y) { + throw UnimplementedError(); + } + + @override + Future updateSettings(WebSettings setting) { + throw UnimplementedError(); + } + + @override + Future loadFile(String absoluteFilePath) { + throw UnimplementedError(); + } + + @override + Future loadHtmlString( + String html, { + String? baseUrl, + }) async { + // ignore: unsafe_html + _element.src = Uri.dataFromString( + html, + mimeType: 'text/html', + encoding: utf8, + ).toString(); + } + + @override + Future loadRequest(WebViewRequest request) async { + if (!request.uri.hasScheme) { + throw ArgumentError('WebViewRequest#uri is required to have a scheme.'); + } + final HttpRequest httpReq = await _httpRequestFactory.request( + request.uri.toString(), + method: request.method.serialize(), + requestHeaders: request.headers, + sendData: request.body); + final String contentType = + httpReq.getResponseHeader('content-type') ?? 'text/html'; + // ignore: unsafe_html + _element.src = Uri.dataFromString( + httpReq.responseText ?? '', + mimeType: contentType, + encoding: utf8, + ).toString(); + } + + @override + Future loadFlutterAsset(String key) { + throw UnimplementedError(); + } +} diff --git a/packages/webview_flutter/webview_flutter_web/lib/webview_flutter_web.dart b/packages/webview_flutter/webview_flutter_web/lib/webview_flutter_web.dart index adf6495b8f2a..f11c85e4bf29 100644 --- a/packages/webview_flutter/webview_flutter_web/lib/webview_flutter_web.dart +++ b/packages/webview_flutter/webview_flutter_web/lib/webview_flutter_web.dart @@ -2,290 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; -import 'dart:convert'; -import 'dart:html'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_web_plugins/flutter_web_plugins.dart'; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; -import 'shims/dart_ui.dart' as ui; +library webview_flutter_web; -/// Builds an iframe based WebView. -/// -/// This is used as the default implementation for [WebView.platform] on web. -class WebWebViewPlatform implements WebViewPlatform { - /// Constructs a new instance of [WebWebViewPlatform]. - WebWebViewPlatform() { - ui.platformViewRegistry.registerViewFactory( - 'webview-iframe', - (int viewId) => IFrameElement() - ..id = 'webview-$viewId' - ..width = '100%' - ..height = '100%' - ..style.border = 'none'); - } - - @override - Widget build({ - required BuildContext context, - required CreationParams creationParams, - required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - required JavascriptChannelRegistry? javascriptChannelRegistry, - WebViewPlatformCreatedCallback? onWebViewPlatformCreated, - Set>? gestureRecognizers, - }) { - return HtmlElementView( - viewType: 'webview-iframe', - onPlatformViewCreated: (int viewId) { - if (onWebViewPlatformCreated == null) { - return; - } - final IFrameElement element = - document.getElementById('webview-$viewId')! as IFrameElement; - if (creationParams.initialUrl != null) { - // ignore: unsafe_html - element.src = creationParams.initialUrl; - } - onWebViewPlatformCreated(WebWebViewPlatformController( - element, - )); - }, - ); - } - - @override - Future clearCookies() async => false; - - /// Gets called when the plugin is registered. - static void registerWith(Registrar registrar) {} -} - -/// Implementation of [WebViewPlatformController] for web. -class WebWebViewPlatformController implements WebViewPlatformController { - /// Constructs a [WebWebViewPlatformController]. - WebWebViewPlatformController(this._element); - - final IFrameElement _element; - HttpRequestFactory _httpRequestFactory = HttpRequestFactory(); - - /// Setter for setting the HttpRequestFactory, for testing purposes. - @visibleForTesting - // ignore: avoid_setters_without_getters - set httpRequestFactory(HttpRequestFactory factory) { - _httpRequestFactory = factory; - } - - @override - Future addJavascriptChannels(Set javascriptChannelNames) { - throw UnimplementedError(); - } - - @override - Future canGoBack() { - throw UnimplementedError(); - } - - @override - Future canGoForward() { - throw UnimplementedError(); - } - - @override - Future clearCache() { - throw UnimplementedError(); - } - - @override - Future currentUrl() { - throw UnimplementedError(); - } - - @override - Future evaluateJavascript(String javascript) { - throw UnimplementedError(); - } - - @override - Future getScrollX() { - throw UnimplementedError(); - } - - @override - Future getScrollY() { - throw UnimplementedError(); - } - - @override - Future getTitle() { - throw UnimplementedError(); - } - - @override - Future goBack() { - throw UnimplementedError(); - } - - @override - Future goForward() { - throw UnimplementedError(); - } - - @override - Future loadUrl(String url, Map? headers) async { - // ignore: unsafe_html - _element.src = url; - } - - @override - Future reload() { - throw UnimplementedError(); - } - - @override - Future removeJavascriptChannels(Set javascriptChannelNames) { - throw UnimplementedError(); - } - - @override - Future runJavascript(String javascript) { - throw UnimplementedError(); - } - - @override - Future runJavascriptReturningResult(String javascript) { - throw UnimplementedError(); - } - - @override - Future scrollBy(int x, int y) { - throw UnimplementedError(); - } - - @override - Future scrollTo(int x, int y) { - throw UnimplementedError(); - } - - @override - Future updateSettings(WebSettings setting) { - throw UnimplementedError(); - } - - @override - Future loadFile(String absoluteFilePath) { - throw UnimplementedError(); - } - - @override - Future loadHtmlString( - String html, { - String? baseUrl, - }) async { - // ignore: unsafe_html - _element.src = Uri.dataFromString( - html, - mimeType: 'text/html', - encoding: utf8, - ).toString(); - } - - @override - Future loadRequest(WebViewRequest request) async { - if (!request.uri.hasScheme) { - throw ArgumentError('WebViewRequest#uri is required to have a scheme.'); - } - final HttpRequest httpReq = await _httpRequestFactory.request( - request.uri.toString(), - method: request.method.serialize(), - requestHeaders: request.headers, - sendData: request.body); - final String contentType = - httpReq.getResponseHeader('content-type') ?? 'text/html'; - // ignore: unsafe_html - _element.src = Uri.dataFromString( - httpReq.responseText ?? '', - mimeType: contentType, - encoding: utf8, - ).toString(); - } - - @override - Future loadFlutterAsset(String key) { - throw UnimplementedError(); - } -} - -/// Factory class for creating [HttpRequest] instances. -class HttpRequestFactory { - /// Creates and sends a URL request for the specified [url]. - /// - /// By default `request` will perform an HTTP GET request, but a different - /// method (`POST`, `PUT`, `DELETE`, etc) can be used by specifying the - /// [method] parameter. (See also [HttpRequest.postFormData] for `POST` - /// requests only. - /// - /// The Future is completed when the response is available. - /// - /// If specified, `sendData` will send data in the form of a [ByteBuffer], - /// [Blob], [Document], [String], or [FormData] along with the HttpRequest. - /// - /// If specified, [responseType] sets the desired response format for the - /// request. By default it is [String], but can also be 'arraybuffer', 'blob', - /// 'document', 'json', or 'text'. See also [HttpRequest.responseType] - /// for more information. - /// - /// The [withCredentials] parameter specified that credentials such as a cookie - /// (already) set in the header or - /// [authorization headers](http://tools.ietf.org/html/rfc1945#section-10.2) - /// should be specified for the request. Details to keep in mind when using - /// credentials: - /// - /// /// Using credentials is only useful for cross-origin requests. - /// /// The `Access-Control-Allow-Origin` header of `url` cannot contain a wildcard (///). - /// /// The `Access-Control-Allow-Credentials` header of `url` must be set to true. - /// /// If `Access-Control-Expose-Headers` has not been set to true, only a subset of all the response headers will be returned when calling [getAllResponseHeaders]. - /// - /// The following is equivalent to the [getString] sample above: - /// - /// var name = Uri.encodeQueryComponent('John'); - /// var id = Uri.encodeQueryComponent('42'); - /// HttpRequest.request('users.json?name=$name&id=$id') - /// .then((HttpRequest resp) { - /// // Do something with the response. - /// }); - /// - /// Here's an example of submitting an entire form with [FormData]. - /// - /// var myForm = querySelector('form#myForm'); - /// var data = new FormData(myForm); - /// HttpRequest.request('/submit', method: 'POST', sendData: data) - /// .then((HttpRequest resp) { - /// // Do something with the response. - /// }); - /// - /// Note that requests for file:// URIs are only supported by Chrome extensions - /// with appropriate permissions in their manifest. Requests to file:// URIs - /// will also never fail- the Future will always complete successfully, even - /// when the file cannot be found. - /// - /// See also: [authorization headers](http://en.wikipedia.org/wiki/Basic_access_authentication). - Future request(String url, - {String? method, - bool? withCredentials, - String? responseType, - String? mimeType, - Map? requestHeaders, - dynamic sendData, - void Function(ProgressEvent e)? onProgress}) { - return HttpRequest.request(url, - method: method, - withCredentials: withCredentials, - responseType: responseType, - mimeType: mimeType, - requestHeaders: requestHeaders, - sendData: sendData, - onProgress: onProgress); - } -} +export 'src/http_request_factory.dart'; +export 'src/web_webview_controller.dart'; +export 'src/web_webview_platform.dart'; diff --git a/packages/webview_flutter/webview_flutter_web/pubspec.yaml b/packages/webview_flutter/webview_flutter_web/pubspec.yaml index f27e6408335a..f3ea67d68dad 100644 --- a/packages/webview_flutter/webview_flutter_web/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_web/pubspec.yaml @@ -2,11 +2,11 @@ name: webview_flutter_web description: A Flutter plugin that provides a WebView widget on web. repository: https://github.com/flutter/plugins/tree/main/packages/webview_flutter/webview_flutter_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 0.1.0+4 +version: 0.2.2 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -21,7 +21,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - webview_flutter_platform_interface: ^1.8.0 + webview_flutter_platform_interface: ^2.0.0 dev_dependencies: build_runner: ^2.1.5 diff --git a/packages/webview_flutter/webview_flutter_web/test/content_type_test.dart b/packages/webview_flutter/webview_flutter_web/test/content_type_test.dart new file mode 100644 index 000000000000..936eeae4f571 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/test/content_type_test.dart @@ -0,0 +1,77 @@ +// 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:webview_flutter_web/src/content_type.dart'; + +void main() { + group('ContentType.parse', () { + test('basic content-type (lowers case)', () { + final ContentType contentType = ContentType.parse('text/pLaIn'); + + expect(contentType.mimeType, 'text/plain'); + expect(contentType.boundary, isNull); + expect(contentType.charset, isNull); + }); + + test('with charset', () { + final ContentType contentType = + ContentType.parse('text/pLaIn; charset=utf-8'); + + expect(contentType.mimeType, 'text/plain'); + expect(contentType.boundary, isNull); + expect(contentType.charset, 'utf-8'); + }); + + test('with boundary', () { + final ContentType contentType = + ContentType.parse('text/pLaIn; boundary=---xyz'); + + expect(contentType.mimeType, 'text/plain'); + expect(contentType.boundary, '---xyz'); + expect(contentType.charset, isNull); + }); + + test('with charset and boundary', () { + final ContentType contentType = + ContentType.parse('text/pLaIn; charset=utf-8; boundary=---xyz'); + + expect(contentType.mimeType, 'text/plain'); + expect(contentType.boundary, '---xyz'); + expect(contentType.charset, 'utf-8'); + }); + + test('with boundary and charset', () { + final ContentType contentType = + ContentType.parse('text/pLaIn; boundary=---xyz; charset=utf-8'); + + expect(contentType.mimeType, 'text/plain'); + expect(contentType.boundary, '---xyz'); + expect(contentType.charset, 'utf-8'); + }); + + test('with a bunch of whitespace, boundary and charset', () { + final ContentType contentType = ContentType.parse( + ' text/pLaIn ; boundary=---xyz; charset=utf-8 '); + + expect(contentType.mimeType, 'text/plain'); + expect(contentType.boundary, '---xyz'); + expect(contentType.charset, 'utf-8'); + }); + + test('empty string', () { + final ContentType contentType = ContentType.parse(''); + + expect(contentType.mimeType, ''); + expect(contentType.boundary, isNull); + expect(contentType.charset, isNull); + }); + + test('unknown parameter (throws)', () { + expect(() { + ContentType.parse('text/pLaIn; wrong=utf-8'); + }, throwsStateError); + }); + }); +} diff --git a/packages/webview_flutter/webview_flutter_web/test/legacy/webview_flutter_web_test.dart b/packages/webview_flutter/webview_flutter_web/test/legacy/webview_flutter_web_test.dart new file mode 100644 index 000000000000..54e53bb11925 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/test/legacy/webview_flutter_web_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 'dart:html'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; +import 'package:webview_flutter_web/src/http_request_factory.dart'; +import 'package:webview_flutter_web/src/webview_flutter_web_legacy.dart'; + +import 'webview_flutter_web_test.mocks.dart'; + +@GenerateMocks([ + IFrameElement, + BuildContext, + CreationParams, + WebViewPlatformCallbacksHandler, + HttpRequestFactory, + HttpRequest, +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('WebWebViewPlatform', () { + test('build returns a HtmlElementView', () { + // Setup + final WebWebViewPlatform platform = WebWebViewPlatform(); + // Run + final Widget widget = platform.build( + context: MockBuildContext(), + creationParams: CreationParams(), + webViewPlatformCallbacksHandler: MockWebViewPlatformCallbacksHandler(), + javascriptChannelRegistry: null, + ); + // Verify + expect(widget, isA()); + }); + }); + + group('WebWebViewPlatformController', () { + test('loadUrl sets url on iframe src attribute', () { + // Setup + final MockIFrameElement mockElement = MockIFrameElement(); + final WebWebViewPlatformController controller = + WebWebViewPlatformController( + mockElement, + ); + // Run + controller.loadUrl('test url', null); + // Verify + verify(mockElement.src = 'test url'); + }); + + group('loadHtmlString', () { + test('loadHtmlString loads html into iframe', () { + // Setup + final MockIFrameElement mockElement = MockIFrameElement(); + final WebWebViewPlatformController controller = + WebWebViewPlatformController( + mockElement, + ); + // Run + controller.loadHtmlString('test html'); + // Verify + verify(mockElement.src = + 'data:text/html;charset=utf-8,${Uri.encodeFull('test html')}'); + }); + + test('loadHtmlString escapes "#" correctly', () { + // Setup + final MockIFrameElement mockElement = MockIFrameElement(); + final WebWebViewPlatformController controller = + WebWebViewPlatformController( + mockElement, + ); + // Run + controller.loadHtmlString('#'); + // Verify + verify(mockElement.src = argThat(contains('%23'))); + }); + }); + + group('loadRequest', () { + test('loadRequest throws ArgumentError on missing scheme', () { + // Setup + final MockIFrameElement mockElement = MockIFrameElement(); + final WebWebViewPlatformController controller = + WebWebViewPlatformController( + mockElement, + ); + // Run & Verify + expect( + () async => controller.loadRequest( + WebViewRequest( + uri: Uri.parse('flutter.dev'), + method: WebViewRequestMethod.get, + ), + ), + throwsA(const TypeMatcher())); + }); + + test('loadRequest makes request and loads response into iframe', + () async { + // Setup + final MockIFrameElement mockElement = MockIFrameElement(); + final WebWebViewPlatformController controller = + WebWebViewPlatformController( + mockElement, + ); + final MockHttpRequest mockHttpRequest = MockHttpRequest(); + when(mockHttpRequest.getResponseHeader('content-type')) + .thenReturn('text/plain'); + when(mockHttpRequest.responseText).thenReturn('test data'); + final MockHttpRequestFactory mockHttpRequestFactory = + MockHttpRequestFactory(); + when(mockHttpRequestFactory.request( + any, + method: anyNamed('method'), + requestHeaders: anyNamed('requestHeaders'), + sendData: anyNamed('sendData'), + )).thenAnswer((_) => Future.value(mockHttpRequest)); + controller.httpRequestFactory = mockHttpRequestFactory; + // Run + await controller.loadRequest( + WebViewRequest( + uri: Uri.parse('https://flutter.dev'), + method: WebViewRequestMethod.post, + body: Uint8List.fromList('test body'.codeUnits), + headers: {'Foo': 'Bar'}), + ); + // Verify + verify(mockHttpRequestFactory.request( + 'https://flutter.dev', + method: 'post', + requestHeaders: {'Foo': 'Bar'}, + sendData: Uint8List.fromList('test body'.codeUnits), + )); + verify(mockElement.src = + 'data:;charset=utf-8,${Uri.encodeFull('test data')}'); + }); + + test('loadRequest escapes "#" correctly', () async { + // Setup + final MockIFrameElement mockElement = MockIFrameElement(); + final WebWebViewPlatformController controller = + WebWebViewPlatformController( + mockElement, + ); + final MockHttpRequest mockHttpRequest = MockHttpRequest(); + when(mockHttpRequest.getResponseHeader('content-type')) + .thenReturn('text/html'); + when(mockHttpRequest.responseText).thenReturn('#'); + final MockHttpRequestFactory mockHttpRequestFactory = + MockHttpRequestFactory(); + when(mockHttpRequestFactory.request( + any, + method: anyNamed('method'), + requestHeaders: anyNamed('requestHeaders'), + sendData: anyNamed('sendData'), + )).thenAnswer((_) => Future.value(mockHttpRequest)); + controller.httpRequestFactory = mockHttpRequestFactory; + // Run + await controller.loadRequest( + WebViewRequest( + uri: Uri.parse('https://flutter.dev'), + method: WebViewRequestMethod.post, + body: Uint8List.fromList('test body'.codeUnits), + headers: {'Foo': 'Bar'}), + ); + // Verify + verify(mockElement.src = argThat(contains('%23'))); + }); + }); + }); +} diff --git a/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.mocks.dart b/packages/webview_flutter/webview_flutter_web/test/legacy/webview_flutter_web_test.mocks.dart similarity index 99% rename from packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.mocks.dart rename to packages/webview_flutter/webview_flutter_web/test/legacy/webview_flutter_web_test.mocks.dart index db442eeea7a3..ac7122eacb63 100644 --- a/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_web/test/legacy/webview_flutter_web_test.mocks.dart @@ -1,5 +1,5 @@ // Mocks generated by Mockito 5.3.2 from annotations -// in webview_flutter_web/test/webview_flutter_web_test.dart. +// in webview_flutter_web/test/legacy/webview_flutter_web_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes @@ -11,10 +11,11 @@ import 'package:flutter/foundation.dart' as _i5; import 'package:flutter/src/widgets/notification_listener.dart' as _i7; import 'package:flutter/widgets.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_platform_interface/src/types/types.dart' as _i8; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' +import 'package:webview_flutter_platform_interface/src/legacy/platform_interface/webview_platform_callbacks_handler.dart' as _i9; -import 'package:webview_flutter_web/webview_flutter_web.dart' as _i10; +import 'package:webview_flutter_platform_interface/src/legacy/types/types.dart' + as _i8; +import 'package:webview_flutter_web/src/http_request_factory.dart' as _i10; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -2117,11 +2118,6 @@ class MockBuildContext extends _i1.Mock implements _i4.BuildContext { ), ) as _i4.Widget); @override - bool get mounted => (super.noSuchMethod( - Invocation.getter(#mounted), - returnValue: false, - ) as bool); - @override bool get debugDoingBuild => (super.noSuchMethod( Invocation.getter(#debugDoingBuild), returnValue: false, diff --git a/packages/webview_flutter/webview_flutter_web/test/web_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_web/test/web_webview_controller_test.dart new file mode 100644 index 000000000000..0a995cbb67e0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/test/web_webview_controller_test.dart @@ -0,0 +1,210 @@ +// 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:convert'; +import 'dart:html'; +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231) +// ignore: unnecessary_import +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_web/webview_flutter_web.dart'; + +import 'web_webview_controller_test.mocks.dart'; + +@GenerateMocks([], customMocks: >[ + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), +]) +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + group('WebWebViewController', () { + group('WebWebViewControllerCreationParams', () { + test('sets iFrame fields', () { + final WebWebViewControllerCreationParams params = + WebWebViewControllerCreationParams(); + + expect(params.iFrame.id, contains('webView')); + expect(params.iFrame.style.width, '100%'); + expect(params.iFrame.style.height, '100%'); + expect(params.iFrame.style.border, 'none'); + }); + }); + + group('loadHtmlString', () { + test('loadHtmlString loads html into iframe', () async { + final WebWebViewController controller = + WebWebViewController(WebWebViewControllerCreationParams()); + + await controller.loadHtmlString('test html'); + expect( + (controller.params as WebWebViewControllerCreationParams).iFrame.src, + 'data:text/html;charset=utf-8,${Uri.encodeFull('test html')}', + ); + }); + + test('loadHtmlString escapes "#" correctly', () async { + final WebWebViewController controller = + WebWebViewController(WebWebViewControllerCreationParams()); + + await controller.loadHtmlString('#'); + expect( + (controller.params as WebWebViewControllerCreationParams).iFrame.src, + contains('%23'), + ); + }); + }); + + group('loadRequest', () { + test('throws ArgumentError on missing scheme', () async { + final WebWebViewController controller = + WebWebViewController(WebWebViewControllerCreationParams()); + + await expectLater( + () async => controller.loadRequest( + LoadRequestParams(uri: Uri.parse('flutter.dev')), + ), + throwsA(const TypeMatcher())); + }); + + test('skips XHR for simple GETs (no headers, no data)', () async { + final MockHttpRequestFactory mockHttpRequestFactory = + MockHttpRequestFactory(); + final WebWebViewController controller = + WebWebViewController(WebWebViewControllerCreationParams( + httpRequestFactory: mockHttpRequestFactory, + )); + + when(mockHttpRequestFactory.request( + any, + method: anyNamed('method'), + requestHeaders: anyNamed('requestHeaders'), + sendData: anyNamed('sendData'), + )).thenThrow( + StateError('The `request` method should not have been called.')); + + await controller.loadRequest(LoadRequestParams( + uri: Uri.parse('https://flutter.dev'), + )); + + expect( + (controller.params as WebWebViewControllerCreationParams).iFrame.src, + 'https://flutter.dev/', + ); + }); + + test('makes request and loads response into iframe', () async { + final MockHttpRequestFactory mockHttpRequestFactory = + MockHttpRequestFactory(); + final WebWebViewController controller = + WebWebViewController(WebWebViewControllerCreationParams( + httpRequestFactory: mockHttpRequestFactory, + )); + + final MockHttpRequest mockHttpRequest = MockHttpRequest(); + when(mockHttpRequest.getResponseHeader('content-type')) + .thenReturn('text/plain'); + when(mockHttpRequest.responseText).thenReturn('test data'); + + when(mockHttpRequestFactory.request( + any, + method: anyNamed('method'), + requestHeaders: anyNamed('requestHeaders'), + sendData: anyNamed('sendData'), + )).thenAnswer((_) => Future.value(mockHttpRequest)); + + await controller.loadRequest(LoadRequestParams( + uri: Uri.parse('https://flutter.dev'), + method: LoadRequestMethod.post, + body: Uint8List.fromList('test body'.codeUnits), + headers: const {'Foo': 'Bar'}, + )); + + verify(mockHttpRequestFactory.request( + 'https://flutter.dev', + method: 'post', + requestHeaders: {'Foo': 'Bar'}, + sendData: Uint8List.fromList('test body'.codeUnits), + )); + + expect( + (controller.params as WebWebViewControllerCreationParams).iFrame.src, + 'data:;charset=utf-8,${Uri.encodeFull('test data')}', + ); + }); + + test('parses content-type response header correctly', () async { + final MockHttpRequestFactory mockHttpRequestFactory = + MockHttpRequestFactory(); + final WebWebViewController controller = + WebWebViewController(WebWebViewControllerCreationParams( + httpRequestFactory: mockHttpRequestFactory, + )); + + final Encoding iso = Encoding.getByName('latin1')!; + + final MockHttpRequest mockHttpRequest = MockHttpRequest(); + when(mockHttpRequest.responseText) + .thenReturn(String.fromCharCodes(iso.encode('España'))); + when(mockHttpRequest.getResponseHeader('content-type')) + .thenReturn('Text/HTmL; charset=latin1'); + + when(mockHttpRequestFactory.request( + any, + method: anyNamed('method'), + requestHeaders: anyNamed('requestHeaders'), + sendData: anyNamed('sendData'), + )).thenAnswer((_) => Future.value(mockHttpRequest)); + + await controller.loadRequest(LoadRequestParams( + uri: Uri.parse('https://flutter.dev'), + method: LoadRequestMethod.post, + )); + + expect( + (controller.params as WebWebViewControllerCreationParams).iFrame.src, + 'data:text/html;charset=iso-8859-1,Espa%F1a', + ); + }); + + test('escapes "#" correctly', () async { + final MockHttpRequestFactory mockHttpRequestFactory = + MockHttpRequestFactory(); + final WebWebViewController controller = + WebWebViewController(WebWebViewControllerCreationParams( + httpRequestFactory: mockHttpRequestFactory, + )); + + final MockHttpRequest mockHttpRequest = MockHttpRequest(); + when(mockHttpRequest.getResponseHeader('content-type')) + .thenReturn('text/html'); + when(mockHttpRequest.responseText).thenReturn('#'); + when(mockHttpRequestFactory.request( + any, + method: anyNamed('method'), + requestHeaders: anyNamed('requestHeaders'), + sendData: anyNamed('sendData'), + )).thenAnswer((_) => Future.value(mockHttpRequest)); + + await controller.loadRequest(LoadRequestParams( + uri: Uri.parse('https://flutter.dev'), + method: LoadRequestMethod.post, + body: Uint8List.fromList('test body'.codeUnits), + headers: const {'Foo': 'Bar'}, + )); + + expect( + (controller.params as WebWebViewControllerCreationParams).iFrame.src, + contains('%23'), + ); + }); + }); + }); +} diff --git a/packages/webview_flutter/webview_flutter_web/test/web_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_web/test/web_webview_controller_test.mocks.dart new file mode 100644 index 000000000000..5cb259a3f01a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/test/web_webview_controller_test.mocks.dart @@ -0,0 +1,360 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter_web/test/web_webview_controller_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; +import 'dart:html' as _i2; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_web/src/http_request_factory.dart' as _i4; + +// 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 + +class _FakeHttpRequestUpload_0 extends _i1.SmartFake + implements _i2.HttpRequestUpload { + _FakeHttpRequestUpload_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeEvents_1 extends _i1.SmartFake implements _i2.Events { + _FakeEvents_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeHttpRequest_2 extends _i1.SmartFake implements _i2.HttpRequest { + _FakeHttpRequest_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [HttpRequest]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHttpRequest extends _i1.Mock implements _i2.HttpRequest { + @override + Map get responseHeaders => (super.noSuchMethod( + Invocation.getter(#responseHeaders), + returnValue: {}, + returnValueForMissingStub: {}, + ) as Map); + @override + int get readyState => (super.noSuchMethod( + Invocation.getter(#readyState), + returnValue: 0, + returnValueForMissingStub: 0, + ) as int); + @override + String get responseType => (super.noSuchMethod( + Invocation.getter(#responseType), + returnValue: '', + returnValueForMissingStub: '', + ) as String); + @override + set responseType(String? value) => super.noSuchMethod( + Invocation.setter( + #responseType, + value, + ), + returnValueForMissingStub: null, + ); + @override + set timeout(int? value) => super.noSuchMethod( + Invocation.setter( + #timeout, + value, + ), + returnValueForMissingStub: null, + ); + @override + _i2.HttpRequestUpload get upload => (super.noSuchMethod( + Invocation.getter(#upload), + returnValue: _FakeHttpRequestUpload_0( + this, + Invocation.getter(#upload), + ), + returnValueForMissingStub: _FakeHttpRequestUpload_0( + this, + Invocation.getter(#upload), + ), + ) as _i2.HttpRequestUpload); + @override + set withCredentials(bool? value) => super.noSuchMethod( + Invocation.setter( + #withCredentials, + value, + ), + returnValueForMissingStub: null, + ); + @override + _i3.Stream<_i2.Event> get onReadyStateChange => (super.noSuchMethod( + Invocation.getter(#onReadyStateChange), + returnValue: _i3.Stream<_i2.Event>.empty(), + returnValueForMissingStub: _i3.Stream<_i2.Event>.empty(), + ) as _i3.Stream<_i2.Event>); + @override + _i3.Stream<_i2.ProgressEvent> get onAbort => (super.noSuchMethod( + Invocation.getter(#onAbort), + returnValue: _i3.Stream<_i2.ProgressEvent>.empty(), + returnValueForMissingStub: _i3.Stream<_i2.ProgressEvent>.empty(), + ) as _i3.Stream<_i2.ProgressEvent>); + @override + _i3.Stream<_i2.ProgressEvent> get onError => (super.noSuchMethod( + Invocation.getter(#onError), + returnValue: _i3.Stream<_i2.ProgressEvent>.empty(), + returnValueForMissingStub: _i3.Stream<_i2.ProgressEvent>.empty(), + ) as _i3.Stream<_i2.ProgressEvent>); + @override + _i3.Stream<_i2.ProgressEvent> get onLoad => (super.noSuchMethod( + Invocation.getter(#onLoad), + returnValue: _i3.Stream<_i2.ProgressEvent>.empty(), + returnValueForMissingStub: _i3.Stream<_i2.ProgressEvent>.empty(), + ) as _i3.Stream<_i2.ProgressEvent>); + @override + _i3.Stream<_i2.ProgressEvent> get onLoadEnd => (super.noSuchMethod( + Invocation.getter(#onLoadEnd), + returnValue: _i3.Stream<_i2.ProgressEvent>.empty(), + returnValueForMissingStub: _i3.Stream<_i2.ProgressEvent>.empty(), + ) as _i3.Stream<_i2.ProgressEvent>); + @override + _i3.Stream<_i2.ProgressEvent> get onLoadStart => (super.noSuchMethod( + Invocation.getter(#onLoadStart), + returnValue: _i3.Stream<_i2.ProgressEvent>.empty(), + returnValueForMissingStub: _i3.Stream<_i2.ProgressEvent>.empty(), + ) as _i3.Stream<_i2.ProgressEvent>); + @override + _i3.Stream<_i2.ProgressEvent> get onProgress => (super.noSuchMethod( + Invocation.getter(#onProgress), + returnValue: _i3.Stream<_i2.ProgressEvent>.empty(), + returnValueForMissingStub: _i3.Stream<_i2.ProgressEvent>.empty(), + ) as _i3.Stream<_i2.ProgressEvent>); + @override + _i3.Stream<_i2.ProgressEvent> get onTimeout => (super.noSuchMethod( + Invocation.getter(#onTimeout), + returnValue: _i3.Stream<_i2.ProgressEvent>.empty(), + returnValueForMissingStub: _i3.Stream<_i2.ProgressEvent>.empty(), + ) as _i3.Stream<_i2.ProgressEvent>); + @override + _i2.Events get on => (super.noSuchMethod( + Invocation.getter(#on), + returnValue: _FakeEvents_1( + this, + Invocation.getter(#on), + ), + returnValueForMissingStub: _FakeEvents_1( + this, + Invocation.getter(#on), + ), + ) as _i2.Events); + @override + void open( + String? method, + String? url, { + bool? async, + String? user, + String? password, + }) => + super.noSuchMethod( + Invocation.method( + #open, + [ + method, + url, + ], + { + #async: async, + #user: user, + #password: password, + }, + ), + returnValueForMissingStub: null, + ); + @override + void abort() => super.noSuchMethod( + Invocation.method( + #abort, + [], + ), + returnValueForMissingStub: null, + ); + @override + String getAllResponseHeaders() => (super.noSuchMethod( + Invocation.method( + #getAllResponseHeaders, + [], + ), + returnValue: '', + returnValueForMissingStub: '', + ) as String); + @override + String? getResponseHeader(String? name) => (super.noSuchMethod( + Invocation.method( + #getResponseHeader, + [name], + ), + returnValueForMissingStub: null, + ) as String?); + @override + void overrideMimeType(String? mime) => super.noSuchMethod( + Invocation.method( + #overrideMimeType, + [mime], + ), + returnValueForMissingStub: null, + ); + @override + void send([dynamic body_OR_data]) => super.noSuchMethod( + Invocation.method( + #send, + [body_OR_data], + ), + returnValueForMissingStub: null, + ); + @override + void setRequestHeader( + String? name, + String? value, + ) => + super.noSuchMethod( + Invocation.method( + #setRequestHeader, + [ + name, + value, + ], + ), + returnValueForMissingStub: null, + ); + @override + void addEventListener( + String? type, + _i2.EventListener? listener, [ + bool? useCapture, + ]) => + super.noSuchMethod( + Invocation.method( + #addEventListener, + [ + type, + listener, + useCapture, + ], + ), + returnValueForMissingStub: null, + ); + @override + void removeEventListener( + String? type, + _i2.EventListener? listener, [ + bool? useCapture, + ]) => + super.noSuchMethod( + Invocation.method( + #removeEventListener, + [ + type, + listener, + useCapture, + ], + ), + returnValueForMissingStub: null, + ); + @override + bool dispatchEvent(_i2.Event? event) => (super.noSuchMethod( + Invocation.method( + #dispatchEvent, + [event], + ), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); +} + +/// A class which mocks [HttpRequestFactory]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockHttpRequestFactory extends _i1.Mock + implements _i4.HttpRequestFactory { + @override + _i3.Future<_i2.HttpRequest> request( + String? url, { + String? method, + bool? withCredentials, + String? responseType, + String? mimeType, + Map? requestHeaders, + dynamic sendData, + void Function(_i2.ProgressEvent)? onProgress, + }) => + (super.noSuchMethod( + Invocation.method( + #request, + [url], + { + #method: method, + #withCredentials: withCredentials, + #responseType: responseType, + #mimeType: mimeType, + #requestHeaders: requestHeaders, + #sendData: sendData, + #onProgress: onProgress, + }, + ), + returnValue: _i3.Future<_i2.HttpRequest>.value(_FakeHttpRequest_2( + this, + Invocation.method( + #request, + [url], + { + #method: method, + #withCredentials: withCredentials, + #responseType: responseType, + #mimeType: mimeType, + #requestHeaders: requestHeaders, + #sendData: sendData, + #onProgress: onProgress, + }, + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.HttpRequest>.value(_FakeHttpRequest_2( + this, + Invocation.method( + #request, + [url], + { + #method: method, + #withCredentials: withCredentials, + #responseType: responseType, + #mimeType: mimeType, + #requestHeaders: requestHeaders, + #sendData: sendData, + #onProgress: onProgress, + }, + ), + )), + ) as _i3.Future<_i2.HttpRequest>); +} diff --git a/packages/webview_flutter/webview_flutter_web/test/web_webview_widget_test.dart b/packages/webview_flutter/webview_flutter_web/test/web_webview_widget_test.dart new file mode 100644 index 000000000000..834d95f3ca20 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/test/web_webview_widget_test.dart @@ -0,0 +1,33 @@ +// 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:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_web/webview_flutter_web.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('WebWebViewWidget', () { + testWidgets('build returns a HtmlElementView', (WidgetTester tester) async { + final WebWebViewController controller = + WebWebViewController(WebWebViewControllerCreationParams()); + + final WebWebViewWidget widget = WebWebViewWidget( + PlatformWebViewWidgetCreationParams( + key: const Key('keyValue'), + controller: controller, + ), + ); + + await tester.pumpWidget( + Builder(builder: (BuildContext context) => widget.build(context)), + ); + + expect(find.byType(HtmlElementView), findsOneWidget); + expect(find.byKey(const Key('keyValue')), findsOneWidget); + }); + }); +} diff --git a/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.dart b/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.dart index 08337e42e661..dbfaf22faa54 100644 --- a/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.dart +++ b/packages/webview_flutter/webview_flutter_web/test/webview_flutter_web_test.dart @@ -2,177 +2,16 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:html'; -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'package:webview_flutter_web/webview_flutter_web.dart'; -import './webview_flutter_web_test.mocks.dart'; -@GenerateMocks([ - IFrameElement, - BuildContext, - CreationParams, - WebViewPlatformCallbacksHandler, - HttpRequestFactory, - HttpRequest, -]) void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - group('WebWebViewPlatform', () { - test('build returns a HtmlElementView', () { - // Setup - final WebWebViewPlatform platform = WebWebViewPlatform(); - // Run - final Widget widget = platform.build( - context: MockBuildContext(), - creationParams: CreationParams(), - webViewPlatformCallbacksHandler: MockWebViewPlatformCallbacksHandler(), - javascriptChannelRegistry: null, - ); - // Verify - expect(widget, isA()); - }); - }); - - group('WebWebViewPlatformController', () { - test('loadUrl sets url on iframe src attribute', () { - // Setup - final MockIFrameElement mockElement = MockIFrameElement(); - final WebWebViewPlatformController controller = - WebWebViewPlatformController( - mockElement, - ); - // Run - controller.loadUrl('test url', null); - // Verify - verify(mockElement.src = 'test url'); - }); - - group('loadHtmlString', () { - test('loadHtmlString loads html into iframe', () { - // Setup - final MockIFrameElement mockElement = MockIFrameElement(); - final WebWebViewPlatformController controller = - WebWebViewPlatformController( - mockElement, - ); - // Run - controller.loadHtmlString('test html'); - // Verify - verify(mockElement.src = - 'data:text/html;charset=utf-8,${Uri.encodeFull('test html')}'); - }); - - test('loadHtmlString escapes "#" correctly', () { - // Setup - final MockIFrameElement mockElement = MockIFrameElement(); - final WebWebViewPlatformController controller = - WebWebViewPlatformController( - mockElement, - ); - // Run - controller.loadHtmlString('#'); - // Verify - verify(mockElement.src = argThat(contains('%23'))); - }); - }); - - group('loadRequest', () { - test('loadRequest throws ArgumentError on missing scheme', () { - // Setup - final MockIFrameElement mockElement = MockIFrameElement(); - final WebWebViewPlatformController controller = - WebWebViewPlatformController( - mockElement, - ); - // Run & Verify - expect( - () async => await controller.loadRequest( - WebViewRequest( - uri: Uri.parse('flutter.dev'), - method: WebViewRequestMethod.get, - ), - ), - throwsA(const TypeMatcher())); - }); - - test('loadRequest makes request and loads response into iframe', - () async { - // Setup - final MockIFrameElement mockElement = MockIFrameElement(); - final WebWebViewPlatformController controller = - WebWebViewPlatformController( - mockElement, - ); - final MockHttpRequest mockHttpRequest = MockHttpRequest(); - when(mockHttpRequest.getResponseHeader('content-type')) - .thenReturn('text/plain'); - when(mockHttpRequest.responseText).thenReturn('test data'); - final MockHttpRequestFactory mockHttpRequestFactory = - MockHttpRequestFactory(); - when(mockHttpRequestFactory.request( - any, - method: anyNamed('method'), - requestHeaders: anyNamed('requestHeaders'), - sendData: anyNamed('sendData'), - )).thenAnswer((_) => Future.value(mockHttpRequest)); - controller.httpRequestFactory = mockHttpRequestFactory; - // Run - await controller.loadRequest( - WebViewRequest( - uri: Uri.parse('https://flutter.dev'), - method: WebViewRequestMethod.post, - body: Uint8List.fromList('test body'.codeUnits), - headers: {'Foo': 'Bar'}), - ); - // Verify - verify(mockHttpRequestFactory.request( - 'https://flutter.dev', - method: 'post', - requestHeaders: {'Foo': 'Bar'}, - sendData: Uint8List.fromList('test body'.codeUnits), - )); - verify(mockElement.src = - 'data:;charset=utf-8,${Uri.encodeFull('test data')}'); - }); - - test('loadRequest escapes "#" correctly', () async { - // Setup - final MockIFrameElement mockElement = MockIFrameElement(); - final WebWebViewPlatformController controller = - WebWebViewPlatformController( - mockElement, - ); - final MockHttpRequest mockHttpRequest = MockHttpRequest(); - when(mockHttpRequest.getResponseHeader('content-type')) - .thenReturn('text/html'); - when(mockHttpRequest.responseText).thenReturn('#'); - final MockHttpRequestFactory mockHttpRequestFactory = - MockHttpRequestFactory(); - when(mockHttpRequestFactory.request( - any, - method: anyNamed('method'), - requestHeaders: anyNamed('requestHeaders'), - sendData: anyNamed('sendData'), - )).thenAnswer((_) => Future.value(mockHttpRequest)); - controller.httpRequestFactory = mockHttpRequestFactory; - // Run - await controller.loadRequest( - WebViewRequest( - uri: Uri.parse('https://flutter.dev'), - method: WebViewRequestMethod.post, - body: Uint8List.fromList('test body'.codeUnits), - headers: {'Foo': 'Bar'}), - ); - // Verify - verify(mockElement.src = argThat(contains('%23'))); - }); + test('registerWith', () { + WebWebViewPlatform.registerWith(Registrar()); + expect(WebViewPlatform.instance, isA()); }); }); } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index c0a2ade72534..d0c5a726b5f7 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,5 +1,34 @@ -## NEXT +## 3.1.0 +* Adds support to access native `WKWebView`. + +## 3.0.5 + +* Renames Pigeon output files. + +## 3.0.4 + +* Fixes bug that prevented the web view from being garbage collected. + +## 3.0.3 + +* Updates example code for `use_build_context_synchronously` lint. + +## 3.0.2 + +* Updates code for stricter lint checks. + +## 3.0.1 + +* Adds support for retrieving navigation type with internal class. +* Updates README with details on contributing. +* Updates pigeon dev dependency to `4.2.13`. + +## 3.0.0 + +* **BREAKING CHANGE** Updates platform implementation to `2.0.0` release of + `webview_flutter_platform_interface`. See + [webview_flutter](https://pub.dev/packages/webview_flutter/versions/4.0.0) for updated usage. * Updates code for `no_leading_underscores_for_local_identifiers` lint. ## 2.9.5 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/README.md b/packages/webview_flutter/webview_flutter_wkwebview/README.md index 2e3a87b7f310..a393a71d2248 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/README.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/README.md @@ -7,5 +7,41 @@ The Apple WKWebView implementation of [`webview_flutter`][1]. This package is [endorsed][2], which means you can simply use `webview_flutter` normally. This package will be automatically included in your app when you do. +### External Native API + +The plugin also provides a native API accessible by the native code of iOS applications or packages. +This API follows the convention of breaking changes of the Dart API, which means that any changes to +the class that are not backwards compatible will only be made with a major version change of the +plugin. Native code other than this external API does not follow breaking change conventions, so +app or plugin clients should not use any other native APIs. + +The API can be accessed by importing the native plugin `webview_flutter_wkwebview`: + +Objective-C: + +```objectivec +@import webview_flutter_wkwebview; +``` + +Then you will have access to the native class `FWFWebViewFlutterWKWebViewExternalAPI`. + +## Contributing + +This package uses [pigeon][3] to generate the communication layer between Flutter and the host +platform (iOS). The communication interface is defined in the `pigeons/web_kit.dart` +file. After editing the communication interface regenerate the communication layer by running +`flutter pub run pigeon --input pigeons/web_kit.dart`. + +Besides [pigeon][3] this package also uses [mockito][4] to generate mock objects for testing +purposes. To generate the mock objects run the following command: +```bash +flutter pub run build_runner build --delete-conflicting-outputs +``` + +If you would like to contribute to the plugin, check out our [contribution guide][5]. + [1]: https://pub.dev/packages/webview_flutter [2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin +[3]: https://pub.dev/packages/pigeon +[4]: https://pub.dev/packages/mockito +[5]: https://github.com/flutter/plugins/blob/main/CONTRIBUTING.md diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/legacy/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/legacy/webview_flutter_test.dart new file mode 100644 index 000000000000..f2bae808df3a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/legacy/webview_flutter_test.dart @@ -0,0 +1,1311 @@ +// 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. + +// This test is run using `flutter drive` by the CI (see /script/tool/README.md +// in this repository for details on driving that tooling manually), but can +// also be run using `flutter test` directly during development. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231) +// ignore: unnecessary_import +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; +import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; +import 'package:webview_flutter_wkwebview/src/common/weak_reference_utils.dart'; +import 'package:webview_flutter_wkwebview_example/legacy/navigation_decision.dart'; +import 'package:webview_flutter_wkwebview_example/legacy/navigation_request.dart'; +import 'package:webview_flutter_wkwebview_example/legacy/web_view.dart'; + +Future main() async { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + final HttpServer server = await HttpServer.bind(InternetAddress.anyIPv4, 0); + server.forEach((HttpRequest request) { + if (request.uri.path == '/hello.txt') { + request.response.writeln('Hello, world.'); + } else if (request.uri.path == '/secondary.txt') { + request.response.writeln('How are you today?'); + } else if (request.uri.path == '/headers') { + request.response.writeln('${request.headers}'); + } else if (request.uri.path == '/favicon.ico') { + request.response.statusCode = HttpStatus.notFound; + } else { + fail('unexpected request: ${request.method} ${request.uri}'); + } + request.response.close(); + }); + final String prefixUrl = 'http://${server.address.address}:${server.port}'; + final String primaryUrl = '$prefixUrl/hello.txt'; + final String secondaryUrl = '$prefixUrl/secondary.txt'; + final String headersUrl = '$prefixUrl/headers'; + + testWidgets('initialUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageFinishedCompleter = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: pageFinishedCompleter.complete, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageFinishedCompleter.future; + + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }); + + testWidgets( + 'withWeakRefenceTo allows encapsulating class to be garbage collected', + (WidgetTester tester) async { + final Completer gcCompleter = Completer(); + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: gcCompleter.complete, + ); + + ClassWithCallbackClass? instance = ClassWithCallbackClass(); + instanceManager.addHostCreatedInstance(instance.callbackClass, 0); + instance = null; + + // Force garbage collection. + await IntegrationTestWidgetsFlutterBinding.instance + .watchPerformance(() async { + await tester.pumpAndSettle(); + }); + + final int gcIdentifier = await gcCompleter.future; + expect(gcIdentifier, 0); + }, timeout: const Timeout(Duration(seconds: 10))); + + testWidgets('loadUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = StreamController(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoads.add(url); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + + await controller.loadUrl(secondaryUrl); + await expectLater( + pageLoads.stream.firstWhere((String url) => url == secondaryUrl), + completion(secondaryUrl), + ); + }); + + testWidgets('evaluateJavascript', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String result = await controller.evaluateJavascript('1 + 1'); + expect(result, equals('2')); + }); + + testWidgets('loadUrl with headers', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageStarts = StreamController(); + final StreamController pageLoads = StreamController(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarts.add(url); + }, + onPageFinished: (String url) { + pageLoads.add(url); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final Map headers = { + 'test_header': 'flutter_test_header' + }; + await controller.loadUrl(headersUrl, headers: headers); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, headersUrl); + + await pageStarts.stream.firstWhere((String url) => url == currentUrl); + await pageLoads.stream.firstWhere((String url) => url == currentUrl); + + final String content = await controller + .runJavascriptReturningResult('document.documentElement.innerText'); + expect(content.contains('flutter_test_header'), isTrue); + }); + + testWidgets('JavascriptChannel', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final Completer channelCompleter = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + // This is the data URL for: '' + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'Echo', + onMessageReceived: (JavascriptMessage message) { + channelCompleter.complete(message.message); + }, + ), + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + expect(channelCompleter.isCompleted, isFalse); + await controller.runJavascript('Echo.postMessage("hello");'); + + await expectLater(channelCompleter.future, completion('hello')); + }); + + testWidgets('resize webview', (WidgetTester tester) async { + final Completer buttonTapResizeCompleter = Completer(); + final Completer onPageFinished = Completer(); + + bool resizeButtonTapped = false; + await tester.pumpWidget(ResizableWebView( + onResize: (_) { + if (resizeButtonTapped) { + buttonTapResizeCompleter.complete(); + } + }, + onPageFinished: () => onPageFinished.complete(), + )); + await onPageFinished.future; + + resizeButtonTapped = true; + await tester.tap(find.byKey(const ValueKey('resizeButton'))); + await tester.pumpAndSettle(); + expect(buttonTapResizeCompleter.future, completes); + }); + + testWidgets('set custom userAgent', (WidgetTester tester) async { + final Completer controllerCompleter1 = + Completer(); + final GlobalKey globalKey = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent1', + onWebViewCreated: (WebViewController controller) { + controllerCompleter1.complete(controller); + }, + ), + ), + ); + final WebViewController controller1 = await controllerCompleter1.future; + final String customUserAgent1 = await _getUserAgent(controller1); + expect(customUserAgent1, 'Custom_User_Agent1'); + // rebuild the WebView with a different user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent2', + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller1); + expect(customUserAgent2, 'Custom_User_Agent2'); + }); + + testWidgets('use default platform userAgent after webView is rebuilt', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final GlobalKey globalKey = GlobalKey(); + // Build the webView with no user agent to get the default platform user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: globalKey, + initialUrl: primaryUrl, + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String defaultPlatformUserAgent = await _getUserAgent(controller); + // rebuild the WebView with a custom user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent', + ), + ), + ); + final String customUserAgent = await _getUserAgent(controller); + expect(customUserAgent, 'Custom_User_Agent'); + // rebuilds the WebView with no user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller); + expect(customUserAgent2, defaultPlatformUserAgent); + }); + + group('Video playback policy', () { + late String videoTestBase64; + setUpAll(() async { + final ByteData videoData = + await rootBundle.load('assets/sample_video.mp4'); + final String base64VideoData = + base64Encode(Uint8List.view(videoData.buffer)); + final String videoTest = ''' + + Video auto play + + + + + + + '''; + videoTestBase64 = base64Encode(const Utf8Encoder().convert(videoTest)); + }); + + testWidgets('Auto media playback', (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(false)); + + controllerCompleter = Completer(); + pageLoaded = Completer(); + + // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + controller = await controllerCompleter.future; + await pageLoaded.future; + + isPaused = await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(true)); + }); + + testWidgets('Changes to initialMediaPlaybackPolicy are ignored', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(false)); + + pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + await controller.reload(); + + await pageLoaded.future; + + isPaused = await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(false)); + }); + + testWidgets('Video plays inline when allowsInlineMediaPlayback is true', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageLoaded = Completer(); + final Completer videoPlaying = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'VideoTestTime', + onMessageReceived: (JavascriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1 && !videoPlaying.isCompleted) { + videoPlaying.complete(null); + } + }, + ), + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + allowsInlineMediaPlayback: true, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + // Pump once to trigger the video play. + await tester.pump(); + + // Makes sure we get the correct event that indicates the video is actually playing. + await videoPlaying.future; + + final String fullScreen = + await controller.runJavascriptReturningResult('isFullScreen();'); + expect(fullScreen, _webviewBool(false)); + }); + + testWidgets( + 'Video plays full screen when allowsInlineMediaPlayback is false', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageLoaded = Completer(); + final Completer videoPlaying = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'VideoTestTime', + onMessageReceived: (JavascriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1 && !videoPlaying.isCompleted) { + videoPlaying.complete(null); + } + }, + ), + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + // Pump once to trigger the video play. + await tester.pump(); + + // Makes sure we get the correct event that indicates the video is actually playing. + await videoPlaying.future; + + final String fullScreen = + await controller.runJavascriptReturningResult('isFullScreen();'); + expect(fullScreen, _webviewBool(true)); + }); + }); + + group('Audio playback policy', () { + late String audioTestBase64; + setUpAll(() async { + final ByteData audioData = + await rootBundle.load('assets/sample_audio.ogg'); + final String base64AudioData = + base64Encode(Uint8List.view(audioData.buffer)); + final String audioTest = ''' + + Audio auto play + + + + + + + '''; + audioTestBase64 = base64Encode(const Utf8Encoder().convert(audioTest)); + }); + + testWidgets('Auto media playback', (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageStarted = Completer(); + Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(false)); + + controllerCompleter = Completer(); + pageStarted = Completer(); + pageLoaded = Completer(); + + // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + isPaused = await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(true)); + }); + + testWidgets('Changes to initialMediaPlaybackPolicy are ignored', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageStarted = Completer(); + Completer pageLoaded = Completer(); + + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(false)); + + pageStarted = Completer(); + pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + await controller.reload(); + + await pageStarted.future; + await pageLoaded.future; + + isPaused = await controller.runJavascriptReturningResult('isPaused();'); + expect(isPaused, _webviewBool(false)); + }); + }); + + testWidgets('getTitle', (WidgetTester tester) async { + const String getTitleTest = ''' + + Some title + + + + + '''; + final String getTitleTestBase64 = + base64Encode(const Utf8Encoder().convert(getTitleTest)); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + // On at least iOS, it does not appear to be guaranteed that the native + // code has the title when the page load completes. Execute some JavaScript + // before checking the title to ensure that the page has been fully parsed + // and processed. + await controller.runJavascript('1;'); + + final String? title = await controller.getTitle(); + expect(title, 'Some title'); + }); + + group('Programmatic Scroll', () { + testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { + const String scrollTestPage = ''' + + + + + + +
+ + + '''; + + final String scrollTestPageBase64 = + base64Encode(const Utf8Encoder().convert(scrollTestPage)); + + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + int scrollPosX = await controller.getScrollX(); + int scrollPosY = await controller.getScrollY(); + + // Check scrollTo() + const int X_SCROLL = 123; + const int Y_SCROLL = 321; + // Get the initial position; this ensures that scrollTo is actually + // changing something, but also gives the native view's scroll position + // time to settle. + expect(scrollPosX, isNot(X_SCROLL)); + expect(scrollPosX, isNot(Y_SCROLL)); + + await controller.scrollTo(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL); + expect(scrollPosY, Y_SCROLL); + + // Check scrollBy() (on top of scrollTo()) + await controller.scrollBy(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL * 2); + expect(scrollPosY, Y_SCROLL * 2); + }); + }); + + group('NavigationDelegate', () { + const String blankPage = ''; + final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + '${base64Encode(const Utf8Encoder().convert(blankPage))}'; + + testWidgets('can allow requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller.runJavascript('location.href = "$secondaryUrl"'); + + await pageLoads.stream.first; // Wait for the next page load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }); + + testWidgets('onWebResourceError', (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://www.notawebsite..com', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + ), + ), + ); + + final WebResourceError error = await errorCompleter.future; + expect(error, isNotNull); + + if (Platform.isIOS) { + expect(error.domain, isNotNull); + expect(error.failingUrl, isNull); + } else if (Platform.isAndroid) { + expect(error.errorType, isNotNull); + expect(error.failingUrl?.startsWith('https://www.notawebsite..com'), + isTrue); + } + }); + + testWidgets('onWebResourceError is not called with valid url', + (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }); + + testWidgets( + 'onWebResourceError only called for main frame', + (WidgetTester tester) async { + const String iframeTest = ''' + + + + WebResourceError test + + + + + + '''; + final String iframeTestBase64 = + base64Encode(const Utf8Encoder().convert(iframeTest)); + + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,$iframeTestBase64', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }, + ); + + testWidgets('can block requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .runJavascript('location.href = "https://www.youtube.com/"'); + + // There should never be any second page load, since our new URL is + // blocked. Still wait for a potential page change for some time in order + // to give the test a chance to fail. + await pageLoads.stream.first + .timeout(const Duration(milliseconds: 500), onTimeout: () => ''); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, isNot(contains('youtube.com'))); + }); + + testWidgets('supports asynchronous decisions', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) async { + NavigationDecision decision = NavigationDecision.prevent; + decision = await Future.delayed( + const Duration(milliseconds: 10), + () => NavigationDecision.navigate); + return decision; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller.runJavascript('location.href = "$secondaryUrl"'); + + await pageLoads.stream.first; // Wait for second page to load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }); + }); + + testWidgets('launches with gestureNavigationEnabled on iOS', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: SizedBox( + width: 400, + height: 300, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + gestureNavigationEnabled: true, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }); + + testWidgets('target _blank opens in same window', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageLoaded = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller.runJavascript('window.open("$primaryUrl", "_blank")'); + await pageLoaded.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }); + + testWidgets( + 'can open new window and go back', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(); + }, + initialUrl: primaryUrl, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + expect(controller.currentUrl(), completion(primaryUrl)); + await pageLoaded.future; + pageLoaded = Completer(); + + await controller.runJavascript('window.open("$secondaryUrl")'); + await pageLoaded.future; + pageLoaded = Completer(); + expect(controller.currentUrl(), completion(secondaryUrl)); + + expect(controller.canGoBack(), completion(true)); + await controller.goBack(); + await pageLoaded.future; + await expectLater(controller.currentUrl(), completion(primaryUrl)); + }, + ); +} + +// JavaScript booleans evaluate to different string values on Android and iOS. +// This utility method returns the string boolean value of the current platform. +String _webviewBool(bool value) { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return value ? '1' : '0'; + } + return value ? 'true' : 'false'; +} + +/// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. +Future _getUserAgent(WebViewController controller) async { + return controller.runJavascriptReturningResult('navigator.userAgent;'); +} + +class ResizableWebView extends StatefulWidget { + const ResizableWebView( + {Key? key, required this.onResize, required this.onPageFinished}) + : super(key: key); + + final JavascriptMessageHandler onResize; + final VoidCallback onPageFinished; + + @override + State createState() => ResizableWebViewState(); +} + +class ResizableWebViewState extends State { + double webViewWidth = 200; + double webViewHeight = 200; + + static const String resizePage = ''' + + Resize test + + + + + + '''; + + @override + Widget build(BuildContext context) { + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizePage)); + return Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: webViewWidth, + height: webViewHeight, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + javascriptChannels: { + JavascriptChannel( + name: 'Resize', + onMessageReceived: widget.onResize, + ), + }, + onPageFinished: (_) => widget.onPageFinished(), + javascriptMode: JavascriptMode.unrestricted, + ), + ), + TextButton( + key: const Key('resizeButton'), + onPressed: () { + setState(() { + webViewWidth += 100.0; + webViewHeight += 100.0; + }); + }, + child: const Text('ResizeButton'), + ), + ], + ), + ); + } +} + +class CopyableObjectWithCallback with Copyable { + CopyableObjectWithCallback(this.callback); + + final VoidCallback callback; + + @override + CopyableObjectWithCallback copy() { + return CopyableObjectWithCallback(callback); + } +} + +class ClassWithCallbackClass { + ClassWithCallbackClass() { + callbackClass = CopyableObjectWithCallback( + withWeakRefenceTo( + this, + (WeakReference weakReference) { + return () { + // Weak reference to `this` in callback. + // ignore: unnecessary_statements + weakReference; + }; + }, + ), + ); + } + + late final CopyableObjectWithCallback callbackClass; +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart index 047d69f0d0ee..16411b8140a5 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart @@ -21,9 +21,8 @@ import 'package:integration_test/integration_test.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; import 'package:webview_flutter_wkwebview/src/common/weak_reference_utils.dart'; -import 'package:webview_flutter_wkwebview_example/navigation_decision.dart'; -import 'package:webview_flutter_wkwebview_example/navigation_request.dart'; -import 'package:webview_flutter_wkwebview_example/web_view.dart'; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; +import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; Future main() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -48,33 +47,8 @@ Future main() async { final String secondaryUrl = '$prefixUrl/secondary.txt'; final String headersUrl = '$prefixUrl/headers'; - testWidgets('initialUrl', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final Completer pageFinishedCompleter = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageFinished: pageFinishedCompleter.complete, - ), - ), - ); - - final WebViewController controller = await controllerCompleter.future; - await pageFinishedCompleter.future; - - final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, primaryUrl); - }); - testWidgets( - 'withWeakRefenceTo allows encapsulating class to be garbage collected', + 'withWeakReferenceTo allows encapsulating class to be garbage collected', (WidgetTester tester) async { final Completer gcCompleter = Completer(); final InstanceManager instanceManager = InstanceManager( @@ -95,137 +69,185 @@ Future main() async { expect(gcIdentifier, 0); }, timeout: const Timeout(Duration(seconds: 10))); - testWidgets('loadUrl', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageLoads = StreamController(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageFinished: (String url) { - pageLoads.add(url); + testWidgets( + 'WKWebView is released by garbage collection', + (WidgetTester tester) async { + final Completer webViewGCCompleter = Completer(); + + late final InstanceManager instanceManager; + instanceManager = + InstanceManager(onWeakReferenceRemoved: (int identifier) { + final Copyable instance = + instanceManager.getInstanceWithWeakReference(identifier)!; + if (instance is WKWebView && !webViewGCCompleter.isCompleted) { + webViewGCCompleter.complete(); + } + }); + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + WebKitWebViewWidgetCreationParams( + instanceManager: instanceManager, + controller: PlatformWebViewController( + WebKitWebViewControllerCreationParams( + instanceManager: instanceManager, + ), + ), + ), + ).build(context); }, ), - ), - ); - final WebViewController controller = await controllerCompleter.future; + ); + await tester.pumpAndSettle(); - await controller.loadUrl(secondaryUrl); - await expectLater( - pageLoads.stream.firstWhere((String url) => url == secondaryUrl), - completion(secondaryUrl), - ); + await tester.pumpWidget(Container()); + + // Force garbage collection. + await IntegrationTestWidgetsFlutterBinding.instance + .watchPerformance(() async { + await tester.pumpAndSettle(); + }); + + await expectLater(webViewGCCompleter.future, completes); + }, + timeout: const Timeout(Duration(seconds: 10)), + ); + + testWidgets('loadRequest', (WidgetTester tester) async { + final Completer pageFinished = Completer(); + + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageFinished.complete()), + ) + ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await pageFinished.future; + + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); }); - testWidgets('evaluateJavascript', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - ), - ), + testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async { + final Completer pageFinished = Completer(); + + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageFinished.complete()), + ) + ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await pageFinished.future; + + await expectLater( + controller.runJavaScriptReturningResult('1 + 1'), + completion(2), ); - final WebViewController controller = await controllerCompleter.future; - final String result = await controller.evaluateJavascript('1 + 1'); - expect(result, equals('2')); }); - testWidgets('loadUrl with headers', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageStarts = StreamController(); - final StreamController pageLoads = StreamController(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (String url) { - pageStarts.add(url); - }, - onPageFinished: (String url) { - pageLoads.add(url); - }, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; + testWidgets('loadRequest with headers', (WidgetTester tester) async { final Map headers = { 'test_header': 'flutter_test_header' }; - await controller.loadUrl(headersUrl, headers: headers); - final String? currentUrl = await controller.currentUrl(); - expect(currentUrl, headersUrl); - await pageStarts.stream.firstWhere((String url) => url == currentUrl); - await pageLoads.stream.firstWhere((String url) => url == currentUrl); + final StreamController pageLoads = StreamController(); + + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + )..setOnPageFinished((String url) => pageLoads.add(url)), + ) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse(headersUrl), + headers: headers, + ), + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await pageLoads.stream.firstWhere((String url) => url == headersUrl); - final String content = await controller - .runJavascriptReturningResult('document.documentElement.innerText'); + final String content = await controller.runJavaScriptReturningResult( + 'document.documentElement.innerText', + ) as String; expect(content.contains('flutter_test_header'), isTrue); }); testWidgets('JavascriptChannel', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final Completer pageStarted = Completer(); - final Completer pageLoaded = Completer(); + final Completer pageFinished = Completer(); + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageFinished.complete()), + ); + final Completer channelCompleter = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - // This is the data URL for: '' - initialUrl: - 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - javascriptChannels: { - JavascriptChannel( - name: 'Echo', - onMessageReceived: (JavascriptMessage message) { - channelCompleter.complete(message.message); - }, - ), - }, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), + await controller.addJavaScriptChannel( + JavaScriptChannelParams( + name: 'Echo', + onMessageReceived: (JavaScriptMessage message) { + channelCompleter.complete(message.message); + }, ), ); - final WebViewController controller = await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - expect(channelCompleter.isCompleted, isFalse); - await controller.runJavascript('Echo.postMessage("hello");'); + controller.loadHtmlString( + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageFinished.future; + + await controller.runJavaScript('Echo.postMessage("hello");'); await expectLater(channelCompleter.future, completion('hello')); }); @@ -235,108 +257,41 @@ Future main() async { bool resizeButtonTapped = false; await tester.pumpWidget(ResizableWebView( - onResize: (_) { + onResize: () { if (resizeButtonTapped) { buttonTapResizeCompleter.complete(); } }, onPageFinished: () => onPageFinished.complete(), )); + await onPageFinished.future; resizeButtonTapped = true; + await tester.tap(find.byKey(const ValueKey('resizeButton'))); await tester.pumpAndSettle(); - expect(buttonTapResizeCompleter.future, completes); - }); - - testWidgets('set custom userAgent', (WidgetTester tester) async { - final Completer controllerCompleter1 = - Completer(); - final GlobalKey globalKey = GlobalKey(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: globalKey, - initialUrl: 'about:blank', - javascriptMode: JavascriptMode.unrestricted, - userAgent: 'Custom_User_Agent1', - onWebViewCreated: (WebViewController controller) { - controllerCompleter1.complete(controller); - }, - ), - ), - ); - final WebViewController controller1 = await controllerCompleter1.future; - final String customUserAgent1 = await _getUserAgent(controller1); - expect(customUserAgent1, 'Custom_User_Agent1'); - // rebuild the WebView with a different user agent. - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: globalKey, - initialUrl: 'about:blank', - javascriptMode: JavascriptMode.unrestricted, - userAgent: 'Custom_User_Agent2', - ), - ), - ); - final String customUserAgent2 = await _getUserAgent(controller1); - expect(customUserAgent2, 'Custom_User_Agent2'); + await expectLater(buttonTapResizeCompleter.future, completes); }); - testWidgets('use default platform userAgent after webView is rebuilt', - (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final GlobalKey globalKey = GlobalKey(); - // Build the webView with no user agent to get the default platform user agent. - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: globalKey, - initialUrl: primaryUrl, - javascriptMode: JavascriptMode.unrestricted, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - final String defaultPlatformUserAgent = await _getUserAgent(controller); - // rebuild the WebView with a custom user agent. - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: globalKey, - initialUrl: 'about:blank', - javascriptMode: JavascriptMode.unrestricted, - userAgent: 'Custom_User_Agent', - ), - ), - ); - final String customUserAgent = await _getUserAgent(controller); - expect(customUserAgent, 'Custom_User_Agent'); - // rebuilds the WebView with no user agent. - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: globalKey, - initialUrl: 'about:blank', - javascriptMode: JavascriptMode.unrestricted, - ), - ), - ); + testWidgets('set custom userAgent', (WidgetTester tester) async { + final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setUserAgent('Custom_User_Agent1'); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); final String customUserAgent2 = await _getUserAgent(controller); - expect(customUserAgent2, defaultPlatformUserAgent); + expect(customUserAgent2, 'Custom_User_Agent1'); }); group('Video playback policy', () { @@ -380,218 +335,184 @@ Future main() async { }); testWidgets('Auto media playback', (WidgetTester tester) async { - Completer controllerCompleter = - Completer(); Completer pageLoaded = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - ), + PlatformWebViewController controller = PlatformWebViewController( + WebKitWebViewControllerCreationParams( + mediaTypesRequiringUserAction: const {}, ), - ); - WebViewController controller = await controllerCompleter.future; - await pageLoaded.future; - - String isPaused = - await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(false)); - - controllerCompleter = Completer(); - pageLoaded = Completer(); - - // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoaded.complete()), + ) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,$videoTestBase64', + ), ), - ), - ); - - controller = await controllerCompleter.future; - await pageLoaded.future; - - isPaused = await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(true)); - }); + ); - testWidgets('Changes to initialMediaPlaybackPolicy are ignored', - (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - Completer pageLoaded = Completer(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); - final GlobalKey key = GlobalKey(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = - await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(false)); + bool isPaused = + await controller.runJavaScriptReturningResult('isPaused();') as bool; + expect(isPaused, false); pageLoaded = Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, + controller = PlatformWebViewController( + WebKitWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoaded.complete()), + ) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,$videoTestBase64', + ), ), - ), - ); + ); - await controller.reload(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoaded.future; - isPaused = await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(false)); + isPaused = + await controller.runJavaScriptReturningResult('isPaused();') as bool; + expect(isPaused, true); }); testWidgets('Video plays inline when allowsInlineMediaPlayback is true', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); final Completer pageLoaded = Completer(); final Completer videoPlaying = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - javascriptChannels: { - JavascriptChannel( - name: 'VideoTestTime', - onMessageReceived: (JavascriptMessage message) { - final double currentTime = double.parse(message.message); - // Let it play for at least 1 second to make sure the related video's properties are set. - if (currentTime > 1 && !videoPlaying.isCompleted) { - videoPlaying.complete(null); - } - }, - ), - }, - onPageFinished: (String url) { - pageLoaded.complete(null); + final PlatformWebViewController controller = PlatformWebViewController( + WebKitWebViewControllerCreationParams( + mediaTypesRequiringUserAction: const {}, + allowsInlineMediaPlayback: true, + ), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoaded.complete()), + ) + ..addJavaScriptChannel( + JavaScriptChannelParams( + name: 'VideoTestTime', + onMessageReceived: (JavaScriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1 && !videoPlaying.isCompleted) { + videoPlaying.complete(null); + } }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - allowsInlineMediaPlayback: true, ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - await pageLoaded.future; + ) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,$videoTestBase64', + ), + ), + ); - // Pump once to trigger the video play. - await tester.pump(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await tester.pumpAndSettle(); + + await pageLoaded.future; // Makes sure we get the correct event that indicates the video is actually playing. await videoPlaying.future; - final String fullScreen = - await controller.runJavascriptReturningResult('isFullScreen();'); - expect(fullScreen, _webviewBool(false)); + final bool fullScreen = await controller + .runJavaScriptReturningResult('isFullScreen();') as bool; + expect(fullScreen, false); }); testWidgets( 'Video plays full screen when allowsInlineMediaPlayback is false', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); final Completer pageLoaded = Completer(); final Completer videoPlaying = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - javascriptChannels: { - JavascriptChannel( - name: 'VideoTestTime', - onMessageReceived: (JavascriptMessage message) { - final double currentTime = double.parse(message.message); - // Let it play for at least 1 second to make sure the related video's properties are set. - if (currentTime > 1 && !videoPlaying.isCompleted) { - videoPlaying.complete(null); - } - }, - ), - }, - onPageFinished: (String url) { - pageLoaded.complete(null); + final PlatformWebViewController controller = PlatformWebViewController( + WebKitWebViewControllerCreationParams( + mediaTypesRequiringUserAction: const {}, + ), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoaded.complete()), + ) + ..addJavaScriptChannel( + JavaScriptChannelParams( + name: 'VideoTestTime', + onMessageReceived: (JavaScriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1 && !videoPlaying.isCompleted) { + videoPlaying.complete(null); + } }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - await pageLoaded.future; + ) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,$videoTestBase64', + ), + ), + ); - // Pump once to trigger the video play. - await tester.pump(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await tester.pumpAndSettle(); + + await pageLoaded.future; // Makes sure we get the correct event that indicates the video is actually playing. await videoPlaying.future; - final String fullScreen = - await controller.runJavascriptReturningResult('isFullScreen();'); - expect(fullScreen, _webviewBool(true)); + final bool fullScreen = await controller + .runJavaScriptReturningResult('isFullScreen();') as bool; + expect(fullScreen, true); }); }); @@ -627,138 +548,72 @@ Future main() async { }); testWidgets('Auto media playback', (WidgetTester tester) async { - Completer controllerCompleter = - Completer(); - Completer pageStarted = Completer(); Completer pageLoaded = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - ), + PlatformWebViewController controller = PlatformWebViewController( + WebKitWebViewControllerCreationParams( + mediaTypesRequiringUserAction: const {}, ), - ); - WebViewController controller = await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - - String isPaused = - await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(false)); - - controllerCompleter = Completer(); - pageStarted = Completer(); - pageLoaded = Completer(); - - // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoaded.complete()), + ) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,$audioTestBase64', + ), ), - ), - ); - - controller = await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - - isPaused = await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(true)); - }); + ); - testWidgets('Changes to initialMediaPlaybackPolicy are ignored', - (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - Completer pageStarted = Completer(); - Completer pageLoaded = Completer(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); - final GlobalKey key = GlobalKey(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - await pageStarted.future; await pageLoaded.future; - String isPaused = - await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(false)); + bool isPaused = + await controller.runJavaScriptReturningResult('isPaused();') as bool; + expect(isPaused, false); - pageStarted = Completer(); pageLoaded = Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, + controller = PlatformWebViewController( + WebKitWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoaded.complete()), + ) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,$audioTestBase64', + ), ), - ), - ); + ); - await controller.reload(); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); - await pageStarted.future; await pageLoaded.future; - isPaused = await controller.runJavascriptReturningResult('isPaused();'); - expect(isPaused, _webviewBool(false)); + isPaused = + await controller.runJavaScriptReturningResult('isPaused();') as bool; + expect(isPaused, true); }); }); @@ -773,39 +628,40 @@ Future main() async { '''; final String getTitleTestBase64 = base64Encode(const Utf8Encoder().convert(getTitleTest)); - final Completer pageStarted = Completer(); final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', - javascriptMode: JavascriptMode.unrestricted, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, + + final PlatformWebViewController controller = PlatformWebViewController( + WebKitWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoaded.complete()), + ) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', + ), ), - ), - ); + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); - final WebViewController controller = await controllerCompleter.future; - await pageStarted.future; await pageLoaded.future; // On at least iOS, it does not appear to be guaranteed that the native // code has the title when the page load completes. Execute some JavaScript // before checking the title to ensure that the page has been fully parsed // and processed. - await controller.runJavascript('1;'); + await controller.runJavaScript('1;'); final String? title = await controller.getTitle(); expect(title, 'Some title'); @@ -838,32 +694,36 @@ Future main() async { base64Encode(const Utf8Encoder().convert(scrollTestPage)); final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - initialUrl: - 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, + final PlatformWebViewController controller = PlatformWebViewController( + WebKitWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoaded.complete()), + ) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + ), ), - ), - ); + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); - final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; await tester.pumpAndSettle(const Duration(seconds: 3)); - int scrollPosX = await controller.getScrollX(); - int scrollPosY = await controller.getScrollY(); + Offset scrollPos = await controller.getScrollPosition(); // Check scrollTo() const int X_SCROLL = 123; @@ -871,21 +731,19 @@ Future main() async { // Get the initial position; this ensures that scrollTo is actually // changing something, but also gives the native view's scroll position // time to settle. - expect(scrollPosX, isNot(X_SCROLL)); - expect(scrollPosX, isNot(Y_SCROLL)); + expect(scrollPos.dx, isNot(X_SCROLL)); + expect(scrollPos.dy, isNot(Y_SCROLL)); await controller.scrollTo(X_SCROLL, Y_SCROLL); - scrollPosX = await controller.getScrollX(); - scrollPosY = await controller.getScrollY(); - expect(scrollPosX, X_SCROLL); - expect(scrollPosY, Y_SCROLL); + scrollPos = await controller.getScrollPosition(); + expect(scrollPos.dx, X_SCROLL); + expect(scrollPos.dy, Y_SCROLL); // Check scrollBy() (on top of scrollTo()) await controller.scrollBy(X_SCROLL, Y_SCROLL); - scrollPosX = await controller.getScrollX(); - scrollPosY = await controller.getScrollY(); - expect(scrollPosX, X_SCROLL * 2); - expect(scrollPosY, Y_SCROLL * 2); + scrollPos = await controller.getScrollPosition(); + expect(scrollPos.dx, X_SCROLL * 2); + expect(scrollPos.dy, Y_SCROLL * 2); }); }); @@ -895,35 +753,41 @@ Future main() async { '${base64Encode(const Utf8Encoder().convert(blankPage))}'; testWidgets('can allow requests', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageLoads = - StreamController.broadcast(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: blankPageEncoded, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - navigationDelegate: (NavigationRequest request) { - return (request.url.contains('youtube.com')) + Completer pageLoaded = Completer(); + + final PlatformWebViewController controller = PlatformWebViewController( + WebKitWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + ) + ..setOnPageFinished((_) => pageLoaded.complete()) + ..setOnNavigationRequest((NavigationRequest navigationRequest) { + return (navigationRequest.url.contains('youtube.com')) ? NavigationDecision.prevent : NavigationDecision.navigate; - }, - onPageFinished: (String url) => pageLoads.add(url), - ), - ), - ); + }), + ) + ..loadRequest( + LoadRequestParams(uri: Uri.parse(blankPageEncoded)), + ); - await pageLoads.stream.first; // Wait for initial page load. - final WebViewController controller = await controllerCompleter.future; - await controller.runJavascript('location.href = "$secondaryUrl"'); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await pageLoaded.future; // Wait for initial page load. + + pageLoaded = Completer(); + await controller.runJavaScript('location.href = "$secondaryUrl"'); + await pageLoaded.future; - await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); expect(currentUrl, secondaryUrl); }); @@ -932,30 +796,33 @@ Future main() async { final Completer errorCompleter = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'https://www.notawebsite..com', - onWebResourceError: (WebResourceError error) { + final PlatformWebViewController controller = PlatformWebViewController( + WebKitWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + )..setOnWebResourceError((WebResourceError error) { errorCompleter.complete(error); - }, - ), - ), - ); + }), + ) + ..loadRequest( + LoadRequestParams(uri: Uri.parse('https://www.notawebsite..com')), + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); final WebResourceError error = await errorCompleter.future; expect(error, isNotNull); - if (Platform.isIOS) { - expect(error.domain, isNotNull); - expect(error.failingUrl, isNull); - } else if (Platform.isAndroid) { - expect(error.errorType, isNotNull); - expect(error.failingUrl?.startsWith('https://www.notawebsite..com'), - isTrue); - } + expect((error as WebKitWebResourceError).domain, isNotNull); }); testWidgets('onWebResourceError is not called with valid url', @@ -964,20 +831,34 @@ Future main() async { Completer(); final Completer pageFinishCompleter = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: - 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', - onWebResourceError: (WebResourceError error) { + final PlatformWebViewController controller = PlatformWebViewController( + WebKitWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + ) + ..setOnPageFinished((_) => pageFinishCompleter.complete()) + ..setOnWebResourceError((WebResourceError error) { errorCompleter.complete(error); - }, - onPageFinished: (_) => pageFinishCompleter.complete(), + }), + ) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + ), ), - ), - ); + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); expect(errorCompleter.future, doesNotComplete); await pageFinishCompleter.future; @@ -987,16 +868,16 @@ Future main() async { 'onWebResourceError only called for main frame', (WidgetTester tester) async { const String iframeTest = ''' - - - - WebResourceError test - - - - - - '''; + + + + WebResourceError test + + + + + + '''; final String iframeTestBase64 = base64Encode(const Utf8Encoder().convert(iframeTest)); @@ -1004,20 +885,34 @@ Future main() async { Completer(); final Completer pageFinishCompleter = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: - 'data:text/html;charset=utf-8;base64,$iframeTestBase64', - onWebResourceError: (WebResourceError error) { + final PlatformWebViewController controller = PlatformWebViewController( + WebKitWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + ) + ..setOnPageFinished((_) => pageFinishCompleter.complete()) + ..setOnWebResourceError((WebResourceError error) { errorCompleter.complete(error); - }, - onPageFinished: (_) => pageFinishCompleter.complete(), + }), + ) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,$iframeTestBase64', + ), ), - ), - ); + ); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); expect(errorCompleter.future, doesNotComplete); await pageFinishCompleter.future; @@ -1025,76 +920,85 @@ Future main() async { ); testWidgets('can block requests', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageLoads = - StreamController.broadcast(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: blankPageEncoded, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - navigationDelegate: (NavigationRequest request) { - return (request.url.contains('youtube.com')) + Completer pageLoaded = Completer(); + + final PlatformWebViewController controller = PlatformWebViewController( + WebKitWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + ) + ..setOnPageFinished((_) => pageLoaded.complete()) + ..setOnNavigationRequest((NavigationRequest navigationRequest) { + return (navigationRequest.url.contains('youtube.com')) ? NavigationDecision.prevent : NavigationDecision.navigate; - }, - onPageFinished: (String url) => pageLoads.add(url), - ), - ), - ); + }), + ) + ..loadRequest(LoadRequestParams(uri: Uri.parse(blankPageEncoded))); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await pageLoaded.future; // Wait for initial page load. - await pageLoads.stream.first; // Wait for initial page load. - final WebViewController controller = await controllerCompleter.future; + pageLoaded = Completer(); await controller - .runJavascript('location.href = "https://www.youtube.com/"'); + .runJavaScript('location.href = "https://www.youtube.com/"'); // There should never be any second page load, since our new URL is // blocked. Still wait for a potential page change for some time in order // to give the test a chance to fail. - await pageLoads.stream.first + await pageLoaded.future .timeout(const Duration(milliseconds: 500), onTimeout: () => ''); final String? currentUrl = await controller.currentUrl(); expect(currentUrl, isNot(contains('youtube.com'))); }); testWidgets('supports asynchronous decisions', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageLoads = - StreamController.broadcast(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: blankPageEncoded, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - navigationDelegate: (NavigationRequest request) async { + Completer pageLoaded = Completer(); + + final PlatformWebViewController controller = PlatformWebViewController( + WebKitWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + ) + ..setOnPageFinished((_) => pageLoaded.complete()) + ..setOnNavigationRequest( + (NavigationRequest navigationRequest) async { NavigationDecision decision = NavigationDecision.prevent; decision = await Future.delayed( const Duration(milliseconds: 10), () => NavigationDecision.navigate); return decision; - }, - onPageFinished: (String url) => pageLoads.add(url), - ), - ), - ); + }), + ) + ..loadRequest(LoadRequestParams(uri: Uri.parse(blankPageEncoded))); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); - await pageLoads.stream.first; // Wait for initial page load. - final WebViewController controller = await controllerCompleter.future; - await controller.runJavascript('location.href = "$secondaryUrl"'); + await pageLoaded.future; // Wait for initial page load. - await pageLoads.stream.first; // Wait for second page to load. + pageLoaded = Completer(); + await controller.runJavaScript('location.href = "$secondaryUrl"'); + + await pageLoaded.future; // Wait for second page to load. final String? currentUrl = await controller.currentUrl(); expect(currentUrl, secondaryUrl); }); @@ -1102,52 +1006,46 @@ Future main() async { testWidgets('launches with gestureNavigationEnabled on iOS', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: SizedBox( - width: 400, - height: 300, - child: WebView( - key: GlobalKey(), - initialUrl: primaryUrl, - gestureNavigationEnabled: true, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - ), - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; + final WebKitWebViewController controller = WebKitWebViewController( + WebKitWebViewControllerCreationParams(), + ) + ..setAllowsBackForwardNavigationGestures(true) + ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); }); testWidgets('target _blank opens in same window', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); final Completer pageLoaded = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - await controller.runJavascript('window.open("$primaryUrl", "_blank")'); + + final PlatformWebViewController controller = PlatformWebViewController( + WebKitWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate(WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoaded.complete())); + + await controller.runJavaScript('window.open("$primaryUrl", "_blank")'); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); @@ -1156,31 +1054,30 @@ Future main() async { testWidgets( 'can open new window and go back', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); Completer pageLoaded = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (String url) { - pageLoaded.complete(); - }, - initialUrl: primaryUrl, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; + + final PlatformWebViewController controller = PlatformWebViewController( + WebKitWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate(WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageLoaded.complete())) + ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + expect(controller.currentUrl(), completion(primaryUrl)); await pageLoaded.future; pageLoaded = Completer(); - await controller.runJavascript('window.open("$secondaryUrl")'); + await controller.runJavaScript('window.open("$secondaryUrl")'); await pageLoaded.future; pageLoaded = Completer(); expect(controller.currentUrl(), completion(secondaryUrl)); @@ -1193,26 +1090,20 @@ Future main() async { ); } -// JavaScript booleans evaluate to different string values on Android and iOS. -// This utility method returns the string boolean value of the current platform. -String _webviewBool(bool value) { - if (defaultTargetPlatform == TargetPlatform.iOS) { - return value ? '1' : '0'; - } - return value ? 'true' : 'false'; -} - /// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. -Future _getUserAgent(WebViewController controller) async { - return await controller.runJavascriptReturningResult('navigator.userAgent;'); +Future _getUserAgent(PlatformWebViewController controller) async { + return await controller.runJavaScriptReturningResult('navigator.userAgent;') + as String; } class ResizableWebView extends StatefulWidget { - const ResizableWebView( - {Key? key, required this.onResize, required this.onPageFinished}) - : super(key: key); + const ResizableWebView({ + Key? key, + required this.onResize, + required this.onPageFinished, + }) : super(key: key); - final JavascriptMessageHandler onResize; + final VoidCallback onResize; final VoidCallback onPageFinished; @override @@ -1220,6 +1111,31 @@ class ResizableWebView extends StatefulWidget { } class ResizableWebViewState extends State { + late final PlatformWebViewController controller = PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => widget.onPageFinished()), + ) + ..addJavaScriptChannel( + JavaScriptChannelParams( + name: 'Resize', + onMessageReceived: (_) { + widget.onResize(); + }, + ), + ) + ..loadRequest( + LoadRequestParams( + uri: Uri.parse( + 'data:text/html;charset=utf-8;base64,${base64Encode(const Utf8Encoder().convert(resizePage))}', + ), + ), + ); + double webViewWidth = 200; double webViewHeight = 200; @@ -1242,8 +1158,6 @@ class ResizableWebViewState extends State { @override Widget build(BuildContext context) { - final String resizeTestBase64 = - base64Encode(const Utf8Encoder().convert(resizePage)); return Directionality( textDirection: TextDirection.ltr, child: Column( @@ -1251,18 +1165,9 @@ class ResizableWebViewState extends State { SizedBox( width: webViewWidth, height: webViewHeight, - child: WebView( - initialUrl: - 'data:text/html;charset=utf-8;base64,$resizeTestBase64', - javascriptChannels: { - JavascriptChannel( - name: 'Resize', - onMessageReceived: widget.onResize, - ), - }, - onPageFinished: (_) => widget.onPageFinished(), - javascriptMode: JavascriptMode.unrestricted, - ), + child: PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context), ), TextButton( key: const Key('resizeButton'), diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj index 1efee8f844ef..9e1038d08279 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,12 +3,13 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 8F4FF949299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F4FF948299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m */; }; 8FA6A87928062CD000A4B183 /* FWFInstanceManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FA6A87828062CD000A4B183 /* FWFInstanceManagerTests.m */; }; 8FB79B5328134C3100C101D3 /* FWFWebViewHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B5228134C3100C101D3 /* FWFWebViewHostApiTests.m */; }; 8FB79B55281B24F600C101D3 /* FWFDataConvertersTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B54281B24F600C101D3 /* FWFDataConvertersTests.m */; }; @@ -76,6 +77,7 @@ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 8F4FF948299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FWFWebViewFlutterWKWebViewExternalAPITests.m; sourceTree = ""; }; 8FA6A87828062CD000A4B183 /* FWFInstanceManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFInstanceManagerTests.m; sourceTree = ""; }; 8FB79B5228134C3100C101D3 /* FWFWebViewHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFWebViewHostApiTests.m; sourceTree = ""; }; 8FB79B54281B24F600C101D3 /* FWFDataConvertersTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFDataConvertersTests.m; sourceTree = ""; }; @@ -145,6 +147,7 @@ isa = PBXGroup; children = ( 68BDCAED23C3F7CB00D9C032 /* Info.plist */, + 8F4FF948299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m */, 8FA6A87828062CD000A4B183 /* FWFInstanceManagerTests.m */, 8FB79B5228134C3100C101D3 /* FWFWebViewHostApiTests.m */, 8FB79B54281B24F600C101D3 /* FWFDataConvertersTests.m */, @@ -379,10 +382,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -415,6 +420,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -463,6 +469,7 @@ 8FB79B5328134C3100C101D3 /* FWFWebViewHostApiTests.m in Sources */, 8FB79B73282096B500C101D3 /* FWFScriptMessageHandlerHostApiTests.m in Sources */, 8FB79B7928209D1300C101D3 /* FWFUserContentControllerHostApiTests.m in Sources */, + 8F4FF949299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m in Sources */, 8FB79B6B28204EE500C101D3 /* FWFWebsiteDataStoreHostApiTests.m in Sources */, 8FB79B8F2820BAB300C101D3 /* FWFScrollViewHostApiTests.m in Sources */, 8FB79B912820BAC700C101D3 /* FWFUIViewHostApiTests.m in Sources */, @@ -533,7 +540,11 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; @@ -547,7 +558,11 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; @@ -672,7 +687,10 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -695,7 +713,10 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -711,7 +732,11 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -724,7 +749,11 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFDataConvertersTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFDataConvertersTests.m index ca7d6f938599..63e13f9e8ecf 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFDataConvertersTests.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFDataConvertersTests.m @@ -59,6 +59,8 @@ - (void)testFWFWKUserScriptFromScriptData { - (void)testFWFWKNavigationActionDataFromNavigationAction { WKNavigationAction *mockNavigationAction = OCMClassMock([WKNavigationAction class]); + OCMStub([mockNavigationAction navigationType]).andReturn(WKNavigationTypeReload); + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.flutter.dev/"]]; OCMStub([mockNavigationAction request]).andReturn(request); @@ -70,6 +72,7 @@ - (void)testFWFWKNavigationActionDataFromNavigationAction { FWFWKNavigationActionData *data = FWFWKNavigationActionDataFromNavigationAction(mockNavigationAction); XCTAssertNotNil(data); + XCTAssertEqual(data.navigationType, FWFWKNavigationTypeReload); } - (void)testFWFNSUrlRequestDataFromNSURLRequest { diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewFlutterWKWebViewExternalAPITests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewFlutterWKWebViewExternalAPITests.m new file mode 100644 index 000000000000..1452edeaa647 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewFlutterWKWebViewExternalAPITests.m @@ -0,0 +1,28 @@ +// 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 +#import + +@import webview_flutter_wkwebview; + +@interface FWFWebViewFlutterWKWebViewExternalAPITests : XCTestCase +@end + +@implementation FWFWebViewFlutterWKWebViewExternalAPITests +- (void)testWebViewForIdentifier { + WKWebView *webView = [[WKWebView alloc] init]; + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:webView withIdentifier:0]; + + id mockPluginRegistry = OCMProtocolMock(@protocol(FlutterPluginRegistry)); + OCMStub([mockPluginRegistry valuePublishedByPlugin:@"FLTWebViewFlutterPlugin"]) + .andReturn(instanceManager); + + XCTAssertEqualObjects( + [FWFWebViewFlutterWKWebViewExternalAPI webViewForIdentifier:0 + withPluginRegistry:mockPluginRegistry], + webView); +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/legacy/navigation_decision.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/legacy/navigation_decision.dart new file mode 100644 index 000000000000..d8178acd8096 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/legacy/navigation_decision.dart @@ -0,0 +1,12 @@ +// 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. + +/// A decision on how to handle a navigation request. +enum NavigationDecision { + /// Prevent the navigation from taking place. + prevent, + + /// Allow the navigation to take place. + navigate, +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_request.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/legacy/navigation_request.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_request.dart rename to packages/webview_flutter/webview_flutter_wkwebview/example/lib/legacy/navigation_request.dart diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/legacy/web_view.dart similarity index 98% rename from packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart rename to packages/webview_flutter/webview_flutter_wkwebview/example/lib/legacy/web_view.dart index c44c4e743669..d99b3095abca 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/legacy/web_view.dart @@ -8,8 +8,10 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; -import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; +// ignore: implementation_imports +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; +// ignore: implementation_imports +import 'package:webview_flutter_wkwebview/src/webview_flutter_wkwebview_legacy.dart'; import 'navigation_decision.dart'; import 'navigation_request.dart'; @@ -639,10 +641,10 @@ class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { required bool isForMainFrame, }) async { if (url.startsWith('https://www.youtube.com/')) { - print('blocking navigation to $url'); + debugPrint('blocking navigation to $url'); return false; } - print('allowing navigation to $url'); + debugPrint('allowing navigation to $url'); return true; } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart index 3f61ebfdd6f8..aef7ece0c2e3 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -12,13 +12,10 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; - -import 'navigation_decision.dart'; -import 'navigation_request.dart'; -import 'web_view.dart'; +import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; void main() { - runApp(const MaterialApp(home: _WebViewExample())); + runApp(const MaterialApp(home: WebViewExample())); } const String kNavigationExamplePage = ''' @@ -36,6 +33,26 @@ The navigation delegate is set to block navigation to the youtube website. '''; +const String kLocalExamplePage = ''' + + + +Load file or HTML string example + + + +

Local demo page

+

+ This is an example page used to demonstrate how to load a local file or HTML + string using the Flutter + webview plugin. +

+ + + +'''; + +// NOTE: This is used by the transparency test in `example/ios/RunnerUITests/FLTWebViewUITests.m`. const String kTransparentBackgroundPage = ''' @@ -57,39 +74,69 @@ const String kTransparentBackgroundPage = ''' '''; -const String kLocalFileExamplePage = ''' - - - -Load file or HTML string example - - +class WebViewExample extends StatefulWidget { + const WebViewExample({Key? key, this.cookieManager}) : super(key: key); -

Local demo page

-

- This is an example page used to demonstrate how to load a local file or HTML - string using the Flutter - webview plugin. -

- - - -'''; - -class _WebViewExample extends StatefulWidget { - const _WebViewExample({Key? key}) : super(key: key); + final PlatformWebViewCookieManager? cookieManager; @override - _WebViewExampleState createState() => _WebViewExampleState(); + State createState() => _WebViewExampleState(); } -class _WebViewExampleState extends State<_WebViewExample> { - final Completer _controller = - Completer(); +class _WebViewExampleState extends State { + late final PlatformWebViewController _controller; @override void initState() { super.initState(); + + _controller = PlatformWebViewController( + WebKitWebViewControllerCreationParams(allowsInlineMediaPlayback: true), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(const Color(0x80000000)) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + ) + ..setOnProgress((int progress) { + debugPrint('WebView is loading (progress : $progress%)'); + }) + ..setOnPageStarted((String url) { + debugPrint('Page started loading: $url'); + }) + ..setOnPageFinished((String url) { + debugPrint('Page finished loading: $url'); + }) + ..setOnWebResourceError((WebResourceError error) { + debugPrint(''' +Page resource error: + code: ${error.errorCode} + description: ${error.description} + errorType: ${error.errorType} + isForMainFrame: ${error.isForMainFrame} + '''); + }) + ..setOnNavigationRequest((NavigationRequest request) { + if (request.url.startsWith('https://www.youtube.com/')) { + debugPrint('blocking navigation to ${request.url}'); + return NavigationDecision.prevent; + } + debugPrint('allowing navigation to ${request.url}'); + return NavigationDecision.navigate; + }), + ) + ..addJavaScriptChannel(JavaScriptChannelParams( + name: 'Toaster', + onMessageReceived: (JavaScriptMessage message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message.message)), + ); + }, + )) + ..loadRequest(LoadRequestParams( + uri: Uri.parse('https://flutter.dev'), + )); } @override @@ -100,64 +147,36 @@ class _WebViewExampleState extends State<_WebViewExample> { title: const Text('Flutter WebView example'), // This drop down menu demonstrates that Flutter widgets can be shown over the web view. actions: [ - _NavigationControls(_controller.future), - _SampleMenu(_controller.future), + NavigationControls(webViewController: _controller), + SampleMenu( + webViewController: _controller, + cookieManager: widget.cookieManager, + ), ], ), - body: WebView( - initialUrl: 'https://flutter.dev/', - onWebViewCreated: (WebViewController controller) { - _controller.complete(controller); - }, - javascriptChannels: _createJavascriptChannels(context), - javascriptMode: JavascriptMode.unrestricted, - navigationDelegate: (NavigationRequest request) { - if (request.url.startsWith('https://www.youtube.com/')) { - print('blocking navigation to $request}'); - return NavigationDecision.prevent; - } - print('allowing navigation to $request'); - return NavigationDecision.navigate; - }, - backgroundColor: const Color(0x80000000), - ), + body: PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: _controller), + ).build(context), floatingActionButton: favoriteButton(), ); } Widget favoriteButton() { - return FutureBuilder( - future: _controller.future, - builder: (BuildContext context, - AsyncSnapshot controller) { - if (controller.hasData) { - return FloatingActionButton( - onPressed: () async { - final String url = (await controller.data!.currentUrl())!; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Favorited $url')), - ); - }, - child: const Icon(Icons.favorite), - ); - } - return Container(); - }); + return FloatingActionButton( + onPressed: () async { + final String? url = await _controller.currentUrl(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Favorited $url')), + ); + } + }, + child: const Icon(Icons.favorite), + ); } } -Set _createJavascriptChannels(BuildContext context) { - return { - JavascriptChannel( - name: 'Snackbar', - onMessageReceived: (JavascriptMessage message) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(message.message))); - }), - }; -} - -enum _MenuOptions { +enum MenuOptions { showUserAgent, listCookies, clearCookies, @@ -165,234 +184,251 @@ enum _MenuOptions { listCache, clearCache, navigationDelegate, - loadFlutterAsset, + doPostRequest, loadLocalFile, + loadFlutterAsset, loadHtmlString, - doPostRequest, - setCookie, transparentBackground, + setCookie, } -class _SampleMenu extends StatelessWidget { - const _SampleMenu(this.controller); +class SampleMenu extends StatelessWidget { + SampleMenu({ + Key? key, + required this.webViewController, + PlatformWebViewCookieManager? cookieManager, + }) : cookieManager = cookieManager ?? + PlatformWebViewCookieManager( + const PlatformWebViewCookieManagerCreationParams(), + ), + super(key: key); - final Future controller; + final PlatformWebViewController webViewController; + late final PlatformWebViewCookieManager cookieManager; @override Widget build(BuildContext context) { - return FutureBuilder( - future: controller, - builder: - (BuildContext context, AsyncSnapshot controller) { - return PopupMenuButton<_MenuOptions>( - key: const ValueKey('ShowPopupMenu'), - onSelected: (_MenuOptions value) { - switch (value) { - case _MenuOptions.showUserAgent: - _onShowUserAgent(controller.data!, context); - break; - case _MenuOptions.listCookies: - _onListCookies(controller.data!, context); - break; - case _MenuOptions.clearCookies: - _onClearCookies(controller.data!, context); - break; - case _MenuOptions.addToCache: - _onAddToCache(controller.data!, context); - break; - case _MenuOptions.listCache: - _onListCache(controller.data!, context); - break; - case _MenuOptions.clearCache: - _onClearCache(controller.data!, context); - break; - case _MenuOptions.navigationDelegate: - _onNavigationDelegateExample(controller.data!, context); - break; - case _MenuOptions.loadFlutterAsset: - _onLoadFlutterAssetExample(controller.data!, context); - break; - case _MenuOptions.loadLocalFile: - _onLoadLocalFileExample(controller.data!, context); - break; - case _MenuOptions.loadHtmlString: - _onLoadHtmlStringExample(controller.data!, context); - break; - case _MenuOptions.doPostRequest: - _onDoPostRequest(controller.data!, context); - break; - case _MenuOptions.setCookie: - _onSetCookie(controller.data!, context); - break; - case _MenuOptions.transparentBackground: - _onTransparentBackground(controller.data!, context); - break; - } - }, - itemBuilder: (BuildContext context) => >[ - PopupMenuItem<_MenuOptions>( - value: _MenuOptions.showUserAgent, - enabled: controller.hasData, - child: const Text('Show user agent'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.listCookies, - child: Text('List cookies'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.clearCookies, - child: Text('Clear cookies'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.addToCache, - child: Text('Add to cache'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.listCache, - child: Text('List cache'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.clearCache, - child: Text('Clear cache'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.navigationDelegate, - child: Text('Navigation Delegate example'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.loadFlutterAsset, - child: Text('Load Flutter Asset'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.loadHtmlString, - child: Text('Load HTML string'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.loadLocalFile, - child: Text('Load local file'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.doPostRequest, - child: Text('Post Request'), - ), - const PopupMenuItem<_MenuOptions>( - value: _MenuOptions.setCookie, - child: Text('Set Cookie'), - ), - const PopupMenuItem<_MenuOptions>( - key: ValueKey('ShowTransparentBackgroundExample'), - value: _MenuOptions.transparentBackground, - child: Text('Transparent background example'), - ), - ], - ); + return PopupMenuButton( + key: const ValueKey('ShowPopupMenu'), + onSelected: (MenuOptions value) { + switch (value) { + case MenuOptions.showUserAgent: + _onShowUserAgent(); + break; + case MenuOptions.listCookies: + _onListCookies(context); + break; + case MenuOptions.clearCookies: + _onClearCookies(context); + break; + case MenuOptions.addToCache: + _onAddToCache(context); + break; + case MenuOptions.listCache: + _onListCache(); + break; + case MenuOptions.clearCache: + _onClearCache(context); + break; + case MenuOptions.navigationDelegate: + _onNavigationDelegateExample(); + break; + case MenuOptions.doPostRequest: + _onDoPostRequest(); + break; + case MenuOptions.loadLocalFile: + _onLoadLocalFileExample(); + break; + case MenuOptions.loadFlutterAsset: + _onLoadFlutterAssetExample(); + break; + case MenuOptions.loadHtmlString: + _onLoadHtmlStringExample(); + break; + case MenuOptions.transparentBackground: + _onTransparentBackground(); + break; + case MenuOptions.setCookie: + _onSetCookie(); + break; + } }, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem( + value: MenuOptions.showUserAgent, + child: Text('Show user agent'), + ), + const PopupMenuItem( + value: MenuOptions.listCookies, + child: Text('List cookies'), + ), + const PopupMenuItem( + value: MenuOptions.clearCookies, + child: Text('Clear cookies'), + ), + const PopupMenuItem( + value: MenuOptions.addToCache, + child: Text('Add to cache'), + ), + const PopupMenuItem( + value: MenuOptions.listCache, + child: Text('List cache'), + ), + const PopupMenuItem( + value: MenuOptions.clearCache, + child: Text('Clear cache'), + ), + const PopupMenuItem( + value: MenuOptions.navigationDelegate, + child: Text('Navigation Delegate example'), + ), + const PopupMenuItem( + value: MenuOptions.doPostRequest, + child: Text('Post Request'), + ), + const PopupMenuItem( + value: MenuOptions.loadHtmlString, + child: Text('Load HTML string'), + ), + const PopupMenuItem( + value: MenuOptions.loadLocalFile, + child: Text('Load local file'), + ), + const PopupMenuItem( + value: MenuOptions.loadFlutterAsset, + child: Text('Load Flutter Asset'), + ), + const PopupMenuItem( + value: MenuOptions.setCookie, + child: Text('Set cookie'), + ), + const PopupMenuItem( + key: ValueKey('ShowTransparentBackgroundExample'), + value: MenuOptions.transparentBackground, + child: Text('Transparent background example'), + ), + ], ); } - Future _onShowUserAgent( - WebViewController controller, BuildContext context) async { - // Send a message with the user agent string to the Snackbar JavaScript channel we registered + Future _onShowUserAgent() { + // Send a message with the user agent string to the Toaster JavaScript channel we registered // with the WebView. - await controller.runJavascript( - 'Snackbar.postMessage("User Agent: " + navigator.userAgent);'); + return webViewController.runJavaScript( + 'Toaster.postMessage("User Agent: " + navigator.userAgent);', + ); } - Future _onListCookies( - WebViewController controller, BuildContext context) async { - final String cookies = - await controller.runJavascriptReturningResult('document.cookie'); - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Column( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Cookies:'), - _getCookieList(cookies), - ], - ), - )); + Future _onListCookies(BuildContext context) async { + final String cookies = await webViewController + .runJavaScriptReturningResult('document.cookie') as String; + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Cookies:'), + _getCookieList(cookies), + ], + ), + )); + } } - Future _onAddToCache( - WebViewController controller, BuildContext context) async { - await controller.runJavascript( - 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Added a test entry to cache.'), - )); + Future _onAddToCache(BuildContext context) async { + await webViewController.runJavaScript( + 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";', + ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Added a test entry to cache.'), + )); + } } - Future _onListCache( - WebViewController controller, BuildContext context) async { - await controller.runJavascript('caches.keys()' + Future _onListCache() { + return webViewController.runJavaScript('caches.keys()' // ignore: missing_whitespace_between_adjacent_strings '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' - '.then((caches) => Snackbar.postMessage(caches))'); + '.then((caches) => Toaster.postMessage(caches))'); } - Future _onClearCache( - WebViewController controller, BuildContext context) async { - await controller.clearCache(); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Cache cleared.'), - )); + Future _onClearCache(BuildContext context) async { + await webViewController.clearCache(); + await webViewController.clearLocalStorage(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Cache cleared.'), + )); + } } - Future _onClearCookies( - WebViewController controller, BuildContext context) async { - final bool hadCookies = await WebView.platform.clearCookies(); + Future _onClearCookies(BuildContext context) async { + final bool hadCookies = await cookieManager.clearCookies(); String message = 'There were cookies. Now, they are gone!'; if (!hadCookies) { message = 'There are no cookies.'; } - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(message), - )); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(message), + )); + } } - Future _onNavigationDelegateExample( - WebViewController controller, BuildContext context) async { - final String contentBase64 = - base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); - await controller.loadUrl('data:text/html;base64,$contentBase64'); + Future _onNavigationDelegateExample() { + final String contentBase64 = base64Encode( + const Utf8Encoder().convert(kNavigationExamplePage), + ); + return webViewController.loadRequest( + LoadRequestParams( + uri: Uri.parse('data:text/html;base64,$contentBase64'), + ), + ); + } + + Future _onSetCookie() async { + await cookieManager.setCookie( + const WebViewCookie( + name: 'foo', + value: 'bar', + domain: 'httpbin.org', + path: '/anything', + ), + ); + await webViewController.loadRequest(LoadRequestParams( + uri: Uri.parse('https://httpbin.org/anything'), + )); } - Future _onLoadFlutterAssetExample( - WebViewController controller, BuildContext context) async { - await controller.loadFlutterAsset('assets/www/index.html'); + Future _onDoPostRequest() { + return webViewController.loadRequest(LoadRequestParams( + uri: Uri.parse('https://httpbin.org/post'), + method: LoadRequestMethod.post, + headers: const { + 'foo': 'bar', + 'Content-Type': 'text/plain', + }, + body: Uint8List.fromList('Test Body'.codeUnits), + )); } - Future _onLoadLocalFileExample( - WebViewController controller, BuildContext context) async { + Future _onLoadLocalFileExample() async { final String pathToIndex = await _prepareLocalFile(); - - await controller.loadFile(pathToIndex); + await webViewController.loadFile(pathToIndex); } - Future _onLoadHtmlStringExample( - WebViewController controller, BuildContext context) async { - await controller.loadHtmlString(kLocalFileExamplePage); + Future _onLoadFlutterAssetExample() { + return webViewController.loadFlutterAsset('assets/www/index.html'); } - Future _onDoPostRequest( - WebViewController controller, BuildContext context) async { - final WebViewRequest request = WebViewRequest( - uri: Uri.parse('https://httpbin.org/post'), - method: WebViewRequestMethod.post, - headers: {'foo': 'bar', 'Content-Type': 'text/plain'}, - body: Uint8List.fromList('Test Body'.codeUnits), - ); - await controller.loadRequest(request); + Future _onLoadHtmlStringExample() { + return webViewController.loadHtmlString(kLocalExamplePage); } - Future _onSetCookie( - WebViewController controller, BuildContext context) async { - await WebViewCookieManager.instance.setCookie( - const WebViewCookie( - name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'), - ); - await controller.loadUrl('https://httpbin.org/anything'); + Future _onTransparentBackground() { + return webViewController.loadHtmlString(kTransparentBackgroundPage); } Widget _getCookieList(String cookies) { @@ -409,85 +445,61 @@ class _SampleMenu extends StatelessWidget { ); } - Future _onTransparentBackground( - WebViewController controller, BuildContext context) async { - await controller.loadHtmlString(kTransparentBackgroundPage); - } - static Future _prepareLocalFile() async { final String tmpDir = (await getTemporaryDirectory()).path; - final File indexFile = File('$tmpDir/www/index.html'); + final File indexFile = File( + {tmpDir, 'www', 'index.html'}.join(Platform.pathSeparator)); - await Directory('$tmpDir/www').create(recursive: true); - await indexFile.writeAsString(kLocalFileExamplePage); + await indexFile.create(recursive: true); + await indexFile.writeAsString(kLocalExamplePage); return indexFile.path; } } -class _NavigationControls extends StatelessWidget { - const _NavigationControls(this._webViewControllerFuture) - : assert(_webViewControllerFuture != null); +class NavigationControls extends StatelessWidget { + const NavigationControls({Key? key, required this.webViewController}) + : super(key: key); - final Future _webViewControllerFuture; + final PlatformWebViewController webViewController; @override Widget build(BuildContext context) { - return FutureBuilder( - future: _webViewControllerFuture, - builder: - (BuildContext context, AsyncSnapshot snapshot) { - final bool webViewReady = - snapshot.connectionState == ConnectionState.done; - final WebViewController? controller = snapshot.data; - - return Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_back_ios), - onPressed: !webViewReady - ? null - : () async { - if (await controller!.canGoBack()) { - await controller.goBack(); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('No back history item')), - ); - return; - } - }, - ), - IconButton( - icon: const Icon(Icons.arrow_forward_ios), - onPressed: !webViewReady - ? null - : () async { - if (await controller!.canGoForward()) { - await controller.goForward(); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('No forward history item')), - ); - return; - } - }, - ), - IconButton( - icon: const Icon(Icons.replay), - onPressed: !webViewReady - ? null - : () { - controller!.reload(); - }, - ), - ], - ); - }, + return Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: () async { + if (await webViewController.canGoBack()) { + await webViewController.goBack(); + } else { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No back history item')), + ); + } + } + }, + ), + IconButton( + icon: const Icon(Icons.arrow_forward_ios), + onPressed: () async { + if (await webViewController.canGoForward()) { + await webViewController.goForward(); + } else { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No forward history item')), + ); + } + } + }, + ), + IconButton( + icon: const Icon(Icons.replay), + onPressed: () => webViewController.reload(), + ), + ], ); } } - -/// Callback type for handling messages sent from JavaScript running in a web view. -typedef JavascriptMessageHandler = void Function(JavascriptMessage message); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml index a3f65b861944..718eb282018b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml @@ -4,12 +4,13 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" + flutter: ">=3.0.0" dependencies: flutter: sdk: flutter path_provider: ^2.0.6 - webview_flutter_platform_interface: ^1.8.0 + webview_flutter_platform_interface: ^2.0.0 webview_flutter_wkwebview: # When depending on this package from a real application you should use: # webview_flutter: ^x.y.z diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.h index 2a80c7d886f2..a1c035e40185 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.h @@ -3,6 +3,11 @@ // found in the LICENSE file. #import +#import + +NS_ASSUME_NONNULL_BEGIN @interface FLTWebViewFlutterPlugin : NSObject @end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h index 2863048726a9..605ed53394b2 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.h @@ -151,4 +151,13 @@ extern FWFNSKeyValueChangeKeyEnumData *FWFNSKeyValueChangeKeyEnumDataFromNSKeyVa */ extern FWFWKScriptMessageData *FWFWKScriptMessageDataFromWKScriptMessage(WKScriptMessage *message); +/** + * Converts a WKNavigationType to an FWFWKNavigationType. + * + * @param type The object containing information to create a FWFWKNavigationType + * + * @return A FWFWKNavigationType. + */ +extern FWFWKNavigationType FWFWKNavigationTypeFromWKNavigationType(WKNavigationType type); + NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m index 8ecc9d303000..528c9565617e 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFDataConverters.m @@ -162,7 +162,8 @@ WKAudiovisualMediaTypes FWFWKAudiovisualMediaTypeFromEnumData( WKNavigationAction *action) { return [FWFWKNavigationActionData makeWithRequest:FWFNSUrlRequestDataFromNSURLRequest(action.request) - targetFrame:FWFWKFrameInfoDataFromWKFrameInfo(action.targetFrame)]; + targetFrame:FWFWKFrameInfoDataFromWKFrameInfo(action.targetFrame) + navigationType:FWFWKNavigationTypeFromWKNavigationType(action.navigationType)]; } FWFNSUrlRequestData *FWFNSUrlRequestDataFromNSURLRequest(NSURLRequest *request) { @@ -218,3 +219,20 @@ WKNavigationActionPolicy FWFWKNavigationActionPolicyFromEnumData( FWFWKScriptMessageData *FWFWKScriptMessageDataFromWKScriptMessage(WKScriptMessage *message) { return [FWFWKScriptMessageData makeWithName:message.name body:message.body]; } + +FWFWKNavigationType FWFWKNavigationTypeFromWKNavigationType(WKNavigationType type) { + switch (type) { + case WKNavigationTypeLinkActivated: + return FWFWKNavigationTypeLinkActivated; + case WKNavigationTypeFormSubmitted: + return FWFWKNavigationTypeFormResubmitted; + case WKNavigationTypeBackForward: + return FWFWKNavigationTypeBackForward; + case WKNavigationTypeReload: + return FWFWKNavigationTypeReload; + case WKNavigationTypeFormResubmitted: + return FWFWKNavigationTypeFormResubmitted; + case WKNavigationTypeOther: + return FWFWKNavigationTypeOther; + } +} 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 8cbd2c7c194c..cc41f4c15040 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 (v3.1.5), do not edit directly. +// Autogenerated from Pigeon (v4.2.13), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @protocol FlutterBinaryMessenger; @@ -11,6 +11,10 @@ NS_ASSUME_NONNULL_BEGIN +/// Mirror of NSKeyValueObservingOptions. +/// +/// See +/// https://developer.apple.com/documentation/foundation/nskeyvalueobservingoptions?language=objc. typedef NS_ENUM(NSUInteger, FWFNSKeyValueObservingOptionsEnum) { FWFNSKeyValueObservingOptionsEnumNewValue = 0, FWFNSKeyValueObservingOptionsEnumOldValue = 1, @@ -18,6 +22,9 @@ typedef NS_ENUM(NSUInteger, FWFNSKeyValueObservingOptionsEnum) { FWFNSKeyValueObservingOptionsEnumPriorNotification = 3, }; +/// Mirror of NSKeyValueChange. +/// +/// See https://developer.apple.com/documentation/foundation/nskeyvaluechange?language=objc. typedef NS_ENUM(NSUInteger, FWFNSKeyValueChangeEnum) { FWFNSKeyValueChangeEnumSetting = 0, FWFNSKeyValueChangeEnumInsertion = 1, @@ -25,6 +32,9 @@ typedef NS_ENUM(NSUInteger, FWFNSKeyValueChangeEnum) { FWFNSKeyValueChangeEnumReplacement = 3, }; +/// Mirror of NSKeyValueChangeKey. +/// +/// See https://developer.apple.com/documentation/foundation/nskeyvaluechangekey?language=objc. typedef NS_ENUM(NSUInteger, FWFNSKeyValueChangeKeyEnum) { FWFNSKeyValueChangeKeyEnumIndexes = 0, FWFNSKeyValueChangeKeyEnumKind = 1, @@ -33,11 +43,18 @@ typedef NS_ENUM(NSUInteger, FWFNSKeyValueChangeKeyEnum) { FWFNSKeyValueChangeKeyEnumOldValue = 4, }; +/// Mirror of WKUserScriptInjectionTime. +/// +/// See https://developer.apple.com/documentation/webkit/wkuserscriptinjectiontime?language=objc. typedef NS_ENUM(NSUInteger, FWFWKUserScriptInjectionTimeEnum) { FWFWKUserScriptInjectionTimeEnumAtDocumentStart = 0, FWFWKUserScriptInjectionTimeEnumAtDocumentEnd = 1, }; +/// Mirror of WKAudiovisualMediaTypes. +/// +/// See +/// [WKAudiovisualMediaTypes](https://developer.apple.com/documentation/webkit/wkaudiovisualmediatypes?language=objc). typedef NS_ENUM(NSUInteger, FWFWKAudiovisualMediaTypeEnum) { FWFWKAudiovisualMediaTypeEnumNone = 0, FWFWKAudiovisualMediaTypeEnumAudio = 1, @@ -45,6 +62,10 @@ typedef NS_ENUM(NSUInteger, FWFWKAudiovisualMediaTypeEnum) { FWFWKAudiovisualMediaTypeEnumAll = 3, }; +/// Mirror of WKWebsiteDataTypes. +/// +/// See +/// https://developer.apple.com/documentation/webkit/wkwebsitedatarecord/data_store_record_types?language=objc. typedef NS_ENUM(NSUInteger, FWFWKWebsiteDataTypeEnum) { FWFWKWebsiteDataTypeEnumCookies = 0, FWFWKWebsiteDataTypeEnumMemoryCache = 1, @@ -56,11 +77,17 @@ typedef NS_ENUM(NSUInteger, FWFWKWebsiteDataTypeEnum) { FWFWKWebsiteDataTypeEnumIndexedDBDatabases = 7, }; +/// Mirror of WKNavigationActionPolicy. +/// +/// See https://developer.apple.com/documentation/webkit/wknavigationactionpolicy?language=objc. typedef NS_ENUM(NSUInteger, FWFWKNavigationActionPolicyEnum) { FWFWKNavigationActionPolicyEnumAllow = 0, FWFWKNavigationActionPolicyEnumCancel = 1, }; +/// Mirror of NSHTTPCookiePropertyKey. +/// +/// See https://developer.apple.com/documentation/foundation/nshttpcookiepropertykey. typedef NS_ENUM(NSUInteger, FWFNSHttpCookiePropertyKeyEnum) { FWFNSHttpCookiePropertyKeyEnumComment = 0, FWFNSHttpCookiePropertyKeyEnumCommentUrl = 1, @@ -78,6 +105,44 @@ typedef NS_ENUM(NSUInteger, FWFNSHttpCookiePropertyKeyEnum) { FWFNSHttpCookiePropertyKeyEnumVersion = 13, }; +/// An object that contains information about an action that causes navigation +/// to occur. +/// +/// Wraps +/// [WKNavigationType](https://developer.apple.com/documentation/webkit/wknavigationaction?language=objc). +typedef NS_ENUM(NSUInteger, FWFWKNavigationType) { + /// A link activation. + /// + /// See + /// https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypelinkactivated?language=objc. + FWFWKNavigationTypeLinkActivated = 0, + /// A request to submit a form. + /// + /// See + /// https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypeformsubmitted?language=objc. + FWFWKNavigationTypeSubmitted = 1, + /// A request for the frame’s next or previous item. + /// + /// See + /// https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypebackforward?language=objc. + FWFWKNavigationTypeBackForward = 2, + /// A request to reload the webpage. + /// + /// See + /// https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypereload?language=objc. + FWFWKNavigationTypeReload = 3, + /// A request to resubmit a form. + /// + /// See + /// https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypeformresubmitted?language=objc. + FWFWKNavigationTypeFormResubmitted = 4, + /// A navigation request that originates for some other reason. + /// + /// See + /// https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypeother?language=objc. + FWFWKNavigationTypeOther = 5, +}; + @class FWFNSKeyValueObservingOptionsEnumData; @class FWFNSKeyValueChangeKeyEnumData; @class FWFWKUserScriptInjectionTimeEnumData; @@ -142,6 +207,9 @@ typedef NS_ENUM(NSUInteger, FWFNSHttpCookiePropertyKeyEnum) { @property(nonatomic, assign) FWFNSHttpCookiePropertyKeyEnum value; @end +/// Mirror of NSURLRequest. +/// +/// See https://developer.apple.com/documentation/foundation/nsurlrequest?language=objc. @interface FWFNSUrlRequestData : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -155,6 +223,9 @@ typedef NS_ENUM(NSUInteger, FWFNSHttpCookiePropertyKeyEnum) { @property(nonatomic, strong) NSDictionary *allHttpHeaderFields; @end +/// Mirror of WKUserScript. +/// +/// See https://developer.apple.com/documentation/webkit/wkuserscript?language=objc. @interface FWFWKUserScriptData : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -166,15 +237,23 @@ typedef NS_ENUM(NSUInteger, FWFNSHttpCookiePropertyKeyEnum) { @property(nonatomic, strong) NSNumber *isMainFrameOnly; @end +/// Mirror of WKNavigationAction. +/// +/// See https://developer.apple.com/documentation/webkit/wknavigationaction. @interface FWFWKNavigationActionData : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; + (instancetype)makeWithRequest:(FWFNSUrlRequestData *)request - targetFrame:(FWFWKFrameInfoData *)targetFrame; + targetFrame:(FWFWKFrameInfoData *)targetFrame + navigationType:(FWFWKNavigationType)navigationType; @property(nonatomic, strong) FWFNSUrlRequestData *request; @property(nonatomic, strong) FWFWKFrameInfoData *targetFrame; +@property(nonatomic, assign) FWFWKNavigationType navigationType; @end +/// Mirror of WKFrameInfo. +/// +/// See https://developer.apple.com/documentation/webkit/wkframeinfo?language=objc. @interface FWFWKFrameInfoData : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -182,6 +261,9 @@ typedef NS_ENUM(NSUInteger, FWFNSHttpCookiePropertyKeyEnum) { @property(nonatomic, strong) NSNumber *isMainFrame; @end +/// Mirror of NSError. +/// +/// See https://developer.apple.com/documentation/foundation/nserror?language=objc. @interface FWFNSErrorData : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -193,6 +275,9 @@ typedef NS_ENUM(NSUInteger, FWFNSHttpCookiePropertyKeyEnum) { @property(nonatomic, copy) NSString *localizedDescription; @end +/// Mirror of WKScriptMessage. +/// +/// See https://developer.apple.com/documentation/webkit/wkscriptmessage?language=objc. @interface FWFWKScriptMessageData : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -201,6 +286,9 @@ typedef NS_ENUM(NSUInteger, FWFNSHttpCookiePropertyKeyEnum) { @property(nonatomic, strong) id body; @end +/// Mirror of NSHttpCookieData. +/// +/// See https://developer.apple.com/documentation/foundation/nshttpcookie?language=objc. @interface FWFNSHttpCookieData : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -213,6 +301,9 @@ typedef NS_ENUM(NSUInteger, FWFNSHttpCookiePropertyKeyEnum) { /// The codec used by FWFWKWebsiteDataStoreHostApi. NSObject *FWFWKWebsiteDataStoreHostApiGetCodec(void); +/// Mirror of WKWebsiteDataStore. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebsitedatastore?language=objc. @protocol FWFWKWebsiteDataStoreHostApi - (void)createFromWebViewConfigurationWithIdentifier:(NSNumber *)identifier configurationIdentifier:(NSNumber *)configurationIdentifier @@ -233,6 +324,9 @@ extern void FWFWKWebsiteDataStoreHostApiSetup( /// The codec used by FWFUIViewHostApi. NSObject *FWFUIViewHostApiGetCodec(void); +/// Mirror of UIView. +/// +/// See https://developer.apple.com/documentation/uikit/uiview?language=objc. @protocol FWFUIViewHostApi - (void)setBackgroundColorForViewWithIdentifier:(NSNumber *)identifier toValue:(nullable NSNumber *)value @@ -248,6 +342,9 @@ extern void FWFUIViewHostApiSetup(id binaryMessenger, /// The codec used by FWFUIScrollViewHostApi. NSObject *FWFUIScrollViewHostApiGetCodec(void); +/// Mirror of UIScrollView. +/// +/// See https://developer.apple.com/documentation/uikit/uiscrollview?language=objc. @protocol FWFUIScrollViewHostApi - (void)createFromWebViewWithIdentifier:(NSNumber *)identifier webViewIdentifier:(NSNumber *)webViewIdentifier @@ -272,6 +369,9 @@ extern void FWFUIScrollViewHostApiSetup(id binaryMesseng /// The codec used by FWFWKWebViewConfigurationHostApi. NSObject *FWFWKWebViewConfigurationHostApiGetCodec(void); +/// Mirror of WKWebViewConfiguration. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebviewconfiguration?language=objc. @protocol FWFWKWebViewConfigurationHostApi - (void)createWithIdentifier:(NSNumber *)identifier error:(FlutterError *_Nullable *_Nonnull)error; - (void)createFromWebViewWithIdentifier:(NSNumber *)identifier @@ -300,6 +400,9 @@ extern void FWFWKWebViewConfigurationHostApiSetup( /// The codec used by FWFWKWebViewConfigurationFlutterApi. NSObject *FWFWKWebViewConfigurationFlutterApiGetCodec(void); +/// Handles callbacks from an WKWebViewConfiguration instance. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebviewconfiguration?language=objc. @interface FWFWKWebViewConfigurationFlutterApi : NSObject - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; - (void)createWithIdentifier:(NSNumber *)identifier @@ -308,6 +411,9 @@ NSObject *FWFWKWebViewConfigurationFlutterApiGetCodec(void) /// The codec used by FWFWKUserContentControllerHostApi. NSObject *FWFWKUserContentControllerHostApiGetCodec(void); +/// Mirror of WKUserContentController. +/// +/// See https://developer.apple.com/documentation/webkit/wkusercontentcontroller?language=objc. @protocol FWFWKUserContentControllerHostApi - (void)createFromWebViewConfigurationWithIdentifier:(NSNumber *)identifier configurationIdentifier:(NSNumber *)configurationIdentifier @@ -338,6 +444,9 @@ extern void FWFWKUserContentControllerHostApiSetup( /// The codec used by FWFWKPreferencesHostApi. NSObject *FWFWKPreferencesHostApiGetCodec(void); +/// Mirror of WKUserPreferences. +/// +/// See https://developer.apple.com/documentation/webkit/wkpreferences?language=objc. @protocol FWFWKPreferencesHostApi - (void)createFromWebViewConfigurationWithIdentifier:(NSNumber *)identifier configurationIdentifier:(NSNumber *)configurationIdentifier @@ -353,6 +462,9 @@ extern void FWFWKPreferencesHostApiSetup(id binaryMessen /// The codec used by FWFWKScriptMessageHandlerHostApi. NSObject *FWFWKScriptMessageHandlerHostApiGetCodec(void); +/// Mirror of WKScriptMessageHandler. +/// +/// See https://developer.apple.com/documentation/webkit/wkscriptmessagehandler?language=objc. @protocol FWFWKScriptMessageHandlerHostApi - (void)createWithIdentifier:(NSNumber *)identifier error:(FlutterError *_Nullable *_Nonnull)error; @end @@ -364,6 +476,9 @@ extern void FWFWKScriptMessageHandlerHostApiSetup( /// The codec used by FWFWKScriptMessageHandlerFlutterApi. NSObject *FWFWKScriptMessageHandlerFlutterApiGetCodec(void); +/// Handles callbacks from an WKScriptMessageHandler instance. +/// +/// See https://developer.apple.com/documentation/webkit/wkscriptmessagehandler?language=objc. @interface FWFWKScriptMessageHandlerFlutterApi : NSObject - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; - (void)didReceiveScriptMessageForHandlerWithIdentifier:(NSNumber *)identifier @@ -374,6 +489,9 @@ NSObject *FWFWKScriptMessageHandlerFlutterApiGetCodec(void) /// The codec used by FWFWKNavigationDelegateHostApi. NSObject *FWFWKNavigationDelegateHostApiGetCodec(void); +/// Mirror of WKNavigationDelegate. +/// +/// See https://developer.apple.com/documentation/webkit/wknavigationdelegate?language=objc. @protocol FWFWKNavigationDelegateHostApi - (void)createWithIdentifier:(NSNumber *)identifier error:(FlutterError *_Nullable *_Nonnull)error; @end @@ -385,6 +503,9 @@ extern void FWFWKNavigationDelegateHostApiSetup( /// The codec used by FWFWKNavigationDelegateFlutterApi. NSObject *FWFWKNavigationDelegateFlutterApiGetCodec(void); +/// Handles callbacks from an WKNavigationDelegate instance. +/// +/// See https://developer.apple.com/documentation/webkit/wknavigationdelegate?language=objc. @interface FWFWKNavigationDelegateFlutterApi : NSObject - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; - (void)didFinishNavigationForDelegateWithIdentifier:(NSNumber *)identifier @@ -422,6 +543,9 @@ NSObject *FWFWKNavigationDelegateFlutterApiGetCodec(void); /// The codec used by FWFNSObjectHostApi. NSObject *FWFNSObjectHostApiGetCodec(void); +/// Mirror of NSObject. +/// +/// See https://developer.apple.com/documentation/objectivec/nsobject. @protocol FWFNSObjectHostApi - (void)disposeObjectWithIdentifier:(NSNumber *)identifier error:(FlutterError *_Nullable *_Nonnull)error; @@ -443,6 +567,9 @@ extern void FWFNSObjectHostApiSetup(id binaryMessenger, /// The codec used by FWFNSObjectFlutterApi. NSObject *FWFNSObjectFlutterApiGetCodec(void); +/// Handles callbacks from an NSObject instance. +/// +/// See https://developer.apple.com/documentation/objectivec/nsobject. @interface FWFNSObjectFlutterApi : NSObject - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; - (void)observeValueForObjectWithIdentifier:(NSNumber *)identifier @@ -457,6 +584,9 @@ NSObject *FWFNSObjectFlutterApiGetCodec(void); /// The codec used by FWFWKWebViewHostApi. NSObject *FWFWKWebViewHostApiGetCodec(void); +/// Mirror of WKWebView. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebview?language=objc. @protocol FWFWKWebViewHostApi - (void)createWithIdentifier:(NSNumber *)identifier configurationIdentifier:(NSNumber *)configurationIdentifier @@ -521,6 +651,9 @@ extern void FWFWKWebViewHostApiSetup(id binaryMessenger, /// The codec used by FWFWKUIDelegateHostApi. NSObject *FWFWKUIDelegateHostApiGetCodec(void); +/// Mirror of WKUIDelegate. +/// +/// See https://developer.apple.com/documentation/webkit/wkuidelegate?language=objc. @protocol FWFWKUIDelegateHostApi - (void)createWithIdentifier:(NSNumber *)identifier error:(FlutterError *_Nullable *_Nonnull)error; @end @@ -531,6 +664,9 @@ extern void FWFWKUIDelegateHostApiSetup(id binaryMesseng /// The codec used by FWFWKUIDelegateFlutterApi. NSObject *FWFWKUIDelegateFlutterApiGetCodec(void); +/// Handles callbacks from an WKUIDelegate instance. +/// +/// See https://developer.apple.com/documentation/webkit/wkuidelegate?language=objc. @interface FWFWKUIDelegateFlutterApi : NSObject - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; - (void)onCreateWebViewForDelegateWithIdentifier:(NSNumber *)identifier @@ -542,6 +678,9 @@ NSObject *FWFWKUIDelegateFlutterApiGetCodec(void); /// The codec used by FWFWKHttpCookieStoreHostApi. NSObject *FWFWKHttpCookieStoreHostApiGetCodec(void); +/// Mirror of WKHttpCookieStore. +/// +/// See https://developer.apple.com/documentation/webkit/wkhttpcookiestore?language=objc. @protocol FWFWKHttpCookieStoreHostApi - (void)createFromWebsiteDataStoreWithIdentifier:(NSNumber *)identifier dataStoreIdentifier:(NSNumber *)websiteDataStoreIdentifier 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 10680227ee43..b2a365b038c3 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 (v3.1.5), do not edit directly. +// Autogenerated from Pigeon (v4.2.13), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "FWFGeneratedWebKitApis.h" #import @@ -10,23 +10,13 @@ #error File requires ARC to be enabled. #endif -static NSDictionary *wrapResult(id result, FlutterError *error) { - NSDictionary *errorDict = (NSDictionary *)[NSNull null]; +static NSArray *wrapResult(id result, FlutterError *error) { if (error) { - errorDict = @{ - @"code" : (error.code ?: [NSNull null]), - @"message" : (error.message ?: [NSNull null]), - @"details" : (error.details ?: [NSNull null]), - }; - } - return @{ - @"result" : (result ?: [NSNull null]), - @"error" : errorDict, - }; -} -static id GetNullableObject(NSDictionary *dict, id key) { - id result = dict[key]; - return (result == [NSNull null]) ? nil : result; + return @[ + error.code ?: [NSNull null], error.message ?: [NSNull null], error.details ?: [NSNull null] + ]; + } + return @[ result ?: [NSNull null] ]; } static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { id result = array[key]; @@ -34,74 +24,74 @@ static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { } @interface FWFNSKeyValueObservingOptionsEnumData () -+ (FWFNSKeyValueObservingOptionsEnumData *)fromMap:(NSDictionary *)dict; -+ (nullable FWFNSKeyValueObservingOptionsEnumData *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (FWFNSKeyValueObservingOptionsEnumData *)fromList:(NSArray *)list; ++ (nullable FWFNSKeyValueObservingOptionsEnumData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end @interface FWFNSKeyValueChangeKeyEnumData () -+ (FWFNSKeyValueChangeKeyEnumData *)fromMap:(NSDictionary *)dict; -+ (nullable FWFNSKeyValueChangeKeyEnumData *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (FWFNSKeyValueChangeKeyEnumData *)fromList:(NSArray *)list; ++ (nullable FWFNSKeyValueChangeKeyEnumData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end @interface FWFWKUserScriptInjectionTimeEnumData () -+ (FWFWKUserScriptInjectionTimeEnumData *)fromMap:(NSDictionary *)dict; -+ (nullable FWFWKUserScriptInjectionTimeEnumData *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (FWFWKUserScriptInjectionTimeEnumData *)fromList:(NSArray *)list; ++ (nullable FWFWKUserScriptInjectionTimeEnumData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end @interface FWFWKAudiovisualMediaTypeEnumData () -+ (FWFWKAudiovisualMediaTypeEnumData *)fromMap:(NSDictionary *)dict; -+ (nullable FWFWKAudiovisualMediaTypeEnumData *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (FWFWKAudiovisualMediaTypeEnumData *)fromList:(NSArray *)list; ++ (nullable FWFWKAudiovisualMediaTypeEnumData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end @interface FWFWKWebsiteDataTypeEnumData () -+ (FWFWKWebsiteDataTypeEnumData *)fromMap:(NSDictionary *)dict; -+ (nullable FWFWKWebsiteDataTypeEnumData *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (FWFWKWebsiteDataTypeEnumData *)fromList:(NSArray *)list; ++ (nullable FWFWKWebsiteDataTypeEnumData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end @interface FWFWKNavigationActionPolicyEnumData () -+ (FWFWKNavigationActionPolicyEnumData *)fromMap:(NSDictionary *)dict; -+ (nullable FWFWKNavigationActionPolicyEnumData *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (FWFWKNavigationActionPolicyEnumData *)fromList:(NSArray *)list; ++ (nullable FWFWKNavigationActionPolicyEnumData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end @interface FWFNSHttpCookiePropertyKeyEnumData () -+ (FWFNSHttpCookiePropertyKeyEnumData *)fromMap:(NSDictionary *)dict; -+ (nullable FWFNSHttpCookiePropertyKeyEnumData *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (FWFNSHttpCookiePropertyKeyEnumData *)fromList:(NSArray *)list; ++ (nullable FWFNSHttpCookiePropertyKeyEnumData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end @interface FWFNSUrlRequestData () -+ (FWFNSUrlRequestData *)fromMap:(NSDictionary *)dict; -+ (nullable FWFNSUrlRequestData *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (FWFNSUrlRequestData *)fromList:(NSArray *)list; ++ (nullable FWFNSUrlRequestData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end @interface FWFWKUserScriptData () -+ (FWFWKUserScriptData *)fromMap:(NSDictionary *)dict; -+ (nullable FWFWKUserScriptData *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (FWFWKUserScriptData *)fromList:(NSArray *)list; ++ (nullable FWFWKUserScriptData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end @interface FWFWKNavigationActionData () -+ (FWFWKNavigationActionData *)fromMap:(NSDictionary *)dict; -+ (nullable FWFWKNavigationActionData *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (FWFWKNavigationActionData *)fromList:(NSArray *)list; ++ (nullable FWFWKNavigationActionData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end @interface FWFWKFrameInfoData () -+ (FWFWKFrameInfoData *)fromMap:(NSDictionary *)dict; -+ (nullable FWFWKFrameInfoData *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (FWFWKFrameInfoData *)fromList:(NSArray *)list; ++ (nullable FWFWKFrameInfoData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end @interface FWFNSErrorData () -+ (FWFNSErrorData *)fromMap:(NSDictionary *)dict; -+ (nullable FWFNSErrorData *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (FWFNSErrorData *)fromList:(NSArray *)list; ++ (nullable FWFNSErrorData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end @interface FWFWKScriptMessageData () -+ (FWFWKScriptMessageData *)fromMap:(NSDictionary *)dict; -+ (nullable FWFWKScriptMessageData *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (FWFWKScriptMessageData *)fromList:(NSArray *)list; ++ (nullable FWFWKScriptMessageData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end @interface FWFNSHttpCookieData () -+ (FWFNSHttpCookieData *)fromMap:(NSDictionary *)dict; -+ (nullable FWFNSHttpCookieData *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (FWFNSHttpCookieData *)fromList:(NSArray *)list; ++ (nullable FWFNSHttpCookieData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end @implementation FWFNSKeyValueObservingOptionsEnumData @@ -111,19 +101,19 @@ + (instancetype)makeWithValue:(FWFNSKeyValueObservingOptionsEnum)value { pigeonResult.value = value; return pigeonResult; } -+ (FWFNSKeyValueObservingOptionsEnumData *)fromMap:(NSDictionary *)dict { ++ (FWFNSKeyValueObservingOptionsEnumData *)fromList:(NSArray *)list { FWFNSKeyValueObservingOptionsEnumData *pigeonResult = [[FWFNSKeyValueObservingOptionsEnumData alloc] init]; - pigeonResult.value = [GetNullableObject(dict, @"value") integerValue]; + pigeonResult.value = [GetNullableObjectAtIndex(list, 0) integerValue]; return pigeonResult; } -+ (nullable FWFNSKeyValueObservingOptionsEnumData *)nullableFromMap:(NSDictionary *)dict { - return (dict) ? [FWFNSKeyValueObservingOptionsEnumData fromMap:dict] : nil; ++ (nullable FWFNSKeyValueObservingOptionsEnumData *)nullableFromList:(NSArray *)list { + return (list) ? [FWFNSKeyValueObservingOptionsEnumData fromList:list] : nil; } -- (NSDictionary *)toMap { - return @{ - @"value" : @(self.value), - }; +- (NSArray *)toList { + return @[ + @(self.value), + ]; } @end @@ -133,18 +123,18 @@ + (instancetype)makeWithValue:(FWFNSKeyValueChangeKeyEnum)value { pigeonResult.value = value; return pigeonResult; } -+ (FWFNSKeyValueChangeKeyEnumData *)fromMap:(NSDictionary *)dict { ++ (FWFNSKeyValueChangeKeyEnumData *)fromList:(NSArray *)list { FWFNSKeyValueChangeKeyEnumData *pigeonResult = [[FWFNSKeyValueChangeKeyEnumData alloc] init]; - pigeonResult.value = [GetNullableObject(dict, @"value") integerValue]; + pigeonResult.value = [GetNullableObjectAtIndex(list, 0) integerValue]; return pigeonResult; } -+ (nullable FWFNSKeyValueChangeKeyEnumData *)nullableFromMap:(NSDictionary *)dict { - return (dict) ? [FWFNSKeyValueChangeKeyEnumData fromMap:dict] : nil; ++ (nullable FWFNSKeyValueChangeKeyEnumData *)nullableFromList:(NSArray *)list { + return (list) ? [FWFNSKeyValueChangeKeyEnumData fromList:list] : nil; } -- (NSDictionary *)toMap { - return @{ - @"value" : @(self.value), - }; +- (NSArray *)toList { + return @[ + @(self.value), + ]; } @end @@ -155,19 +145,19 @@ + (instancetype)makeWithValue:(FWFWKUserScriptInjectionTimeEnum)value { pigeonResult.value = value; return pigeonResult; } -+ (FWFWKUserScriptInjectionTimeEnumData *)fromMap:(NSDictionary *)dict { ++ (FWFWKUserScriptInjectionTimeEnumData *)fromList:(NSArray *)list { FWFWKUserScriptInjectionTimeEnumData *pigeonResult = [[FWFWKUserScriptInjectionTimeEnumData alloc] init]; - pigeonResult.value = [GetNullableObject(dict, @"value") integerValue]; + pigeonResult.value = [GetNullableObjectAtIndex(list, 0) integerValue]; return pigeonResult; } -+ (nullable FWFWKUserScriptInjectionTimeEnumData *)nullableFromMap:(NSDictionary *)dict { - return (dict) ? [FWFWKUserScriptInjectionTimeEnumData fromMap:dict] : nil; ++ (nullable FWFWKUserScriptInjectionTimeEnumData *)nullableFromList:(NSArray *)list { + return (list) ? [FWFWKUserScriptInjectionTimeEnumData fromList:list] : nil; } -- (NSDictionary *)toMap { - return @{ - @"value" : @(self.value), - }; +- (NSArray *)toList { + return @[ + @(self.value), + ]; } @end @@ -178,19 +168,19 @@ + (instancetype)makeWithValue:(FWFWKAudiovisualMediaTypeEnum)value { pigeonResult.value = value; return pigeonResult; } -+ (FWFWKAudiovisualMediaTypeEnumData *)fromMap:(NSDictionary *)dict { ++ (FWFWKAudiovisualMediaTypeEnumData *)fromList:(NSArray *)list { FWFWKAudiovisualMediaTypeEnumData *pigeonResult = [[FWFWKAudiovisualMediaTypeEnumData alloc] init]; - pigeonResult.value = [GetNullableObject(dict, @"value") integerValue]; + pigeonResult.value = [GetNullableObjectAtIndex(list, 0) integerValue]; return pigeonResult; } -+ (nullable FWFWKAudiovisualMediaTypeEnumData *)nullableFromMap:(NSDictionary *)dict { - return (dict) ? [FWFWKAudiovisualMediaTypeEnumData fromMap:dict] : nil; ++ (nullable FWFWKAudiovisualMediaTypeEnumData *)nullableFromList:(NSArray *)list { + return (list) ? [FWFWKAudiovisualMediaTypeEnumData fromList:list] : nil; } -- (NSDictionary *)toMap { - return @{ - @"value" : @(self.value), - }; +- (NSArray *)toList { + return @[ + @(self.value), + ]; } @end @@ -200,18 +190,18 @@ + (instancetype)makeWithValue:(FWFWKWebsiteDataTypeEnum)value { pigeonResult.value = value; return pigeonResult; } -+ (FWFWKWebsiteDataTypeEnumData *)fromMap:(NSDictionary *)dict { ++ (FWFWKWebsiteDataTypeEnumData *)fromList:(NSArray *)list { FWFWKWebsiteDataTypeEnumData *pigeonResult = [[FWFWKWebsiteDataTypeEnumData alloc] init]; - pigeonResult.value = [GetNullableObject(dict, @"value") integerValue]; + pigeonResult.value = [GetNullableObjectAtIndex(list, 0) integerValue]; return pigeonResult; } -+ (nullable FWFWKWebsiteDataTypeEnumData *)nullableFromMap:(NSDictionary *)dict { - return (dict) ? [FWFWKWebsiteDataTypeEnumData fromMap:dict] : nil; ++ (nullable FWFWKWebsiteDataTypeEnumData *)nullableFromList:(NSArray *)list { + return (list) ? [FWFWKWebsiteDataTypeEnumData fromList:list] : nil; } -- (NSDictionary *)toMap { - return @{ - @"value" : @(self.value), - }; +- (NSArray *)toList { + return @[ + @(self.value), + ]; } @end @@ -222,19 +212,19 @@ + (instancetype)makeWithValue:(FWFWKNavigationActionPolicyEnum)value { pigeonResult.value = value; return pigeonResult; } -+ (FWFWKNavigationActionPolicyEnumData *)fromMap:(NSDictionary *)dict { ++ (FWFWKNavigationActionPolicyEnumData *)fromList:(NSArray *)list { FWFWKNavigationActionPolicyEnumData *pigeonResult = [[FWFWKNavigationActionPolicyEnumData alloc] init]; - pigeonResult.value = [GetNullableObject(dict, @"value") integerValue]; + pigeonResult.value = [GetNullableObjectAtIndex(list, 0) integerValue]; return pigeonResult; } -+ (nullable FWFWKNavigationActionPolicyEnumData *)nullableFromMap:(NSDictionary *)dict { - return (dict) ? [FWFWKNavigationActionPolicyEnumData fromMap:dict] : nil; ++ (nullable FWFWKNavigationActionPolicyEnumData *)nullableFromList:(NSArray *)list { + return (list) ? [FWFWKNavigationActionPolicyEnumData fromList:list] : nil; } -- (NSDictionary *)toMap { - return @{ - @"value" : @(self.value), - }; +- (NSArray *)toList { + return @[ + @(self.value), + ]; } @end @@ -245,19 +235,19 @@ + (instancetype)makeWithValue:(FWFNSHttpCookiePropertyKeyEnum)value { pigeonResult.value = value; return pigeonResult; } -+ (FWFNSHttpCookiePropertyKeyEnumData *)fromMap:(NSDictionary *)dict { ++ (FWFNSHttpCookiePropertyKeyEnumData *)fromList:(NSArray *)list { FWFNSHttpCookiePropertyKeyEnumData *pigeonResult = [[FWFNSHttpCookiePropertyKeyEnumData alloc] init]; - pigeonResult.value = [GetNullableObject(dict, @"value") integerValue]; + pigeonResult.value = [GetNullableObjectAtIndex(list, 0) integerValue]; return pigeonResult; } -+ (nullable FWFNSHttpCookiePropertyKeyEnumData *)nullableFromMap:(NSDictionary *)dict { - return (dict) ? [FWFNSHttpCookiePropertyKeyEnumData fromMap:dict] : nil; ++ (nullable FWFNSHttpCookiePropertyKeyEnumData *)nullableFromList:(NSArray *)list { + return (list) ? [FWFNSHttpCookiePropertyKeyEnumData fromList:list] : nil; } -- (NSDictionary *)toMap { - return @{ - @"value" : @(self.value), - }; +- (NSArray *)toList { + return @[ + @(self.value), + ]; } @end @@ -273,26 +263,26 @@ + (instancetype)makeWithUrl:(NSString *)url pigeonResult.allHttpHeaderFields = allHttpHeaderFields; return pigeonResult; } -+ (FWFNSUrlRequestData *)fromMap:(NSDictionary *)dict { ++ (FWFNSUrlRequestData *)fromList:(NSArray *)list { FWFNSUrlRequestData *pigeonResult = [[FWFNSUrlRequestData alloc] init]; - pigeonResult.url = GetNullableObject(dict, @"url"); + pigeonResult.url = GetNullableObjectAtIndex(list, 0); NSAssert(pigeonResult.url != nil, @""); - pigeonResult.httpMethod = GetNullableObject(dict, @"httpMethod"); - pigeonResult.httpBody = GetNullableObject(dict, @"httpBody"); - pigeonResult.allHttpHeaderFields = GetNullableObject(dict, @"allHttpHeaderFields"); + pigeonResult.httpMethod = GetNullableObjectAtIndex(list, 1); + pigeonResult.httpBody = GetNullableObjectAtIndex(list, 2); + pigeonResult.allHttpHeaderFields = GetNullableObjectAtIndex(list, 3); NSAssert(pigeonResult.allHttpHeaderFields != nil, @""); return pigeonResult; } -+ (nullable FWFNSUrlRequestData *)nullableFromMap:(NSDictionary *)dict { - return (dict) ? [FWFNSUrlRequestData fromMap:dict] : nil; ++ (nullable FWFNSUrlRequestData *)nullableFromList:(NSArray *)list { + return (list) ? [FWFNSUrlRequestData fromList:list] : nil; } -- (NSDictionary *)toMap { - return @{ - @"url" : (self.url ?: [NSNull null]), - @"httpMethod" : (self.httpMethod ?: [NSNull null]), - @"httpBody" : (self.httpBody ?: [NSNull null]), - @"allHttpHeaderFields" : (self.allHttpHeaderFields ?: [NSNull null]), - }; +- (NSArray *)toList { + return @[ + (self.url ?: [NSNull null]), + (self.httpMethod ?: [NSNull null]), + (self.httpBody ?: [NSNull null]), + (self.allHttpHeaderFields ?: [NSNull null]), + ]; } @end @@ -306,53 +296,57 @@ + (instancetype)makeWithSource:(NSString *)source pigeonResult.isMainFrameOnly = isMainFrameOnly; return pigeonResult; } -+ (FWFWKUserScriptData *)fromMap:(NSDictionary *)dict { ++ (FWFWKUserScriptData *)fromList:(NSArray *)list { FWFWKUserScriptData *pigeonResult = [[FWFWKUserScriptData alloc] init]; - pigeonResult.source = GetNullableObject(dict, @"source"); + pigeonResult.source = GetNullableObjectAtIndex(list, 0); NSAssert(pigeonResult.source != nil, @""); - pigeonResult.injectionTime = [FWFWKUserScriptInjectionTimeEnumData - nullableFromMap:GetNullableObject(dict, @"injectionTime")]; - pigeonResult.isMainFrameOnly = GetNullableObject(dict, @"isMainFrameOnly"); + pigeonResult.injectionTime = + [FWFWKUserScriptInjectionTimeEnumData nullableFromList:(GetNullableObjectAtIndex(list, 1))]; + pigeonResult.isMainFrameOnly = GetNullableObjectAtIndex(list, 2); NSAssert(pigeonResult.isMainFrameOnly != nil, @""); return pigeonResult; } -+ (nullable FWFWKUserScriptData *)nullableFromMap:(NSDictionary *)dict { - return (dict) ? [FWFWKUserScriptData fromMap:dict] : nil; ++ (nullable FWFWKUserScriptData *)nullableFromList:(NSArray *)list { + return (list) ? [FWFWKUserScriptData fromList:list] : nil; } -- (NSDictionary *)toMap { - return @{ - @"source" : (self.source ?: [NSNull null]), - @"injectionTime" : (self.injectionTime ? [self.injectionTime toMap] : [NSNull null]), - @"isMainFrameOnly" : (self.isMainFrameOnly ?: [NSNull null]), - }; +- (NSArray *)toList { + return @[ + (self.source ?: [NSNull null]), + (self.injectionTime ? [self.injectionTime toList] : [NSNull null]), + (self.isMainFrameOnly ?: [NSNull null]), + ]; } @end @implementation FWFWKNavigationActionData + (instancetype)makeWithRequest:(FWFNSUrlRequestData *)request - targetFrame:(FWFWKFrameInfoData *)targetFrame { + targetFrame:(FWFWKFrameInfoData *)targetFrame + navigationType:(FWFWKNavigationType)navigationType { FWFWKNavigationActionData *pigeonResult = [[FWFWKNavigationActionData alloc] init]; pigeonResult.request = request; pigeonResult.targetFrame = targetFrame; + pigeonResult.navigationType = navigationType; return pigeonResult; } -+ (FWFWKNavigationActionData *)fromMap:(NSDictionary *)dict { ++ (FWFWKNavigationActionData *)fromList:(NSArray *)list { FWFWKNavigationActionData *pigeonResult = [[FWFWKNavigationActionData alloc] init]; - pigeonResult.request = [FWFNSUrlRequestData nullableFromMap:GetNullableObject(dict, @"request")]; + pigeonResult.request = [FWFNSUrlRequestData nullableFromList:(GetNullableObjectAtIndex(list, 0))]; NSAssert(pigeonResult.request != nil, @""); pigeonResult.targetFrame = - [FWFWKFrameInfoData nullableFromMap:GetNullableObject(dict, @"targetFrame")]; + [FWFWKFrameInfoData nullableFromList:(GetNullableObjectAtIndex(list, 1))]; NSAssert(pigeonResult.targetFrame != nil, @""); + pigeonResult.navigationType = [GetNullableObjectAtIndex(list, 2) integerValue]; return pigeonResult; } -+ (nullable FWFWKNavigationActionData *)nullableFromMap:(NSDictionary *)dict { - return (dict) ? [FWFWKNavigationActionData fromMap:dict] : nil; ++ (nullable FWFWKNavigationActionData *)nullableFromList:(NSArray *)list { + return (list) ? [FWFWKNavigationActionData fromList:list] : nil; } -- (NSDictionary *)toMap { - return @{ - @"request" : (self.request ? [self.request toMap] : [NSNull null]), - @"targetFrame" : (self.targetFrame ? [self.targetFrame toMap] : [NSNull null]), - }; +- (NSArray *)toList { + return @[ + (self.request ? [self.request toList] : [NSNull null]), + (self.targetFrame ? [self.targetFrame toList] : [NSNull null]), + @(self.navigationType), + ]; } @end @@ -362,19 +356,19 @@ + (instancetype)makeWithIsMainFrame:(NSNumber *)isMainFrame { pigeonResult.isMainFrame = isMainFrame; return pigeonResult; } -+ (FWFWKFrameInfoData *)fromMap:(NSDictionary *)dict { ++ (FWFWKFrameInfoData *)fromList:(NSArray *)list { FWFWKFrameInfoData *pigeonResult = [[FWFWKFrameInfoData alloc] init]; - pigeonResult.isMainFrame = GetNullableObject(dict, @"isMainFrame"); + pigeonResult.isMainFrame = GetNullableObjectAtIndex(list, 0); NSAssert(pigeonResult.isMainFrame != nil, @""); return pigeonResult; } -+ (nullable FWFWKFrameInfoData *)nullableFromMap:(NSDictionary *)dict { - return (dict) ? [FWFWKFrameInfoData fromMap:dict] : nil; ++ (nullable FWFWKFrameInfoData *)nullableFromList:(NSArray *)list { + return (list) ? [FWFWKFrameInfoData fromList:list] : nil; } -- (NSDictionary *)toMap { - return @{ - @"isMainFrame" : (self.isMainFrame ?: [NSNull null]), - }; +- (NSArray *)toList { + return @[ + (self.isMainFrame ?: [NSNull null]), + ]; } @end @@ -388,25 +382,25 @@ + (instancetype)makeWithCode:(NSNumber *)code pigeonResult.localizedDescription = localizedDescription; return pigeonResult; } -+ (FWFNSErrorData *)fromMap:(NSDictionary *)dict { ++ (FWFNSErrorData *)fromList:(NSArray *)list { FWFNSErrorData *pigeonResult = [[FWFNSErrorData alloc] init]; - pigeonResult.code = GetNullableObject(dict, @"code"); + pigeonResult.code = GetNullableObjectAtIndex(list, 0); NSAssert(pigeonResult.code != nil, @""); - pigeonResult.domain = GetNullableObject(dict, @"domain"); + pigeonResult.domain = GetNullableObjectAtIndex(list, 1); NSAssert(pigeonResult.domain != nil, @""); - pigeonResult.localizedDescription = GetNullableObject(dict, @"localizedDescription"); + pigeonResult.localizedDescription = GetNullableObjectAtIndex(list, 2); NSAssert(pigeonResult.localizedDescription != nil, @""); return pigeonResult; } -+ (nullable FWFNSErrorData *)nullableFromMap:(NSDictionary *)dict { - return (dict) ? [FWFNSErrorData fromMap:dict] : nil; ++ (nullable FWFNSErrorData *)nullableFromList:(NSArray *)list { + return (list) ? [FWFNSErrorData fromList:list] : nil; } -- (NSDictionary *)toMap { - return @{ - @"code" : (self.code ?: [NSNull null]), - @"domain" : (self.domain ?: [NSNull null]), - @"localizedDescription" : (self.localizedDescription ?: [NSNull null]), - }; +- (NSArray *)toList { + return @[ + (self.code ?: [NSNull null]), + (self.domain ?: [NSNull null]), + (self.localizedDescription ?: [NSNull null]), + ]; } @end @@ -417,21 +411,21 @@ + (instancetype)makeWithName:(NSString *)name body:(id)body { pigeonResult.body = body; return pigeonResult; } -+ (FWFWKScriptMessageData *)fromMap:(NSDictionary *)dict { ++ (FWFWKScriptMessageData *)fromList:(NSArray *)list { FWFWKScriptMessageData *pigeonResult = [[FWFWKScriptMessageData alloc] init]; - pigeonResult.name = GetNullableObject(dict, @"name"); + pigeonResult.name = GetNullableObjectAtIndex(list, 0); NSAssert(pigeonResult.name != nil, @""); - pigeonResult.body = GetNullableObject(dict, @"body"); + pigeonResult.body = GetNullableObjectAtIndex(list, 1); return pigeonResult; } -+ (nullable FWFWKScriptMessageData *)nullableFromMap:(NSDictionary *)dict { - return (dict) ? [FWFWKScriptMessageData fromMap:dict] : nil; ++ (nullable FWFWKScriptMessageData *)nullableFromList:(NSArray *)list { + return (list) ? [FWFWKScriptMessageData fromList:list] : nil; } -- (NSDictionary *)toMap { - return @{ - @"name" : (self.name ?: [NSNull null]), - @"body" : (self.body ?: [NSNull null]), - }; +- (NSArray *)toList { + return @[ + (self.name ?: [NSNull null]), + (self.body ?: [NSNull null]), + ]; } @end @@ -443,22 +437,22 @@ + (instancetype)makeWithPropertyKeys:(NSArray *FWFWKWebsiteDataStoreHostApiGetCodec() { - static dispatch_once_t sPred = 0; static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ FWFWKWebsiteDataStoreHostApiCodecReaderWriter *readerWriter = [[FWFWKWebsiteDataStoreHostApiCodecReaderWriter alloc] init]; @@ -591,35 +585,9 @@ void FWFWKWebsiteDataStoreHostApiSetup(id binaryMessenge } } } -@interface FWFUIViewHostApiCodecReader : FlutterStandardReader -@end -@implementation FWFUIViewHostApiCodecReader -@end - -@interface FWFUIViewHostApiCodecWriter : FlutterStandardWriter -@end -@implementation FWFUIViewHostApiCodecWriter -@end - -@interface FWFUIViewHostApiCodecReaderWriter : FlutterStandardReaderWriter -@end -@implementation FWFUIViewHostApiCodecReaderWriter -- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { - return [[FWFUIViewHostApiCodecWriter alloc] initWithData:data]; -} -- (FlutterStandardReader *)readerWithData:(NSData *)data { - return [[FWFUIViewHostApiCodecReader alloc] initWithData:data]; -} -@end - NSObject *FWFUIViewHostApiGetCodec() { - static dispatch_once_t sPred = 0; static FlutterStandardMessageCodec *sSharedObject = nil; - dispatch_once(&sPred, ^{ - FWFUIViewHostApiCodecReaderWriter *readerWriter = - [[FWFUIViewHostApiCodecReaderWriter alloc] init]; - sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; - }); + sSharedObject = [FlutterStandardMessageCodec sharedInstance]; return sSharedObject; } @@ -671,35 +639,9 @@ void FWFUIViewHostApiSetup(id binaryMessenger, } } } -@interface FWFUIScrollViewHostApiCodecReader : FlutterStandardReader -@end -@implementation FWFUIScrollViewHostApiCodecReader -@end - -@interface FWFUIScrollViewHostApiCodecWriter : FlutterStandardWriter -@end -@implementation FWFUIScrollViewHostApiCodecWriter -@end - -@interface FWFUIScrollViewHostApiCodecReaderWriter : FlutterStandardReaderWriter -@end -@implementation FWFUIScrollViewHostApiCodecReaderWriter -- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { - return [[FWFUIScrollViewHostApiCodecWriter alloc] initWithData:data]; -} -- (FlutterStandardReader *)readerWithData:(NSData *)data { - return [[FWFUIScrollViewHostApiCodecReader alloc] initWithData:data]; -} -@end - NSObject *FWFUIScrollViewHostApiGetCodec() { - static dispatch_once_t sPred = 0; static FlutterStandardMessageCodec *sSharedObject = nil; - dispatch_once(&sPred, ^{ - FWFUIScrollViewHostApiCodecReaderWriter *readerWriter = - [[FWFUIScrollViewHostApiCodecReaderWriter alloc] init]; - sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; - }); + sSharedObject = [FlutterStandardMessageCodec sharedInstance]; return sSharedObject; } @@ -809,7 +751,7 @@ @implementation FWFWKWebViewConfigurationHostApiCodecReader - (nullable id)readValueOfType:(UInt8)type { switch (type) { case 128: - return [FWFWKAudiovisualMediaTypeEnumData fromMap:[self readValue]]; + return [FWFWKAudiovisualMediaTypeEnumData fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -823,7 +765,7 @@ @implementation FWFWKWebViewConfigurationHostApiCodecWriter - (void)writeValue:(id)value { if ([value isKindOfClass:[FWFWKAudiovisualMediaTypeEnumData class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -842,8 +784,8 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { @end NSObject *FWFWKWebViewConfigurationHostApiGetCodec() { - static dispatch_once_t sPred = 0; static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ FWFWKWebViewConfigurationHostApiCodecReaderWriter *readerWriter = [[FWFWKWebViewConfigurationHostApiCodecReaderWriter alloc] init]; @@ -956,35 +898,9 @@ void FWFWKWebViewConfigurationHostApiSetup(id binaryMess } } } -@interface FWFWKWebViewConfigurationFlutterApiCodecReader : FlutterStandardReader -@end -@implementation FWFWKWebViewConfigurationFlutterApiCodecReader -@end - -@interface FWFWKWebViewConfigurationFlutterApiCodecWriter : FlutterStandardWriter -@end -@implementation FWFWKWebViewConfigurationFlutterApiCodecWriter -@end - -@interface FWFWKWebViewConfigurationFlutterApiCodecReaderWriter : FlutterStandardReaderWriter -@end -@implementation FWFWKWebViewConfigurationFlutterApiCodecReaderWriter -- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { - return [[FWFWKWebViewConfigurationFlutterApiCodecWriter alloc] initWithData:data]; -} -- (FlutterStandardReader *)readerWithData:(NSData *)data { - return [[FWFWKWebViewConfigurationFlutterApiCodecReader alloc] initWithData:data]; -} -@end - NSObject *FWFWKWebViewConfigurationFlutterApiGetCodec() { - static dispatch_once_t sPred = 0; static FlutterStandardMessageCodec *sSharedObject = nil; - dispatch_once(&sPred, ^{ - FWFWKWebViewConfigurationFlutterApiCodecReaderWriter *readerWriter = - [[FWFWKWebViewConfigurationFlutterApiCodecReaderWriter alloc] init]; - sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; - }); + sSharedObject = [FlutterStandardMessageCodec sharedInstance]; return sSharedObject; } @@ -1019,10 +935,10 @@ @implementation FWFWKUserContentControllerHostApiCodecReader - (nullable id)readValueOfType:(UInt8)type { switch (type) { case 128: - return [FWFWKUserScriptData fromMap:[self readValue]]; + return [FWFWKUserScriptData fromList:[self readValue]]; case 129: - return [FWFWKUserScriptInjectionTimeEnumData fromMap:[self readValue]]; + return [FWFWKUserScriptInjectionTimeEnumData fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -1036,10 +952,10 @@ @implementation FWFWKUserContentControllerHostApiCodecWriter - (void)writeValue:(id)value { if ([value isKindOfClass:[FWFWKUserScriptData class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKUserScriptInjectionTimeEnumData class]]) { [self writeByte:129]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -1058,8 +974,8 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { @end NSObject *FWFWKUserContentControllerHostApiGetCodec() { - static dispatch_once_t sPred = 0; static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ FWFWKUserContentControllerHostApiCodecReaderWriter *readerWriter = [[FWFWKUserContentControllerHostApiCodecReaderWriter alloc] init]; @@ -1223,35 +1139,9 @@ void FWFWKUserContentControllerHostApiSetup(id binaryMes } } } -@interface FWFWKPreferencesHostApiCodecReader : FlutterStandardReader -@end -@implementation FWFWKPreferencesHostApiCodecReader -@end - -@interface FWFWKPreferencesHostApiCodecWriter : FlutterStandardWriter -@end -@implementation FWFWKPreferencesHostApiCodecWriter -@end - -@interface FWFWKPreferencesHostApiCodecReaderWriter : FlutterStandardReaderWriter -@end -@implementation FWFWKPreferencesHostApiCodecReaderWriter -- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { - return [[FWFWKPreferencesHostApiCodecWriter alloc] initWithData:data]; -} -- (FlutterStandardReader *)readerWithData:(NSData *)data { - return [[FWFWKPreferencesHostApiCodecReader alloc] initWithData:data]; -} -@end - NSObject *FWFWKPreferencesHostApiGetCodec() { - static dispatch_once_t sPred = 0; static FlutterStandardMessageCodec *sSharedObject = nil; - dispatch_once(&sPred, ^{ - FWFWKPreferencesHostApiCodecReaderWriter *readerWriter = - [[FWFWKPreferencesHostApiCodecReaderWriter alloc] init]; - sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; - }); + sSharedObject = [FlutterStandardMessageCodec sharedInstance]; return sSharedObject; } @@ -1309,35 +1199,9 @@ void FWFWKPreferencesHostApiSetup(id binaryMessenger, } } } -@interface FWFWKScriptMessageHandlerHostApiCodecReader : FlutterStandardReader -@end -@implementation FWFWKScriptMessageHandlerHostApiCodecReader -@end - -@interface FWFWKScriptMessageHandlerHostApiCodecWriter : FlutterStandardWriter -@end -@implementation FWFWKScriptMessageHandlerHostApiCodecWriter -@end - -@interface FWFWKScriptMessageHandlerHostApiCodecReaderWriter : FlutterStandardReaderWriter -@end -@implementation FWFWKScriptMessageHandlerHostApiCodecReaderWriter -- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { - return [[FWFWKScriptMessageHandlerHostApiCodecWriter alloc] initWithData:data]; -} -- (FlutterStandardReader *)readerWithData:(NSData *)data { - return [[FWFWKScriptMessageHandlerHostApiCodecReader alloc] initWithData:data]; -} -@end - NSObject *FWFWKScriptMessageHandlerHostApiGetCodec() { - static dispatch_once_t sPred = 0; static FlutterStandardMessageCodec *sSharedObject = nil; - dispatch_once(&sPred, ^{ - FWFWKScriptMessageHandlerHostApiCodecReaderWriter *readerWriter = - [[FWFWKScriptMessageHandlerHostApiCodecReaderWriter alloc] init]; - sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; - }); + sSharedObject = [FlutterStandardMessageCodec sharedInstance]; return sSharedObject; } @@ -1371,7 +1235,7 @@ @implementation FWFWKScriptMessageHandlerFlutterApiCodecReader - (nullable id)readValueOfType:(UInt8)type { switch (type) { case 128: - return [FWFWKScriptMessageData fromMap:[self readValue]]; + return [FWFWKScriptMessageData fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -1385,7 +1249,7 @@ @implementation FWFWKScriptMessageHandlerFlutterApiCodecWriter - (void)writeValue:(id)value { if ([value isKindOfClass:[FWFWKScriptMessageData class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -1404,8 +1268,8 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { @end NSObject *FWFWKScriptMessageHandlerFlutterApiGetCodec() { - static dispatch_once_t sPred = 0; static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ FWFWKScriptMessageHandlerFlutterApiCodecReaderWriter *readerWriter = [[FWFWKScriptMessageHandlerFlutterApiCodecReaderWriter alloc] init]; @@ -1446,35 +1310,9 @@ - (void)didReceiveScriptMessageForHandlerWithIdentifier:(NSNumber *)arg_identifi }]; } @end -@interface FWFWKNavigationDelegateHostApiCodecReader : FlutterStandardReader -@end -@implementation FWFWKNavigationDelegateHostApiCodecReader -@end - -@interface FWFWKNavigationDelegateHostApiCodecWriter : FlutterStandardWriter -@end -@implementation FWFWKNavigationDelegateHostApiCodecWriter -@end - -@interface FWFWKNavigationDelegateHostApiCodecReaderWriter : FlutterStandardReaderWriter -@end -@implementation FWFWKNavigationDelegateHostApiCodecReaderWriter -- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { - return [[FWFWKNavigationDelegateHostApiCodecWriter alloc] initWithData:data]; -} -- (FlutterStandardReader *)readerWithData:(NSData *)data { - return [[FWFWKNavigationDelegateHostApiCodecReader alloc] initWithData:data]; -} -@end - NSObject *FWFWKNavigationDelegateHostApiGetCodec() { - static dispatch_once_t sPred = 0; static FlutterStandardMessageCodec *sSharedObject = nil; - dispatch_once(&sPred, ^{ - FWFWKNavigationDelegateHostApiCodecReaderWriter *readerWriter = - [[FWFWKNavigationDelegateHostApiCodecReaderWriter alloc] init]; - sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; - }); + sSharedObject = [FlutterStandardMessageCodec sharedInstance]; return sSharedObject; } @@ -1508,19 +1346,19 @@ @implementation FWFWKNavigationDelegateFlutterApiCodecReader - (nullable id)readValueOfType:(UInt8)type { switch (type) { case 128: - return [FWFNSErrorData fromMap:[self readValue]]; + return [FWFNSErrorData fromList:[self readValue]]; case 129: - return [FWFNSUrlRequestData fromMap:[self readValue]]; + return [FWFNSUrlRequestData fromList:[self readValue]]; case 130: - return [FWFWKFrameInfoData fromMap:[self readValue]]; + return [FWFWKFrameInfoData fromList:[self readValue]]; case 131: - return [FWFWKNavigationActionData fromMap:[self readValue]]; + return [FWFWKNavigationActionData fromList:[self readValue]]; case 132: - return [FWFWKNavigationActionPolicyEnumData fromMap:[self readValue]]; + return [FWFWKNavigationActionPolicyEnumData fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -1534,19 +1372,19 @@ @implementation FWFWKNavigationDelegateFlutterApiCodecWriter - (void)writeValue:(id)value { if ([value isKindOfClass:[FWFNSErrorData class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFNSUrlRequestData class]]) { [self writeByte:129]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKFrameInfoData class]]) { [self writeByte:130]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKNavigationActionData class]]) { [self writeByte:131]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKNavigationActionPolicyEnumData class]]) { [self writeByte:132]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -1565,8 +1403,8 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { @end NSObject *FWFWKNavigationDelegateFlutterApiGetCodec() { - static dispatch_once_t sPred = 0; static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ FWFWKNavigationDelegateFlutterApiCodecReaderWriter *readerWriter = [[FWFWKNavigationDelegateFlutterApiCodecReaderWriter alloc] init]; @@ -1702,7 +1540,7 @@ @implementation FWFNSObjectHostApiCodecReader - (nullable id)readValueOfType:(UInt8)type { switch (type) { case 128: - return [FWFNSKeyValueObservingOptionsEnumData fromMap:[self readValue]]; + return [FWFNSKeyValueObservingOptionsEnumData fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -1716,7 +1554,7 @@ @implementation FWFNSObjectHostApiCodecWriter - (void)writeValue:(id)value { if ([value isKindOfClass:[FWFNSKeyValueObservingOptionsEnumData class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -1735,8 +1573,8 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { @end NSObject *FWFNSObjectHostApiGetCodec() { - static dispatch_once_t sPred = 0; static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ FWFNSObjectHostApiCodecReaderWriter *readerWriter = [[FWFNSObjectHostApiCodecReaderWriter alloc] init]; @@ -1835,46 +1673,46 @@ @implementation FWFNSObjectFlutterApiCodecReader - (nullable id)readValueOfType:(UInt8)type { switch (type) { case 128: - return [FWFNSErrorData fromMap:[self readValue]]; + return [FWFNSErrorData fromList:[self readValue]]; case 129: - return [FWFNSHttpCookieData fromMap:[self readValue]]; + return [FWFNSHttpCookieData fromList:[self readValue]]; case 130: - return [FWFNSHttpCookiePropertyKeyEnumData fromMap:[self readValue]]; + return [FWFNSHttpCookiePropertyKeyEnumData fromList:[self readValue]]; case 131: - return [FWFNSKeyValueChangeKeyEnumData fromMap:[self readValue]]; + return [FWFNSKeyValueChangeKeyEnumData fromList:[self readValue]]; case 132: - return [FWFNSKeyValueObservingOptionsEnumData fromMap:[self readValue]]; + return [FWFNSKeyValueObservingOptionsEnumData fromList:[self readValue]]; case 133: - return [FWFNSUrlRequestData fromMap:[self readValue]]; + return [FWFNSUrlRequestData fromList:[self readValue]]; case 134: - return [FWFWKAudiovisualMediaTypeEnumData fromMap:[self readValue]]; + return [FWFWKAudiovisualMediaTypeEnumData fromList:[self readValue]]; case 135: - return [FWFWKFrameInfoData fromMap:[self readValue]]; + return [FWFWKFrameInfoData fromList:[self readValue]]; case 136: - return [FWFWKNavigationActionData fromMap:[self readValue]]; + return [FWFWKNavigationActionData fromList:[self readValue]]; case 137: - return [FWFWKNavigationActionPolicyEnumData fromMap:[self readValue]]; + return [FWFWKNavigationActionPolicyEnumData fromList:[self readValue]]; case 138: - return [FWFWKScriptMessageData fromMap:[self readValue]]; + return [FWFWKScriptMessageData fromList:[self readValue]]; case 139: - return [FWFWKUserScriptData fromMap:[self readValue]]; + return [FWFWKUserScriptData fromList:[self readValue]]; case 140: - return [FWFWKUserScriptInjectionTimeEnumData fromMap:[self readValue]]; + return [FWFWKUserScriptInjectionTimeEnumData fromList:[self readValue]]; case 141: - return [FWFWKWebsiteDataTypeEnumData fromMap:[self readValue]]; + return [FWFWKWebsiteDataTypeEnumData fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -1888,46 +1726,46 @@ @implementation FWFNSObjectFlutterApiCodecWriter - (void)writeValue:(id)value { if ([value isKindOfClass:[FWFNSErrorData class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFNSHttpCookieData class]]) { [self writeByte:129]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFNSHttpCookiePropertyKeyEnumData class]]) { [self writeByte:130]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFNSKeyValueChangeKeyEnumData class]]) { [self writeByte:131]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFNSKeyValueObservingOptionsEnumData class]]) { [self writeByte:132]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFNSUrlRequestData class]]) { [self writeByte:133]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKAudiovisualMediaTypeEnumData class]]) { [self writeByte:134]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKFrameInfoData class]]) { [self writeByte:135]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKNavigationActionData class]]) { [self writeByte:136]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKNavigationActionPolicyEnumData class]]) { [self writeByte:137]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKScriptMessageData class]]) { [self writeByte:138]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKUserScriptData class]]) { [self writeByte:139]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKUserScriptInjectionTimeEnumData class]]) { [self writeByte:140]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKWebsiteDataTypeEnumData class]]) { [self writeByte:141]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -1946,8 +1784,8 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { @end NSObject *FWFNSObjectFlutterApiGetCodec() { - static dispatch_once_t sPred = 0; static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ FWFNSObjectFlutterApiCodecReaderWriter *readerWriter = [[FWFNSObjectFlutterApiCodecReaderWriter alloc] init]; @@ -2007,46 +1845,46 @@ @implementation FWFWKWebViewHostApiCodecReader - (nullable id)readValueOfType:(UInt8)type { switch (type) { case 128: - return [FWFNSErrorData fromMap:[self readValue]]; + return [FWFNSErrorData fromList:[self readValue]]; case 129: - return [FWFNSHttpCookieData fromMap:[self readValue]]; + return [FWFNSHttpCookieData fromList:[self readValue]]; case 130: - return [FWFNSHttpCookiePropertyKeyEnumData fromMap:[self readValue]]; + return [FWFNSHttpCookiePropertyKeyEnumData fromList:[self readValue]]; case 131: - return [FWFNSKeyValueChangeKeyEnumData fromMap:[self readValue]]; + return [FWFNSKeyValueChangeKeyEnumData fromList:[self readValue]]; case 132: - return [FWFNSKeyValueObservingOptionsEnumData fromMap:[self readValue]]; + return [FWFNSKeyValueObservingOptionsEnumData fromList:[self readValue]]; case 133: - return [FWFNSUrlRequestData fromMap:[self readValue]]; + return [FWFNSUrlRequestData fromList:[self readValue]]; case 134: - return [FWFWKAudiovisualMediaTypeEnumData fromMap:[self readValue]]; + return [FWFWKAudiovisualMediaTypeEnumData fromList:[self readValue]]; case 135: - return [FWFWKFrameInfoData fromMap:[self readValue]]; + return [FWFWKFrameInfoData fromList:[self readValue]]; case 136: - return [FWFWKNavigationActionData fromMap:[self readValue]]; + return [FWFWKNavigationActionData fromList:[self readValue]]; case 137: - return [FWFWKNavigationActionPolicyEnumData fromMap:[self readValue]]; + return [FWFWKNavigationActionPolicyEnumData fromList:[self readValue]]; case 138: - return [FWFWKScriptMessageData fromMap:[self readValue]]; + return [FWFWKScriptMessageData fromList:[self readValue]]; case 139: - return [FWFWKUserScriptData fromMap:[self readValue]]; + return [FWFWKUserScriptData fromList:[self readValue]]; case 140: - return [FWFWKUserScriptInjectionTimeEnumData fromMap:[self readValue]]; + return [FWFWKUserScriptInjectionTimeEnumData fromList:[self readValue]]; case 141: - return [FWFWKWebsiteDataTypeEnumData fromMap:[self readValue]]; + return [FWFWKWebsiteDataTypeEnumData fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -2060,46 +1898,46 @@ @implementation FWFWKWebViewHostApiCodecWriter - (void)writeValue:(id)value { if ([value isKindOfClass:[FWFNSErrorData class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFNSHttpCookieData class]]) { [self writeByte:129]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFNSHttpCookiePropertyKeyEnumData class]]) { [self writeByte:130]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFNSKeyValueChangeKeyEnumData class]]) { [self writeByte:131]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFNSKeyValueObservingOptionsEnumData class]]) { [self writeByte:132]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFNSUrlRequestData class]]) { [self writeByte:133]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKAudiovisualMediaTypeEnumData class]]) { [self writeByte:134]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKFrameInfoData class]]) { [self writeByte:135]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKNavigationActionData class]]) { [self writeByte:136]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKNavigationActionPolicyEnumData class]]) { [self writeByte:137]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKScriptMessageData class]]) { [self writeByte:138]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKUserScriptData class]]) { [self writeByte:139]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKUserScriptInjectionTimeEnumData class]]) { [self writeByte:140]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKWebsiteDataTypeEnumData class]]) { [self writeByte:141]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -2118,8 +1956,8 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { @end NSObject *FWFWKWebViewHostApiGetCodec() { - static dispatch_once_t sPred = 0; static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ FWFWKWebViewHostApiCodecReaderWriter *readerWriter = [[FWFWKWebViewHostApiCodecReaderWriter alloc] init]; @@ -2555,35 +2393,9 @@ void FWFWKWebViewHostApiSetup(id binaryMessenger, } } } -@interface FWFWKUIDelegateHostApiCodecReader : FlutterStandardReader -@end -@implementation FWFWKUIDelegateHostApiCodecReader -@end - -@interface FWFWKUIDelegateHostApiCodecWriter : FlutterStandardWriter -@end -@implementation FWFWKUIDelegateHostApiCodecWriter -@end - -@interface FWFWKUIDelegateHostApiCodecReaderWriter : FlutterStandardReaderWriter -@end -@implementation FWFWKUIDelegateHostApiCodecReaderWriter -- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { - return [[FWFWKUIDelegateHostApiCodecWriter alloc] initWithData:data]; -} -- (FlutterStandardReader *)readerWithData:(NSData *)data { - return [[FWFWKUIDelegateHostApiCodecReader alloc] initWithData:data]; -} -@end - NSObject *FWFWKUIDelegateHostApiGetCodec() { - static dispatch_once_t sPred = 0; static FlutterStandardMessageCodec *sSharedObject = nil; - dispatch_once(&sPred, ^{ - FWFWKUIDelegateHostApiCodecReaderWriter *readerWriter = - [[FWFWKUIDelegateHostApiCodecReaderWriter alloc] init]; - sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; - }); + sSharedObject = [FlutterStandardMessageCodec sharedInstance]; return sSharedObject; } @@ -2617,13 +2429,13 @@ @implementation FWFWKUIDelegateFlutterApiCodecReader - (nullable id)readValueOfType:(UInt8)type { switch (type) { case 128: - return [FWFNSUrlRequestData fromMap:[self readValue]]; + return [FWFNSUrlRequestData fromList:[self readValue]]; case 129: - return [FWFWKFrameInfoData fromMap:[self readValue]]; + return [FWFWKFrameInfoData fromList:[self readValue]]; case 130: - return [FWFWKNavigationActionData fromMap:[self readValue]]; + return [FWFWKNavigationActionData fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -2637,13 +2449,13 @@ @implementation FWFWKUIDelegateFlutterApiCodecWriter - (void)writeValue:(id)value { if ([value isKindOfClass:[FWFNSUrlRequestData class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKFrameInfoData class]]) { [self writeByte:129]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFWKNavigationActionData class]]) { [self writeByte:130]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -2662,8 +2474,8 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { @end NSObject *FWFWKUIDelegateFlutterApiGetCodec() { - static dispatch_once_t sPred = 0; static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ FWFWKUIDelegateFlutterApiCodecReaderWriter *readerWriter = [[FWFWKUIDelegateFlutterApiCodecReaderWriter alloc] init]; @@ -2709,10 +2521,10 @@ @implementation FWFWKHttpCookieStoreHostApiCodecReader - (nullable id)readValueOfType:(UInt8)type { switch (type) { case 128: - return [FWFNSHttpCookieData fromMap:[self readValue]]; + return [FWFNSHttpCookieData fromList:[self readValue]]; case 129: - return [FWFNSHttpCookiePropertyKeyEnumData fromMap:[self readValue]]; + return [FWFNSHttpCookiePropertyKeyEnumData fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -2726,10 +2538,10 @@ @implementation FWFWKHttpCookieStoreHostApiCodecWriter - (void)writeValue:(id)value { if ([value isKindOfClass:[FWFNSHttpCookieData class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FWFNSHttpCookiePropertyKeyEnumData class]]) { [self writeByte:129]; - [self writeValue:[value toMap]]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -2748,8 +2560,8 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { @end NSObject *FWFWKHttpCookieStoreHostApiGetCodec() { - static dispatch_once_t sPred = 0; static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ FWFWKHttpCookieStoreHostApiCodecReaderWriter *readerWriter = [[FWFWKHttpCookieStoreHostApiCodecReaderWriter alloc] init]; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIViewHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIViewHostApi.m index a990561c4fba..1e168d0a8fcb 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIViewHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIViewHostApi.m @@ -25,7 +25,7 @@ - (UIView *)viewForIdentifier:(NSNumber *)identifier { - (void)setBackgroundColorForViewWithIdentifier:(nonnull NSNumber *)identifier toValue:(nullable NSNumber *)color error:(FlutterError *_Nullable *_Nonnull)error { - if (!color) { + if (color == nil) { [[self viewForIdentifier:identifier] setBackgroundColor:nil]; } int colorInt = color.intValue; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewFlutterWKWebViewExternalAPI.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewFlutterWKWebViewExternalAPI.h new file mode 100644 index 000000000000..297f8c37ec3e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewFlutterWKWebViewExternalAPI.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. + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * App and package facing native API provided by the `webview_flutter_wkwebview` plugin. + * + * This class follows the convention of breaking changes of the Dart API, which means that any + * changes to the class that are not backwards compatible will only be made with a major version + * change of the plugin. Native code other than this external API does not follow breaking change + * conventions, so app or plugin clients should not use any other native APIs. + */ +@interface FWFWebViewFlutterWKWebViewExternalAPI : NSObject +/** + * Retrieves the `WKWebView` that is associated with `identifier`. + * + * See the Dart method `WebKitWebViewController.webViewIdentifier` to get the identifier of an + * underlying `WKWebView`. + * + * @param identifier The associated identifier of the `WebView`. + * @param registry The plugin registry the `FLTWebViewFlutterPlugin` should belong to. If + * the registry doesn't contain an attached instance of `FLTWebViewFlutterPlugin`, + * this method returns nil. + * @return The `WKWebView` associated with `identifier` or nil if a `WKWebView` instance associated + * with `identifier` could not be found. + */ ++ (nullable WKWebView *)webViewForIdentifier:(long)identifier + withPluginRegistry:(id)registry; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewFlutterWKWebViewExternalAPI.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewFlutterWKWebViewExternalAPI.m new file mode 100644 index 000000000000..4e5d6efeb129 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewFlutterWKWebViewExternalAPI.m @@ -0,0 +1,21 @@ +// 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 "FWFWebViewFlutterWKWebViewExternalAPI.h" +#import "FWFInstanceManager.h" + +@implementation FWFWebViewFlutterWKWebViewExternalAPI ++ (nullable WKWebView *)webViewForIdentifier:(long)identifier + withPluginRegistry:(id)registry { + FWFInstanceManager *instanceManager = + (FWFInstanceManager *)[registry valuePublishedByPlugin:@"FLTWebViewFlutterPlugin"]; + + id instance = [instanceManager instanceForIdentifier:identifier]; + if ([instance isKindOfClass:[WKWebView class]]) { + return instance; + } + + return nil; +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/webview-umbrella.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/webview-umbrella.h index dbcd876d15c9..b9ba942b4ed5 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/webview-umbrella.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/webview-umbrella.h @@ -17,5 +17,6 @@ #import #import #import +#import #import #import diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.pigeon.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart similarity index 70% rename from packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.pigeon.dart rename to packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart index 54bb3015af64..26f215e2684c 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart @@ -1,16 +1,18 @@ // 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 (v3.1.5), do not edit directly. +// Autogenerated from Pigeon (v4.2.13), 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 -// @dart = 2.12 +// 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 import 'dart:async'; -import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; -import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; +/// Mirror of NSKeyValueObservingOptions. +/// +/// See https://developer.apple.com/documentation/foundation/nskeyvalueobservingoptions?language=objc. enum NSKeyValueObservingOptionsEnum { newValue, oldValue, @@ -18,6 +20,9 @@ enum NSKeyValueObservingOptionsEnum { priorNotification, } +/// Mirror of NSKeyValueChange. +/// +/// See https://developer.apple.com/documentation/foundation/nskeyvaluechange?language=objc. enum NSKeyValueChangeEnum { setting, insertion, @@ -25,6 +30,9 @@ enum NSKeyValueChangeEnum { replacement, } +/// Mirror of NSKeyValueChangeKey. +/// +/// See https://developer.apple.com/documentation/foundation/nskeyvaluechangekey?language=objc. enum NSKeyValueChangeKeyEnum { indexes, kind, @@ -33,11 +41,17 @@ enum NSKeyValueChangeKeyEnum { oldValue, } +/// Mirror of WKUserScriptInjectionTime. +/// +/// See https://developer.apple.com/documentation/webkit/wkuserscriptinjectiontime?language=objc. enum WKUserScriptInjectionTimeEnum { atDocumentStart, atDocumentEnd, } +/// Mirror of WKAudiovisualMediaTypes. +/// +/// See [WKAudiovisualMediaTypes](https://developer.apple.com/documentation/webkit/wkaudiovisualmediatypes?language=objc). enum WKAudiovisualMediaTypeEnum { none, audio, @@ -45,6 +59,9 @@ enum WKAudiovisualMediaTypeEnum { all, } +/// Mirror of WKWebsiteDataTypes. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebsitedatarecord/data_store_record_types?language=objc. enum WKWebsiteDataTypeEnum { cookies, memoryCache, @@ -56,11 +73,17 @@ enum WKWebsiteDataTypeEnum { indexedDBDatabases, } +/// Mirror of WKNavigationActionPolicy. +/// +/// See https://developer.apple.com/documentation/webkit/wknavigationactionpolicy?language=objc. enum WKNavigationActionPolicyEnum { allow, cancel, } +/// Mirror of NSHTTPCookiePropertyKey. +/// +/// See https://developer.apple.com/documentation/foundation/nshttpcookiepropertykey. enum NSHttpCookiePropertyKeyEnum { comment, commentUrl, @@ -78,6 +101,42 @@ enum NSHttpCookiePropertyKeyEnum { version, } +/// An object that contains information about an action that causes navigation +/// to occur. +/// +/// Wraps [WKNavigationType](https://developer.apple.com/documentation/webkit/wknavigationaction?language=objc). +enum WKNavigationType { + /// A link activation. + /// + /// See https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypelinkactivated?language=objc. + linkActivated, + + /// A request to submit a form. + /// + /// See https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypeformsubmitted?language=objc. + submitted, + + /// A request for the frame’s next or previous item. + /// + /// See https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypebackforward?language=objc. + backForward, + + /// A request to reload the webpage. + /// + /// See https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypereload?language=objc. + reload, + + /// A request to resubmit a form. + /// + /// See https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypeformresubmitted?language=objc. + formResubmitted, + + /// A navigation request that originates for some other reason. + /// + /// See https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypeother?language=objc. + other, +} + class NSKeyValueObservingOptionsEnumData { NSKeyValueObservingOptionsEnumData({ required this.value, @@ -86,15 +145,15 @@ class NSKeyValueObservingOptionsEnumData { NSKeyValueObservingOptionsEnum value; Object encode() { - final Map pigeonMap = {}; - pigeonMap['value'] = value.index; - return pigeonMap; + return [ + value.index, + ]; } - static NSKeyValueObservingOptionsEnumData decode(Object message) { - final Map pigeonMap = message as Map; + static NSKeyValueObservingOptionsEnumData decode(Object result) { + result as List; return NSKeyValueObservingOptionsEnumData( - value: NSKeyValueObservingOptionsEnum.values[pigeonMap['value']! as int], + value: NSKeyValueObservingOptionsEnum.values[result[0]! as int], ); } } @@ -107,15 +166,15 @@ class NSKeyValueChangeKeyEnumData { NSKeyValueChangeKeyEnum value; Object encode() { - final Map pigeonMap = {}; - pigeonMap['value'] = value.index; - return pigeonMap; + return [ + value.index, + ]; } - static NSKeyValueChangeKeyEnumData decode(Object message) { - final Map pigeonMap = message as Map; + static NSKeyValueChangeKeyEnumData decode(Object result) { + result as List; return NSKeyValueChangeKeyEnumData( - value: NSKeyValueChangeKeyEnum.values[pigeonMap['value']! as int], + value: NSKeyValueChangeKeyEnum.values[result[0]! as int], ); } } @@ -128,15 +187,15 @@ class WKUserScriptInjectionTimeEnumData { WKUserScriptInjectionTimeEnum value; Object encode() { - final Map pigeonMap = {}; - pigeonMap['value'] = value.index; - return pigeonMap; + return [ + value.index, + ]; } - static WKUserScriptInjectionTimeEnumData decode(Object message) { - final Map pigeonMap = message as Map; + static WKUserScriptInjectionTimeEnumData decode(Object result) { + result as List; return WKUserScriptInjectionTimeEnumData( - value: WKUserScriptInjectionTimeEnum.values[pigeonMap['value']! as int], + value: WKUserScriptInjectionTimeEnum.values[result[0]! as int], ); } } @@ -149,15 +208,15 @@ class WKAudiovisualMediaTypeEnumData { WKAudiovisualMediaTypeEnum value; Object encode() { - final Map pigeonMap = {}; - pigeonMap['value'] = value.index; - return pigeonMap; + return [ + value.index, + ]; } - static WKAudiovisualMediaTypeEnumData decode(Object message) { - final Map pigeonMap = message as Map; + static WKAudiovisualMediaTypeEnumData decode(Object result) { + result as List; return WKAudiovisualMediaTypeEnumData( - value: WKAudiovisualMediaTypeEnum.values[pigeonMap['value']! as int], + value: WKAudiovisualMediaTypeEnum.values[result[0]! as int], ); } } @@ -170,15 +229,15 @@ class WKWebsiteDataTypeEnumData { WKWebsiteDataTypeEnum value; Object encode() { - final Map pigeonMap = {}; - pigeonMap['value'] = value.index; - return pigeonMap; + return [ + value.index, + ]; } - static WKWebsiteDataTypeEnumData decode(Object message) { - final Map pigeonMap = message as Map; + static WKWebsiteDataTypeEnumData decode(Object result) { + result as List; return WKWebsiteDataTypeEnumData( - value: WKWebsiteDataTypeEnum.values[pigeonMap['value']! as int], + value: WKWebsiteDataTypeEnum.values[result[0]! as int], ); } } @@ -191,15 +250,15 @@ class WKNavigationActionPolicyEnumData { WKNavigationActionPolicyEnum value; Object encode() { - final Map pigeonMap = {}; - pigeonMap['value'] = value.index; - return pigeonMap; + return [ + value.index, + ]; } - static WKNavigationActionPolicyEnumData decode(Object message) { - final Map pigeonMap = message as Map; + static WKNavigationActionPolicyEnumData decode(Object result) { + result as List; return WKNavigationActionPolicyEnumData( - value: WKNavigationActionPolicyEnum.values[pigeonMap['value']! as int], + value: WKNavigationActionPolicyEnum.values[result[0]! as int], ); } } @@ -212,19 +271,22 @@ class NSHttpCookiePropertyKeyEnumData { NSHttpCookiePropertyKeyEnum value; Object encode() { - final Map pigeonMap = {}; - pigeonMap['value'] = value.index; - return pigeonMap; + return [ + value.index, + ]; } - static NSHttpCookiePropertyKeyEnumData decode(Object message) { - final Map pigeonMap = message as Map; + static NSHttpCookiePropertyKeyEnumData decode(Object result) { + result as List; return NSHttpCookiePropertyKeyEnumData( - value: NSHttpCookiePropertyKeyEnum.values[pigeonMap['value']! as int], + value: NSHttpCookiePropertyKeyEnum.values[result[0]! as int], ); } } +/// Mirror of NSURLRequest. +/// +/// See https://developer.apple.com/documentation/foundation/nsurlrequest?language=objc. class NSUrlRequestData { NSUrlRequestData({ required this.url, @@ -234,32 +296,37 @@ class NSUrlRequestData { }); String url; + String? httpMethod; + Uint8List? httpBody; + Map allHttpHeaderFields; Object encode() { - final Map pigeonMap = {}; - pigeonMap['url'] = url; - pigeonMap['httpMethod'] = httpMethod; - pigeonMap['httpBody'] = httpBody; - pigeonMap['allHttpHeaderFields'] = allHttpHeaderFields; - return pigeonMap; + return [ + url, + httpMethod, + httpBody, + allHttpHeaderFields, + ]; } - static NSUrlRequestData decode(Object message) { - final Map pigeonMap = message as Map; + static NSUrlRequestData decode(Object result) { + result as List; return NSUrlRequestData( - url: pigeonMap['url']! as String, - httpMethod: pigeonMap['httpMethod'] as String?, - httpBody: pigeonMap['httpBody'] as Uint8List?, + url: result[0]! as String, + httpMethod: result[1] as String?, + httpBody: result[2] as Uint8List?, allHttpHeaderFields: - (pigeonMap['allHttpHeaderFields'] as Map?)! - .cast(), + (result[3] as Map?)!.cast(), ); } } +/// Mirror of WKUserScript. +/// +/// See https://developer.apple.com/documentation/webkit/wkuserscript?language=objc. class WKUserScriptData { WKUserScriptData({ required this.source, @@ -268,55 +335,69 @@ class WKUserScriptData { }); String source; + WKUserScriptInjectionTimeEnumData? injectionTime; + bool isMainFrameOnly; Object encode() { - final Map pigeonMap = {}; - pigeonMap['source'] = source; - pigeonMap['injectionTime'] = injectionTime?.encode(); - pigeonMap['isMainFrameOnly'] = isMainFrameOnly; - return pigeonMap; + return [ + source, + injectionTime?.encode(), + isMainFrameOnly, + ]; } - static WKUserScriptData decode(Object message) { - final Map pigeonMap = message as Map; + static WKUserScriptData decode(Object result) { + result as List; return WKUserScriptData( - source: pigeonMap['source']! as String, - injectionTime: pigeonMap['injectionTime'] != null + source: result[0]! as String, + injectionTime: result[1] != null ? WKUserScriptInjectionTimeEnumData.decode( - pigeonMap['injectionTime']!) + result[1]! as List) : null, - isMainFrameOnly: pigeonMap['isMainFrameOnly']! as bool, + isMainFrameOnly: result[2]! as bool, ); } } +/// Mirror of WKNavigationAction. +/// +/// See https://developer.apple.com/documentation/webkit/wknavigationaction. class WKNavigationActionData { WKNavigationActionData({ required this.request, required this.targetFrame, + required this.navigationType, }); NSUrlRequestData request; + WKFrameInfoData targetFrame; + WKNavigationType navigationType; + Object encode() { - final Map pigeonMap = {}; - pigeonMap['request'] = request.encode(); - pigeonMap['targetFrame'] = targetFrame.encode(); - return pigeonMap; + return [ + request.encode(), + targetFrame.encode(), + navigationType.index, + ]; } - static WKNavigationActionData decode(Object message) { - final Map pigeonMap = message as Map; + static WKNavigationActionData decode(Object result) { + result as List; return WKNavigationActionData( - request: NSUrlRequestData.decode(pigeonMap['request']!), - targetFrame: WKFrameInfoData.decode(pigeonMap['targetFrame']!), + request: NSUrlRequestData.decode(result[0]! as List), + targetFrame: WKFrameInfoData.decode(result[1]! as List), + navigationType: WKNavigationType.values[result[2]! as int], ); } } +/// Mirror of WKFrameInfo. +/// +/// See https://developer.apple.com/documentation/webkit/wkframeinfo?language=objc. class WKFrameInfoData { WKFrameInfoData({ required this.isMainFrame, @@ -325,19 +406,22 @@ class WKFrameInfoData { bool isMainFrame; Object encode() { - final Map pigeonMap = {}; - pigeonMap['isMainFrame'] = isMainFrame; - return pigeonMap; + return [ + isMainFrame, + ]; } - static WKFrameInfoData decode(Object message) { - final Map pigeonMap = message as Map; + static WKFrameInfoData decode(Object result) { + result as List; return WKFrameInfoData( - isMainFrame: pigeonMap['isMainFrame']! as bool, + isMainFrame: result[0]! as bool, ); } } +/// Mirror of NSError. +/// +/// See https://developer.apple.com/documentation/foundation/nserror?language=objc. class NSErrorData { NSErrorData({ required this.code, @@ -346,27 +430,32 @@ class NSErrorData { }); int code; + String domain; + String localizedDescription; Object encode() { - final Map pigeonMap = {}; - pigeonMap['code'] = code; - pigeonMap['domain'] = domain; - pigeonMap['localizedDescription'] = localizedDescription; - return pigeonMap; + return [ + code, + domain, + localizedDescription, + ]; } - static NSErrorData decode(Object message) { - final Map pigeonMap = message as Map; + static NSErrorData decode(Object result) { + result as List; return NSErrorData( - code: pigeonMap['code']! as int, - domain: pigeonMap['domain']! as String, - localizedDescription: pigeonMap['localizedDescription']! as String, + code: result[0]! as int, + domain: result[1]! as String, + localizedDescription: result[2]! as String, ); } } +/// Mirror of WKScriptMessage. +/// +/// See https://developer.apple.com/documentation/webkit/wkscriptmessage?language=objc. class WKScriptMessageData { WKScriptMessageData({ required this.name, @@ -374,24 +463,28 @@ class WKScriptMessageData { }); String name; + Object? body; Object encode() { - final Map pigeonMap = {}; - pigeonMap['name'] = name; - pigeonMap['body'] = body; - return pigeonMap; + return [ + name, + body, + ]; } - static WKScriptMessageData decode(Object message) { - final Map pigeonMap = message as Map; + static WKScriptMessageData decode(Object result) { + result as List; return WKScriptMessageData( - name: pigeonMap['name']! as String, - body: pigeonMap['body'] as Object?, + name: result[0]! as String, + body: result[1] as Object?, ); } } +/// Mirror of NSHttpCookieData. +/// +/// See https://developer.apple.com/documentation/foundation/nshttpcookie?language=objc. class NSHttpCookieData { NSHttpCookieData({ required this.propertyKeys, @@ -399,22 +492,22 @@ class NSHttpCookieData { }); List propertyKeys; + List propertyValues; Object encode() { - final Map pigeonMap = {}; - pigeonMap['propertyKeys'] = propertyKeys; - pigeonMap['propertyValues'] = propertyValues; - return pigeonMap; + return [ + propertyKeys, + propertyValues, + ]; } - static NSHttpCookieData decode(Object message) { - final Map pigeonMap = message as Map; + static NSHttpCookieData decode(Object result) { + result as List; return NSHttpCookieData( - propertyKeys: (pigeonMap['propertyKeys'] as List?)! + propertyKeys: (result[0] as List?)! .cast(), - propertyValues: - (pigeonMap['propertyValues'] as List?)!.cast(), + propertyValues: (result[1] as List?)!.cast(), ); } } @@ -443,13 +536,15 @@ class _WKWebsiteDataStoreHostApiCodec extends StandardMessageCodec { } } +/// Mirror of WKWebsiteDataStore. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebsitedatastore?language=objc. class WKWebsiteDataStoreHostApi { /// Constructor for [WKWebsiteDataStoreHostApi]. 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. WKWebsiteDataStoreHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _WKWebsiteDataStoreHostApiCodec(); @@ -460,21 +555,19 @@ class WKWebsiteDataStoreHostApi { 'dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createFromWebViewConfiguration', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel + final List? replyList = await channel .send([arg_identifier, arg_configurationIdentifier]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -486,20 +579,18 @@ class WKWebsiteDataStoreHostApi { 'dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createDefaultDataStore', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -513,68 +604,62 @@ class WKWebsiteDataStoreHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebsiteDataStoreHostApi.removeDataOfTypes', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel.send([ + final List? replyList = await channel.send([ arg_identifier, arg_dataTypes, arg_modificationTimeInSecondsSinceEpoch - ]) as Map?; - if (replyMap == null) { + ]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as bool?)!; + return (replyList[0] as bool?)!; } } } -class _UIViewHostApiCodec extends StandardMessageCodec { - const _UIViewHostApiCodec(); -} - +/// Mirror of UIView. +/// +/// See https://developer.apple.com/documentation/uikit/uiview?language=objc. class UIViewHostApi { /// Constructor for [UIViewHostApi]. 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. UIViewHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _UIViewHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future setBackgroundColor(int arg_identifier, int? arg_value) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UIViewHostApi.setBackgroundColor', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_identifier, arg_value]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_identifier, arg_value]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -585,20 +670,18 @@ class UIViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UIViewHostApi.setOpaque', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_identifier, arg_opaque]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_identifier, arg_opaque]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -606,41 +689,37 @@ class UIViewHostApi { } } -class _UIScrollViewHostApiCodec extends StandardMessageCodec { - const _UIScrollViewHostApiCodec(); -} - +/// Mirror of UIScrollView. +/// +/// See https://developer.apple.com/documentation/uikit/uiscrollview?language=objc. class UIScrollViewHostApi { /// Constructor for [UIScrollViewHostApi]. 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. UIScrollViewHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _UIScrollViewHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future createFromWebView( int arg_identifier, int arg_webViewIdentifier) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UIScrollViewHostApi.createFromWebView', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = + final List? replyList = await channel.send([arg_identifier, arg_webViewIdentifier]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -651,28 +730,26 @@ class UIScrollViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UIScrollViewHostApi.getContentOffset', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as List?)!.cast(); + return (replyList[0] as List?)!.cast(); } } @@ -680,21 +757,18 @@ class UIScrollViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UIScrollViewHostApi.scrollBy', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier, arg_x, arg_y]) - as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_identifier, arg_x, arg_y]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -706,21 +780,18 @@ class UIScrollViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UIScrollViewHostApi.setContentOffset', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier, arg_x, arg_y]) - as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_identifier, arg_x, arg_y]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -752,13 +823,15 @@ class _WKWebViewConfigurationHostApiCodec extends StandardMessageCodec { } } +/// Mirror of WKWebViewConfiguration. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebviewconfiguration?language=objc. class WKWebViewConfigurationHostApi { /// Constructor for [WKWebViewConfigurationHostApi]. 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. WKWebViewConfigurationHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = @@ -768,20 +841,18 @@ class WKWebViewConfigurationHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.create', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -794,21 +865,19 @@ class WKWebViewConfigurationHostApi { 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.createFromWebView', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = + final List? replyList = await channel.send([arg_identifier, arg_webViewIdentifier]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -821,20 +890,18 @@ class WKWebViewConfigurationHostApi { 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.setAllowsInlineMediaPlayback', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_identifier, arg_allow]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_identifier, arg_allow]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -847,20 +914,18 @@ class WKWebViewConfigurationHostApi { 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.setMediaTypesRequiringUserActionForPlayback', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_identifier, arg_types]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_identifier, arg_types]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -868,15 +933,14 @@ class WKWebViewConfigurationHostApi { } } -class _WKWebViewConfigurationFlutterApiCodec extends StandardMessageCodec { - const _WKWebViewConfigurationFlutterApiCodec(); -} - +/// Handles callbacks from an WKWebViewConfiguration instance. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebviewconfiguration?language=objc. abstract class WKWebViewConfigurationFlutterApi { - static const MessageCodec codec = - _WKWebViewConfigurationFlutterApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void create(int identifier); + static void setup(WKWebViewConfigurationFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -931,13 +995,15 @@ class _WKUserContentControllerHostApiCodec extends StandardMessageCodec { } } +/// Mirror of WKUserContentController. +/// +/// See https://developer.apple.com/documentation/webkit/wkusercontentcontroller?language=objc. class WKUserContentControllerHostApi { /// Constructor for [WKUserContentControllerHostApi]. 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. WKUserContentControllerHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = @@ -949,21 +1015,19 @@ class WKUserContentControllerHostApi { 'dev.flutter.pigeon.WKUserContentControllerHostApi.createFromWebViewConfiguration', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel + final List? replyList = await channel .send([arg_identifier, arg_configurationIdentifier]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -976,21 +1040,19 @@ class WKUserContentControllerHostApi { 'dev.flutter.pigeon.WKUserContentControllerHostApi.addScriptMessageHandler', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel + final List? replyList = await channel .send([arg_identifier, arg_handlerIdentifier, arg_name]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1003,20 +1065,18 @@ class WKUserContentControllerHostApi { 'dev.flutter.pigeon.WKUserContentControllerHostApi.removeScriptMessageHandler', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_identifier, arg_name]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_identifier, arg_name]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1028,20 +1088,18 @@ class WKUserContentControllerHostApi { 'dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllScriptMessageHandlers', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1054,21 +1112,18 @@ class WKUserContentControllerHostApi { 'dev.flutter.pigeon.WKUserContentControllerHostApi.addUserScript', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier, arg_userScript]) - as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_identifier, arg_userScript]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1080,20 +1135,18 @@ class WKUserContentControllerHostApi { 'dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllUserScripts', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1101,20 +1154,18 @@ class WKUserContentControllerHostApi { } } -class _WKPreferencesHostApiCodec extends StandardMessageCodec { - const _WKPreferencesHostApiCodec(); -} - +/// Mirror of WKUserPreferences. +/// +/// See https://developer.apple.com/documentation/webkit/wkpreferences?language=objc. class WKPreferencesHostApi { /// Constructor for [WKPreferencesHostApi]. 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. WKPreferencesHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _WKPreferencesHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future createFromWebViewConfiguration( int arg_identifier, int arg_configurationIdentifier) async { @@ -1122,21 +1173,19 @@ class WKPreferencesHostApi { 'dev.flutter.pigeon.WKPreferencesHostApi.createFromWebViewConfiguration', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel + final List? replyList = await channel .send([arg_identifier, arg_configurationIdentifier]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1148,20 +1197,18 @@ class WKPreferencesHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKPreferencesHostApi.setJavaScriptEnabled', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_identifier, arg_enabled]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_identifier, arg_enabled]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1169,40 +1216,35 @@ class WKPreferencesHostApi { } } -class _WKScriptMessageHandlerHostApiCodec extends StandardMessageCodec { - const _WKScriptMessageHandlerHostApiCodec(); -} - +/// Mirror of WKScriptMessageHandler. +/// +/// See https://developer.apple.com/documentation/webkit/wkscriptmessagehandler?language=objc. class WKScriptMessageHandlerHostApi { /// Constructor for [WKScriptMessageHandlerHostApi]. 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. WKScriptMessageHandlerHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = - _WKScriptMessageHandlerHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future create(int arg_identifier) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKScriptMessageHandlerHostApi.create', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1234,12 +1276,16 @@ class _WKScriptMessageHandlerFlutterApiCodec extends StandardMessageCodec { } } +/// Handles callbacks from an WKScriptMessageHandler instance. +/// +/// See https://developer.apple.com/documentation/webkit/wkscriptmessagehandler?language=objc. abstract class WKScriptMessageHandlerFlutterApi { static const MessageCodec codec = _WKScriptMessageHandlerFlutterApiCodec(); void didReceiveScriptMessage(int identifier, int userContentControllerIdentifier, WKScriptMessageData message); + static void setup(WKScriptMessageHandlerFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1273,40 +1319,35 @@ abstract class WKScriptMessageHandlerFlutterApi { } } -class _WKNavigationDelegateHostApiCodec extends StandardMessageCodec { - const _WKNavigationDelegateHostApiCodec(); -} - +/// Mirror of WKNavigationDelegate. +/// +/// See https://developer.apple.com/documentation/webkit/wknavigationdelegate?language=objc. class WKNavigationDelegateHostApi { /// Constructor for [WKNavigationDelegateHostApi]. 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. WKNavigationDelegateHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = - _WKNavigationDelegateHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future create(int arg_identifier) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKNavigationDelegateHostApi.create', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1362,23 +1403,32 @@ class _WKNavigationDelegateFlutterApiCodec extends StandardMessageCodec { } } +/// Handles callbacks from an WKNavigationDelegate instance. +/// +/// See https://developer.apple.com/documentation/webkit/wknavigationdelegate?language=objc. abstract class WKNavigationDelegateFlutterApi { static const MessageCodec codec = _WKNavigationDelegateFlutterApiCodec(); void didFinishNavigation(int identifier, int webViewIdentifier, String? url); + void didStartProvisionalNavigation( int identifier, int webViewIdentifier, String? url); + Future decidePolicyForNavigationAction( int identifier, int webViewIdentifier, WKNavigationActionData navigationAction); + void didFailNavigation( int identifier, int webViewIdentifier, NSErrorData error); + void didFailProvisionalNavigation( int identifier, int webViewIdentifier, NSErrorData error); + void webViewWebContentProcessDidTerminate( int identifier, int webViewIdentifier); + static void setup(WKNavigationDelegateFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1565,13 +1615,15 @@ class _NSObjectHostApiCodec extends StandardMessageCodec { } } +/// Mirror of NSObject. +/// +/// See https://developer.apple.com/documentation/objectivec/nsobject. class NSObjectHostApi { /// Constructor for [NSObjectHostApi]. 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. NSObjectHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _NSObjectHostApiCodec(); @@ -1580,20 +1632,18 @@ class NSObjectHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.NSObjectHostApi.dispose', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1608,24 +1658,22 @@ class NSObjectHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.NSObjectHostApi.addObserver', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel.send([ + final List? replyList = await channel.send([ arg_identifier, arg_observerIdentifier, arg_keyPath, arg_options - ]) as Map?; - if (replyMap == null) { + ]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1637,21 +1685,19 @@ class NSObjectHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.NSObjectHostApi.removeObserver', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel.send( + final List? replyList = await channel.send( [arg_identifier, arg_observerIdentifier, arg_keyPath]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1761,6 +1807,9 @@ class _NSObjectFlutterApiCodec extends StandardMessageCodec { } } +/// Handles callbacks from an NSObject instance. +/// +/// See https://developer.apple.com/documentation/objectivec/nsobject. abstract class NSObjectFlutterApi { static const MessageCodec codec = _NSObjectFlutterApiCodec(); @@ -1770,7 +1819,9 @@ abstract class NSObjectFlutterApi { int objectIdentifier, List changeKeys, List changeValues); + void dispose(int identifier); + static void setup(NSObjectFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1931,13 +1982,15 @@ class _WKWebViewHostApiCodec extends StandardMessageCodec { } } +/// Mirror of WKWebView. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebview?language=objc. class WKWebViewHostApi { /// Constructor for [WKWebViewHostApi]. 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. WKWebViewHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _WKWebViewHostApiCodec(); @@ -1947,21 +2000,19 @@ class WKWebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebViewHostApi.create', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel + final List? replyList = await channel .send([arg_identifier, arg_configurationIdentifier]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1973,21 +2024,19 @@ class WKWebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebViewHostApi.setUIDelegate', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = + final List? replyList = await channel.send([arg_identifier, arg_uiDelegateIdentifier]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1999,21 +2048,19 @@ class WKWebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebViewHostApi.setNavigationDelegate', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel + final List? replyList = await channel .send([arg_identifier, arg_navigationDelegateIdentifier]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2024,23 +2071,21 @@ class WKWebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebViewHostApi.getUrl', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { - return (replyMap['result'] as String?); + return (replyList[0] as String?); } } @@ -2048,28 +2093,26 @@ class WKWebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebViewHostApi.getEstimatedProgress', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as double?)!; + return (replyList[0] as double?)!; } } @@ -2078,20 +2121,18 @@ class WKWebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebViewHostApi.loadRequest', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_identifier, arg_request]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_identifier, arg_request]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2103,21 +2144,19 @@ class WKWebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebViewHostApi.loadHtmlString', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = + final List? replyList = await channel.send([arg_identifier, arg_string, arg_baseUrl]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2129,21 +2168,19 @@ class WKWebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebViewHostApi.loadFileUrl', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel + final List? replyList = await channel .send([arg_identifier, arg_url, arg_readAccessUrl]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2154,20 +2191,18 @@ class WKWebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebViewHostApi.loadFlutterAsset', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_identifier, arg_key]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_identifier, arg_key]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2178,28 +2213,26 @@ class WKWebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebViewHostApi.canGoBack', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as bool?)!; + return (replyList[0] as bool?)!; } } @@ -2207,28 +2240,26 @@ class WKWebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebViewHostApi.canGoForward', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as bool?)!; + return (replyList[0] as bool?)!; } } @@ -2236,20 +2267,18 @@ class WKWebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebViewHostApi.goBack', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2260,20 +2289,18 @@ class WKWebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebViewHostApi.goForward', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2284,20 +2311,18 @@ class WKWebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebViewHostApi.reload', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2308,23 +2333,21 @@ class WKWebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebViewHostApi.getTitle', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { - return (replyMap['result'] as String?); + return (replyList[0] as String?); } } @@ -2334,20 +2357,18 @@ class WKWebViewHostApi { 'dev.flutter.pigeon.WKWebViewHostApi.setAllowsBackForwardNavigationGestures', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_identifier, arg_allow]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_identifier, arg_allow]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2359,21 +2380,18 @@ class WKWebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebViewHostApi.setCustomUserAgent', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier, arg_userAgent]) - as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_identifier, arg_userAgent]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2385,61 +2403,55 @@ class WKWebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebViewHostApi.evaluateJavaScript', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = + final List? replyList = await channel.send([arg_identifier, arg_javaScriptString]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { - return (replyMap['result'] as Object?); + return (replyList[0] as Object?); } } } -class _WKUIDelegateHostApiCodec extends StandardMessageCodec { - const _WKUIDelegateHostApiCodec(); -} - +/// Mirror of WKUIDelegate. +/// +/// See https://developer.apple.com/documentation/webkit/wkuidelegate?language=objc. class WKUIDelegateHostApi { /// Constructor for [WKUIDelegateHostApi]. 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. WKUIDelegateHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _WKUIDelegateHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future create(int arg_identifier) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKUIDelegateHostApi.create', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2483,11 +2495,15 @@ class _WKUIDelegateFlutterApiCodec extends StandardMessageCodec { } } +/// Handles callbacks from an WKUIDelegate instance. +/// +/// See https://developer.apple.com/documentation/webkit/wkuidelegate?language=objc. abstract class WKUIDelegateFlutterApi { static const MessageCodec codec = _WKUIDelegateFlutterApiCodec(); void onCreateWebView(int identifier, int webViewIdentifier, int configurationIdentifier, WKNavigationActionData navigationAction); + static void setup(WKUIDelegateFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -2553,13 +2569,15 @@ class _WKHttpCookieStoreHostApiCodec extends StandardMessageCodec { } } +/// Mirror of WKHttpCookieStore. +/// +/// See https://developer.apple.com/documentation/webkit/wkhttpcookiestore?language=objc. class WKHttpCookieStoreHostApi { /// Constructor for [WKHttpCookieStoreHostApi]. 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. WKHttpCookieStoreHostApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _WKHttpCookieStoreHostApiCodec(); @@ -2570,21 +2588,19 @@ class WKHttpCookieStoreHostApi { 'dev.flutter.pigeon.WKHttpCookieStoreHostApi.createFromWebsiteDataStore', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel + final List? replyList = await channel .send([arg_identifier, arg_websiteDataStoreIdentifier]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2596,20 +2612,18 @@ class WKHttpCookieStoreHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKHttpCookieStoreHostApi.setCookie', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_identifier, arg_cookie]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_identifier, arg_cookie]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation_api_impls.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation_api_impls.dart index d2310e0a5df8..445e232bb0ac 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation_api_impls.dart @@ -6,7 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import '../common/instance_manager.dart'; -import '../common/web_kit.pigeon.dart'; +import '../common/web_kit.g.dart'; import 'foundation.dart'; Iterable diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit_webview_widget.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/legacy/web_kit_webview_widget.dart similarity index 98% rename from packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit_webview_widget.dart rename to packages/webview_flutter/webview_flutter_wkwebview/lib/src/legacy/web_kit_webview_widget.dart index 327210983ae2..4d10db96a291 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit_webview_widget.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/legacy/web_kit_webview_widget.dart @@ -9,11 +9,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:path/path.dart' as path; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +// ignore: implementation_imports +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; -import 'common/weak_reference_utils.dart'; -import 'foundation/foundation.dart'; -import 'web_kit/web_kit.dart'; +import '../common/weak_reference_utils.dart'; +import '../foundation/foundation.dart'; +import '../web_kit/web_kit.dart'; /// A [Widget] that displays a [WKWebView]. class WebKitWebViewWidget extends StatefulWidget { @@ -352,9 +353,9 @@ class WebKitWebViewPlatformController extends WebViewPlatformController { // unsupported. This also goes for `null` and `undefined` on iOS 14+. For // example, when running a void function. For ease of use, this specific // error is ignored when no return value is expected. - if (exception.details is! NSError || - exception.details.code != - WKErrorCode.javaScriptResultTypeIsUnsupported) { + final Object? details = exception.details; + if (details is! NSError || + details.code != WKErrorCode.javaScriptResultTypeIsUnsupported) { rethrow; } } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/legacy/webview_cupertino.dart similarity index 92% rename from packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.dart rename to packages/webview_flutter/webview_flutter_wkwebview/lib/src/legacy/webview_cupertino.dart index 616f00c59fa3..5ad959ca79be 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/legacy/webview_cupertino.dart @@ -8,9 +8,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +// ignore: implementation_imports +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; -import 'foundation/foundation.dart'; +import '../foundation/foundation.dart'; import 'web_kit_webview_widget.dart'; /// Builds an iOS webview. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/wkwebview_cookie_manager.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/legacy/wkwebview_cookie_manager.dart similarity index 89% rename from packages/webview_flutter/webview_flutter_wkwebview/lib/src/wkwebview_cookie_manager.dart rename to packages/webview_flutter/webview_flutter_wkwebview/lib/src/legacy/wkwebview_cookie_manager.dart index cdbf2620f968..59dce559f12c 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/wkwebview_cookie_manager.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/legacy/wkwebview_cookie_manager.dart @@ -2,10 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +// ignore: implementation_imports +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; -import 'foundation/foundation.dart'; -import 'web_kit/web_kit.dart'; +import '../foundation/foundation.dart'; +import '../web_kit/web_kit.dart'; /// Handles all cookie operations for the WebView platform. class WKWebViewCookieManager extends WebViewCookieManagerPlatform { diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/ui_kit/ui_kit_api_impls.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/ui_kit/ui_kit_api_impls.dart index ae12a11820d8..4749c6afca3c 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/ui_kit/ui_kit_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/ui_kit/ui_kit_api_impls.dart @@ -11,7 +11,7 @@ import 'package:flutter/painting.dart' show Color; import 'package:flutter/services.dart'; import '../common/instance_manager.dart'; -import '../common/web_kit.pigeon.dart'; +import '../common/web_kit.g.dart'; import '../foundation/foundation.dart'; import '../web_kit/web_kit.dart'; import 'ui_kit.dart'; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/webview_flutter_wkwebview.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/webview_flutter_wkwebview.dart deleted file mode 100644 index f54fb73bcda3..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/webview_flutter_wkwebview.dart +++ /dev/null @@ -1,9 +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. - -library webview_flutter_wkwebview; - -export 'src/webkit_webview_controller.dart'; -export 'src/webkit_webview_cookie_manager.dart'; -export 'src/webkit_webview_platform.dart'; 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 566c46fda8bb..467fa8735d6b 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 @@ -10,6 +10,8 @@ import '../foundation/foundation.dart'; import '../ui_kit/ui_kit.dart'; import 'web_kit_api_impls.dart'; +export 'web_kit_api_impls.dart' show WKNavigationType; + /// Times at which to inject script content into a webpage. /// /// Wraps [WKUserScriptInjectionTime](https://developer.apple.com/documentation/webkit/wkuserscriptinjectiontime?language=objc). @@ -144,13 +146,20 @@ class WKWebsiteDataRecord { @immutable class WKNavigationAction { /// Constructs a [WKNavigationAction]. - const WKNavigationAction({required this.request, required this.targetFrame}); + const WKNavigationAction({ + required this.request, + required this.targetFrame, + required this.navigationType, + }); /// The URL request object associated with the navigation action. final NSUrlRequest request; /// The frame in which to display the new content. final WKFrameInfo targetFrame; + + /// The type of action that triggered the navigation. + final WKNavigationType navigationType; } /// An object that contains information about a frame on a webpage. 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 614d0793e5f9..7cd29da3e716 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 @@ -6,10 +6,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import '../common/instance_manager.dart'; -import '../common/web_kit.pigeon.dart'; +import '../common/web_kit.g.dart'; import '../foundation/foundation.dart'; import 'web_kit.dart'; +export '../common/web_kit.g.dart' show WKNavigationType; + Iterable _toWKWebsiteDataTypeEnumData( Iterable types) { return types.map((WKWebsiteDataType type) { @@ -169,6 +171,7 @@ extension _NavigationActionDataConverter on WKNavigationActionData { return WKNavigationAction( request: request.toNSUrlRequest(), targetFrame: targetFrame.toWKFrameInfo(), + navigationType: navigationType, ); } } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_proxy.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart similarity index 82% rename from packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_proxy.dart rename to packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart index e3d1f609ef9c..3e8d6796069b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_proxy.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart @@ -2,8 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import '../../foundation/foundation.dart'; -import '../../web_kit/web_kit.dart'; +import 'common/instance_manager.dart'; +import 'foundation/foundation.dart'; +import 'web_kit/web_kit.dart'; // This convenience method was added because Dart doesn't support constant // function literals: https://github.com/dart-lang/language/issues/1048. @@ -27,6 +28,7 @@ class WebKitProxy { this.createScriptMessageHandler = WKScriptMessageHandler.new, this.defaultWebsiteDataStore = _defaultWebsiteDataStore, this.createNavigationDelegate = WKNavigationDelegate.new, + this.createUIDelegate = WKUIDelegate.new, }); /// Constructs a [WKWebView]. @@ -38,10 +40,13 @@ class WebKitProxy { Map change, )? observeValue, + InstanceManager? instanceManager, }) createWebView; /// Constructs a [WKWebViewConfiguration]. - final WKWebViewConfiguration Function() createWebViewConfiguration; + final WKWebViewConfiguration Function({ + InstanceManager? instanceManager, + }) createWebViewConfiguration; /// Constructs a [WKScriptMessageHandler]. final WKScriptMessageHandler Function({ @@ -70,4 +75,14 @@ class WebKitProxy { didFailProvisionalNavigation, void Function(WKWebView webView)? webViewWebContentProcessDidTerminate, }) createNavigationDelegate; + + /// Constructs a [WKUIDelegate]. + final WKUIDelegate Function({ + void Function( + WKWebView webView, + WKWebViewConfiguration configuration, + WKNavigationAction navigationAction, + )? + onCreateWebView, + }) createUIDelegate; } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_controller.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart similarity index 74% rename from packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_controller.dart rename to packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart index 117aa784dc3f..8abd0c1afe8a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart @@ -9,14 +9,34 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:path/path.dart' as path; -import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; -import '../../common/instance_manager.dart'; -import '../../common/weak_reference_utils.dart'; -import '../../foundation/foundation.dart'; -import '../../web_kit/web_kit.dart'; +import 'common/instance_manager.dart'; +import 'common/weak_reference_utils.dart'; +import 'foundation/foundation.dart'; +import 'web_kit/web_kit.dart'; import 'webkit_proxy.dart'; +/// Media types that can require a user gesture to begin playing. +/// +/// See [WebKitWebViewControllerCreationParams.mediaTypesRequiringUserAction]. +enum PlaybackMediaTypes { + /// A media type that contains audio. + audio, + + /// A media type that contains video. + video; + + WKAudiovisualMediaType _toWKAudiovisualMediaType() { + switch (this) { + case PlaybackMediaTypes.audio: + return WKAudiovisualMediaType.audio; + case PlaybackMediaTypes.video: + return WKAudiovisualMediaType.video; + } + } +} + /// Object specifying creation parameters for a [WebKitWebViewController]. @immutable class WebKitWebViewControllerCreationParams @@ -24,7 +44,32 @@ class WebKitWebViewControllerCreationParams /// Constructs a [WebKitWebViewControllerCreationParams]. WebKitWebViewControllerCreationParams({ @visibleForTesting this.webKitProxy = const WebKitProxy(), - }) : _configuration = webKitProxy.createWebViewConfiguration(); + this.mediaTypesRequiringUserAction = const { + PlaybackMediaTypes.audio, + PlaybackMediaTypes.video, + }, + this.allowsInlineMediaPlayback = false, + @visibleForTesting InstanceManager? instanceManager, + }) : _instanceManager = instanceManager ?? NSObject.globalInstanceManager { + _configuration = webKitProxy.createWebViewConfiguration( + instanceManager: _instanceManager, + ); + + if (mediaTypesRequiringUserAction.isEmpty) { + _configuration.setMediaTypesRequiringUserActionForPlayback( + {WKAudiovisualMediaType.none}, + ); + } else { + _configuration.setMediaTypesRequiringUserActionForPlayback( + mediaTypesRequiringUserAction + .map( + (PlaybackMediaTypes type) => type._toWKAudiovisualMediaType(), + ) + .toSet(), + ); + } + _configuration.setAllowsInlineMediaPlayback(allowsInlineMediaPlayback); + } /// Constructs a [WebKitWebViewControllerCreationParams] using a /// [PlatformWebViewControllerCreationParams]. @@ -33,14 +78,41 @@ class WebKitWebViewControllerCreationParams // ignore: avoid_unused_constructor_parameters PlatformWebViewControllerCreationParams params, { @visibleForTesting WebKitProxy webKitProxy = const WebKitProxy(), - }) : this(webKitProxy: webKitProxy); + Set mediaTypesRequiringUserAction = + const { + PlaybackMediaTypes.audio, + PlaybackMediaTypes.video, + }, + bool allowsInlineMediaPlayback = false, + @visibleForTesting InstanceManager? instanceManager, + }) : this( + webKitProxy: webKitProxy, + mediaTypesRequiringUserAction: mediaTypesRequiringUserAction, + allowsInlineMediaPlayback: allowsInlineMediaPlayback, + instanceManager: instanceManager, + ); + + late final WKWebViewConfiguration _configuration; + + /// Media types that require a user gesture to begin playing. + /// + /// Defaults to include [PlaybackMediaTypes.audio] and + /// [PlaybackMediaTypes.video]. + final Set mediaTypesRequiringUserAction; - final WKWebViewConfiguration _configuration; + /// Whether inline playback of HTML5 videos is allowed. + /// + /// Defaults to false. + final bool allowsInlineMediaPlayback; /// Handles constructing objects and calling static methods for the WebKit /// native library. @visibleForTesting final WebKitProxy webKitProxy; + + // Maintains instances used to communicate with the native objects they + // represent. + final InstanceManager _instanceManager; } /// An implementation of [PlatformWebViewController] with the WebKit api. @@ -61,34 +133,47 @@ class WebKitWebViewController extends PlatformWebViewController { } /// The WebKit WebView being controlled. - late final WKWebView _webView = withWeakRefenceTo(this, ( - WeakReference weakReference, - ) { - return _webKitParams.webKitProxy.createWebView( - _webKitParams._configuration, - observeValue: ( + late final WKWebView _webView = _webKitParams.webKitProxy.createWebView( + _webKitParams._configuration, + observeValue: withWeakRefenceTo(this, ( + WeakReference weakReference, + ) { + return ( String keyPath, NSObject object, Map change, ) { - if (weakReference.target?._onProgress != null) { + final ProgressCallback? progressCallback = + weakReference.target?._currentNavigationDelegate?._onProgress; + if (progressCallback != null) { final double progress = change[NSKeyValueChangeKey.newValue]! as double; - weakReference.target!._onProgress!((progress * 100).round()); + progressCallback((progress * 100).round()); } - }, - ); - }); + }; + }), + instanceManager: _webKitParams._instanceManager, + ); final Map _javaScriptChannelParams = {}; bool _zoomEnabled = true; - void Function(int progress)? _onProgress; + WebKitNavigationDelegate? _currentNavigationDelegate; WebKitWebViewControllerCreationParams get _webKitParams => params as WebKitWebViewControllerCreationParams; + /// Identifier used to retrieve the underlying native `WKWebView`. + /// + /// This is typically used by other plugins to retrieve the native `WKWebView` + /// from an `FWFInstanceManager`. + /// + /// See Objective-C method + /// `FLTWebViewFlutterPlugin:webViewForIdentifier:withPluginRegistry`. + int get webViewIdentifier => + _webKitParams._instanceManager.getIdentifier(_webView)!; + @override Future loadFile(String absoluteFilePath) { return _webView.loadFileUrl( @@ -206,16 +291,16 @@ class WebKitWebViewController extends PlatformWebViewController { // unsupported. This also goes for `null` and `undefined` on iOS 14+. For // example, when running a void function. For ease of use, this specific // error is ignored when no return value is expected. - if (exception.details is! NSError || - exception.details.code != - WKErrorCode.javaScriptResultTypeIsUnsupported) { + final Object? details = exception.details; + if (details is! NSError || + details.code != WKErrorCode.javaScriptResultTypeIsUnsupported) { rethrow; } } } @override - Future runJavaScriptReturningResult(String javaScript) async { + Future runJavaScriptReturningResult(String javaScript) async { final Object? result = await _webView.evaluateJavaScript(javaScript); if (result == null) { throw ArgumentError( @@ -223,12 +308,7 @@ class WebKitWebViewController extends PlatformWebViewController { 'Use `runJavascript` when expecting a null return value.', ); } - return result.toString(); - } - - /// Controls whether inline playback of HTML5 videos is allowed. - Future setAllowsInlineMediaPlayback(bool allow) { - return _webView.configuration.setAllowsInlineMediaPlayback(allow); + return result; } @override @@ -251,25 +331,23 @@ class WebKitWebViewController extends PlatformWebViewController { } @override - Future> getScrollPosition() async { + Future getScrollPosition() async { final Point offset = await _webView.scrollView.getContentOffset(); - return Point(offset.x.round(), offset.y.round()); + return Offset(offset.x, offset.y); } - // TODO(bparrishMines): This is unique to iOS. Override should be removed if - // this is removed from the platform interface before webview_flutter version - // 4.0.0. - @override - Future enableGestureNavigation(bool enabled) { + /// Whether horizontal swipe gestures trigger page navigation. + Future setAllowsBackForwardNavigationGestures(bool enabled) { return _webView.setAllowsBackForwardNavigationGestures(enabled); } @override Future setBackgroundColor(Color color) { return Future.wait(>[ - _webView.scrollView.setBackgroundColor(color), _webView.setOpaque(false), _webView.setBackgroundColor(Colors.transparent), + // This method must be called last. + _webView.scrollView.setBackgroundColor(color), ]); } @@ -306,8 +384,11 @@ class WebKitWebViewController extends PlatformWebViewController { Future setPlatformNavigationDelegate( covariant WebKitNavigationDelegate handler, ) { - _onProgress = handler._onProgress; - return _webView.setNavigationDelegate(handler._navigationDelegate); + _currentNavigationDelegate = handler; + return Future.wait(>[ + _webView.setUIDelegate(handler._uiDelegate), + _webView.setNavigationDelegate(handler._navigationDelegate) + ]); } Future _disableZoom() { @@ -441,6 +522,7 @@ class WebKitWebViewWidget extends PlatformWebViewWidget { @override Widget build(BuildContext context) { return UiKitView( + key: _webKitParams.key, viewType: 'plugins.flutter.io/webview', onPlatformViewCreated: (_) {}, layoutDirection: params.layoutDirection, @@ -454,11 +536,12 @@ class WebKitWebViewWidget extends PlatformWebViewWidget { /// An implementation of [WebResourceError] with the WebKit API. class WebKitWebResourceError extends WebResourceError { - WebKitWebResourceError._(this._nsError) + WebKitWebResourceError._(this._nsError, {required bool isForMainFrame}) : super( errorCode: _nsError.code, description: _nsError.localizedDescription, errorType: _toWebResourceErrorType(_nsError.code), + isForMainFrame: isForMainFrame, ); static WebResourceErrorType? _toWebResourceErrorType(int code) { @@ -518,9 +601,10 @@ class WebKitNavigationDelegate extends PlatformNavigationDelegate { .fromPlatformNavigationDelegateCreationParams(params)) { final WeakReference weakThis = WeakReference(this); - _navigationDelegate = (params as WebKitNavigationDelegateCreationParams) - .webKitProxy - .createNavigationDelegate( + _navigationDelegate = + (this.params as WebKitNavigationDelegateCreationParams) + .webKitProxy + .createNavigationDelegate( didFinishNavigation: (WKWebView webView, String? url) { if (weakThis.target?._onPageFinished != null) { weakThis.target!._onPageFinished!(url ?? ''); @@ -536,27 +620,31 @@ class WebKitNavigationDelegate extends PlatformNavigationDelegate { WKNavigationAction action, ) async { if (weakThis.target?._onNavigationRequest != null) { - final bool allow = await weakThis.target!._onNavigationRequest!( + final NavigationDecision decision = + await weakThis.target!._onNavigationRequest!(NavigationRequest( url: action.request.url, - isForMainFrame: action.targetFrame.isMainFrame, - ); - return allow - ? WKNavigationActionPolicy.allow - : WKNavigationActionPolicy.cancel; + isMainFrame: action.targetFrame.isMainFrame, + )); + switch (decision) { + case NavigationDecision.prevent: + return WKNavigationActionPolicy.cancel; + case NavigationDecision.navigate: + return WKNavigationActionPolicy.allow; + } } return WKNavigationActionPolicy.allow; }, didFailNavigation: (WKWebView webView, NSError error) { if (weakThis.target?._onWebResourceError != null) { weakThis.target!._onWebResourceError!( - WebKitWebResourceError._(error), + WebKitWebResourceError._(error, isForMainFrame: true), ); } }, didFailProvisionalNavigation: (WKWebView webView, NSError error) { if (weakThis.target?._onWebResourceError != null) { weakThis.target!._onWebResourceError!( - WebKitWebResourceError._(error), + WebKitWebResourceError._(error, isForMainFrame: true), ); } }, @@ -570,51 +658,65 @@ class WebKitNavigationDelegate extends PlatformNavigationDelegate { domain: 'WKErrorDomain', localizedDescription: '', ), + isForMainFrame: true, ), ); } }, ); + + _uiDelegate = (this.params as WebKitNavigationDelegateCreationParams) + .webKitProxy + .createUIDelegate( + onCreateWebView: ( + WKWebView webView, + WKWebViewConfiguration configuration, + WKNavigationAction navigationAction, + ) { + if (!navigationAction.targetFrame.isMainFrame) { + webView.loadRequest(navigationAction.request); + } + }, + ); } // Used to set `WKWebView.setNavigationDelegate` in `WebKitWebViewController`. late final WKNavigationDelegate _navigationDelegate; - void Function(String url)? _onPageFinished; - void Function(String url)? _onPageStarted; - void Function(int progress)? _onProgress; - void Function(WebResourceError error)? _onWebResourceError; - FutureOr Function({required String url, required bool isForMainFrame})? - _onNavigationRequest; + // Used to set `WKWebView.setUIDelegate` in `WebKitWebViewController`. + late final WKUIDelegate _uiDelegate; + + PageEventCallback? _onPageFinished; + PageEventCallback? _onPageStarted; + ProgressCallback? _onProgress; + WebResourceErrorCallback? _onWebResourceError; + NavigationRequestCallback? _onNavigationRequest; @override - Future setOnPageFinished( - void Function(String url) onPageFinished, - ) async { + Future setOnPageFinished(PageEventCallback onPageFinished) async { _onPageFinished = onPageFinished; } @override - Future setOnPageStarted(void Function(String url) onPageStarted) async { + Future setOnPageStarted(PageEventCallback onPageStarted) async { _onPageStarted = onPageStarted; } @override - Future setOnProgress(void Function(int progress) onProgress) async { + Future setOnProgress(ProgressCallback onProgress) async { _onProgress = onProgress; } @override Future setOnWebResourceError( - void Function(WebResourceError error) onWebResourceError, + WebResourceErrorCallback onWebResourceError, ) async { _onWebResourceError = onWebResourceError; } @override Future setOnNavigationRequest( - FutureOr Function({required String url, required bool isForMainFrame}) - onNavigationRequest, + NavigationRequestCallback onNavigationRequest, ) async { _onNavigationRequest = onNavigationRequest; } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_cookie_manager.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_cookie_manager.dart similarity index 94% rename from packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_cookie_manager.dart rename to packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_cookie_manager.dart index 423b3bdb7f4e..00e97011c559 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_cookie_manager.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_cookie_manager.dart @@ -3,10 +3,10 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart'; -import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; -import '../../foundation/foundation.dart'; -import '../../web_kit/web_kit.dart'; +import 'foundation/foundation.dart'; +import 'web_kit/web_kit.dart'; import 'webkit_proxy.dart'; /// Object specifying creation parameters for a [WebKitWebViewCookieManager]. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_platform.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_platform.dart similarity index 80% rename from packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_platform.dart rename to packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_platform.dart index 13116bb30b5c..018d7c0f3752 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/v4/src/webkit_webview_platform.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_platform.dart @@ -2,13 +2,18 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'webkit_webview_controller.dart'; import 'webkit_webview_cookie_manager.dart'; /// Implementation of [WebViewPlatform] using the WebKit API. class WebKitWebViewPlatform extends WebViewPlatform { + /// Registers this class as the default instance of [WebViewPlatform]. + static void registerWith() { + WebViewPlatform.instance = WebKitWebViewPlatform(); + } + @override WebKitWebViewController createPlatformWebViewController( PlatformWebViewControllerCreationParams params, diff --git a/packages/path_provider/path_provider_ios/example/ios/Runner/AppDelegate.h b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_flutter_wkwebview_legacy.dart similarity index 61% rename from packages/path_provider/path_provider_ios/example/ios/Runner/AppDelegate.h rename to packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_flutter_wkwebview_legacy.dart index 0681d288bb70..f4a2ad162b9c 100644 --- a/packages/path_provider/path_provider_ios/example/ios/Runner/AppDelegate.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_flutter_wkwebview_legacy.dart @@ -2,9 +2,5 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end +export 'legacy/webview_cupertino.dart'; +export 'legacy/wkwebview_cookie_manager.dart'; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart index f647ab38a41b..f54fb73bcda3 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart @@ -2,5 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -export 'src/webview_cupertino.dart'; -export 'src/wkwebview_cookie_manager.dart'; +library webview_flutter_wkwebview; + +export 'src/webkit_webview_controller.dart'; +export 'src/webkit_webview_cookie_manager.dart'; +export 'src/webkit_webview_platform.dart'; 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 c20a10ebfadd..9b334c2411ff 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart @@ -6,8 +6,8 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon( PigeonOptions( - dartOut: 'lib/src/common/web_kit.pigeon.dart', - dartTestOut: 'test/src/common/test_web_kit.pigeon.dart', + dartOut: 'lib/src/common/web_kit.g.dart', + dartTestOut: 'test/src/common/test_web_kit.g.dart', dartOptions: DartOptions(copyrightHeader: [ 'Copyright 2013 The Flutter Authors. All rights reserved.', 'Use of this source code is governed by a BSD-style license that can be', @@ -166,6 +166,42 @@ class NSHttpCookiePropertyKeyEnumData { late NSHttpCookiePropertyKeyEnum value; } +/// An object that contains information about an action that causes navigation +/// to occur. +/// +/// Wraps [WKNavigationType](https://developer.apple.com/documentation/webkit/wknavigationaction?language=objc). +enum WKNavigationType { + /// A link activation. + /// + /// See https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypelinkactivated?language=objc. + linkActivated, + + /// A request to submit a form. + /// + /// See https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypeformsubmitted?language=objc. + submitted, + + /// A request for the frame’s next or previous item. + /// + /// See https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypebackforward?language=objc. + backForward, + + /// A request to reload the webpage. + /// + /// See https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypereload?language=objc. + reload, + + /// A request to resubmit a form. + /// + /// See https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypeformresubmitted?language=objc. + formResubmitted, + + /// A navigation request that originates for some other reason. + /// + /// See https://developer.apple.com/documentation/webkit/wknavigationtype/wknavigationtypeother?language=objc. + other, +} + /// Mirror of NSURLRequest. /// /// See https://developer.apple.com/documentation/foundation/nsurlrequest?language=objc. @@ -191,6 +227,7 @@ class WKUserScriptData { class WKNavigationActionData { late NSUrlRequestData request; late WKFrameInfoData targetFrame; + late WKNavigationType navigationType; } /// Mirror of WKFrameInfo. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 3de385fec06c..d1aaa7cf9203 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/plugins/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: 2.9.5 +version: 3.1.0 environment: sdk: ">=2.17.0 <3.0.0" @@ -14,12 +14,13 @@ flutter: platforms: ios: pluginClass: FLTWebViewFlutterPlugin + dartPluginClass: WebKitWebViewPlatform dependencies: flutter: sdk: flutter path: ^1.8.0 - webview_flutter_platform_interface: ^1.9.3 + webview_flutter_platform_interface: ^2.0.0 dev_dependencies: build_runner: ^2.1.5 @@ -27,5 +28,5 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter - mockito: ^5.1.0 - pigeon: ^3.0.3 + mockito: ^5.3.2 + pigeon: ^4.2.13 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_cookie_manager_test.dart similarity index 93% rename from packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.dart rename to packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_cookie_manager_test.dart index 73d8c8f33a11..4f775df9e11c 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_cookie_manager_test.dart @@ -5,10 +5,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; +import 'package:webview_flutter_wkwebview/src/legacy/wkwebview_cookie_manager.dart'; import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; -import 'package:webview_flutter_wkwebview/src/wkwebview_cookie_manager.dart'; import 'web_kit_cookie_manager_test.mocks.dart'; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_cookie_manager_test.mocks.dart new file mode 100644 index 000000000000..860e8dbeb4ce --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_cookie_manager_test.mocks.dart @@ -0,0 +1,191 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter_wkwebview/test/legacy/web_kit_cookie_manager_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart' + as _i4; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart' as _i2; + +// 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 + +class _FakeWKHttpCookieStore_0 extends _i1.SmartFake + implements _i2.WKHttpCookieStore { + _FakeWKHttpCookieStore_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKWebsiteDataStore_1 extends _i1.SmartFake + implements _i2.WKWebsiteDataStore { + _FakeWKWebsiteDataStore_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [WKHttpCookieStore]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKHttpCookieStore extends _i1.Mock implements _i2.WKHttpCookieStore { + MockWKHttpCookieStore() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future setCookie(_i4.NSHttpCookie? cookie) => (super.noSuchMethod( + Invocation.method( + #setCookie, + [cookie], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + @override + _i2.WKHttpCookieStore copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKHttpCookieStore_0( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WKHttpCookieStore); + @override + _i3.Future addObserver( + _i4.NSObject? observer, { + required String? keyPath, + required Set<_i4.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future removeObserver( + _i4.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); +} + +/// A class which mocks [WKWebsiteDataStore]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKWebsiteDataStore extends _i1.Mock + implements _i2.WKWebsiteDataStore { + MockWKWebsiteDataStore() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WKHttpCookieStore get httpCookieStore => (super.noSuchMethod( + Invocation.getter(#httpCookieStore), + returnValue: _FakeWKHttpCookieStore_0( + this, + Invocation.getter(#httpCookieStore), + ), + ) as _i2.WKHttpCookieStore); + @override + _i3.Future removeDataOfTypes( + Set<_i2.WKWebsiteDataType>? dataTypes, + DateTime? since, + ) => + (super.noSuchMethod( + Invocation.method( + #removeDataOfTypes, + [ + dataTypes, + since, + ], + ), + returnValue: _i3.Future.value(false), + ) as _i3.Future); + @override + _i2.WKWebsiteDataStore copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKWebsiteDataStore_1( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WKWebsiteDataStore); + @override + _i3.Future addObserver( + _i4.NSObject? observer, { + required String? keyPath, + required Set<_i4.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future removeObserver( + _i4.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_webview_widget_test.dart similarity index 95% rename from packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.dart rename to packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_webview_widget_test.dart index 5e6186ccb4b9..7982be1c0353 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_webview_widget_test.dart @@ -12,11 +12,11 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; +import 'package:webview_flutter_wkwebview/src/legacy/web_kit_webview_widget.dart'; import 'package:webview_flutter_wkwebview/src/ui_kit/ui_kit.dart'; import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; -import 'package:webview_flutter_wkwebview/src/web_kit_webview_widget.dart'; import 'web_kit_webview_widget_test.mocks.dart'; @@ -137,13 +137,13 @@ void main() { (WidgetTester tester) async { await buildWidget(tester); - final dynamic onCreateWebView = verify( - mockWebViewWidgetProxy.createUIDelgate( + final void Function(WKWebView, WKWebViewConfiguration, WKNavigationAction) + onCreateWebView = verify(mockWebViewWidgetProxy.createUIDelgate( onCreateWebView: captureAnyNamed('onCreateWebView'))) - .captured - .single - as void Function( - WKWebView, WKWebViewConfiguration, WKNavigationAction); + .captured + .single + as void Function( + WKWebView, WKWebViewConfiguration, WKNavigationAction); const NSUrlRequest request = NSUrlRequest(url: 'https://google.com'); onCreateWebView( @@ -152,6 +152,7 @@ void main() { const WKNavigationAction( request: request, targetFrame: WKFrameInfo(isMainFrame: false), + navigationType: WKNavigationType.linkActivated, ), ); @@ -363,11 +364,6 @@ void main() { testWidgets( 'enabling zoom removes script', (WidgetTester tester) async { - when(mockWebViewWidgetProxy.createScriptMessageHandler()) - .thenReturn( - MockWKScriptMessageHandler(), - ); - await buildWidget( tester, creationParams: CreationParams( @@ -490,7 +486,7 @@ void main() { await buildWidget(tester); expect( - () async => await testController.loadRequest( + () async => testController.loadRequest( WebViewRequest( uri: Uri.parse('www.google.com'), method: WebViewRequestMethod.get, @@ -993,7 +989,7 @@ void main() { testWidgets('onPageStarted', (WidgetTester tester) async { await buildWidget(tester); - final dynamic didStartProvisionalNavigation = + final void Function(WKWebView, String) didStartProvisionalNavigation = verify(mockWebViewWidgetProxy.createNavigationDelegate( didFinishNavigation: anyNamed('didFinishNavigation'), didStartProvisionalNavigation: @@ -1014,7 +1010,7 @@ void main() { testWidgets('onPageFinished', (WidgetTester tester) async { await buildWidget(tester); - final dynamic didFinishNavigation = + final void Function(WKWebView, String) didFinishNavigation = verify(mockWebViewWidgetProxy.createNavigationDelegate( didFinishNavigation: captureAnyNamed('didFinishNavigation'), didStartProvisionalNavigation: @@ -1036,7 +1032,7 @@ void main() { (WidgetTester tester) async { await buildWidget(tester); - final dynamic didFailNavigation = + final void Function(WKWebView, NSError) didFailNavigation = verify(mockWebViewWidgetProxy.createNavigationDelegate( didFinishNavigation: anyNamed('didFinishNavigation'), didStartProvisionalNavigation: @@ -1073,7 +1069,7 @@ void main() { (WidgetTester tester) async { await buildWidget(tester); - final dynamic didFailProvisionalNavigation = + final void Function(WKWebView, NSError) didFailProvisionalNavigation = verify(mockWebViewWidgetProxy.createNavigationDelegate( didFinishNavigation: anyNamed('didFinishNavigation'), didStartProvisionalNavigation: @@ -1114,7 +1110,7 @@ void main() { (WidgetTester tester) async { await buildWidget(tester); - final dynamic webViewWebContentProcessDidTerminate = + final void Function(WKWebView) webViewWebContentProcessDidTerminate = verify(mockWebViewWidgetProxy.createNavigationDelegate( didFinishNavigation: anyNamed('didFinishNavigation'), didStartProvisionalNavigation: @@ -1146,7 +1142,8 @@ void main() { (WidgetTester tester) async { await buildWidget(tester, hasNavigationDelegate: true); - final dynamic decidePolicyForNavigationAction = + final Future Function( + WKWebView, WKNavigationAction) decidePolicyForNavigationAction = verify(mockWebViewWidgetProxy.createNavigationDelegate( didFinishNavigation: anyNamed('didFinishNavigation'), didStartProvisionalNavigation: @@ -1172,6 +1169,7 @@ void main() { const WKNavigationAction( request: NSUrlRequest(url: 'https://google.com'), targetFrame: WKFrameInfo(isMainFrame: false), + navigationType: WKNavigationType.linkActivated, ), ), completion(WKNavigationActionPolicy.allow), @@ -1194,15 +1192,13 @@ void main() { }, )); - final dynamic observeValue = verify( - mockWebViewWidgetProxy.createWebView(any, - observeValue: captureAnyNamed('observeValue'))) - .captured - .single as void Function( - String keyPath, - NSObject object, - Map change, - ); + final void Function(String, NSObject, Map) + observeValue = verify(mockWebViewWidgetProxy.createWebView(any, + observeValue: captureAnyNamed('observeValue'))) + .captured + .single + as void Function( + String, NSObject, Map); observeValue( 'estimatedProgress', @@ -1237,15 +1233,14 @@ void main() { await buildWidget(tester); await testController.addJavascriptChannels({'hello'}); - final dynamic didReceiveScriptMessage = verify( - mockWebViewWidgetProxy.createScriptMessageHandler( - didReceiveScriptMessage: - captureAnyNamed('didReceiveScriptMessage'))) - .captured - .single as void Function( - WKUserContentController userContentController, - WKScriptMessage message, - ); + final void Function(WKUserContentController, WKScriptMessage) + didReceiveScriptMessage = verify( + mockWebViewWidgetProxy.createScriptMessageHandler( + didReceiveScriptMessage: + captureAnyNamed('didReceiveScriptMessage'))) + .captured + .single + as void Function(WKUserContentController, WKScriptMessage); didReceiveScriptMessage( mockUserContentController, diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_webview_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_webview_widget_test.mocks.dart new file mode 100644 index 000000000000..1680997d5856 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_webview_widget_test.mocks.dart @@ -0,0 +1,1300 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter_wkwebview/test/legacy/web_kit_webview_widget_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; +import 'dart:math' as _i2; +import 'dart:ui' as _i6; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_platform_interface/src/legacy/types/javascript_channel.dart' + as _i9; +import 'package:webview_flutter_platform_interface/src/legacy/types/types.dart' + as _i10; +import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart' + as _i8; +import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart' + as _i7; +import 'package:webview_flutter_wkwebview/src/legacy/web_kit_webview_widget.dart' + as _i11; +import 'package:webview_flutter_wkwebview/src/ui_kit/ui_kit.dart' as _i3; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart' as _i4; + +// 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 + +class _FakePoint_0 extends _i1.SmartFake + implements _i2.Point { + _FakePoint_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeUIScrollView_1 extends _i1.SmartFake implements _i3.UIScrollView { + _FakeUIScrollView_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKNavigationDelegate_2 extends _i1.SmartFake + implements _i4.WKNavigationDelegate { + _FakeWKNavigationDelegate_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKPreferences_3 extends _i1.SmartFake implements _i4.WKPreferences { + _FakeWKPreferences_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKScriptMessageHandler_4 extends _i1.SmartFake + implements _i4.WKScriptMessageHandler { + _FakeWKScriptMessageHandler_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKWebViewConfiguration_5 extends _i1.SmartFake + implements _i4.WKWebViewConfiguration { + _FakeWKWebViewConfiguration_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKWebView_6 extends _i1.SmartFake implements _i4.WKWebView { + _FakeWKWebView_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKUserContentController_7 extends _i1.SmartFake + implements _i4.WKUserContentController { + _FakeWKUserContentController_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKWebsiteDataStore_8 extends _i1.SmartFake + implements _i4.WKWebsiteDataStore { + _FakeWKWebsiteDataStore_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKHttpCookieStore_9 extends _i1.SmartFake + implements _i4.WKHttpCookieStore { + _FakeWKHttpCookieStore_9( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKUIDelegate_10 extends _i1.SmartFake implements _i4.WKUIDelegate { + _FakeWKUIDelegate_10( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [UIScrollView]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockUIScrollView extends _i1.Mock implements _i3.UIScrollView { + MockUIScrollView() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future<_i2.Point> getContentOffset() => (super.noSuchMethod( + Invocation.method( + #getContentOffset, + [], + ), + returnValue: _i5.Future<_i2.Point>.value(_FakePoint_0( + this, + Invocation.method( + #getContentOffset, + [], + ), + )), + ) as _i5.Future<_i2.Point>); + @override + _i5.Future scrollBy(_i2.Point? offset) => (super.noSuchMethod( + Invocation.method( + #scrollBy, + [offset], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setContentOffset(_i2.Point? offset) => + (super.noSuchMethod( + Invocation.method( + #setContentOffset, + [offset], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i3.UIScrollView copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeUIScrollView_1( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i3.UIScrollView); + @override + _i5.Future setBackgroundColor(_i6.Color? color) => (super.noSuchMethod( + Invocation.method( + #setBackgroundColor, + [color], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setOpaque(bool? opaque) => (super.noSuchMethod( + Invocation.method( + #setOpaque, + [opaque], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future addObserver( + _i7.NSObject? observer, { + required String? keyPath, + required Set<_i7.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeObserver( + _i7.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [WKNavigationDelegate]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKNavigationDelegate extends _i1.Mock + implements _i4.WKNavigationDelegate { + MockWKNavigationDelegate() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.WKNavigationDelegate copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKNavigationDelegate_2( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i4.WKNavigationDelegate); + @override + _i5.Future addObserver( + _i7.NSObject? observer, { + required String? keyPath, + required Set<_i7.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeObserver( + _i7.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [WKPreferences]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKPreferences extends _i1.Mock implements _i4.WKPreferences { + MockWKPreferences() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future setJavaScriptEnabled(bool? enabled) => (super.noSuchMethod( + Invocation.method( + #setJavaScriptEnabled, + [enabled], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i4.WKPreferences copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKPreferences_3( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i4.WKPreferences); + @override + _i5.Future addObserver( + _i7.NSObject? observer, { + required String? keyPath, + required Set<_i7.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeObserver( + _i7.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [WKScriptMessageHandler]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKScriptMessageHandler extends _i1.Mock + implements _i4.WKScriptMessageHandler { + MockWKScriptMessageHandler() { + _i1.throwOnMissingStub(this); + } + + @override + void Function( + _i4.WKUserContentController, + _i4.WKScriptMessage, + ) get didReceiveScriptMessage => (super.noSuchMethod( + Invocation.getter(#didReceiveScriptMessage), + returnValue: ( + _i4.WKUserContentController userContentController, + _i4.WKScriptMessage message, + ) {}, + ) as void Function( + _i4.WKUserContentController, + _i4.WKScriptMessage, + )); + @override + _i4.WKScriptMessageHandler copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKScriptMessageHandler_4( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i4.WKScriptMessageHandler); + @override + _i5.Future addObserver( + _i7.NSObject? observer, { + required String? keyPath, + required Set<_i7.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeObserver( + _i7.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [WKWebView]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKWebView extends _i1.Mock implements _i4.WKWebView { + MockWKWebView() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.WKWebViewConfiguration get configuration => (super.noSuchMethod( + Invocation.getter(#configuration), + returnValue: _FakeWKWebViewConfiguration_5( + this, + Invocation.getter(#configuration), + ), + ) as _i4.WKWebViewConfiguration); + @override + _i3.UIScrollView get scrollView => (super.noSuchMethod( + Invocation.getter(#scrollView), + returnValue: _FakeUIScrollView_1( + this, + Invocation.getter(#scrollView), + ), + ) as _i3.UIScrollView); + @override + _i5.Future setUIDelegate(_i4.WKUIDelegate? delegate) => + (super.noSuchMethod( + Invocation.method( + #setUIDelegate, + [delegate], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setNavigationDelegate(_i4.WKNavigationDelegate? delegate) => + (super.noSuchMethod( + Invocation.method( + #setNavigationDelegate, + [delegate], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getUrl() => (super.noSuchMethod( + Invocation.method( + #getUrl, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getEstimatedProgress() => (super.noSuchMethod( + Invocation.method( + #getEstimatedProgress, + [], + ), + returnValue: _i5.Future.value(0.0), + ) as _i5.Future); + @override + _i5.Future loadRequest(_i7.NSUrlRequest? request) => + (super.noSuchMethod( + Invocation.method( + #loadRequest, + [request], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future loadHtmlString( + String? string, { + String? baseUrl, + }) => + (super.noSuchMethod( + Invocation.method( + #loadHtmlString, + [string], + {#baseUrl: baseUrl}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future loadFileUrl( + String? url, { + required String? readAccessUrl, + }) => + (super.noSuchMethod( + Invocation.method( + #loadFileUrl, + [url], + {#readAccessUrl: readAccessUrl}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future loadFlutterAsset(String? key) => (super.noSuchMethod( + Invocation.method( + #loadFlutterAsset, + [key], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future canGoBack() => (super.noSuchMethod( + Invocation.method( + #canGoBack, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future canGoForward() => (super.noSuchMethod( + Invocation.method( + #canGoForward, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future goBack() => (super.noSuchMethod( + Invocation.method( + #goBack, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future goForward() => (super.noSuchMethod( + Invocation.method( + #goForward, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future reload() => (super.noSuchMethod( + Invocation.method( + #reload, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getTitle() => (super.noSuchMethod( + Invocation.method( + #getTitle, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setAllowsBackForwardNavigationGestures(bool? allow) => + (super.noSuchMethod( + Invocation.method( + #setAllowsBackForwardNavigationGestures, + [allow], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setCustomUserAgent(String? userAgent) => (super.noSuchMethod( + Invocation.method( + #setCustomUserAgent, + [userAgent], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future evaluateJavaScript(String? javaScriptString) => + (super.noSuchMethod( + Invocation.method( + #evaluateJavaScript, + [javaScriptString], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i4.WKWebView copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKWebView_6( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i4.WKWebView); + @override + _i5.Future setBackgroundColor(_i6.Color? color) => (super.noSuchMethod( + Invocation.method( + #setBackgroundColor, + [color], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setOpaque(bool? opaque) => (super.noSuchMethod( + Invocation.method( + #setOpaque, + [opaque], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future addObserver( + _i7.NSObject? observer, { + required String? keyPath, + required Set<_i7.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeObserver( + _i7.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [WKWebViewConfiguration]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKWebViewConfiguration extends _i1.Mock + implements _i4.WKWebViewConfiguration { + MockWKWebViewConfiguration() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.WKUserContentController get userContentController => (super.noSuchMethod( + Invocation.getter(#userContentController), + returnValue: _FakeWKUserContentController_7( + this, + Invocation.getter(#userContentController), + ), + ) as _i4.WKUserContentController); + @override + _i4.WKPreferences get preferences => (super.noSuchMethod( + Invocation.getter(#preferences), + returnValue: _FakeWKPreferences_3( + this, + Invocation.getter(#preferences), + ), + ) as _i4.WKPreferences); + @override + _i4.WKWebsiteDataStore get websiteDataStore => (super.noSuchMethod( + Invocation.getter(#websiteDataStore), + returnValue: _FakeWKWebsiteDataStore_8( + this, + Invocation.getter(#websiteDataStore), + ), + ) as _i4.WKWebsiteDataStore); + @override + _i5.Future setAllowsInlineMediaPlayback(bool? allow) => + (super.noSuchMethod( + Invocation.method( + #setAllowsInlineMediaPlayback, + [allow], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setMediaTypesRequiringUserActionForPlayback( + Set<_i4.WKAudiovisualMediaType>? types) => + (super.noSuchMethod( + Invocation.method( + #setMediaTypesRequiringUserActionForPlayback, + [types], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i4.WKWebViewConfiguration copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKWebViewConfiguration_5( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i4.WKWebViewConfiguration); + @override + _i5.Future addObserver( + _i7.NSObject? observer, { + required String? keyPath, + required Set<_i7.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeObserver( + _i7.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [WKWebsiteDataStore]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKWebsiteDataStore extends _i1.Mock + implements _i4.WKWebsiteDataStore { + MockWKWebsiteDataStore() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.WKHttpCookieStore get httpCookieStore => (super.noSuchMethod( + Invocation.getter(#httpCookieStore), + returnValue: _FakeWKHttpCookieStore_9( + this, + Invocation.getter(#httpCookieStore), + ), + ) as _i4.WKHttpCookieStore); + @override + _i5.Future removeDataOfTypes( + Set<_i4.WKWebsiteDataType>? dataTypes, + DateTime? since, + ) => + (super.noSuchMethod( + Invocation.method( + #removeDataOfTypes, + [ + dataTypes, + since, + ], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i4.WKWebsiteDataStore copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKWebsiteDataStore_8( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i4.WKWebsiteDataStore); + @override + _i5.Future addObserver( + _i7.NSObject? observer, { + required String? keyPath, + required Set<_i7.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeObserver( + _i7.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [WKUIDelegate]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKUIDelegate extends _i1.Mock implements _i4.WKUIDelegate { + MockWKUIDelegate() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.WKUIDelegate copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKUIDelegate_10( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i4.WKUIDelegate); + @override + _i5.Future addObserver( + _i7.NSObject? observer, { + required String? keyPath, + required Set<_i7.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeObserver( + _i7.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [WKUserContentController]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKUserContentController extends _i1.Mock + implements _i4.WKUserContentController { + MockWKUserContentController() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future addScriptMessageHandler( + _i4.WKScriptMessageHandler? handler, + String? name, + ) => + (super.noSuchMethod( + Invocation.method( + #addScriptMessageHandler, + [ + handler, + name, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeScriptMessageHandler(String? name) => + (super.noSuchMethod( + Invocation.method( + #removeScriptMessageHandler, + [name], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeAllScriptMessageHandlers() => (super.noSuchMethod( + Invocation.method( + #removeAllScriptMessageHandlers, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future addUserScript(_i4.WKUserScript? userScript) => + (super.noSuchMethod( + Invocation.method( + #addUserScript, + [userScript], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeAllUserScripts() => (super.noSuchMethod( + Invocation.method( + #removeAllUserScripts, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i4.WKUserContentController copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKUserContentController_7( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i4.WKUserContentController); + @override + _i5.Future addObserver( + _i7.NSObject? observer, { + required String? keyPath, + required Set<_i7.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeObserver( + _i7.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [JavascriptChannelRegistry]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockJavascriptChannelRegistry extends _i1.Mock + implements _i8.JavascriptChannelRegistry { + MockJavascriptChannelRegistry() { + _i1.throwOnMissingStub(this); + } + + @override + Map get channels => (super.noSuchMethod( + Invocation.getter(#channels), + returnValue: {}, + ) as Map); + @override + void onJavascriptChannelMessage( + String? channel, + String? message, + ) => + super.noSuchMethod( + Invocation.method( + #onJavascriptChannelMessage, + [ + channel, + message, + ], + ), + returnValueForMissingStub: null, + ); + @override + void updateJavascriptChannelsFromSet(Set<_i9.JavascriptChannel>? channels) => + super.noSuchMethod( + Invocation.method( + #updateJavascriptChannelsFromSet, + [channels], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [WebViewPlatformCallbacksHandler]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewPlatformCallbacksHandler extends _i1.Mock + implements _i8.WebViewPlatformCallbacksHandler { + MockWebViewPlatformCallbacksHandler() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.FutureOr onNavigationRequest({ + required String? url, + required bool? isForMainFrame, + }) => + (super.noSuchMethod( + Invocation.method( + #onNavigationRequest, + [], + { + #url: url, + #isForMainFrame: isForMainFrame, + }, + ), + returnValue: _i5.Future.value(false), + ) as _i5.FutureOr); + @override + void onPageStarted(String? url) => super.noSuchMethod( + Invocation.method( + #onPageStarted, + [url], + ), + returnValueForMissingStub: null, + ); + @override + void onPageFinished(String? url) => super.noSuchMethod( + Invocation.method( + #onPageFinished, + [url], + ), + returnValueForMissingStub: null, + ); + @override + void onProgress(int? progress) => super.noSuchMethod( + Invocation.method( + #onProgress, + [progress], + ), + returnValueForMissingStub: null, + ); + @override + void onWebResourceError(_i10.WebResourceError? error) => super.noSuchMethod( + Invocation.method( + #onWebResourceError, + [error], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [WebViewWidgetProxy]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebViewWidgetProxy extends _i1.Mock + implements _i11.WebViewWidgetProxy { + MockWebViewWidgetProxy() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.WKWebView createWebView( + _i4.WKWebViewConfiguration? configuration, { + void Function( + String, + _i7.NSObject, + Map<_i7.NSKeyValueChangeKey, Object?>, + )? + observeValue, + }) => + (super.noSuchMethod( + Invocation.method( + #createWebView, + [configuration], + {#observeValue: observeValue}, + ), + returnValue: _FakeWKWebView_6( + this, + Invocation.method( + #createWebView, + [configuration], + {#observeValue: observeValue}, + ), + ), + ) as _i4.WKWebView); + @override + _i4.WKScriptMessageHandler createScriptMessageHandler( + {required void Function( + _i4.WKUserContentController, + _i4.WKScriptMessage, + )? + didReceiveScriptMessage}) => + (super.noSuchMethod( + Invocation.method( + #createScriptMessageHandler, + [], + {#didReceiveScriptMessage: didReceiveScriptMessage}, + ), + returnValue: _FakeWKScriptMessageHandler_4( + this, + Invocation.method( + #createScriptMessageHandler, + [], + {#didReceiveScriptMessage: didReceiveScriptMessage}, + ), + ), + ) as _i4.WKScriptMessageHandler); + @override + _i4.WKUIDelegate createUIDelgate( + {void Function( + _i4.WKWebView, + _i4.WKWebViewConfiguration, + _i4.WKNavigationAction, + )? + onCreateWebView}) => + (super.noSuchMethod( + Invocation.method( + #createUIDelgate, + [], + {#onCreateWebView: onCreateWebView}, + ), + returnValue: _FakeWKUIDelegate_10( + this, + Invocation.method( + #createUIDelgate, + [], + {#onCreateWebView: onCreateWebView}, + ), + ), + ) as _i4.WKUIDelegate); + @override + _i4.WKNavigationDelegate createNavigationDelegate({ + void Function( + _i4.WKWebView, + String?, + )? + didFinishNavigation, + void Function( + _i4.WKWebView, + String?, + )? + didStartProvisionalNavigation, + _i5.Future<_i4.WKNavigationActionPolicy> Function( + _i4.WKWebView, + _i4.WKNavigationAction, + )? + decidePolicyForNavigationAction, + void Function( + _i4.WKWebView, + _i7.NSError, + )? + didFailNavigation, + void Function( + _i4.WKWebView, + _i7.NSError, + )? + didFailProvisionalNavigation, + void Function(_i4.WKWebView)? webViewWebContentProcessDidTerminate, + }) => + (super.noSuchMethod( + Invocation.method( + #createNavigationDelegate, + [], + { + #didFinishNavigation: didFinishNavigation, + #didStartProvisionalNavigation: didStartProvisionalNavigation, + #decidePolicyForNavigationAction: decidePolicyForNavigationAction, + #didFailNavigation: didFailNavigation, + #didFailProvisionalNavigation: didFailProvisionalNavigation, + #webViewWebContentProcessDidTerminate: + webViewWebContentProcessDidTerminate, + }, + ), + returnValue: _FakeWKNavigationDelegate_2( + this, + Invocation.method( + #createNavigationDelegate, + [], + { + #didFinishNavigation: didFinishNavigation, + #didStartProvisionalNavigation: didStartProvisionalNavigation, + #decidePolicyForNavigationAction: decidePolicyForNavigationAction, + #didFailNavigation: didFailNavigation, + #didFailProvisionalNavigation: didFailProvisionalNavigation, + #webViewWebContentProcessDidTerminate: + webViewWebContentProcessDidTerminate, + }, + ), + ), + ) as _i4.WKNavigationDelegate); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.pigeon.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart similarity index 94% rename from packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.pigeon.dart rename to packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart index a9e5c8bb1db4..5c31f63c3add 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart @@ -1,18 +1,17 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v3.1.5), do not edit directly. +// Autogenerated from Pigeon (v4.2.13), 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 +// 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 -// @dart = 2.12 import 'dart:async'; -import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; -import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart'; +import 'package:webview_flutter_wkwebview/src/common/web_kit.g.dart'; class _TestWKWebsiteDataStoreHostApiCodec extends StandardMessageCodec { const _TestWKWebsiteDataStoreHostApiCodec(); @@ -38,17 +37,23 @@ class _TestWKWebsiteDataStoreHostApiCodec extends StandardMessageCodec { } } +/// Mirror of WKWebsiteDataStore. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebsitedatastore?language=objc. abstract class TestWKWebsiteDataStoreHostApi { static const MessageCodec codec = _TestWKWebsiteDataStoreHostApiCodec(); void createFromWebViewConfiguration( int identifier, int configurationIdentifier); + void createDefaultDataStore(int identifier); + Future removeDataOfTypes( int identifier, List dataTypes, double modificationTimeInSecondsSinceEpoch); + static void setup(TestWKWebsiteDataStoreHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -71,7 +76,7 @@ abstract class TestWKWebsiteDataStoreHostApi { 'Argument for dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createFromWebViewConfiguration was null, expected non-null int.'); api.createFromWebViewConfiguration( arg_identifier!, arg_configurationIdentifier!); - return {}; + return []; }); } } @@ -91,7 +96,7 @@ abstract class TestWKWebsiteDataStoreHostApi { assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.WKWebsiteDataStoreHostApi.createDefaultDataStore was null, expected non-null int.'); api.createDefaultDataStore(arg_identifier!); - return {}; + return []; }); } } @@ -120,22 +125,23 @@ abstract class TestWKWebsiteDataStoreHostApi { 'Argument for dev.flutter.pigeon.WKWebsiteDataStoreHostApi.removeDataOfTypes was null, expected non-null double.'); final bool output = await api.removeDataOfTypes(arg_identifier!, arg_dataTypes!, arg_modificationTimeInSecondsSinceEpoch!); - return {'result': output}; + return [output]; }); } } } } -class _TestUIViewHostApiCodec extends StandardMessageCodec { - const _TestUIViewHostApiCodec(); -} - +/// Mirror of UIView. +/// +/// See https://developer.apple.com/documentation/uikit/uiview?language=objc. abstract class TestUIViewHostApi { - static const MessageCodec codec = _TestUIViewHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void setBackgroundColor(int identifier, int? value); + void setOpaque(int identifier, bool opaque); + static void setup(TestUIViewHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -154,7 +160,7 @@ abstract class TestUIViewHostApi { 'Argument for dev.flutter.pigeon.UIViewHostApi.setBackgroundColor was null, expected non-null int.'); final int? arg_value = (args[1] as int?); api.setBackgroundColor(arg_identifier!, arg_value); - return {}; + return []; }); } } @@ -176,24 +182,27 @@ abstract class TestUIViewHostApi { assert(arg_opaque != null, 'Argument for dev.flutter.pigeon.UIViewHostApi.setOpaque was null, expected non-null bool.'); api.setOpaque(arg_identifier!, arg_opaque!); - return {}; + return []; }); } } } } -class _TestUIScrollViewHostApiCodec extends StandardMessageCodec { - const _TestUIScrollViewHostApiCodec(); -} - +/// Mirror of UIScrollView. +/// +/// See https://developer.apple.com/documentation/uikit/uiscrollview?language=objc. abstract class TestUIScrollViewHostApi { - static const MessageCodec codec = _TestUIScrollViewHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void createFromWebView(int identifier, int webViewIdentifier); + List getContentOffset(int identifier); + void scrollBy(int identifier, double x, double y); + void setContentOffset(int identifier, double x, double y); + static void setup(TestUIScrollViewHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -214,7 +223,7 @@ abstract class TestUIScrollViewHostApi { assert(arg_webViewIdentifier != null, 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.createFromWebView was null, expected non-null int.'); api.createFromWebView(arg_identifier!, arg_webViewIdentifier!); - return {}; + return []; }); } } @@ -233,7 +242,7 @@ abstract class TestUIScrollViewHostApi { assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.getContentOffset was null, expected non-null int.'); final List output = api.getContentOffset(arg_identifier!); - return {'result': output}; + return [output]; }); } } @@ -258,7 +267,7 @@ abstract class TestUIScrollViewHostApi { assert(arg_y != null, 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.scrollBy was null, expected non-null double.'); api.scrollBy(arg_identifier!, arg_x!, arg_y!); - return {}; + return []; }); } } @@ -283,7 +292,7 @@ abstract class TestUIScrollViewHostApi { assert(arg_y != null, 'Argument for dev.flutter.pigeon.UIScrollViewHostApi.setContentOffset was null, expected non-null double.'); api.setContentOffset(arg_identifier!, arg_x!, arg_y!); - return {}; + return []; }); } } @@ -314,15 +323,22 @@ class _TestWKWebViewConfigurationHostApiCodec extends StandardMessageCodec { } } +/// Mirror of WKWebViewConfiguration. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebviewconfiguration?language=objc. abstract class TestWKWebViewConfigurationHostApi { static const MessageCodec codec = _TestWKWebViewConfigurationHostApiCodec(); void create(int identifier); + void createFromWebView(int identifier, int webViewIdentifier); + void setAllowsInlineMediaPlayback(int identifier, bool allow); + void setMediaTypesRequiringUserActionForPlayback( int identifier, List types); + static void setup(TestWKWebViewConfigurationHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -340,7 +356,7 @@ abstract class TestWKWebViewConfigurationHostApi { assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.create was null, expected non-null int.'); api.create(arg_identifier!); - return {}; + return []; }); } } @@ -363,7 +379,7 @@ abstract class TestWKWebViewConfigurationHostApi { assert(arg_webViewIdentifier != null, 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.createFromWebView was null, expected non-null int.'); api.createFromWebView(arg_identifier!, arg_webViewIdentifier!); - return {}; + return []; }); } } @@ -386,7 +402,7 @@ abstract class TestWKWebViewConfigurationHostApi { assert(arg_allow != null, 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.setAllowsInlineMediaPlayback was null, expected non-null bool.'); api.setAllowsInlineMediaPlayback(arg_identifier!, arg_allow!); - return {}; + return []; }); } } @@ -412,7 +428,7 @@ abstract class TestWKWebViewConfigurationHostApi { 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.setMediaTypesRequiringUserActionForPlayback was null, expected non-null List.'); api.setMediaTypesRequiringUserActionForPlayback( arg_identifier!, arg_types!); - return {}; + return []; }); } } @@ -449,18 +465,27 @@ class _TestWKUserContentControllerHostApiCodec extends StandardMessageCodec { } } +/// Mirror of WKUserContentController. +/// +/// See https://developer.apple.com/documentation/webkit/wkusercontentcontroller?language=objc. abstract class TestWKUserContentControllerHostApi { static const MessageCodec codec = _TestWKUserContentControllerHostApiCodec(); void createFromWebViewConfiguration( int identifier, int configurationIdentifier); + void addScriptMessageHandler( int identifier, int handlerIdentifier, String name); + void removeScriptMessageHandler(int identifier, String name); + void removeAllScriptMessageHandlers(int identifier); + void addUserScript(int identifier, WKUserScriptData userScript); + void removeAllUserScripts(int identifier); + static void setup(TestWKUserContentControllerHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -483,7 +508,7 @@ abstract class TestWKUserContentControllerHostApi { 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.createFromWebViewConfiguration was null, expected non-null int.'); api.createFromWebViewConfiguration( arg_identifier!, arg_configurationIdentifier!); - return {}; + return []; }); } } @@ -510,7 +535,7 @@ abstract class TestWKUserContentControllerHostApi { 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.addScriptMessageHandler was null, expected non-null String.'); api.addScriptMessageHandler( arg_identifier!, arg_handlerIdentifier!, arg_name!); - return {}; + return []; }); } } @@ -533,7 +558,7 @@ abstract class TestWKUserContentControllerHostApi { assert(arg_name != null, 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.removeScriptMessageHandler was null, expected non-null String.'); api.removeScriptMessageHandler(arg_identifier!, arg_name!); - return {}; + return []; }); } } @@ -553,7 +578,7 @@ abstract class TestWKUserContentControllerHostApi { assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllScriptMessageHandlers was null, expected non-null int.'); api.removeAllScriptMessageHandlers(arg_identifier!); - return {}; + return []; }); } } @@ -577,7 +602,7 @@ abstract class TestWKUserContentControllerHostApi { assert(arg_userScript != null, 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.addUserScript was null, expected non-null WKUserScriptData.'); api.addUserScript(arg_identifier!, arg_userScript!); - return {}; + return []; }); } } @@ -597,23 +622,24 @@ abstract class TestWKUserContentControllerHostApi { assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.WKUserContentControllerHostApi.removeAllUserScripts was null, expected non-null int.'); api.removeAllUserScripts(arg_identifier!); - return {}; + return []; }); } } } } -class _TestWKPreferencesHostApiCodec extends StandardMessageCodec { - const _TestWKPreferencesHostApiCodec(); -} - +/// Mirror of WKUserPreferences. +/// +/// See https://developer.apple.com/documentation/webkit/wkpreferences?language=objc. abstract class TestWKPreferencesHostApi { - static const MessageCodec codec = _TestWKPreferencesHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void createFromWebViewConfiguration( int identifier, int configurationIdentifier); + void setJavaScriptEnabled(int identifier, bool enabled); + static void setup(TestWKPreferencesHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -636,7 +662,7 @@ abstract class TestWKPreferencesHostApi { 'Argument for dev.flutter.pigeon.WKPreferencesHostApi.createFromWebViewConfiguration was null, expected non-null int.'); api.createFromWebViewConfiguration( arg_identifier!, arg_configurationIdentifier!); - return {}; + return []; }); } } @@ -658,22 +684,21 @@ abstract class TestWKPreferencesHostApi { assert(arg_enabled != null, 'Argument for dev.flutter.pigeon.WKPreferencesHostApi.setJavaScriptEnabled was null, expected non-null bool.'); api.setJavaScriptEnabled(arg_identifier!, arg_enabled!); - return {}; + return []; }); } } } } -class _TestWKScriptMessageHandlerHostApiCodec extends StandardMessageCodec { - const _TestWKScriptMessageHandlerHostApiCodec(); -} - +/// Mirror of WKScriptMessageHandler. +/// +/// See https://developer.apple.com/documentation/webkit/wkscriptmessagehandler?language=objc. abstract class TestWKScriptMessageHandlerHostApi { - static const MessageCodec codec = - _TestWKScriptMessageHandlerHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void create(int identifier); + static void setup(TestWKScriptMessageHandlerHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -691,22 +716,21 @@ abstract class TestWKScriptMessageHandlerHostApi { assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.WKScriptMessageHandlerHostApi.create was null, expected non-null int.'); api.create(arg_identifier!); - return {}; + return []; }); } } } } -class _TestWKNavigationDelegateHostApiCodec extends StandardMessageCodec { - const _TestWKNavigationDelegateHostApiCodec(); -} - +/// Mirror of WKNavigationDelegate. +/// +/// See https://developer.apple.com/documentation/webkit/wknavigationdelegate?language=objc. abstract class TestWKNavigationDelegateHostApi { - static const MessageCodec codec = - _TestWKNavigationDelegateHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void create(int identifier); + static void setup(TestWKNavigationDelegateHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -724,7 +748,7 @@ abstract class TestWKNavigationDelegateHostApi { assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.WKNavigationDelegateHostApi.create was null, expected non-null int.'); api.create(arg_identifier!); - return {}; + return []; }); } } @@ -755,13 +779,19 @@ class _TestNSObjectHostApiCodec extends StandardMessageCodec { } } +/// Mirror of NSObject. +/// +/// See https://developer.apple.com/documentation/objectivec/nsobject. abstract class TestNSObjectHostApi { static const MessageCodec codec = _TestNSObjectHostApiCodec(); void dispose(int identifier); + void addObserver(int identifier, int observerIdentifier, String keyPath, List options); + void removeObserver(int identifier, int observerIdentifier, String keyPath); + static void setup(TestNSObjectHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -779,7 +809,7 @@ abstract class TestNSObjectHostApi { assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.NSObjectHostApi.dispose was null, expected non-null int.'); api.dispose(arg_identifier!); - return {}; + return []; }); } } @@ -810,7 +840,7 @@ abstract class TestNSObjectHostApi { 'Argument for dev.flutter.pigeon.NSObjectHostApi.addObserver was null, expected non-null List.'); api.addObserver(arg_identifier!, arg_observerIdentifier!, arg_keyPath!, arg_options!); - return {}; + return []; }); } } @@ -836,7 +866,7 @@ abstract class TestNSObjectHostApi { 'Argument for dev.flutter.pigeon.NSObjectHostApi.removeObserver was null, expected non-null String.'); api.removeObserver( arg_identifier!, arg_observerIdentifier!, arg_keyPath!); - return {}; + return []; }); } } @@ -945,27 +975,48 @@ class _TestWKWebViewHostApiCodec extends StandardMessageCodec { } } +/// Mirror of WKWebView. +/// +/// See https://developer.apple.com/documentation/webkit/wkwebview?language=objc. abstract class TestWKWebViewHostApi { static const MessageCodec codec = _TestWKWebViewHostApiCodec(); void create(int identifier, int configurationIdentifier); + void setUIDelegate(int identifier, int? uiDelegateIdentifier); + void setNavigationDelegate(int identifier, int? navigationDelegateIdentifier); + String? getUrl(int identifier); + double getEstimatedProgress(int identifier); + void loadRequest(int identifier, NSUrlRequestData request); + void loadHtmlString(int identifier, String string, String? baseUrl); + void loadFileUrl(int identifier, String url, String readAccessUrl); + void loadFlutterAsset(int identifier, String key); + bool canGoBack(int identifier); + bool canGoForward(int identifier); + void goBack(int identifier); + void goForward(int identifier); + void reload(int identifier); + String? getTitle(int identifier); + void setAllowsBackForwardNavigationGestures(int identifier, bool allow); + void setCustomUserAgent(int identifier, String? userAgent); + Future evaluateJavaScript(int identifier, String javaScriptString); + static void setup(TestWKWebViewHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -986,7 +1037,7 @@ abstract class TestWKWebViewHostApi { assert(arg_configurationIdentifier != null, 'Argument for dev.flutter.pigeon.WKWebViewHostApi.create was null, expected non-null int.'); api.create(arg_identifier!, arg_configurationIdentifier!); - return {}; + return []; }); } } @@ -1006,7 +1057,7 @@ abstract class TestWKWebViewHostApi { 'Argument for dev.flutter.pigeon.WKWebViewHostApi.setUIDelegate was null, expected non-null int.'); final int? arg_uiDelegateIdentifier = (args[1] as int?); api.setUIDelegate(arg_identifier!, arg_uiDelegateIdentifier); - return {}; + return []; }); } } @@ -1027,7 +1078,7 @@ abstract class TestWKWebViewHostApi { final int? arg_navigationDelegateIdentifier = (args[1] as int?); api.setNavigationDelegate( arg_identifier!, arg_navigationDelegateIdentifier); - return {}; + return []; }); } } @@ -1046,7 +1097,7 @@ abstract class TestWKWebViewHostApi { assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.WKWebViewHostApi.getUrl was null, expected non-null int.'); final String? output = api.getUrl(arg_identifier!); - return {'result': output}; + return [output]; }); } } @@ -1065,7 +1116,7 @@ abstract class TestWKWebViewHostApi { assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.WKWebViewHostApi.getEstimatedProgress was null, expected non-null int.'); final double output = api.getEstimatedProgress(arg_identifier!); - return {'result': output}; + return [output]; }); } } @@ -1087,7 +1138,7 @@ abstract class TestWKWebViewHostApi { assert(arg_request != null, 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadRequest was null, expected non-null NSUrlRequestData.'); api.loadRequest(arg_identifier!, arg_request!); - return {}; + return []; }); } } @@ -1110,7 +1161,7 @@ abstract class TestWKWebViewHostApi { 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadHtmlString was null, expected non-null String.'); final String? arg_baseUrl = (args[2] as String?); api.loadHtmlString(arg_identifier!, arg_string!, arg_baseUrl); - return {}; + return []; }); } } @@ -1135,7 +1186,7 @@ abstract class TestWKWebViewHostApi { assert(arg_readAccessUrl != null, 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadFileUrl was null, expected non-null String.'); api.loadFileUrl(arg_identifier!, arg_url!, arg_readAccessUrl!); - return {}; + return []; }); } } @@ -1157,7 +1208,7 @@ abstract class TestWKWebViewHostApi { assert(arg_key != null, 'Argument for dev.flutter.pigeon.WKWebViewHostApi.loadFlutterAsset was null, expected non-null String.'); api.loadFlutterAsset(arg_identifier!, arg_key!); - return {}; + return []; }); } } @@ -1176,7 +1227,7 @@ abstract class TestWKWebViewHostApi { assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.WKWebViewHostApi.canGoBack was null, expected non-null int.'); final bool output = api.canGoBack(arg_identifier!); - return {'result': output}; + return [output]; }); } } @@ -1195,7 +1246,7 @@ abstract class TestWKWebViewHostApi { assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.WKWebViewHostApi.canGoForward was null, expected non-null int.'); final bool output = api.canGoForward(arg_identifier!); - return {'result': output}; + return [output]; }); } } @@ -1214,7 +1265,7 @@ abstract class TestWKWebViewHostApi { assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.WKWebViewHostApi.goBack was null, expected non-null int.'); api.goBack(arg_identifier!); - return {}; + return []; }); } } @@ -1233,7 +1284,7 @@ abstract class TestWKWebViewHostApi { assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.WKWebViewHostApi.goForward was null, expected non-null int.'); api.goForward(arg_identifier!); - return {}; + return []; }); } } @@ -1252,7 +1303,7 @@ abstract class TestWKWebViewHostApi { assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.WKWebViewHostApi.reload was null, expected non-null int.'); api.reload(arg_identifier!); - return {}; + return []; }); } } @@ -1271,7 +1322,7 @@ abstract class TestWKWebViewHostApi { assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.WKWebViewHostApi.getTitle was null, expected non-null int.'); final String? output = api.getTitle(arg_identifier!); - return {'result': output}; + return [output]; }); } } @@ -1295,7 +1346,7 @@ abstract class TestWKWebViewHostApi { 'Argument for dev.flutter.pigeon.WKWebViewHostApi.setAllowsBackForwardNavigationGestures was null, expected non-null bool.'); api.setAllowsBackForwardNavigationGestures( arg_identifier!, arg_allow!); - return {}; + return []; }); } } @@ -1315,7 +1366,7 @@ abstract class TestWKWebViewHostApi { 'Argument for dev.flutter.pigeon.WKWebViewHostApi.setCustomUserAgent was null, expected non-null int.'); final String? arg_userAgent = (args[1] as String?); api.setCustomUserAgent(arg_identifier!, arg_userAgent); - return {}; + return []; }); } } @@ -1338,21 +1389,21 @@ abstract class TestWKWebViewHostApi { 'Argument for dev.flutter.pigeon.WKWebViewHostApi.evaluateJavaScript was null, expected non-null String.'); final Object? output = await api.evaluateJavaScript( arg_identifier!, arg_javaScriptString!); - return {'result': output}; + return [output]; }); } } } } -class _TestWKUIDelegateHostApiCodec extends StandardMessageCodec { - const _TestWKUIDelegateHostApiCodec(); -} - +/// Mirror of WKUIDelegate. +/// +/// See https://developer.apple.com/documentation/webkit/wkuidelegate?language=objc. abstract class TestWKUIDelegateHostApi { - static const MessageCodec codec = _TestWKUIDelegateHostApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); void create(int identifier); + static void setup(TestWKUIDelegateHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1370,7 +1421,7 @@ abstract class TestWKUIDelegateHostApi { assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.WKUIDelegateHostApi.create was null, expected non-null int.'); api.create(arg_identifier!); - return {}; + return []; }); } } @@ -1407,13 +1458,18 @@ class _TestWKHttpCookieStoreHostApiCodec extends StandardMessageCodec { } } +/// Mirror of WKHttpCookieStore. +/// +/// See https://developer.apple.com/documentation/webkit/wkhttpcookiestore?language=objc. abstract class TestWKHttpCookieStoreHostApi { static const MessageCodec codec = _TestWKHttpCookieStoreHostApiCodec(); void createFromWebsiteDataStore( int identifier, int websiteDataStoreIdentifier); + Future setCookie(int identifier, NSHttpCookieData cookie); + static void setup(TestWKHttpCookieStoreHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1436,7 +1492,7 @@ abstract class TestWKHttpCookieStoreHostApi { 'Argument for dev.flutter.pigeon.WKHttpCookieStoreHostApi.createFromWebsiteDataStore was null, expected non-null int.'); api.createFromWebsiteDataStore( arg_identifier!, arg_websiteDataStoreIdentifier!); - return {}; + return []; }); } } @@ -1458,7 +1514,7 @@ abstract class TestWKHttpCookieStoreHostApi { assert(arg_cookie != null, 'Argument for dev.flutter.pigeon.WKHttpCookieStoreHostApi.setCookie was null, expected non-null NSHttpCookieData.'); await api.setCookie(arg_identifier!, arg_cookie!); - return {}; + return []; }); } } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.dart index 87b659885b52..b9536208c716 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.dart @@ -8,11 +8,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; -import 'package:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart'; +import 'package:webview_flutter_wkwebview/src/common/web_kit.g.dart'; import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; import 'package:webview_flutter_wkwebview/src/foundation/foundation_api_impls.dart'; -import '../common/test_web_kit.pigeon.dart'; +import '../common/test_web_kit.g.dart'; import 'foundation_test.mocks.dart'; @GenerateMocks([ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart index 62a51e17bc75..d93198ed9d2f 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart @@ -1,12 +1,12 @@ -// Mocks generated by Mockito 5.2.0 from annotations -// in webview_flutter_wkwebview/example/ios/.symlinks/plugins/webview_flutter_wkwebview/test/src/foundation/foundation_test.dart. +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter_wkwebview/test/src/foundation/foundation_test.dart. // Do not manually edit this file. +// ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart' - as _i3; +import 'package:webview_flutter_wkwebview/src/common/web_kit.g.dart' as _i3; -import '../common/test_web_kit.pigeon.dart' as _i2; +import '../common/test_web_kit.g.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -17,6 +17,7 @@ import '../common/test_web_kit.pigeon.dart' as _i2; // 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 [TestNSObjectHostApi]. /// @@ -28,21 +29,47 @@ class MockTestNSObjectHostApi extends _i1.Mock } @override - void dispose(int? identifier) => - super.noSuchMethod(Invocation.method(#dispose, [identifier]), - returnValueForMissingStub: null); + void dispose(int? identifier) => super.noSuchMethod( + Invocation.method( + #dispose, + [identifier], + ), + returnValueForMissingStub: null, + ); @override - void addObserver(int? identifier, int? observerIdentifier, String? keyPath, - List<_i3.NSKeyValueObservingOptionsEnumData?>? options) => + void addObserver( + int? identifier, + int? observerIdentifier, + String? keyPath, + List<_i3.NSKeyValueObservingOptionsEnumData?>? options, + ) => super.noSuchMethod( - Invocation.method( - #addObserver, [identifier, observerIdentifier, keyPath, options]), - returnValueForMissingStub: null); + Invocation.method( + #addObserver, + [ + identifier, + observerIdentifier, + keyPath, + options, + ], + ), + returnValueForMissingStub: null, + ); @override void removeObserver( - int? identifier, int? observerIdentifier, String? keyPath) => + int? identifier, + int? observerIdentifier, + String? keyPath, + ) => super.noSuchMethod( - Invocation.method( - #removeObserver, [identifier, observerIdentifier, keyPath]), - returnValueForMissingStub: null); + Invocation.method( + #removeObserver, + [ + identifier, + observerIdentifier, + keyPath, + ], + ), + returnValueForMissingStub: null, + ); } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.dart index f2250e1ac423..f6295668363f 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.dart @@ -12,7 +12,7 @@ import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; import 'package:webview_flutter_wkwebview/src/ui_kit/ui_kit.dart'; import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; -import '../common/test_web_kit.pigeon.dart'; +import '../common/test_web_kit.g.dart'; import 'ui_kit_test.mocks.dart'; @GenerateMocks([ 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 a382ecff677c..6200b8dbcadf 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 @@ -1,14 +1,14 @@ -// Mocks generated by Mockito 5.2.0 from annotations -// in webview_flutter_wkwebview/example/ios/.symlinks/plugins/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.dart. +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.dart. // Do not manually edit this file. +// ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart' - as _i3; +import 'package:webview_flutter_wkwebview/src/common/web_kit.g.dart' as _i3; -import '../common/test_web_kit.pigeon.dart' as _i2; +import '../common/test_web_kit.g.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -19,6 +19,7 @@ import '../common/test_web_kit.pigeon.dart' as _i2; // 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 [TestWKWebViewConfigurationHostApi]. /// @@ -30,27 +31,58 @@ class MockTestWKWebViewConfigurationHostApi extends _i1.Mock } @override - void create(int? identifier) => - super.noSuchMethod(Invocation.method(#create, [identifier]), - returnValueForMissingStub: null); + void create(int? identifier) => super.noSuchMethod( + Invocation.method( + #create, + [identifier], + ), + returnValueForMissingStub: null, + ); @override - void createFromWebView(int? identifier, int? webViewIdentifier) => + void createFromWebView( + int? identifier, + int? webViewIdentifier, + ) => super.noSuchMethod( - Invocation.method( - #createFromWebView, [identifier, webViewIdentifier]), - returnValueForMissingStub: null); + Invocation.method( + #createFromWebView, + [ + identifier, + webViewIdentifier, + ], + ), + returnValueForMissingStub: null, + ); @override - void setAllowsInlineMediaPlayback(int? identifier, bool? allow) => + void setAllowsInlineMediaPlayback( + int? identifier, + bool? allow, + ) => super.noSuchMethod( - Invocation.method(#setAllowsInlineMediaPlayback, [identifier, allow]), - returnValueForMissingStub: null); + Invocation.method( + #setAllowsInlineMediaPlayback, + [ + identifier, + allow, + ], + ), + returnValueForMissingStub: null, + ); @override void setMediaTypesRequiringUserActionForPlayback( - int? identifier, List<_i3.WKAudiovisualMediaTypeEnumData?>? types) => + int? identifier, + List<_i3.WKAudiovisualMediaTypeEnumData?>? types, + ) => super.noSuchMethod( - Invocation.method(#setMediaTypesRequiringUserActionForPlayback, - [identifier, types]), - returnValueForMissingStub: null); + Invocation.method( + #setMediaTypesRequiringUserActionForPlayback, + [ + identifier, + types, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestWKWebViewHostApi]. @@ -63,89 +95,217 @@ class MockTestWKWebViewHostApi extends _i1.Mock } @override - void create(int? identifier, int? configurationIdentifier) => + void create( + int? identifier, + int? configurationIdentifier, + ) => super.noSuchMethod( - Invocation.method(#create, [identifier, configurationIdentifier]), - returnValueForMissingStub: null); + Invocation.method( + #create, + [ + identifier, + configurationIdentifier, + ], + ), + returnValueForMissingStub: null, + ); @override - void setUIDelegate(int? identifier, int? uiDelegateIdentifier) => + void setUIDelegate( + int? identifier, + int? uiDelegateIdentifier, + ) => super.noSuchMethod( - Invocation.method(#setUIDelegate, [identifier, uiDelegateIdentifier]), - returnValueForMissingStub: null); + Invocation.method( + #setUIDelegate, + [ + identifier, + uiDelegateIdentifier, + ], + ), + returnValueForMissingStub: null, + ); @override void setNavigationDelegate( - int? identifier, int? navigationDelegateIdentifier) => + int? identifier, + int? navigationDelegateIdentifier, + ) => super.noSuchMethod( - Invocation.method(#setNavigationDelegate, - [identifier, navigationDelegateIdentifier]), - returnValueForMissingStub: null); + Invocation.method( + #setNavigationDelegate, + [ + identifier, + navigationDelegateIdentifier, + ], + ), + returnValueForMissingStub: null, + ); @override - String? getUrl(int? identifier) => - (super.noSuchMethod(Invocation.method(#getUrl, [identifier])) as String?); + String? getUrl(int? identifier) => (super.noSuchMethod(Invocation.method( + #getUrl, + [identifier], + )) as String?); @override double getEstimatedProgress(int? identifier) => (super.noSuchMethod( - Invocation.method(#getEstimatedProgress, [identifier]), - returnValue: 0.0) as double); + Invocation.method( + #getEstimatedProgress, + [identifier], + ), + returnValue: 0.0, + ) as double); @override - void loadRequest(int? identifier, _i3.NSUrlRequestData? request) => - super.noSuchMethod(Invocation.method(#loadRequest, [identifier, request]), - returnValueForMissingStub: null); + void loadRequest( + int? identifier, + _i3.NSUrlRequestData? request, + ) => + super.noSuchMethod( + Invocation.method( + #loadRequest, + [ + identifier, + request, + ], + ), + returnValueForMissingStub: null, + ); @override - void loadHtmlString(int? identifier, String? string, String? baseUrl) => + void loadHtmlString( + int? identifier, + String? string, + String? baseUrl, + ) => super.noSuchMethod( - Invocation.method(#loadHtmlString, [identifier, string, baseUrl]), - returnValueForMissingStub: null); + Invocation.method( + #loadHtmlString, + [ + identifier, + string, + baseUrl, + ], + ), + returnValueForMissingStub: null, + ); @override - void loadFileUrl(int? identifier, String? url, String? readAccessUrl) => + void loadFileUrl( + int? identifier, + String? url, + String? readAccessUrl, + ) => super.noSuchMethod( - Invocation.method(#loadFileUrl, [identifier, url, readAccessUrl]), - returnValueForMissingStub: null); + Invocation.method( + #loadFileUrl, + [ + identifier, + url, + readAccessUrl, + ], + ), + returnValueForMissingStub: null, + ); @override - void loadFlutterAsset(int? identifier, String? key) => super.noSuchMethod( - Invocation.method(#loadFlutterAsset, [identifier, key]), - returnValueForMissingStub: null); + void loadFlutterAsset( + int? identifier, + String? key, + ) => + super.noSuchMethod( + Invocation.method( + #loadFlutterAsset, + [ + identifier, + key, + ], + ), + returnValueForMissingStub: null, + ); @override - bool canGoBack(int? identifier) => - (super.noSuchMethod(Invocation.method(#canGoBack, [identifier]), - returnValue: false) as bool); + bool canGoBack(int? identifier) => (super.noSuchMethod( + Invocation.method( + #canGoBack, + [identifier], + ), + returnValue: false, + ) as bool); @override - bool canGoForward(int? identifier) => - (super.noSuchMethod(Invocation.method(#canGoForward, [identifier]), - returnValue: false) as bool); + bool canGoForward(int? identifier) => (super.noSuchMethod( + Invocation.method( + #canGoForward, + [identifier], + ), + returnValue: false, + ) as bool); @override - void goBack(int? identifier) => - super.noSuchMethod(Invocation.method(#goBack, [identifier]), - returnValueForMissingStub: null); + void goBack(int? identifier) => super.noSuchMethod( + Invocation.method( + #goBack, + [identifier], + ), + returnValueForMissingStub: null, + ); @override - void goForward(int? identifier) => - super.noSuchMethod(Invocation.method(#goForward, [identifier]), - returnValueForMissingStub: null); + void goForward(int? identifier) => super.noSuchMethod( + Invocation.method( + #goForward, + [identifier], + ), + returnValueForMissingStub: null, + ); @override - void reload(int? identifier) => - super.noSuchMethod(Invocation.method(#reload, [identifier]), - returnValueForMissingStub: null); + void reload(int? identifier) => super.noSuchMethod( + Invocation.method( + #reload, + [identifier], + ), + returnValueForMissingStub: null, + ); @override - String? getTitle(int? identifier) => - (super.noSuchMethod(Invocation.method(#getTitle, [identifier])) - as String?); + String? getTitle(int? identifier) => (super.noSuchMethod(Invocation.method( + #getTitle, + [identifier], + )) as String?); @override - void setAllowsBackForwardNavigationGestures(int? identifier, bool? allow) => + void setAllowsBackForwardNavigationGestures( + int? identifier, + bool? allow, + ) => super.noSuchMethod( - Invocation.method( - #setAllowsBackForwardNavigationGestures, [identifier, allow]), - returnValueForMissingStub: null); + Invocation.method( + #setAllowsBackForwardNavigationGestures, + [ + identifier, + allow, + ], + ), + returnValueForMissingStub: null, + ); @override - void setCustomUserAgent(int? identifier, String? userAgent) => + void setCustomUserAgent( + int? identifier, + String? userAgent, + ) => super.noSuchMethod( - Invocation.method(#setCustomUserAgent, [identifier, userAgent]), - returnValueForMissingStub: null); + Invocation.method( + #setCustomUserAgent, + [ + identifier, + userAgent, + ], + ), + returnValueForMissingStub: null, + ); @override _i4.Future evaluateJavaScript( - int? identifier, String? javaScriptString) => + int? identifier, + String? javaScriptString, + ) => (super.noSuchMethod( - Invocation.method( - #evaluateJavaScript, [identifier, javaScriptString]), - returnValue: Future.value()) as _i4.Future); + Invocation.method( + #evaluateJavaScript, + [ + identifier, + javaScriptString, + ], + ), + returnValue: _i4.Future.value(), + ) as _i4.Future); } /// A class which mocks [TestUIScrollViewHostApi]. @@ -158,23 +318,62 @@ class MockTestUIScrollViewHostApi extends _i1.Mock } @override - void createFromWebView(int? identifier, int? webViewIdentifier) => + void createFromWebView( + int? identifier, + int? webViewIdentifier, + ) => super.noSuchMethod( - Invocation.method( - #createFromWebView, [identifier, webViewIdentifier]), - returnValueForMissingStub: null); + Invocation.method( + #createFromWebView, + [ + identifier, + webViewIdentifier, + ], + ), + returnValueForMissingStub: null, + ); @override - List getContentOffset(int? identifier) => - (super.noSuchMethod(Invocation.method(#getContentOffset, [identifier]), - returnValue: []) as List); + List getContentOffset(int? identifier) => (super.noSuchMethod( + Invocation.method( + #getContentOffset, + [identifier], + ), + returnValue: [], + ) as List); @override - void scrollBy(int? identifier, double? x, double? y) => - super.noSuchMethod(Invocation.method(#scrollBy, [identifier, x, y]), - returnValueForMissingStub: null); + void scrollBy( + int? identifier, + double? x, + double? y, + ) => + super.noSuchMethod( + Invocation.method( + #scrollBy, + [ + identifier, + x, + y, + ], + ), + returnValueForMissingStub: null, + ); @override - void setContentOffset(int? identifier, double? x, double? y) => super - .noSuchMethod(Invocation.method(#setContentOffset, [identifier, x, y]), - returnValueForMissingStub: null); + void setContentOffset( + int? identifier, + double? x, + double? y, + ) => + super.noSuchMethod( + Invocation.method( + #setContentOffset, + [ + identifier, + x, + y, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestUIViewHostApi]. @@ -186,11 +385,33 @@ class MockTestUIViewHostApi extends _i1.Mock implements _i2.TestUIViewHostApi { } @override - void setBackgroundColor(int? identifier, int? value) => super.noSuchMethod( - Invocation.method(#setBackgroundColor, [identifier, value]), - returnValueForMissingStub: null); + void setBackgroundColor( + int? identifier, + int? value, + ) => + super.noSuchMethod( + Invocation.method( + #setBackgroundColor, + [ + identifier, + value, + ], + ), + returnValueForMissingStub: null, + ); @override - void setOpaque(int? identifier, bool? opaque) => - super.noSuchMethod(Invocation.method(#setOpaque, [identifier, opaque]), - returnValueForMissingStub: null); + void setOpaque( + int? identifier, + bool? opaque, + ) => + super.noSuchMethod( + Invocation.method( + #setOpaque, + [ + identifier, + opaque, + ], + ), + returnValueForMissingStub: null, + ); } 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 4000e0d718da..dd007869f0e3 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 @@ -9,12 +9,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; -import 'package:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart'; +import 'package:webview_flutter_wkwebview/src/common/web_kit.g.dart'; import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; import 'package:webview_flutter_wkwebview/src/web_kit/web_kit_api_impls.dart'; -import '../common/test_web_kit.pigeon.dart'; +import '../common/test_web_kit.g.dart'; import 'web_kit_test.mocks.dart'; @GenerateMocks([ @@ -116,13 +116,15 @@ void main() { completion(true), ); - final List typeData = + final List capturedArgs = verify(mockPlatformHostApi.removeDataOfTypes( instanceManager.getIdentifier(websiteDataStore), captureAny, 5.0, - )).captured.single.cast() - as List; + )).captured; + final List typeData = + (capturedArgs.single as List) + .cast(); expect(typeData.single.value, WKWebsiteDataTypeEnum.cookies); }); @@ -575,6 +577,7 @@ void main() { allHttpHeaderFields: {}, ), targetFrame: WKFrameInfoData(isMainFrame: false), + navigationType: WKNavigationType.linkActivated, ), ); @@ -922,6 +925,7 @@ void main() { allHttpHeaderFields: {}, ), targetFrame: WKFrameInfoData(isMainFrame: false), + navigationType: WKNavigationType.linkActivated, ), ); 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 18f30d434952..50e09560ed19 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 @@ -1,14 +1,14 @@ -// Mocks generated by Mockito 5.2.0 from annotations -// in webview_flutter_wkwebview/example/ios/.symlinks/plugins/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart. +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart. // Do not manually edit this file. +// ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart' - as _i4; +import 'package:webview_flutter_wkwebview/src/common/web_kit.g.dart' as _i4; -import '../common/test_web_kit.pigeon.dart' as _i2; +import '../common/test_web_kit.g.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -19,6 +19,7 @@ import '../common/test_web_kit.pigeon.dart' as _i2; // 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 [TestWKHttpCookieStoreHostApi]. /// @@ -31,16 +32,35 @@ class MockTestWKHttpCookieStoreHostApi extends _i1.Mock @override void createFromWebsiteDataStore( - int? identifier, int? websiteDataStoreIdentifier) => + int? identifier, + int? websiteDataStoreIdentifier, + ) => super.noSuchMethod( - Invocation.method(#createFromWebsiteDataStore, - [identifier, websiteDataStoreIdentifier]), - returnValueForMissingStub: null); - @override - _i3.Future setCookie(int? identifier, _i4.NSHttpCookieData? cookie) => - (super.noSuchMethod(Invocation.method(#setCookie, [identifier, cookie]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); + Invocation.method( + #createFromWebsiteDataStore, + [ + identifier, + websiteDataStoreIdentifier, + ], + ), + returnValueForMissingStub: null, + ); + @override + _i3.Future setCookie( + int? identifier, + _i4.NSHttpCookieData? cookie, + ) => + (super.noSuchMethod( + Invocation.method( + #setCookie, + [ + identifier, + cookie, + ], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); } /// A class which mocks [TestWKNavigationDelegateHostApi]. @@ -53,9 +73,13 @@ class MockTestWKNavigationDelegateHostApi extends _i1.Mock } @override - void create(int? identifier) => - super.noSuchMethod(Invocation.method(#create, [identifier]), - returnValueForMissingStub: null); + void create(int? identifier) => super.noSuchMethod( + Invocation.method( + #create, + [identifier], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestWKPreferencesHostApi]. @@ -69,16 +93,34 @@ class MockTestWKPreferencesHostApi extends _i1.Mock @override void createFromWebViewConfiguration( - int? identifier, int? configurationIdentifier) => + int? identifier, + int? configurationIdentifier, + ) => super.noSuchMethod( - Invocation.method(#createFromWebViewConfiguration, - [identifier, configurationIdentifier]), - returnValueForMissingStub: null); - @override - void setJavaScriptEnabled(int? identifier, bool? enabled) => + Invocation.method( + #createFromWebViewConfiguration, + [ + identifier, + configurationIdentifier, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setJavaScriptEnabled( + int? identifier, + bool? enabled, + ) => super.noSuchMethod( - Invocation.method(#setJavaScriptEnabled, [identifier, enabled]), - returnValueForMissingStub: null); + Invocation.method( + #setJavaScriptEnabled, + [ + identifier, + enabled, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestWKScriptMessageHandlerHostApi]. @@ -91,9 +133,13 @@ class MockTestWKScriptMessageHandlerHostApi extends _i1.Mock } @override - void create(int? identifier) => - super.noSuchMethod(Invocation.method(#create, [identifier]), - returnValueForMissingStub: null); + void create(int? identifier) => super.noSuchMethod( + Invocation.method( + #create, + [identifier], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestWKUIDelegateHostApi]. @@ -106,9 +152,13 @@ class MockTestWKUIDelegateHostApi extends _i1.Mock } @override - void create(int? identifier) => - super.noSuchMethod(Invocation.method(#create, [identifier]), - returnValueForMissingStub: null); + void create(int? identifier) => super.noSuchMethod( + Invocation.method( + #create, + [identifier], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestWKUserContentControllerHostApi]. @@ -122,35 +172,82 @@ class MockTestWKUserContentControllerHostApi extends _i1.Mock @override void createFromWebViewConfiguration( - int? identifier, int? configurationIdentifier) => + int? identifier, + int? configurationIdentifier, + ) => super.noSuchMethod( - Invocation.method(#createFromWebViewConfiguration, - [identifier, configurationIdentifier]), - returnValueForMissingStub: null); + Invocation.method( + #createFromWebViewConfiguration, + [ + identifier, + configurationIdentifier, + ], + ), + returnValueForMissingStub: null, + ); @override void addScriptMessageHandler( - int? identifier, int? handlerIdentifier, String? name) => + int? identifier, + int? handlerIdentifier, + String? name, + ) => super.noSuchMethod( - Invocation.method( - #addScriptMessageHandler, [identifier, handlerIdentifier, name]), - returnValueForMissingStub: null); - @override - void removeScriptMessageHandler(int? identifier, String? name) => + Invocation.method( + #addScriptMessageHandler, + [ + identifier, + handlerIdentifier, + name, + ], + ), + returnValueForMissingStub: null, + ); + @override + void removeScriptMessageHandler( + int? identifier, + String? name, + ) => super.noSuchMethod( - Invocation.method(#removeScriptMessageHandler, [identifier, name]), - returnValueForMissingStub: null); + Invocation.method( + #removeScriptMessageHandler, + [ + identifier, + name, + ], + ), + returnValueForMissingStub: null, + ); @override void removeAllScriptMessageHandlers(int? identifier) => super.noSuchMethod( - Invocation.method(#removeAllScriptMessageHandlers, [identifier]), - returnValueForMissingStub: null); - @override - void addUserScript(int? identifier, _i4.WKUserScriptData? userScript) => super - .noSuchMethod(Invocation.method(#addUserScript, [identifier, userScript]), - returnValueForMissingStub: null); - @override - void removeAllUserScripts(int? identifier) => - super.noSuchMethod(Invocation.method(#removeAllUserScripts, [identifier]), - returnValueForMissingStub: null); + Invocation.method( + #removeAllScriptMessageHandlers, + [identifier], + ), + returnValueForMissingStub: null, + ); + @override + void addUserScript( + int? identifier, + _i4.WKUserScriptData? userScript, + ) => + super.noSuchMethod( + Invocation.method( + #addUserScript, + [ + identifier, + userScript, + ], + ), + returnValueForMissingStub: null, + ); + @override + void removeAllUserScripts(int? identifier) => super.noSuchMethod( + Invocation.method( + #removeAllUserScripts, + [identifier], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestWKWebViewConfigurationHostApi]. @@ -163,27 +260,58 @@ class MockTestWKWebViewConfigurationHostApi extends _i1.Mock } @override - void create(int? identifier) => - super.noSuchMethod(Invocation.method(#create, [identifier]), - returnValueForMissingStub: null); - @override - void createFromWebView(int? identifier, int? webViewIdentifier) => + void create(int? identifier) => super.noSuchMethod( + Invocation.method( + #create, + [identifier], + ), + returnValueForMissingStub: null, + ); + @override + void createFromWebView( + int? identifier, + int? webViewIdentifier, + ) => super.noSuchMethod( - Invocation.method( - #createFromWebView, [identifier, webViewIdentifier]), - returnValueForMissingStub: null); - @override - void setAllowsInlineMediaPlayback(int? identifier, bool? allow) => + Invocation.method( + #createFromWebView, + [ + identifier, + webViewIdentifier, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setAllowsInlineMediaPlayback( + int? identifier, + bool? allow, + ) => super.noSuchMethod( - Invocation.method(#setAllowsInlineMediaPlayback, [identifier, allow]), - returnValueForMissingStub: null); + Invocation.method( + #setAllowsInlineMediaPlayback, + [ + identifier, + allow, + ], + ), + returnValueForMissingStub: null, + ); @override void setMediaTypesRequiringUserActionForPlayback( - int? identifier, List<_i4.WKAudiovisualMediaTypeEnumData?>? types) => + int? identifier, + List<_i4.WKAudiovisualMediaTypeEnumData?>? types, + ) => super.noSuchMethod( - Invocation.method(#setMediaTypesRequiringUserActionForPlayback, - [identifier, types]), - returnValueForMissingStub: null); + Invocation.method( + #setMediaTypesRequiringUserActionForPlayback, + [ + identifier, + types, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestWKWebViewHostApi]. @@ -196,89 +324,217 @@ class MockTestWKWebViewHostApi extends _i1.Mock } @override - void create(int? identifier, int? configurationIdentifier) => + void create( + int? identifier, + int? configurationIdentifier, + ) => super.noSuchMethod( - Invocation.method(#create, [identifier, configurationIdentifier]), - returnValueForMissingStub: null); - @override - void setUIDelegate(int? identifier, int? uiDelegateIdentifier) => + Invocation.method( + #create, + [ + identifier, + configurationIdentifier, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setUIDelegate( + int? identifier, + int? uiDelegateIdentifier, + ) => super.noSuchMethod( - Invocation.method(#setUIDelegate, [identifier, uiDelegateIdentifier]), - returnValueForMissingStub: null); + Invocation.method( + #setUIDelegate, + [ + identifier, + uiDelegateIdentifier, + ], + ), + returnValueForMissingStub: null, + ); @override void setNavigationDelegate( - int? identifier, int? navigationDelegateIdentifier) => + int? identifier, + int? navigationDelegateIdentifier, + ) => super.noSuchMethod( - Invocation.method(#setNavigationDelegate, - [identifier, navigationDelegateIdentifier]), - returnValueForMissingStub: null); - @override - String? getUrl(int? identifier) => - (super.noSuchMethod(Invocation.method(#getUrl, [identifier])) as String?); + Invocation.method( + #setNavigationDelegate, + [ + identifier, + navigationDelegateIdentifier, + ], + ), + returnValueForMissingStub: null, + ); + @override + String? getUrl(int? identifier) => (super.noSuchMethod(Invocation.method( + #getUrl, + [identifier], + )) as String?); @override double getEstimatedProgress(int? identifier) => (super.noSuchMethod( - Invocation.method(#getEstimatedProgress, [identifier]), - returnValue: 0.0) as double); - @override - void loadRequest(int? identifier, _i4.NSUrlRequestData? request) => - super.noSuchMethod(Invocation.method(#loadRequest, [identifier, request]), - returnValueForMissingStub: null); - @override - void loadHtmlString(int? identifier, String? string, String? baseUrl) => + Invocation.method( + #getEstimatedProgress, + [identifier], + ), + returnValue: 0.0, + ) as double); + @override + void loadRequest( + int? identifier, + _i4.NSUrlRequestData? request, + ) => super.noSuchMethod( - Invocation.method(#loadHtmlString, [identifier, string, baseUrl]), - returnValueForMissingStub: null); - @override - void loadFileUrl(int? identifier, String? url, String? readAccessUrl) => + Invocation.method( + #loadRequest, + [ + identifier, + request, + ], + ), + returnValueForMissingStub: null, + ); + @override + void loadHtmlString( + int? identifier, + String? string, + String? baseUrl, + ) => super.noSuchMethod( - Invocation.method(#loadFileUrl, [identifier, url, readAccessUrl]), - returnValueForMissingStub: null); - @override - void loadFlutterAsset(int? identifier, String? key) => super.noSuchMethod( - Invocation.method(#loadFlutterAsset, [identifier, key]), - returnValueForMissingStub: null); - @override - bool canGoBack(int? identifier) => - (super.noSuchMethod(Invocation.method(#canGoBack, [identifier]), - returnValue: false) as bool); - @override - bool canGoForward(int? identifier) => - (super.noSuchMethod(Invocation.method(#canGoForward, [identifier]), - returnValue: false) as bool); - @override - void goBack(int? identifier) => - super.noSuchMethod(Invocation.method(#goBack, [identifier]), - returnValueForMissingStub: null); - @override - void goForward(int? identifier) => - super.noSuchMethod(Invocation.method(#goForward, [identifier]), - returnValueForMissingStub: null); - @override - void reload(int? identifier) => - super.noSuchMethod(Invocation.method(#reload, [identifier]), - returnValueForMissingStub: null); - @override - String? getTitle(int? identifier) => - (super.noSuchMethod(Invocation.method(#getTitle, [identifier])) - as String?); - @override - void setAllowsBackForwardNavigationGestures(int? identifier, bool? allow) => + Invocation.method( + #loadHtmlString, + [ + identifier, + string, + baseUrl, + ], + ), + returnValueForMissingStub: null, + ); + @override + void loadFileUrl( + int? identifier, + String? url, + String? readAccessUrl, + ) => super.noSuchMethod( - Invocation.method( - #setAllowsBackForwardNavigationGestures, [identifier, allow]), - returnValueForMissingStub: null); - @override - void setCustomUserAgent(int? identifier, String? userAgent) => + Invocation.method( + #loadFileUrl, + [ + identifier, + url, + readAccessUrl, + ], + ), + returnValueForMissingStub: null, + ); + @override + void loadFlutterAsset( + int? identifier, + String? key, + ) => + super.noSuchMethod( + Invocation.method( + #loadFlutterAsset, + [ + identifier, + key, + ], + ), + returnValueForMissingStub: null, + ); + @override + bool canGoBack(int? identifier) => (super.noSuchMethod( + Invocation.method( + #canGoBack, + [identifier], + ), + returnValue: false, + ) as bool); + @override + bool canGoForward(int? identifier) => (super.noSuchMethod( + Invocation.method( + #canGoForward, + [identifier], + ), + returnValue: false, + ) as bool); + @override + void goBack(int? identifier) => super.noSuchMethod( + Invocation.method( + #goBack, + [identifier], + ), + returnValueForMissingStub: null, + ); + @override + void goForward(int? identifier) => super.noSuchMethod( + Invocation.method( + #goForward, + [identifier], + ), + returnValueForMissingStub: null, + ); + @override + void reload(int? identifier) => super.noSuchMethod( + Invocation.method( + #reload, + [identifier], + ), + returnValueForMissingStub: null, + ); + @override + String? getTitle(int? identifier) => (super.noSuchMethod(Invocation.method( + #getTitle, + [identifier], + )) as String?); + @override + void setAllowsBackForwardNavigationGestures( + int? identifier, + bool? allow, + ) => + super.noSuchMethod( + Invocation.method( + #setAllowsBackForwardNavigationGestures, + [ + identifier, + allow, + ], + ), + returnValueForMissingStub: null, + ); + @override + void setCustomUserAgent( + int? identifier, + String? userAgent, + ) => super.noSuchMethod( - Invocation.method(#setCustomUserAgent, [identifier, userAgent]), - returnValueForMissingStub: null); + Invocation.method( + #setCustomUserAgent, + [ + identifier, + userAgent, + ], + ), + returnValueForMissingStub: null, + ); @override _i3.Future evaluateJavaScript( - int? identifier, String? javaScriptString) => + int? identifier, + String? javaScriptString, + ) => (super.noSuchMethod( - Invocation.method( - #evaluateJavaScript, [identifier, javaScriptString]), - returnValue: Future.value()) as _i3.Future); + Invocation.method( + #evaluateJavaScript, + [ + identifier, + javaScriptString, + ], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); } /// A class which mocks [TestWKWebsiteDataStoreHostApi]. @@ -292,22 +548,42 @@ class MockTestWKWebsiteDataStoreHostApi extends _i1.Mock @override void createFromWebViewConfiguration( - int? identifier, int? configurationIdentifier) => + int? identifier, + int? configurationIdentifier, + ) => super.noSuchMethod( - Invocation.method(#createFromWebViewConfiguration, - [identifier, configurationIdentifier]), - returnValueForMissingStub: null); + Invocation.method( + #createFromWebViewConfiguration, + [ + identifier, + configurationIdentifier, + ], + ), + returnValueForMissingStub: null, + ); @override void createDefaultDataStore(int? identifier) => super.noSuchMethod( - Invocation.method(#createDefaultDataStore, [identifier]), - returnValueForMissingStub: null); + Invocation.method( + #createDefaultDataStore, + [identifier], + ), + returnValueForMissingStub: null, + ); @override _i3.Future removeDataOfTypes( - int? identifier, - List<_i4.WKWebsiteDataTypeEnumData?>? dataTypes, - double? modificationTimeInSecondsSinceEpoch) => + int? identifier, + List<_i4.WKWebsiteDataTypeEnumData?>? dataTypes, + double? modificationTimeInSecondsSinceEpoch, + ) => (super.noSuchMethod( - Invocation.method(#removeDataOfTypes, - [identifier, dataTypes, modificationTimeInSecondsSinceEpoch]), - returnValue: Future.value(false)) as _i3.Future); + Invocation.method( + #removeDataOfTypes, + [ + identifier, + dataTypes, + modificationTimeInSecondsSinceEpoch, + ], + ), + returnValue: _i3.Future.value(false), + ) as _i3.Future); } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.mocks.dart deleted file mode 100644 index e44e7b13a205..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.mocks.dart +++ /dev/null @@ -1,100 +0,0 @@ -// Mocks generated by Mockito 5.2.0 from annotations -// in webview_flutter_wkwebview/example/ios/.symlinks/plugins/webview_flutter_wkwebview/test/src/web_kit_cookie_manager_test.dart. -// Do not manually edit this file. - -import 'dart:async' as _i3; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart' - as _i4; -import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart' as _i2; - -// 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 - -class _FakeWKHttpCookieStore_0 extends _i1.Fake - implements _i2.WKHttpCookieStore {} - -class _FakeWKWebsiteDataStore_1 extends _i1.Fake - implements _i2.WKWebsiteDataStore {} - -/// A class which mocks [WKHttpCookieStore]. -/// -/// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable -class MockWKHttpCookieStore extends _i1.Mock implements _i2.WKHttpCookieStore { - MockWKHttpCookieStore() { - _i1.throwOnMissingStub(this); - } - - @override - _i3.Future setCookie(_i4.NSHttpCookie? cookie) => - (super.noSuchMethod(Invocation.method(#setCookie, [cookie]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); - @override - _i2.WKHttpCookieStore copy() => - (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWKHttpCookieStore_0()) as _i2.WKHttpCookieStore); - @override - _i3.Future addObserver(_i4.NSObject? observer, - {String? keyPath, Set<_i4.NSKeyValueObservingOptions>? options}) => - (super.noSuchMethod( - Invocation.method( - #addObserver, [observer], {#keyPath: keyPath, #options: options}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); - @override - _i3.Future removeObserver(_i4.NSObject? observer, {String? keyPath}) => - (super.noSuchMethod( - Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); -} - -/// A class which mocks [WKWebsiteDataStore]. -/// -/// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable -class MockWKWebsiteDataStore extends _i1.Mock - implements _i2.WKWebsiteDataStore { - MockWKWebsiteDataStore() { - _i1.throwOnMissingStub(this); - } - - @override - _i2.WKHttpCookieStore get httpCookieStore => - (super.noSuchMethod(Invocation.getter(#httpCookieStore), - returnValue: _FakeWKHttpCookieStore_0()) as _i2.WKHttpCookieStore); - @override - _i3.Future removeDataOfTypes( - Set<_i2.WKWebsiteDataType>? dataTypes, DateTime? since) => - (super.noSuchMethod( - Invocation.method(#removeDataOfTypes, [dataTypes, since]), - returnValue: Future.value(false)) as _i3.Future); - @override - _i2.WKWebsiteDataStore copy() => - (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWKWebsiteDataStore_1()) as _i2.WKWebsiteDataStore); - @override - _i3.Future addObserver(_i4.NSObject? observer, - {String? keyPath, Set<_i4.NSKeyValueObservingOptions>? options}) => - (super.noSuchMethod( - Invocation.method( - #addObserver, [observer], {#keyPath: keyPath, #options: options}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); - @override - _i3.Future removeObserver(_i4.NSObject? observer, {String? keyPath}) => - (super.noSuchMethod( - Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); -} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.mocks.dart deleted file mode 100644 index f216711ca0b2..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.mocks.dart +++ /dev/null @@ -1,648 +0,0 @@ -// Mocks generated by Mockito 5.2.0 from annotations -// in webview_flutter_wkwebview/example/ios/.symlinks/plugins/webview_flutter_wkwebview/test/src/web_kit_webview_widget_test.dart. -// Do not manually edit this file. - -import 'dart:async' as _i5; -import 'dart:math' as _i2; -import 'dart:ui' as _i6; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_platform_interface/src/types/javascript_channel.dart' - as _i9; -import 'package:webview_flutter_platform_interface/src/types/types.dart' - as _i10; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' - as _i8; -import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart' - as _i7; -import 'package:webview_flutter_wkwebview/src/ui_kit/ui_kit.dart' as _i3; -import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart' as _i4; -import 'package:webview_flutter_wkwebview/src/web_kit_webview_widget.dart' - as _i11; - -// 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 - -class _FakePoint_0 extends _i1.Fake implements _i2.Point {} - -class _FakeUIScrollView_1 extends _i1.Fake implements _i3.UIScrollView {} - -class _FakeWKNavigationDelegate_2 extends _i1.Fake - implements _i4.WKNavigationDelegate {} - -class _FakeWKPreferences_3 extends _i1.Fake implements _i4.WKPreferences {} - -class _FakeWKScriptMessageHandler_4 extends _i1.Fake - implements _i4.WKScriptMessageHandler {} - -class _FakeWKWebViewConfiguration_5 extends _i1.Fake - implements _i4.WKWebViewConfiguration {} - -class _FakeWKWebView_6 extends _i1.Fake implements _i4.WKWebView {} - -class _FakeWKUserContentController_7 extends _i1.Fake - implements _i4.WKUserContentController {} - -class _FakeWKWebsiteDataStore_8 extends _i1.Fake - implements _i4.WKWebsiteDataStore {} - -class _FakeWKHttpCookieStore_9 extends _i1.Fake - implements _i4.WKHttpCookieStore {} - -class _FakeWKUIDelegate_10 extends _i1.Fake implements _i4.WKUIDelegate {} - -/// A class which mocks [UIScrollView]. -/// -/// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable -class MockUIScrollView extends _i1.Mock implements _i3.UIScrollView { - MockUIScrollView() { - _i1.throwOnMissingStub(this); - } - - @override - _i5.Future<_i2.Point> getContentOffset() => (super.noSuchMethod( - Invocation.method(#getContentOffset, []), - returnValue: Future<_i2.Point>.value(_FakePoint_0())) - as _i5.Future<_i2.Point>); - @override - _i5.Future scrollBy(_i2.Point? offset) => - (super.noSuchMethod(Invocation.method(#scrollBy, [offset]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setContentOffset(_i2.Point? offset) => - (super.noSuchMethod(Invocation.method(#setContentOffset, [offset]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i3.UIScrollView copy() => (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeUIScrollView_1()) as _i3.UIScrollView); - @override - _i5.Future setBackgroundColor(_i6.Color? color) => - (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setOpaque(bool? opaque) => - (super.noSuchMethod(Invocation.method(#setOpaque, [opaque]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future addObserver(_i7.NSObject? observer, - {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => - (super.noSuchMethod( - Invocation.method( - #addObserver, [observer], {#keyPath: keyPath, #options: options}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => - (super.noSuchMethod( - Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); -} - -/// A class which mocks [WKNavigationDelegate]. -/// -/// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable -class MockWKNavigationDelegate extends _i1.Mock - implements _i4.WKNavigationDelegate { - MockWKNavigationDelegate() { - _i1.throwOnMissingStub(this); - } - - @override - _i4.WKNavigationDelegate copy() => (super.noSuchMethod( - Invocation.method(#copy, []), - returnValue: _FakeWKNavigationDelegate_2()) as _i4.WKNavigationDelegate); - @override - _i5.Future addObserver(_i7.NSObject? observer, - {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => - (super.noSuchMethod( - Invocation.method( - #addObserver, [observer], {#keyPath: keyPath, #options: options}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => - (super.noSuchMethod( - Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); -} - -/// A class which mocks [WKPreferences]. -/// -/// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable -class MockWKPreferences extends _i1.Mock implements _i4.WKPreferences { - MockWKPreferences() { - _i1.throwOnMissingStub(this); - } - - @override - _i5.Future setJavaScriptEnabled(bool? enabled) => - (super.noSuchMethod(Invocation.method(#setJavaScriptEnabled, [enabled]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i4.WKPreferences copy() => (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWKPreferences_3()) as _i4.WKPreferences); - @override - _i5.Future addObserver(_i7.NSObject? observer, - {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => - (super.noSuchMethod( - Invocation.method( - #addObserver, [observer], {#keyPath: keyPath, #options: options}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => - (super.noSuchMethod( - Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); -} - -/// A class which mocks [WKScriptMessageHandler]. -/// -/// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable -class MockWKScriptMessageHandler extends _i1.Mock - implements _i4.WKScriptMessageHandler { - MockWKScriptMessageHandler() { - _i1.throwOnMissingStub(this); - } - - @override - void Function(_i4.WKUserContentController, _i4.WKScriptMessage) - get didReceiveScriptMessage => - (super.noSuchMethod(Invocation.getter(#didReceiveScriptMessage), - returnValue: (_i4.WKUserContentController userContentController, - _i4.WKScriptMessage message) {}) as void Function( - _i4.WKUserContentController, _i4.WKScriptMessage)); - @override - _i4.WKScriptMessageHandler copy() => - (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWKScriptMessageHandler_4()) - as _i4.WKScriptMessageHandler); - @override - _i5.Future addObserver(_i7.NSObject? observer, - {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => - (super.noSuchMethod( - Invocation.method( - #addObserver, [observer], {#keyPath: keyPath, #options: options}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => - (super.noSuchMethod( - Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); -} - -/// A class which mocks [WKWebView]. -/// -/// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable -class MockWKWebView extends _i1.Mock implements _i4.WKWebView { - MockWKWebView() { - _i1.throwOnMissingStub(this); - } - - @override - _i4.WKWebViewConfiguration get configuration => - (super.noSuchMethod(Invocation.getter(#configuration), - returnValue: _FakeWKWebViewConfiguration_5()) - as _i4.WKWebViewConfiguration); - @override - _i3.UIScrollView get scrollView => - (super.noSuchMethod(Invocation.getter(#scrollView), - returnValue: _FakeUIScrollView_1()) as _i3.UIScrollView); - @override - _i5.Future setUIDelegate(_i4.WKUIDelegate? delegate) => - (super.noSuchMethod(Invocation.method(#setUIDelegate, [delegate]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setNavigationDelegate(_i4.WKNavigationDelegate? delegate) => - (super.noSuchMethod(Invocation.method(#setNavigationDelegate, [delegate]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future getUrl() => - (super.noSuchMethod(Invocation.method(#getUrl, []), - returnValue: Future.value()) as _i5.Future); - @override - _i5.Future getEstimatedProgress() => - (super.noSuchMethod(Invocation.method(#getEstimatedProgress, []), - returnValue: Future.value(0.0)) as _i5.Future); - @override - _i5.Future loadRequest(_i7.NSUrlRequest? request) => - (super.noSuchMethod(Invocation.method(#loadRequest, [request]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future loadHtmlString(String? string, {String? baseUrl}) => - (super.noSuchMethod( - Invocation.method(#loadHtmlString, [string], {#baseUrl: baseUrl}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future loadFileUrl(String? url, {String? readAccessUrl}) => - (super.noSuchMethod( - Invocation.method( - #loadFileUrl, [url], {#readAccessUrl: readAccessUrl}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future loadFlutterAsset(String? key) => - (super.noSuchMethod(Invocation.method(#loadFlutterAsset, [key]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future canGoBack() => - (super.noSuchMethod(Invocation.method(#canGoBack, []), - returnValue: Future.value(false)) as _i5.Future); - @override - _i5.Future canGoForward() => - (super.noSuchMethod(Invocation.method(#canGoForward, []), - returnValue: Future.value(false)) as _i5.Future); - @override - _i5.Future goBack() => - (super.noSuchMethod(Invocation.method(#goBack, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future goForward() => - (super.noSuchMethod(Invocation.method(#goForward, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future reload() => - (super.noSuchMethod(Invocation.method(#reload, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future getTitle() => - (super.noSuchMethod(Invocation.method(#getTitle, []), - returnValue: Future.value()) as _i5.Future); - @override - _i5.Future setAllowsBackForwardNavigationGestures(bool? allow) => - (super.noSuchMethod( - Invocation.method(#setAllowsBackForwardNavigationGestures, [allow]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setCustomUserAgent(String? userAgent) => - (super.noSuchMethod(Invocation.method(#setCustomUserAgent, [userAgent]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future evaluateJavaScript(String? javaScriptString) => (super - .noSuchMethod(Invocation.method(#evaluateJavaScript, [javaScriptString]), - returnValue: Future.value()) as _i5.Future); - @override - _i4.WKWebView copy() => (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWKWebView_6()) as _i4.WKWebView); - @override - _i5.Future setBackgroundColor(_i6.Color? color) => - (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setOpaque(bool? opaque) => - (super.noSuchMethod(Invocation.method(#setOpaque, [opaque]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future addObserver(_i7.NSObject? observer, - {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => - (super.noSuchMethod( - Invocation.method( - #addObserver, [observer], {#keyPath: keyPath, #options: options}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => - (super.noSuchMethod( - Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); -} - -/// A class which mocks [WKWebViewConfiguration]. -/// -/// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable -class MockWKWebViewConfiguration extends _i1.Mock - implements _i4.WKWebViewConfiguration { - MockWKWebViewConfiguration() { - _i1.throwOnMissingStub(this); - } - - @override - _i4.WKUserContentController get userContentController => - (super.noSuchMethod(Invocation.getter(#userContentController), - returnValue: _FakeWKUserContentController_7()) - as _i4.WKUserContentController); - @override - _i4.WKPreferences get preferences => - (super.noSuchMethod(Invocation.getter(#preferences), - returnValue: _FakeWKPreferences_3()) as _i4.WKPreferences); - @override - _i4.WKWebsiteDataStore get websiteDataStore => - (super.noSuchMethod(Invocation.getter(#websiteDataStore), - returnValue: _FakeWKWebsiteDataStore_8()) as _i4.WKWebsiteDataStore); - @override - _i5.Future setAllowsInlineMediaPlayback(bool? allow) => (super - .noSuchMethod(Invocation.method(#setAllowsInlineMediaPlayback, [allow]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setMediaTypesRequiringUserActionForPlayback( - Set<_i4.WKAudiovisualMediaType>? types) => - (super.noSuchMethod( - Invocation.method( - #setMediaTypesRequiringUserActionForPlayback, [types]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i4.WKWebViewConfiguration copy() => - (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWKWebViewConfiguration_5()) - as _i4.WKWebViewConfiguration); - @override - _i5.Future addObserver(_i7.NSObject? observer, - {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => - (super.noSuchMethod( - Invocation.method( - #addObserver, [observer], {#keyPath: keyPath, #options: options}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => - (super.noSuchMethod( - Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); -} - -/// A class which mocks [WKWebsiteDataStore]. -/// -/// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable -class MockWKWebsiteDataStore extends _i1.Mock - implements _i4.WKWebsiteDataStore { - MockWKWebsiteDataStore() { - _i1.throwOnMissingStub(this); - } - - @override - _i4.WKHttpCookieStore get httpCookieStore => - (super.noSuchMethod(Invocation.getter(#httpCookieStore), - returnValue: _FakeWKHttpCookieStore_9()) as _i4.WKHttpCookieStore); - @override - _i5.Future removeDataOfTypes( - Set<_i4.WKWebsiteDataType>? dataTypes, DateTime? since) => - (super.noSuchMethod( - Invocation.method(#removeDataOfTypes, [dataTypes, since]), - returnValue: Future.value(false)) as _i5.Future); - @override - _i4.WKWebsiteDataStore copy() => - (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWKWebsiteDataStore_8()) as _i4.WKWebsiteDataStore); - @override - _i5.Future addObserver(_i7.NSObject? observer, - {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => - (super.noSuchMethod( - Invocation.method( - #addObserver, [observer], {#keyPath: keyPath, #options: options}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => - (super.noSuchMethod( - Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); -} - -/// A class which mocks [WKUIDelegate]. -/// -/// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable -class MockWKUIDelegate extends _i1.Mock implements _i4.WKUIDelegate { - MockWKUIDelegate() { - _i1.throwOnMissingStub(this); - } - - @override - _i4.WKUIDelegate copy() => (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWKUIDelegate_10()) as _i4.WKUIDelegate); - @override - _i5.Future addObserver(_i7.NSObject? observer, - {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => - (super.noSuchMethod( - Invocation.method( - #addObserver, [observer], {#keyPath: keyPath, #options: options}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => - (super.noSuchMethod( - Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); -} - -/// A class which mocks [WKUserContentController]. -/// -/// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable -class MockWKUserContentController extends _i1.Mock - implements _i4.WKUserContentController { - MockWKUserContentController() { - _i1.throwOnMissingStub(this); - } - - @override - _i5.Future addScriptMessageHandler( - _i4.WKScriptMessageHandler? handler, String? name) => - (super.noSuchMethod( - Invocation.method(#addScriptMessageHandler, [handler, name]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeScriptMessageHandler(String? name) => (super - .noSuchMethod(Invocation.method(#removeScriptMessageHandler, [name]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeAllScriptMessageHandlers() => (super.noSuchMethod( - Invocation.method(#removeAllScriptMessageHandlers, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future addUserScript(_i4.WKUserScript? userScript) => - (super.noSuchMethod(Invocation.method(#addUserScript, [userScript]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeAllUserScripts() => - (super.noSuchMethod(Invocation.method(#removeAllUserScripts, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i4.WKUserContentController copy() => - (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWKUserContentController_7()) - as _i4.WKUserContentController); - @override - _i5.Future addObserver(_i7.NSObject? observer, - {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => - (super.noSuchMethod( - Invocation.method( - #addObserver, [observer], {#keyPath: keyPath, #options: options}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => - (super.noSuchMethod( - Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); -} - -/// A class which mocks [JavascriptChannelRegistry]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockJavascriptChannelRegistry extends _i1.Mock - implements _i8.JavascriptChannelRegistry { - MockJavascriptChannelRegistry() { - _i1.throwOnMissingStub(this); - } - - @override - Map get channels => - (super.noSuchMethod(Invocation.getter(#channels), - returnValue: {}) - as Map); - @override - void onJavascriptChannelMessage(String? channel, String? message) => - super.noSuchMethod( - Invocation.method(#onJavascriptChannelMessage, [channel, message]), - returnValueForMissingStub: null); - @override - void updateJavascriptChannelsFromSet(Set<_i9.JavascriptChannel>? channels) => - super.noSuchMethod( - Invocation.method(#updateJavascriptChannelsFromSet, [channels]), - returnValueForMissingStub: null); -} - -/// A class which mocks [WebViewPlatformCallbacksHandler]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockWebViewPlatformCallbacksHandler extends _i1.Mock - implements _i8.WebViewPlatformCallbacksHandler { - MockWebViewPlatformCallbacksHandler() { - _i1.throwOnMissingStub(this); - } - - @override - _i5.FutureOr onNavigationRequest({String? url, bool? isForMainFrame}) => - (super.noSuchMethod( - Invocation.method(#onNavigationRequest, [], - {#url: url, #isForMainFrame: isForMainFrame}), - returnValue: Future.value(false)) as _i5.FutureOr); - @override - void onPageStarted(String? url) => - super.noSuchMethod(Invocation.method(#onPageStarted, [url]), - returnValueForMissingStub: null); - @override - void onPageFinished(String? url) => - super.noSuchMethod(Invocation.method(#onPageFinished, [url]), - returnValueForMissingStub: null); - @override - void onProgress(int? progress) => - super.noSuchMethod(Invocation.method(#onProgress, [progress]), - returnValueForMissingStub: null); - @override - void onWebResourceError(_i10.WebResourceError? error) => - super.noSuchMethod(Invocation.method(#onWebResourceError, [error]), - returnValueForMissingStub: null); -} - -/// A class which mocks [WebViewWidgetProxy]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockWebViewWidgetProxy extends _i1.Mock - implements _i11.WebViewWidgetProxy { - MockWebViewWidgetProxy() { - _i1.throwOnMissingStub(this); - } - - @override - _i4.WKWebView createWebView(_i4.WKWebViewConfiguration? configuration, - {void Function( - String, _i7.NSObject, Map<_i7.NSKeyValueChangeKey, Object?>)? - observeValue}) => - (super.noSuchMethod( - Invocation.method( - #createWebView, [configuration], {#observeValue: observeValue}), - returnValue: _FakeWKWebView_6()) as _i4.WKWebView); - @override - _i4.WKScriptMessageHandler createScriptMessageHandler( - {void Function(_i4.WKUserContentController, _i4.WKScriptMessage)? - didReceiveScriptMessage}) => - (super.noSuchMethod( - Invocation.method(#createScriptMessageHandler, [], - {#didReceiveScriptMessage: didReceiveScriptMessage}), - returnValue: _FakeWKScriptMessageHandler_4()) - as _i4.WKScriptMessageHandler); - @override - _i4.WKUIDelegate createUIDelgate( - {void Function(_i4.WKWebView, _i4.WKWebViewConfiguration, - _i4.WKNavigationAction)? - onCreateWebView}) => - (super.noSuchMethod( - Invocation.method( - #createUIDelgate, [], {#onCreateWebView: onCreateWebView}), - returnValue: _FakeWKUIDelegate_10()) as _i4.WKUIDelegate); - @override - _i4.WKNavigationDelegate createNavigationDelegate( - {void Function(_i4.WKWebView, String?)? didFinishNavigation, - void Function(_i4.WKWebView, String?)? didStartProvisionalNavigation, - _i5.Future<_i4.WKNavigationActionPolicy> Function( - _i4.WKWebView, _i4.WKNavigationAction)? - decidePolicyForNavigationAction, - void Function(_i4.WKWebView, _i7.NSError)? didFailNavigation, - void Function(_i4.WKWebView, _i7.NSError)? - didFailProvisionalNavigation, - void Function(_i4.WKWebView)? - webViewWebContentProcessDidTerminate}) => - (super.noSuchMethod( - Invocation.method(#createNavigationDelegate, [], { - #didFinishNavigation: didFinishNavigation, - #didStartProvisionalNavigation: didStartProvisionalNavigation, - #decidePolicyForNavigationAction: - decidePolicyForNavigationAction, - #didFailNavigation: didFailNavigation, - #didFailProvisionalNavigation: didFailProvisionalNavigation, - #webViewWebContentProcessDidTerminate: - webViewWebContentProcessDidTerminate - }), - returnValue: _FakeWKNavigationDelegate_2()) - as _i4.WKNavigationDelegate); -} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.mocks.dart deleted file mode 100644 index 17d47917b2ee..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.mocks.dart +++ /dev/null @@ -1,414 +0,0 @@ -// Mocks generated by Mockito 5.2.0 from annotations -// in webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.dart. -// Do not manually edit this file. - -import 'dart:async' as _i5; -import 'dart:math' as _i2; -import 'dart:ui' as _i6; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart' - as _i7; -import 'package:webview_flutter_wkwebview/src/ui_kit/ui_kit.dart' as _i3; -import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart' as _i4; - -// 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 - -class _FakePoint_0 extends _i1.Fake implements _i2.Point {} - -class _FakeUIScrollView_1 extends _i1.Fake implements _i3.UIScrollView {} - -class _FakeWKPreferences_2 extends _i1.Fake implements _i4.WKPreferences {} - -class _FakeWKUserContentController_3 extends _i1.Fake - implements _i4.WKUserContentController {} - -class _FakeWKHttpCookieStore_4 extends _i1.Fake - implements _i4.WKHttpCookieStore {} - -class _FakeWKWebsiteDataStore_5 extends _i1.Fake - implements _i4.WKWebsiteDataStore {} - -class _FakeWKWebViewConfiguration_6 extends _i1.Fake - implements _i4.WKWebViewConfiguration {} - -class _FakeWKWebView_7 extends _i1.Fake implements _i4.WKWebView {} - -/// A class which mocks [UIScrollView]. -/// -/// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable -class MockUIScrollView extends _i1.Mock implements _i3.UIScrollView { - MockUIScrollView() { - _i1.throwOnMissingStub(this); - } - - @override - _i5.Future<_i2.Point> getContentOffset() => (super.noSuchMethod( - Invocation.method(#getContentOffset, []), - returnValue: Future<_i2.Point>.value(_FakePoint_0())) - as _i5.Future<_i2.Point>); - @override - _i5.Future scrollBy(_i2.Point? offset) => - (super.noSuchMethod(Invocation.method(#scrollBy, [offset]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setContentOffset(_i2.Point? offset) => - (super.noSuchMethod(Invocation.method(#setContentOffset, [offset]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i3.UIScrollView copy() => (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeUIScrollView_1()) as _i3.UIScrollView); - @override - _i5.Future setBackgroundColor(_i6.Color? color) => - (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setOpaque(bool? opaque) => - (super.noSuchMethod(Invocation.method(#setOpaque, [opaque]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future addObserver(_i7.NSObject? observer, - {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => - (super.noSuchMethod( - Invocation.method( - #addObserver, [observer], {#keyPath: keyPath, #options: options}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => - (super.noSuchMethod( - Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); -} - -/// A class which mocks [WKPreferences]. -/// -/// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable -class MockWKPreferences extends _i1.Mock implements _i4.WKPreferences { - MockWKPreferences() { - _i1.throwOnMissingStub(this); - } - - @override - _i5.Future setJavaScriptEnabled(bool? enabled) => - (super.noSuchMethod(Invocation.method(#setJavaScriptEnabled, [enabled]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i4.WKPreferences copy() => (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWKPreferences_2()) as _i4.WKPreferences); - @override - _i5.Future addObserver(_i7.NSObject? observer, - {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => - (super.noSuchMethod( - Invocation.method( - #addObserver, [observer], {#keyPath: keyPath, #options: options}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => - (super.noSuchMethod( - Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); -} - -/// A class which mocks [WKUserContentController]. -/// -/// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable -class MockWKUserContentController extends _i1.Mock - implements _i4.WKUserContentController { - MockWKUserContentController() { - _i1.throwOnMissingStub(this); - } - - @override - _i5.Future addScriptMessageHandler( - _i4.WKScriptMessageHandler? handler, String? name) => - (super.noSuchMethod( - Invocation.method(#addScriptMessageHandler, [handler, name]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeScriptMessageHandler(String? name) => (super - .noSuchMethod(Invocation.method(#removeScriptMessageHandler, [name]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeAllScriptMessageHandlers() => (super.noSuchMethod( - Invocation.method(#removeAllScriptMessageHandlers, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future addUserScript(_i4.WKUserScript? userScript) => - (super.noSuchMethod(Invocation.method(#addUserScript, [userScript]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeAllUserScripts() => - (super.noSuchMethod(Invocation.method(#removeAllUserScripts, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i4.WKUserContentController copy() => - (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWKUserContentController_3()) - as _i4.WKUserContentController); - @override - _i5.Future addObserver(_i7.NSObject? observer, - {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => - (super.noSuchMethod( - Invocation.method( - #addObserver, [observer], {#keyPath: keyPath, #options: options}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => - (super.noSuchMethod( - Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); -} - -/// A class which mocks [WKWebsiteDataStore]. -/// -/// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable -class MockWKWebsiteDataStore extends _i1.Mock - implements _i4.WKWebsiteDataStore { - MockWKWebsiteDataStore() { - _i1.throwOnMissingStub(this); - } - - @override - _i4.WKHttpCookieStore get httpCookieStore => - (super.noSuchMethod(Invocation.getter(#httpCookieStore), - returnValue: _FakeWKHttpCookieStore_4()) as _i4.WKHttpCookieStore); - @override - _i5.Future removeDataOfTypes( - Set<_i4.WKWebsiteDataType>? dataTypes, DateTime? since) => - (super.noSuchMethod( - Invocation.method(#removeDataOfTypes, [dataTypes, since]), - returnValue: Future.value(false)) as _i5.Future); - @override - _i4.WKWebsiteDataStore copy() => - (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWKWebsiteDataStore_5()) as _i4.WKWebsiteDataStore); - @override - _i5.Future addObserver(_i7.NSObject? observer, - {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => - (super.noSuchMethod( - Invocation.method( - #addObserver, [observer], {#keyPath: keyPath, #options: options}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => - (super.noSuchMethod( - Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); -} - -/// A class which mocks [WKWebView]. -/// -/// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable -class MockWKWebView extends _i1.Mock implements _i4.WKWebView { - MockWKWebView() { - _i1.throwOnMissingStub(this); - } - - @override - _i4.WKWebViewConfiguration get configuration => - (super.noSuchMethod(Invocation.getter(#configuration), - returnValue: _FakeWKWebViewConfiguration_6()) - as _i4.WKWebViewConfiguration); - @override - _i3.UIScrollView get scrollView => - (super.noSuchMethod(Invocation.getter(#scrollView), - returnValue: _FakeUIScrollView_1()) as _i3.UIScrollView); - @override - _i5.Future setUIDelegate(_i4.WKUIDelegate? delegate) => - (super.noSuchMethod(Invocation.method(#setUIDelegate, [delegate]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setNavigationDelegate(_i4.WKNavigationDelegate? delegate) => - (super.noSuchMethod(Invocation.method(#setNavigationDelegate, [delegate]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future getUrl() => - (super.noSuchMethod(Invocation.method(#getUrl, []), - returnValue: Future.value()) as _i5.Future); - @override - _i5.Future getEstimatedProgress() => - (super.noSuchMethod(Invocation.method(#getEstimatedProgress, []), - returnValue: Future.value(0.0)) as _i5.Future); - @override - _i5.Future loadRequest(_i7.NSUrlRequest? request) => - (super.noSuchMethod(Invocation.method(#loadRequest, [request]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future loadHtmlString(String? string, {String? baseUrl}) => - (super.noSuchMethod( - Invocation.method(#loadHtmlString, [string], {#baseUrl: baseUrl}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future loadFileUrl(String? url, {String? readAccessUrl}) => - (super.noSuchMethod( - Invocation.method( - #loadFileUrl, [url], {#readAccessUrl: readAccessUrl}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future loadFlutterAsset(String? key) => - (super.noSuchMethod(Invocation.method(#loadFlutterAsset, [key]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future canGoBack() => - (super.noSuchMethod(Invocation.method(#canGoBack, []), - returnValue: Future.value(false)) as _i5.Future); - @override - _i5.Future canGoForward() => - (super.noSuchMethod(Invocation.method(#canGoForward, []), - returnValue: Future.value(false)) as _i5.Future); - @override - _i5.Future goBack() => - (super.noSuchMethod(Invocation.method(#goBack, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future goForward() => - (super.noSuchMethod(Invocation.method(#goForward, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future reload() => - (super.noSuchMethod(Invocation.method(#reload, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future getTitle() => - (super.noSuchMethod(Invocation.method(#getTitle, []), - returnValue: Future.value()) as _i5.Future); - @override - _i5.Future setAllowsBackForwardNavigationGestures(bool? allow) => - (super.noSuchMethod( - Invocation.method(#setAllowsBackForwardNavigationGestures, [allow]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setCustomUserAgent(String? userAgent) => - (super.noSuchMethod(Invocation.method(#setCustomUserAgent, [userAgent]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future evaluateJavaScript(String? javaScriptString) => (super - .noSuchMethod(Invocation.method(#evaluateJavaScript, [javaScriptString]), - returnValue: Future.value()) as _i5.Future); - @override - _i4.WKWebView copy() => (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWKWebView_7()) as _i4.WKWebView); - @override - _i5.Future setBackgroundColor(_i6.Color? color) => - (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setOpaque(bool? opaque) => - (super.noSuchMethod(Invocation.method(#setOpaque, [opaque]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future addObserver(_i7.NSObject? observer, - {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => - (super.noSuchMethod( - Invocation.method( - #addObserver, [observer], {#keyPath: keyPath, #options: options}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => - (super.noSuchMethod( - Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); -} - -/// A class which mocks [WKWebViewConfiguration]. -/// -/// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable -class MockWKWebViewConfiguration extends _i1.Mock - implements _i4.WKWebViewConfiguration { - MockWKWebViewConfiguration() { - _i1.throwOnMissingStub(this); - } - - @override - _i4.WKUserContentController get userContentController => - (super.noSuchMethod(Invocation.getter(#userContentController), - returnValue: _FakeWKUserContentController_3()) - as _i4.WKUserContentController); - @override - _i4.WKPreferences get preferences => - (super.noSuchMethod(Invocation.getter(#preferences), - returnValue: _FakeWKPreferences_2()) as _i4.WKPreferences); - @override - _i4.WKWebsiteDataStore get websiteDataStore => - (super.noSuchMethod(Invocation.getter(#websiteDataStore), - returnValue: _FakeWKWebsiteDataStore_5()) as _i4.WKWebsiteDataStore); - @override - _i5.Future setAllowsInlineMediaPlayback(bool? allow) => (super - .noSuchMethod(Invocation.method(#setAllowsInlineMediaPlayback, [allow]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future setMediaTypesRequiringUserActionForPlayback( - Set<_i4.WKAudiovisualMediaType>? types) => - (super.noSuchMethod( - Invocation.method( - #setMediaTypesRequiringUserActionForPlayback, [types]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i4.WKWebViewConfiguration copy() => - (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWKWebViewConfiguration_6()) - as _i4.WKWebViewConfiguration); - @override - _i5.Future addObserver(_i7.NSObject? observer, - {String? keyPath, Set<_i7.NSKeyValueObservingOptions>? options}) => - (super.noSuchMethod( - Invocation.method( - #addObserver, [observer], {#keyPath: keyPath, #options: options}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future removeObserver(_i7.NSObject? observer, {String? keyPath}) => - (super.noSuchMethod( - Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); -} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_cookie_manager_test.mocks.dart deleted file mode 100644 index 90bf2522ab77..000000000000 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_cookie_manager_test.mocks.dart +++ /dev/null @@ -1,100 +0,0 @@ -// Mocks generated by Mockito 5.2.0 from annotations -// in webview_flutter_wkwebview/test/v4/webkit_webview_cookie_manager_test.dart. -// Do not manually edit this file. - -import 'dart:async' as _i3; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart' - as _i4; -import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart' as _i2; - -// 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 - -class _FakeWKHttpCookieStore_0 extends _i1.Fake - implements _i2.WKHttpCookieStore {} - -class _FakeWKWebsiteDataStore_1 extends _i1.Fake - implements _i2.WKWebsiteDataStore {} - -/// A class which mocks [WKWebsiteDataStore]. -/// -/// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable -class MockWKWebsiteDataStore extends _i1.Mock - implements _i2.WKWebsiteDataStore { - MockWKWebsiteDataStore() { - _i1.throwOnMissingStub(this); - } - - @override - _i2.WKHttpCookieStore get httpCookieStore => - (super.noSuchMethod(Invocation.getter(#httpCookieStore), - returnValue: _FakeWKHttpCookieStore_0()) as _i2.WKHttpCookieStore); - @override - _i3.Future removeDataOfTypes( - Set<_i2.WKWebsiteDataType>? dataTypes, DateTime? since) => - (super.noSuchMethod( - Invocation.method(#removeDataOfTypes, [dataTypes, since]), - returnValue: Future.value(false)) as _i3.Future); - @override - _i2.WKWebsiteDataStore copy() => - (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWKWebsiteDataStore_1()) as _i2.WKWebsiteDataStore); - @override - _i3.Future addObserver(_i4.NSObject? observer, - {String? keyPath, Set<_i4.NSKeyValueObservingOptions>? options}) => - (super.noSuchMethod( - Invocation.method( - #addObserver, [observer], {#keyPath: keyPath, #options: options}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); - @override - _i3.Future removeObserver(_i4.NSObject? observer, {String? keyPath}) => - (super.noSuchMethod( - Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); -} - -/// A class which mocks [WKHttpCookieStore]. -/// -/// See the documentation for Mockito's code generation for more information. -// ignore: must_be_immutable -class MockWKHttpCookieStore extends _i1.Mock implements _i2.WKHttpCookieStore { - MockWKHttpCookieStore() { - _i1.throwOnMissingStub(this); - } - - @override - _i3.Future setCookie(_i4.NSHttpCookie? cookie) => - (super.noSuchMethod(Invocation.method(#setCookie, [cookie]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); - @override - _i2.WKHttpCookieStore copy() => - (super.noSuchMethod(Invocation.method(#copy, []), - returnValue: _FakeWKHttpCookieStore_0()) as _i2.WKHttpCookieStore); - @override - _i3.Future addObserver(_i4.NSObject? observer, - {String? keyPath, Set<_i4.NSKeyValueObservingOptions>? options}) => - (super.noSuchMethod( - Invocation.method( - #addObserver, [observer], {#keyPath: keyPath, #options: options}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); - @override - _i3.Future removeObserver(_i4.NSObject? observer, {String? keyPath}) => - (super.noSuchMethod( - Invocation.method(#removeObserver, [observer], {#keyPath: keyPath}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i3.Future); -} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart similarity index 69% rename from packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_navigation_delegate_test.dart rename to packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart index fe57e94364bf..62889b0dd4af 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_navigation_delegate_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart @@ -6,21 +6,38 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; -import 'package:webview_flutter_wkwebview/src/v4/src/webkit_proxy.dart'; -import 'package:webview_flutter_wkwebview/src/v4/webview_flutter_wkwebview.dart'; import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; +import 'package:webview_flutter_wkwebview/src/webkit_proxy.dart'; +import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; +import 'webkit_navigation_delegate_test.mocks.dart'; + +@GenerateMocks([WKWebView]) void main() { WidgetsFlutterBinding.ensureInitialized(); group('WebKitNavigationDelegate', () { + test('WebKitNavigationDelegate uses params field in constructor', () async { + await runZonedGuarded( + () async => WebKitNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + ), + (Object error, __) { + expect(error, isNot(isA())); + }, + ); + }); + test('setOnPageFinished', () { final WebKitNavigationDelegate webKitDelgate = WebKitNavigationDelegate( const WebKitNavigationDelegateCreationParams( webKitProxy: WebKitProxy( createNavigationDelegate: CapturingNavigationDelegate.new, + createUIDelegate: CapturingUIDelegate.new, ), ), ); @@ -41,6 +58,7 @@ void main() { const WebKitNavigationDelegateCreationParams( webKitProxy: WebKitProxy( createNavigationDelegate: CapturingNavigationDelegate.new, + createUIDelegate: CapturingUIDelegate.new, ), ), ); @@ -62,6 +80,7 @@ void main() { const WebKitNavigationDelegateCreationParams( webKitProxy: WebKitProxy( createNavigationDelegate: CapturingNavigationDelegate.new, + createUIDelegate: CapturingUIDelegate.new, ), ), ); @@ -86,6 +105,7 @@ void main() { expect(callbackError.errorCode, WKErrorCode.webViewInvalidated); expect(callbackError.domain, 'domain'); expect(callbackError.errorType, WebResourceErrorType.webViewInvalidated); + expect(callbackError.isForMainFrame, true); }); test('onWebResourceError from didFailProvisionalNavigation', () { @@ -93,6 +113,7 @@ void main() { const WebKitNavigationDelegateCreationParams( webKitProxy: WebKitProxy( createNavigationDelegate: CapturingNavigationDelegate.new, + createUIDelegate: CapturingUIDelegate.new, ), ), ); @@ -118,6 +139,7 @@ void main() { expect(callbackError.errorCode, WKErrorCode.webViewInvalidated); expect(callbackError.domain, 'domain'); expect(callbackError.errorType, WebResourceErrorType.webViewInvalidated); + expect(callbackError.isForMainFrame, true); }); test('onWebResourceError from webViewWebContentProcessDidTerminate', () { @@ -125,6 +147,7 @@ void main() { const WebKitNavigationDelegateCreationParams( webKitProxy: WebKitProxy( createNavigationDelegate: CapturingNavigationDelegate.new, + createUIDelegate: CapturingUIDelegate.new, ), ), ); @@ -148,6 +171,7 @@ void main() { callbackError.errorType, WebResourceErrorType.webContentProcessTerminated, ); + expect(callbackError.isForMainFrame, true); }); test('onNavigationRequest from decidePolicyForNavigationAction', () { @@ -155,19 +179,16 @@ void main() { const WebKitNavigationDelegateCreationParams( webKitProxy: WebKitProxy( createNavigationDelegate: CapturingNavigationDelegate.new, + createUIDelegate: CapturingUIDelegate.new, ), ), ); - late final String callbackUrl; - late final bool callbackIsMainFrame; - FutureOr onNavigationRequest({ - required String url, - required bool isForMainFrame, - }) { - callbackUrl = url; - callbackIsMainFrame = isForMainFrame; - return true; + late final NavigationRequest callbackRequest; + FutureOr onNavigationRequest( + NavigationRequest request) { + callbackRequest = request; + return NavigationDecision.navigate; } webKitDelgate.setOnNavigationRequest(onNavigationRequest); @@ -179,13 +200,41 @@ void main() { const WKNavigationAction( request: NSUrlRequest(url: 'https://www.google.com'), targetFrame: WKFrameInfo(isMainFrame: false), + navigationType: WKNavigationType.linkActivated, ), ), completion(WKNavigationActionPolicy.allow), ); - expect(callbackUrl, 'https://www.google.com'); - expect(callbackIsMainFrame, isFalse); + expect(callbackRequest.url, 'https://www.google.com'); + expect(callbackRequest.isMainFrame, isFalse); + }); + + test('Requests to open a new window loads request in same window', () { + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams( + webKitProxy: WebKitProxy( + createNavigationDelegate: CapturingNavigationDelegate.new, + createUIDelegate: CapturingUIDelegate.new, + ), + ), + ); + + final MockWKWebView mockWebView = MockWKWebView(); + + const NSUrlRequest request = NSUrlRequest(url: 'https://www.google.com'); + + CapturingUIDelegate.lastCreatedDelegate.onCreateWebView!( + mockWebView, + WKWebViewConfiguration.detached(), + const WKNavigationAction( + request: request, + targetFrame: WKFrameInfo(isMainFrame: false), + navigationType: WKNavigationType.linkActivated, + ), + ); + + verify(mockWebView.loadRequest(request)); }); }); } @@ -205,3 +254,11 @@ class CapturingNavigationDelegate extends WKNavigationDelegate { static CapturingNavigationDelegate lastCreatedDelegate = CapturingNavigationDelegate(); } + +// Records the last created instance of itself. +class CapturingUIDelegate extends WKUIDelegate { + CapturingUIDelegate({super.onCreateWebView}) : super.detached() { + lastCreatedDelegate = this; + } + static CapturingUIDelegate lastCreatedDelegate = CapturingUIDelegate(); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.mocks.dart new file mode 100644 index 000000000000..9eab6dd9a3db --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.mocks.dart @@ -0,0 +1,308 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i4; +import 'dart:ui' as _i6; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart' + as _i5; +import 'package:webview_flutter_wkwebview/src/ui_kit/ui_kit.dart' as _i3; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart' as _i2; + +// 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 + +class _FakeWKWebViewConfiguration_0 extends _i1.SmartFake + implements _i2.WKWebViewConfiguration { + _FakeWKWebViewConfiguration_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeUIScrollView_1 extends _i1.SmartFake implements _i3.UIScrollView { + _FakeUIScrollView_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKWebView_2 extends _i1.SmartFake implements _i2.WKWebView { + _FakeWKWebView_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [WKWebView]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKWebView extends _i1.Mock implements _i2.WKWebView { + MockWKWebView() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WKWebViewConfiguration get configuration => (super.noSuchMethod( + Invocation.getter(#configuration), + returnValue: _FakeWKWebViewConfiguration_0( + this, + Invocation.getter(#configuration), + ), + ) as _i2.WKWebViewConfiguration); + @override + _i3.UIScrollView get scrollView => (super.noSuchMethod( + Invocation.getter(#scrollView), + returnValue: _FakeUIScrollView_1( + this, + Invocation.getter(#scrollView), + ), + ) as _i3.UIScrollView); + @override + _i4.Future setUIDelegate(_i2.WKUIDelegate? delegate) => + (super.noSuchMethod( + Invocation.method( + #setUIDelegate, + [delegate], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future setNavigationDelegate(_i2.WKNavigationDelegate? delegate) => + (super.noSuchMethod( + Invocation.method( + #setNavigationDelegate, + [delegate], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future getUrl() => (super.noSuchMethod( + Invocation.method( + #getUrl, + [], + ), + returnValue: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future getEstimatedProgress() => (super.noSuchMethod( + Invocation.method( + #getEstimatedProgress, + [], + ), + returnValue: _i4.Future.value(0.0), + ) as _i4.Future); + @override + _i4.Future loadRequest(_i5.NSUrlRequest? request) => + (super.noSuchMethod( + Invocation.method( + #loadRequest, + [request], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future loadHtmlString( + String? string, { + String? baseUrl, + }) => + (super.noSuchMethod( + Invocation.method( + #loadHtmlString, + [string], + {#baseUrl: baseUrl}, + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future loadFileUrl( + String? url, { + required String? readAccessUrl, + }) => + (super.noSuchMethod( + Invocation.method( + #loadFileUrl, + [url], + {#readAccessUrl: readAccessUrl}, + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future loadFlutterAsset(String? key) => (super.noSuchMethod( + Invocation.method( + #loadFlutterAsset, + [key], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future canGoBack() => (super.noSuchMethod( + Invocation.method( + #canGoBack, + [], + ), + returnValue: _i4.Future.value(false), + ) as _i4.Future); + @override + _i4.Future canGoForward() => (super.noSuchMethod( + Invocation.method( + #canGoForward, + [], + ), + returnValue: _i4.Future.value(false), + ) as _i4.Future); + @override + _i4.Future goBack() => (super.noSuchMethod( + Invocation.method( + #goBack, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future goForward() => (super.noSuchMethod( + Invocation.method( + #goForward, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future reload() => (super.noSuchMethod( + Invocation.method( + #reload, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future getTitle() => (super.noSuchMethod( + Invocation.method( + #getTitle, + [], + ), + returnValue: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future setAllowsBackForwardNavigationGestures(bool? allow) => + (super.noSuchMethod( + Invocation.method( + #setAllowsBackForwardNavigationGestures, + [allow], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future setCustomUserAgent(String? userAgent) => (super.noSuchMethod( + Invocation.method( + #setCustomUserAgent, + [userAgent], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future evaluateJavaScript(String? javaScriptString) => + (super.noSuchMethod( + Invocation.method( + #evaluateJavaScript, + [javaScriptString], + ), + returnValue: _i4.Future.value(), + ) as _i4.Future); + @override + _i2.WKWebView copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKWebView_2( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WKWebView); + @override + _i4.Future setBackgroundColor(_i6.Color? color) => (super.noSuchMethod( + Invocation.method( + #setBackgroundColor, + [color], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future setOpaque(bool? opaque) => (super.noSuchMethod( + Invocation.method( + #setOpaque, + [opaque], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future addObserver( + _i5.NSObject? observer, { + required String? keyPath, + required Set<_i5.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future removeObserver( + _i5.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart similarity index 79% rename from packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.dart rename to packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart index 87a90db9a766..b7b729a97926 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart @@ -12,12 +12,13 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; import 'package:webview_flutter_wkwebview/src/ui_kit/ui_kit.dart'; -import 'package:webview_flutter_wkwebview/src/v4/src/webkit_proxy.dart'; -import 'package:webview_flutter_wkwebview/src/v4/webview_flutter_wkwebview.dart'; import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; +import 'package:webview_flutter_wkwebview/src/webkit_proxy.dart'; +import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; import 'webkit_webview_controller_test.mocks.dart'; @@ -49,6 +50,7 @@ void main() { })? createMockWebView, MockWKWebViewConfiguration? mockWebViewConfiguration, + InstanceManager? instanceManager, }) { final MockWKWebViewConfiguration nonNullMockWebViewConfiguration = mockWebViewConfiguration ?? MockWKWebViewConfiguration(); @@ -57,7 +59,9 @@ void main() { final PlatformWebViewControllerCreationParams controllerCreationParams = WebKitWebViewControllerCreationParams( webKitProxy: WebKitProxy( - createWebViewConfiguration: () => nonNullMockWebViewConfiguration, + createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return nonNullMockWebViewConfiguration; + }, createWebView: ( _, { void Function( @@ -66,6 +70,7 @@ void main() { Map change, )? observeValue, + InstanceManager? instanceManager, }) { nonNullMockWebView = createMockWebView == null ? MockWKWebView() @@ -76,6 +81,7 @@ void main() { return nonNullMockWebView; }, ), + instanceManager: instanceManager, ); final WebKitWebViewController controller = WebKitWebViewController( @@ -97,6 +103,94 @@ void main() { return controller; } + group('WebKitWebViewControllerCreationParams', () { + test('allowsInlineMediaPlayback', () { + final MockWKWebViewConfiguration mockConfiguration = + MockWKWebViewConfiguration(); + + WebKitWebViewControllerCreationParams( + webKitProxy: WebKitProxy( + createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return mockConfiguration; + }, + ), + allowsInlineMediaPlayback: true, + ); + + verify( + mockConfiguration.setAllowsInlineMediaPlayback(true), + ); + }); + + test('mediaTypesRequiringUserAction', () { + final MockWKWebViewConfiguration mockConfiguration = + MockWKWebViewConfiguration(); + + WebKitWebViewControllerCreationParams( + webKitProxy: WebKitProxy( + createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return mockConfiguration; + }, + ), + mediaTypesRequiringUserAction: const { + PlaybackMediaTypes.video, + }, + ); + + verify( + mockConfiguration.setMediaTypesRequiringUserActionForPlayback( + { + WKAudiovisualMediaType.video, + }, + ), + ); + }); + + test('mediaTypesRequiringUserAction defaults to include audio and video', + () { + final MockWKWebViewConfiguration mockConfiguration = + MockWKWebViewConfiguration(); + + WebKitWebViewControllerCreationParams( + webKitProxy: WebKitProxy( + createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return mockConfiguration; + }, + ), + ); + + verify( + mockConfiguration.setMediaTypesRequiringUserActionForPlayback( + { + WKAudiovisualMediaType.audio, + WKAudiovisualMediaType.video, + }, + ), + ); + }); + + test('mediaTypesRequiringUserAction sets value to none if set is empty', + () { + final MockWKWebViewConfiguration mockConfiguration = + MockWKWebViewConfiguration(); + + WebKitWebViewControllerCreationParams( + webKitProxy: WebKitProxy( + createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return mockConfiguration; + }, + ), + mediaTypesRequiringUserAction: const {}, + ); + + verify( + mockConfiguration.setMediaTypesRequiringUserActionForPlayback( + {WKAudiovisualMediaType.none}, + ), + ); + }); + }); + test('loadFile', () async { final MockWKWebView mockWebView = MockWKWebView(); @@ -147,12 +241,8 @@ void main() { ); expect( - () async => await controller.loadRequest( - LoadRequestParams( - uri: Uri.parse('www.google.com'), - method: LoadRequestMethod.get, - headers: const {}, - ), + () async => controller.loadRequest( + LoadRequestParams(uri: Uri.parse('www.google.com')), ), throwsA(isA()), ); @@ -166,11 +256,7 @@ void main() { ); await controller.loadRequest( - LoadRequestParams( - uri: Uri.parse('https://www.google.com'), - method: LoadRequestMethod.get, - headers: const {}, - ), + LoadRequestParams(uri: Uri.parse('https://www.google.com')), ); final NSUrlRequest request = verify(mockWebView.loadRequest(captureAny)) @@ -191,7 +277,6 @@ void main() { await controller.loadRequest( LoadRequestParams( uri: Uri.parse('https://www.google.com'), - method: LoadRequestMethod.get, headers: const {'a': 'header'}, ), ); @@ -214,7 +299,6 @@ void main() { await controller.loadRequest(LoadRequestParams( uri: Uri.parse('https://www.google.com'), method: LoadRequestMethod.post, - headers: const {}, )); final NSUrlRequest request = verify(mockWebView.loadRequest(captureAny)) @@ -235,7 +319,6 @@ void main() { uri: Uri.parse('https://www.google.com'), method: LoadRequestMethod.post, body: Uint8List.fromList('Test Body'.codeUnits), - headers: const {}, )); final NSUrlRequest request = verify(mockWebView.loadRequest(captureAny)) @@ -309,14 +392,14 @@ void main() { verify(mockWebView.reload()); }); - test('enableGestureNavigation', () async { + test('setAllowsBackForwardNavigationGestures', () async { final MockWKWebView mockWebView = MockWKWebView(); final WebKitWebViewController controller = createControllerWithMocks( createMockWebView: (_, {dynamic observeValue}) => mockWebView, ); - await controller.enableGestureNavigation(true); + await controller.setAllowsBackForwardNavigationGestures(true); verify(mockWebView.setAllowsBackForwardNavigationGestures(true)); }); @@ -327,12 +410,13 @@ void main() { createMockWebView: (_, {dynamic observeValue}) => mockWebView, ); + final Object result = Object(); when(mockWebView.evaluateJavaScript('runJavaScript')).thenAnswer( - (_) => Future.value('returnString'), + (_) => Future.value(result), ); expect( controller.runJavaScriptReturningResult('runJavaScript'), - completion('returnString'), + completion(result), ); }); @@ -449,7 +533,7 @@ void main() { ); expect( controller.getScrollPosition(), - completion(const Point(8.0, 16.0)), + completion(const Offset(8.0, 16.0)), ); }); @@ -490,9 +574,12 @@ void main() { controller.setBackgroundColor(Colors.red); - verify(mockWebView.setOpaque(false)); - verify(mockWebView.setBackgroundColor(Colors.transparent)); - verify(mockScrollView.setBackgroundColor(Colors.red)); + // UIScrollView.setBackgroundColor must be called last. + verifyInOrder([ + mockWebView.setOpaque(false), + mockWebView.setBackgroundColor(Colors.transparent), + mockScrollView.setBackgroundColor(Colors.red), + ]); }); test('userAgent', () async { @@ -716,6 +803,7 @@ void main() { const WebKitNavigationDelegateCreationParams( webKitProxy: WebKitProxy( createNavigationDelegate: CapturingNavigationDelegate.new, + createUIDelegate: CapturingUIDelegate.new, ), ), ); @@ -727,6 +815,11 @@ void main() { CapturingNavigationDelegate.lastCreatedDelegate, ), ); + verify( + mockWebView.setUIDelegate( + CapturingUIDelegate.lastCreatedDelegate, + ), + ); }); test('setPlatformNavigationDelegate onProgress', () async { @@ -768,6 +861,7 @@ void main() { const WebKitNavigationDelegateCreationParams( webKitProxy: WebKitProxy( createNavigationDelegate: CapturingNavigationDelegate.new, + createUIDelegate: WKUIDelegate.detached, ), ), ); @@ -787,6 +881,80 @@ void main() { expect(callbackProgress, 0); }); + + test( + 'setPlatformNavigationDelegate onProgress can be changed by the WebKitNavigationDelegage', + () async { + final MockWKWebView mockWebView = MockWKWebView(); + + late final void Function( + String keyPath, + NSObject object, + Map change, + ) webViewObserveValue; + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: ( + _, { + void Function( + String keyPath, + NSObject object, + Map change, + )? + observeValue, + }) { + webViewObserveValue = observeValue!; + return mockWebView; + }, + ); + + final WebKitNavigationDelegate navigationDelegate = + WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams( + webKitProxy: WebKitProxy( + createNavigationDelegate: CapturingNavigationDelegate.new, + createUIDelegate: WKUIDelegate.detached, + ), + ), + ); + + // First value of onProgress does nothing. + await navigationDelegate.setOnProgress((_) {}); + await controller.setPlatformNavigationDelegate(navigationDelegate); + + // Second value of onProgress sets `callbackProgress`. + late final int callbackProgress; + await navigationDelegate.setOnProgress( + (int progress) => callbackProgress = progress, + ); + + webViewObserveValue( + 'estimatedProgress', + mockWebView, + {NSKeyValueChangeKey.newValue: 0.0}, + ); + + expect(callbackProgress, 0); + }); + + test('webViewIdentifier', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final MockWKWebView mockWebView = MockWKWebView(); + when(mockWebView.copy()).thenReturn(MockWKWebView()); + instanceManager.addHostCreatedInstance(mockWebView, 0); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + instanceManager: instanceManager, + ); + + expect( + controller.webViewIdentifier, + instanceManager.getIdentifier(mockWebView), + ); + }); }); group('WebKitJavaScriptChannelParams', () { @@ -842,3 +1010,11 @@ class CapturingNavigationDelegate extends WKNavigationDelegate { static CapturingNavigationDelegate lastCreatedDelegate = CapturingNavigationDelegate(); } + +// Records the last created instance of itself. +class CapturingUIDelegate extends WKUIDelegate { + CapturingUIDelegate({super.onCreateWebView}) : super.detached() { + lastCreatedDelegate = this; + } + static CapturingUIDelegate lastCreatedDelegate = CapturingUIDelegate(); +} 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 new file mode 100644 index 000000000000..288105c0067e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.mocks.dart @@ -0,0 +1,833 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter_wkwebview/test/webkit_webview_controller_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; +import 'dart:math' as _i2; +import 'dart:ui' as _i6; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart' + as _i7; +import 'package:webview_flutter_wkwebview/src/ui_kit/ui_kit.dart' as _i3; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart' as _i4; + +// 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 + +class _FakePoint_0 extends _i1.SmartFake + implements _i2.Point { + _FakePoint_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeUIScrollView_1 extends _i1.SmartFake implements _i3.UIScrollView { + _FakeUIScrollView_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKPreferences_2 extends _i1.SmartFake implements _i4.WKPreferences { + _FakeWKPreferences_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKUserContentController_3 extends _i1.SmartFake + implements _i4.WKUserContentController { + _FakeWKUserContentController_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKHttpCookieStore_4 extends _i1.SmartFake + implements _i4.WKHttpCookieStore { + _FakeWKHttpCookieStore_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKWebsiteDataStore_5 extends _i1.SmartFake + implements _i4.WKWebsiteDataStore { + _FakeWKWebsiteDataStore_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKWebViewConfiguration_6 extends _i1.SmartFake + implements _i4.WKWebViewConfiguration { + _FakeWKWebViewConfiguration_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKWebView_7 extends _i1.SmartFake implements _i4.WKWebView { + _FakeWKWebView_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [UIScrollView]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockUIScrollView extends _i1.Mock implements _i3.UIScrollView { + MockUIScrollView() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future<_i2.Point> getContentOffset() => (super.noSuchMethod( + Invocation.method( + #getContentOffset, + [], + ), + returnValue: _i5.Future<_i2.Point>.value(_FakePoint_0( + this, + Invocation.method( + #getContentOffset, + [], + ), + )), + ) as _i5.Future<_i2.Point>); + @override + _i5.Future scrollBy(_i2.Point? offset) => (super.noSuchMethod( + Invocation.method( + #scrollBy, + [offset], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setContentOffset(_i2.Point? offset) => + (super.noSuchMethod( + Invocation.method( + #setContentOffset, + [offset], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i3.UIScrollView copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeUIScrollView_1( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i3.UIScrollView); + @override + _i5.Future setBackgroundColor(_i6.Color? color) => (super.noSuchMethod( + Invocation.method( + #setBackgroundColor, + [color], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setOpaque(bool? opaque) => (super.noSuchMethod( + Invocation.method( + #setOpaque, + [opaque], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future addObserver( + _i7.NSObject? observer, { + required String? keyPath, + required Set<_i7.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeObserver( + _i7.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [WKPreferences]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKPreferences extends _i1.Mock implements _i4.WKPreferences { + MockWKPreferences() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future setJavaScriptEnabled(bool? enabled) => (super.noSuchMethod( + Invocation.method( + #setJavaScriptEnabled, + [enabled], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i4.WKPreferences copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKPreferences_2( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i4.WKPreferences); + @override + _i5.Future addObserver( + _i7.NSObject? observer, { + required String? keyPath, + required Set<_i7.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeObserver( + _i7.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [WKUserContentController]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKUserContentController extends _i1.Mock + implements _i4.WKUserContentController { + MockWKUserContentController() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Future addScriptMessageHandler( + _i4.WKScriptMessageHandler? handler, + String? name, + ) => + (super.noSuchMethod( + Invocation.method( + #addScriptMessageHandler, + [ + handler, + name, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeScriptMessageHandler(String? name) => + (super.noSuchMethod( + Invocation.method( + #removeScriptMessageHandler, + [name], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeAllScriptMessageHandlers() => (super.noSuchMethod( + Invocation.method( + #removeAllScriptMessageHandlers, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future addUserScript(_i4.WKUserScript? userScript) => + (super.noSuchMethod( + Invocation.method( + #addUserScript, + [userScript], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeAllUserScripts() => (super.noSuchMethod( + Invocation.method( + #removeAllUserScripts, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i4.WKUserContentController copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKUserContentController_3( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i4.WKUserContentController); + @override + _i5.Future addObserver( + _i7.NSObject? observer, { + required String? keyPath, + required Set<_i7.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeObserver( + _i7.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [WKWebsiteDataStore]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKWebsiteDataStore extends _i1.Mock + implements _i4.WKWebsiteDataStore { + MockWKWebsiteDataStore() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.WKHttpCookieStore get httpCookieStore => (super.noSuchMethod( + Invocation.getter(#httpCookieStore), + returnValue: _FakeWKHttpCookieStore_4( + this, + Invocation.getter(#httpCookieStore), + ), + ) as _i4.WKHttpCookieStore); + @override + _i5.Future removeDataOfTypes( + Set<_i4.WKWebsiteDataType>? dataTypes, + DateTime? since, + ) => + (super.noSuchMethod( + Invocation.method( + #removeDataOfTypes, + [ + dataTypes, + since, + ], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i4.WKWebsiteDataStore copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKWebsiteDataStore_5( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i4.WKWebsiteDataStore); + @override + _i5.Future addObserver( + _i7.NSObject? observer, { + required String? keyPath, + required Set<_i7.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeObserver( + _i7.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [WKWebView]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKWebView extends _i1.Mock implements _i4.WKWebView { + MockWKWebView() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.WKWebViewConfiguration get configuration => (super.noSuchMethod( + Invocation.getter(#configuration), + returnValue: _FakeWKWebViewConfiguration_6( + this, + Invocation.getter(#configuration), + ), + ) as _i4.WKWebViewConfiguration); + @override + _i3.UIScrollView get scrollView => (super.noSuchMethod( + Invocation.getter(#scrollView), + returnValue: _FakeUIScrollView_1( + this, + Invocation.getter(#scrollView), + ), + ) as _i3.UIScrollView); + @override + _i5.Future setUIDelegate(_i4.WKUIDelegate? delegate) => + (super.noSuchMethod( + Invocation.method( + #setUIDelegate, + [delegate], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setNavigationDelegate(_i4.WKNavigationDelegate? delegate) => + (super.noSuchMethod( + Invocation.method( + #setNavigationDelegate, + [delegate], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getUrl() => (super.noSuchMethod( + Invocation.method( + #getUrl, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getEstimatedProgress() => (super.noSuchMethod( + Invocation.method( + #getEstimatedProgress, + [], + ), + returnValue: _i5.Future.value(0.0), + ) as _i5.Future); + @override + _i5.Future loadRequest(_i7.NSUrlRequest? request) => + (super.noSuchMethod( + Invocation.method( + #loadRequest, + [request], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future loadHtmlString( + String? string, { + String? baseUrl, + }) => + (super.noSuchMethod( + Invocation.method( + #loadHtmlString, + [string], + {#baseUrl: baseUrl}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future loadFileUrl( + String? url, { + required String? readAccessUrl, + }) => + (super.noSuchMethod( + Invocation.method( + #loadFileUrl, + [url], + {#readAccessUrl: readAccessUrl}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future loadFlutterAsset(String? key) => (super.noSuchMethod( + Invocation.method( + #loadFlutterAsset, + [key], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future canGoBack() => (super.noSuchMethod( + Invocation.method( + #canGoBack, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future canGoForward() => (super.noSuchMethod( + Invocation.method( + #canGoForward, + [], + ), + returnValue: _i5.Future.value(false), + ) as _i5.Future); + @override + _i5.Future goBack() => (super.noSuchMethod( + Invocation.method( + #goBack, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future goForward() => (super.noSuchMethod( + Invocation.method( + #goForward, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future reload() => (super.noSuchMethod( + Invocation.method( + #reload, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future getTitle() => (super.noSuchMethod( + Invocation.method( + #getTitle, + [], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setAllowsBackForwardNavigationGestures(bool? allow) => + (super.noSuchMethod( + Invocation.method( + #setAllowsBackForwardNavigationGestures, + [allow], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setCustomUserAgent(String? userAgent) => (super.noSuchMethod( + Invocation.method( + #setCustomUserAgent, + [userAgent], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future evaluateJavaScript(String? javaScriptString) => + (super.noSuchMethod( + Invocation.method( + #evaluateJavaScript, + [javaScriptString], + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); + @override + _i4.WKWebView copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKWebView_7( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i4.WKWebView); + @override + _i5.Future setBackgroundColor(_i6.Color? color) => (super.noSuchMethod( + Invocation.method( + #setBackgroundColor, + [color], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setOpaque(bool? opaque) => (super.noSuchMethod( + Invocation.method( + #setOpaque, + [opaque], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future addObserver( + _i7.NSObject? observer, { + required String? keyPath, + required Set<_i7.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeObserver( + _i7.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [WKWebViewConfiguration]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKWebViewConfiguration extends _i1.Mock + implements _i4.WKWebViewConfiguration { + MockWKWebViewConfiguration() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.WKUserContentController get userContentController => (super.noSuchMethod( + Invocation.getter(#userContentController), + returnValue: _FakeWKUserContentController_3( + this, + Invocation.getter(#userContentController), + ), + ) as _i4.WKUserContentController); + @override + _i4.WKPreferences get preferences => (super.noSuchMethod( + Invocation.getter(#preferences), + returnValue: _FakeWKPreferences_2( + this, + Invocation.getter(#preferences), + ), + ) as _i4.WKPreferences); + @override + _i4.WKWebsiteDataStore get websiteDataStore => (super.noSuchMethod( + Invocation.getter(#websiteDataStore), + returnValue: _FakeWKWebsiteDataStore_5( + this, + Invocation.getter(#websiteDataStore), + ), + ) as _i4.WKWebsiteDataStore); + @override + _i5.Future setAllowsInlineMediaPlayback(bool? allow) => + (super.noSuchMethod( + Invocation.method( + #setAllowsInlineMediaPlayback, + [allow], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future setMediaTypesRequiringUserActionForPlayback( + Set<_i4.WKAudiovisualMediaType>? types) => + (super.noSuchMethod( + Invocation.method( + #setMediaTypesRequiringUserActionForPlayback, + [types], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i4.WKWebViewConfiguration copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKWebViewConfiguration_6( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i4.WKWebViewConfiguration); + @override + _i5.Future addObserver( + _i7.NSObject? observer, { + required String? keyPath, + required Set<_i7.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future removeObserver( + _i7.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_cookie_manager_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_cookie_manager_test.dart similarity index 93% rename from packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_cookie_manager_test.dart rename to packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_cookie_manager_test.dart index 71107cb563a6..a9dd742bd670 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_cookie_manager_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_cookie_manager_test.dart @@ -6,11 +6,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:webview_flutter_platform_interface/v4/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; -import 'package:webview_flutter_wkwebview/src/v4/src/webkit_proxy.dart'; -import 'package:webview_flutter_wkwebview/src/v4/webview_flutter_wkwebview.dart'; import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; +import 'package:webview_flutter_wkwebview/src/webkit_proxy.dart'; +import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; import 'webkit_webview_cookie_manager_test.mocks.dart'; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_cookie_manager_test.mocks.dart new file mode 100644 index 000000000000..c552d96ca316 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_cookie_manager_test.mocks.dart @@ -0,0 +1,191 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter_wkwebview/test/webkit_webview_cookie_manager_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart' + as _i4; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart' as _i2; + +// 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 + +class _FakeWKHttpCookieStore_0 extends _i1.SmartFake + implements _i2.WKHttpCookieStore { + _FakeWKHttpCookieStore_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKWebsiteDataStore_1 extends _i1.SmartFake + implements _i2.WKWebsiteDataStore { + _FakeWKWebsiteDataStore_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [WKWebsiteDataStore]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKWebsiteDataStore extends _i1.Mock + implements _i2.WKWebsiteDataStore { + MockWKWebsiteDataStore() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WKHttpCookieStore get httpCookieStore => (super.noSuchMethod( + Invocation.getter(#httpCookieStore), + returnValue: _FakeWKHttpCookieStore_0( + this, + Invocation.getter(#httpCookieStore), + ), + ) as _i2.WKHttpCookieStore); + @override + _i3.Future removeDataOfTypes( + Set<_i2.WKWebsiteDataType>? dataTypes, + DateTime? since, + ) => + (super.noSuchMethod( + Invocation.method( + #removeDataOfTypes, + [ + dataTypes, + since, + ], + ), + returnValue: _i3.Future.value(false), + ) as _i3.Future); + @override + _i2.WKWebsiteDataStore copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKWebsiteDataStore_1( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WKWebsiteDataStore); + @override + _i3.Future addObserver( + _i4.NSObject? observer, { + required String? keyPath, + required Set<_i4.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future removeObserver( + _i4.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); +} + +/// A class which mocks [WKHttpCookieStore]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKHttpCookieStore extends _i1.Mock implements _i2.WKHttpCookieStore { + MockWKHttpCookieStore() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future setCookie(_i4.NSHttpCookie? cookie) => (super.noSuchMethod( + Invocation.method( + #setCookie, + [cookie], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + @override + _i2.WKHttpCookieStore copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKHttpCookieStore_0( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WKHttpCookieStore); + @override + _i3.Future addObserver( + _i4.NSObject? observer, { + required String? keyPath, + required Set<_i4.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future removeObserver( + _i4.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_widget_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart similarity index 66% rename from packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_widget_test.dart rename to packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart index 36a95d6f1cab..2a6434be4f03 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/v4/webkit_webview_widget_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart @@ -4,18 +4,22 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; -import 'package:webview_flutter_wkwebview/src/v4/src/webkit_proxy.dart'; -import 'package:webview_flutter_wkwebview/src/v4/webview_flutter_wkwebview.dart'; import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; +import 'package:webview_flutter_wkwebview/src/webkit_proxy.dart'; +import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; +import 'webkit_webview_widget_test.mocks.dart'; + +@GenerateMocks([WKWebViewConfiguration]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('WebKitWebViewWidget', () { testWidgets('build', (WidgetTester tester) async { - final InstanceManager instanceManager = InstanceManager( + final InstanceManager testInstanceManager = InstanceManager( onWeakReferenceRemoved: (_) {}, ); @@ -30,22 +34,26 @@ void main() { Map change, )? observeValue, + InstanceManager? instanceManager, }) { final WKWebView webView = WKWebView.detached( - instanceManager: instanceManager, + instanceManager: testInstanceManager, ); - instanceManager.addDartCreatedInstance(webView); + testInstanceManager.addDartCreatedInstance(webView); return webView; }, - createWebViewConfiguration: WKWebViewConfiguration.detached, + createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return MockWKWebViewConfiguration(); + }, ), ), ); final WebKitWebViewWidget widget = WebKitWebViewWidget( WebKitWebViewWidgetCreationParams( + key: const Key('keyValue'), controller: controller, - instanceManager: instanceManager, + instanceManager: testInstanceManager, ), ); @@ -54,6 +62,7 @@ void main() { ); expect(find.byType(UiKitView), findsOneWidget); + expect(find.byKey(const Key('keyValue')), findsOneWidget); }); }); } 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 new file mode 100644 index 000000000000..0f48af4d5daa --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart @@ -0,0 +1,168 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in webview_flutter_wkwebview/test/webkit_webview_widget_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart' + as _i4; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart' as _i2; + +// 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 + +class _FakeWKUserContentController_0 extends _i1.SmartFake + implements _i2.WKUserContentController { + _FakeWKUserContentController_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKPreferences_1 extends _i1.SmartFake implements _i2.WKPreferences { + _FakeWKPreferences_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKWebsiteDataStore_2 extends _i1.SmartFake + implements _i2.WKWebsiteDataStore { + _FakeWKWebsiteDataStore_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWKWebViewConfiguration_3 extends _i1.SmartFake + implements _i2.WKWebViewConfiguration { + _FakeWKWebViewConfiguration_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [WKWebViewConfiguration]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockWKWebViewConfiguration extends _i1.Mock + implements _i2.WKWebViewConfiguration { + MockWKWebViewConfiguration() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WKUserContentController get userContentController => (super.noSuchMethod( + Invocation.getter(#userContentController), + returnValue: _FakeWKUserContentController_0( + this, + Invocation.getter(#userContentController), + ), + ) as _i2.WKUserContentController); + @override + _i2.WKPreferences get preferences => (super.noSuchMethod( + Invocation.getter(#preferences), + returnValue: _FakeWKPreferences_1( + this, + Invocation.getter(#preferences), + ), + ) as _i2.WKPreferences); + @override + _i2.WKWebsiteDataStore get websiteDataStore => (super.noSuchMethod( + Invocation.getter(#websiteDataStore), + returnValue: _FakeWKWebsiteDataStore_2( + this, + Invocation.getter(#websiteDataStore), + ), + ) as _i2.WKWebsiteDataStore); + @override + _i3.Future setAllowsInlineMediaPlayback(bool? allow) => + (super.noSuchMethod( + Invocation.method( + #setAllowsInlineMediaPlayback, + [allow], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future setMediaTypesRequiringUserActionForPlayback( + Set<_i2.WKAudiovisualMediaType>? types) => + (super.noSuchMethod( + Invocation.method( + #setMediaTypesRequiringUserActionForPlayback, + [types], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + @override + _i2.WKWebViewConfiguration copy() => (super.noSuchMethod( + Invocation.method( + #copy, + [], + ), + returnValue: _FakeWKWebViewConfiguration_3( + this, + Invocation.method( + #copy, + [], + ), + ), + ) as _i2.WKWebViewConfiguration); + @override + _i3.Future addObserver( + _i4.NSObject? observer, { + required String? keyPath, + required Set<_i4.NSKeyValueObservingOptions>? options, + }) => + (super.noSuchMethod( + Invocation.method( + #addObserver, + [observer], + { + #keyPath: keyPath, + #options: options, + }, + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future removeObserver( + _i4.NSObject? observer, { + required String? keyPath, + }) => + (super.noSuchMethod( + Invocation.method( + #removeObserver, + [observer], + {#keyPath: keyPath}, + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); +} diff --git a/script/configs/exclude_all_plugins_app.yaml b/script/configs/exclude_all_packages_app.yaml similarity index 100% rename from script/configs/exclude_all_plugins_app.yaml rename to script/configs/exclude_all_packages_app.yaml diff --git a/script/configs/temp_exclude_excerpt.yaml b/script/configs/temp_exclude_excerpt.yaml index c59983efd058..40346297e999 100644 --- a/script/configs/temp_exclude_excerpt.yaml +++ b/script/configs/temp_exclude_excerpt.yaml @@ -18,6 +18,5 @@ - plugin_platform_interface - quick_actions/quick_actions - shared_preferences/shared_preferences -- webview_flutter/webview_flutter - webview_flutter_android - webview_flutter_web diff --git a/script/install_chromium.sh b/script/install_chromium.sh index 0d360fe98cfe..ed55776a5c19 100755 --- a/script/install_chromium.sh +++ b/script/install_chromium.sh @@ -2,41 +2,51 @@ # 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. + +# This script may be run as: +# $ CHROME_DOWNLOAD_DIR=./whatever script/install_chromium.sh set -e set -x -readonly TARGET_DIR=$1 +# The target directory where chromium is going to be downloaded +: "${CHROME_DOWNLOAD_DIR:=/tmp/chromium}" # Default value for the CHROME_DOWNLOAD_DIR env. # The build of Chromium used to test web functionality. # # Chromium builds can be located here: https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Linux_x64/ # -# Check: https://github.com/flutter/engine/blob/main/lib/web_ui/dev/browser_lock.yaml -readonly CHROMIUM_BUILD=929514 +# Check: https://github.com/flutter/engine/blob/master/lib/web_ui/dev/browser_lock.yaml +: "${CHROMIUM_BUILD:=950363}" # Default value for the CHROMIUM_BUILD env. + +# Convenience defaults for CHROME_EXECUTABLE and CHROMEDRIVER_EXECUTABLE. These +# two values should be set in the environment from CI, so this script can validate +# that it has completed downloading chrome and driver successfully (and the expected +# files are executable) +: "${CHROME_EXECUTABLE:=$CHROME_DOWNLOAD_DIR/chrome-linux/chrome}" +: "${CHROMEDRIVER_EXECUTABLE:=$CHROME_DOWNLOAD_DIR/chromedriver/chromedriver}" # The correct ChromeDriver is distributed alongside the chromium build above, as # `chromedriver_linux64.zip`, so no need to hardcode any extra info about it. readonly DOWNLOAD_ROOT="https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Linux_x64%2F${CHROMIUM_BUILD}%2F" # Install Chromium. -mkdir "$TARGET_DIR" -readonly CHROMIUM_ZIP_FILE="$TARGET_DIR/chromium.zip" +mkdir "$CHROME_DOWNLOAD_DIR" +readonly CHROMIUM_ZIP_FILE="$CHROME_DOWNLOAD_DIR/chromium.zip" wget --no-verbose "${DOWNLOAD_ROOT}chrome-linux.zip?alt=media" -O "$CHROMIUM_ZIP_FILE" -unzip -q "$CHROMIUM_ZIP_FILE" -d "$TARGET_DIR/" +unzip -q "$CHROMIUM_ZIP_FILE" -d "$CHROME_DOWNLOAD_DIR/" # Install ChromeDriver. -readonly DRIVER_ZIP_FILE="$TARGET_DIR/chromedriver.zip" +readonly DRIVER_ZIP_FILE="$CHROME_DOWNLOAD_DIR/chromedriver.zip" wget --no-verbose "${DOWNLOAD_ROOT}chromedriver_linux64.zip?alt=media" -O "$DRIVER_ZIP_FILE" -unzip -q "$DRIVER_ZIP_FILE" -d "$TARGET_DIR/" -# Rename TARGET_DIR/chromedriver_linux64 to the expected TARGET_DIR/chromedriver -mv -T "$TARGET_DIR/chromedriver_linux64" "$TARGET_DIR/chromedriver" - -export CHROME_EXECUTABLE="$TARGET_DIR/chrome-linux/chrome" +unzip -q "$DRIVER_ZIP_FILE" -d "$CHROME_DOWNLOAD_DIR/" +# Rename CHROME_DOWNLOAD_DIR/chromedriver_linux64 to the expected CHROME_DOWNLOAD_DIR/chromedriver +mv -T "$CHROME_DOWNLOAD_DIR/chromedriver_linux64" "$CHROME_DOWNLOAD_DIR/chromedriver" # Echo info at the end for ease of debugging. +# +# exports from this script cannot be used elsewhere in the .cirrus.yml file. set +x echo -readonly CHROMEDRIVER_EXECUTABLE="$TARGET_DIR/chromedriver/chromedriver" echo "$CHROME_EXECUTABLE" "$CHROME_EXECUTABLE" --version echo "$CHROMEDRIVER_EXECUTABLE" diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index a346ac093c68..3c4905ad7071 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,31 @@ -## NEXT +## 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. diff --git a/script/tool/README.md b/script/tool/README.md index 9f0ac84145f2..aa4c0517ce71 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -1,191 +1,13 @@ -# Flutter Plugin Tools +# Removed -This is a set of utilities used in the flutter/plugins and flutter/packages -repositories. It is no longer explictily maintained as a general-purpose tool -for multi-package repositories, so your mileage may vary if using it in other -repositories. +See https://github.com/flutter/packages/blob/main/script/tool/README.md for the +current location of this tooling. -Note: The commands in tools are designed to run at the root of the repository or `/packages/`. +## Temporary shim -## Getting Started +This is a temporary, minimal version of the tools sufficient to keep the +following scripts running until the repository merge is complete and they are +updated to use flutter/packages instead: -In flutter/plugins, the tool is run from source. In flutter/packages, the -[published version](https://pub.dev/packages/flutter_plugin_tools) is used -instead. (It is marked as Discontinued since it is no longer maintained as -a general-purpose tool, but updates are still published for use in -flutter/packages.) - -The commands in tools require the Flutter-bundled version of Dart to be the first `dart` loaded in the path. - -### Extra Setup - -When updating sample code excerpts (`update-excerpts`) for the README.md files, -there is some [extra setup for -submodules](#update-readmemd-from-example-sources) that is necessary. - -### From Source (flutter/plugins only) - -Set up: - -```sh -cd ./script/tool && dart pub get && cd ../../ -``` - -Run: - -```sh -dart run ./script/tool/bin/flutter_plugin_tools.dart -``` - -### Published Version - -Set up: - -```sh -dart pub global activate flutter_plugin_tools -``` - -Run: - -```sh -dart pub global run flutter_plugin_tools -``` - -## Commands - -Run with `--help` for a full list of commands and arguments, but the -following shows a number of common commands being run for a specific package. - -All examples assume running from source; see above for running the -published version instead. - -Most commands take a `--packages` argument to control which package(s) the -command is targetting. An package name can be any of: -- The name of a package (e.g., `path_provider_android`). -- The name of a federated plugin (e.g., `path_provider`), in which case all - packages that make up that plugin will be targetted. -- A combination federated_plugin_name/package_name (e.g., - `path_provider/path_provider` for the app-facing 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 -``` - -Replace `--apk`/`--android` with the platform you want to test against -(omit it to get a list of valid options). - -### Run Native Tests - -`native-test` takes one or more platform flags to run tests for. By default it -runs both unit tests and (on platforms that support it) integration tests, but -`--no-unit` or `--no-integration` can be used to run just one type. - -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: -dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --macos --packages package_name -# Run all tests for Windows: -dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --windows --packages package_name -``` - -### Update README.md from Example Sources - -`update-excerpts` requires sources that are in a submodule. If you didn't clone -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 -``` - -### Update CHANGELOG and Version - -`update-release-info` will automatically update the version and `CHANGELOG.md` -following standard repository style and practice. It can be used for -single-package updates to handle the details of getting the `CHANGELOG.md` -format correct, but is especially useful for bulk updates across multiple packages. - -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 \ - --changelog="Fixes violations of new analysis option some_new_option." -``` - -The `minimal` option for `--version` will skip unchanged packages, and treat -each changed package as either `bugfix` or `next` depending on the files that -have changed in that package, so it is often the best choice for a bulk change. - -For cases where you know the change time, `minor` or `bugfix` will make the -corresponding version bump, or `next` will update only `CHANGELOG.md` without -changing the version. - -### Publish a Release - -**Releases are automated for `flutter/plugins` and `flutter/packages`.** - -The manual procedure described here is _deprecated_, and should only be used when -the automated process fails. Please, read -[Releasing a Plugin or Package](https://github.com/flutter/flutter/wiki/Releasing-a-Plugin-or-Package) -on the Flutter Wiki first. - -```sh -cd -git checkout -dart run ./script/tool/bin/flutter_plugin_tools.dart publish --packages -``` - -By default the tool tries to push tags to the `upstream` remote, but some -additional settings can be configured. Run `dart run ./script/tool/bin/flutter_plugin_tools.dart -publish --help` for more usage information. - -The tool wraps `pub publish` for pushing the package to pub, and then will -automatically use git to try to create and push tags. It has some additional -safety checking around `pub publish` too. By default `pub publish` publishes -_everything_, including untracked or uncommitted files in version control. -`publish` will first check the status of the local -directory and refuse to publish if there are any mismatched files with version -control present. - -## Updating the Tool - -For flutter/plugins, just changing the source here is all that's needed. - -For changes that are relevant to flutter/packages, you will also need to: -- Update the tool's pubspec.yaml and CHANGELOG -- Publish the tool -- Update the pinned version in - [flutter/packages](https://github.com/flutter/packages/blob/main/.cirrus.yml) +- [dart-lang analysis](https://github.com/dart-lang/sdk/blob/main/tools/bots/flutter/analyze_flutter_plugins.sh) +- [flutter/flutter analysis](https://github.com/flutter/flutter/blob/master/dev/bots/test.dart) diff --git a/script/tool/analysis_options.yaml b/script/tool/analysis_options.yaml new file mode 100644 index 000000000000..efd40175a208 --- /dev/null +++ b/script/tool/analysis_options.yaml @@ -0,0 +1,5 @@ +include: ../../analysis_options.yaml + +linter: + rules: + avoid_print: false # The tool is a CLI, so printing is normal diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index c7a953c50cac..3d9e4e5c9802 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -32,17 +32,9 @@ class AnalyzeCommand extends PackageLoopingCommand { valueHelp: 'dart-sdk', help: 'An optional path to a Dart SDK; this is used to override the ' 'SDK used to provide analysis.'); - argParser.addFlag(_downgradeFlag, - help: 'Runs "flutter pub downgrade" before analysis to verify that ' - 'the minimum constraints are sufficiently new for APIs used.'); - argParser.addFlag(_libOnlyFlag, - help: 'Only analyze the lib/ directory of the main package, not the ' - 'entire package.'); } static const String _customAnalysisFlag = 'custom-analysis'; - static const String _downgradeFlag = 'downgrade'; - static const String _libOnlyFlag = 'lib-only'; static const String _analysisSdk = 'analysis-sdk'; late String _dartBinaryPath; @@ -111,18 +103,6 @@ class AnalyzeCommand extends PackageLoopingCommand { @override Future runForPackage(RepositoryPackage package) async { - final bool libOnly = getBoolArg(_libOnlyFlag); - - if (libOnly && !package.libDirectory.existsSync()) { - return PackageResult.skip('No lib/ directory.'); - } - - if (getBoolArg(_downgradeFlag)) { - if (!await _runPubCommand(package, 'downgrade')) { - return PackageResult.fail(['Unable to downgrade dependencies']); - } - } - // Analysis runs over the package and all subpackages (unless only lib/ is // being analyzed), so all of them need `flutter pub get` run before // analyzing. `example` packages can be skipped since 'flutter packages get' @@ -130,7 +110,7 @@ class AnalyzeCommand extends PackageLoopingCommand { // directory. final List packagesToGet = [ package, - if (!libOnly) ...await getSubpackages(package).toList(), + ...await getSubpackages(package).toList(), ]; for (final RepositoryPackage packageToGet in packagesToGet) { if (packageToGet.directory.basename != 'example' || @@ -146,8 +126,8 @@ class AnalyzeCommand extends PackageLoopingCommand { if (_hasUnexpecetdAnalysisOptions(package)) { return PackageResult.fail(['Unexpected local analysis options']); } - final int exitCode = await processRunner.runAndStream(_dartBinaryPath, - ['analyze', '--fatal-infos', if (libOnly) 'lib'], + final int exitCode = await processRunner.runAndStream( + _dartBinaryPath, ['analyze', '--fatal-infos'], workingDir: package.directory); if (exitCode != 0) { return PackageResult.fail(); diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart deleted file mode 100644 index 1aade3575559..000000000000 --- a/script/tool/lib/src/build_examples_command.dart +++ /dev/null @@ -1,314 +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:async'; - -import 'package:file/file.dart'; -import 'package:platform/platform.dart'; -import 'package:yaml/yaml.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/plugin_utils.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -/// Key for APK. -const String _platformFlagApk = 'apk'; - -const String _pluginToolsConfigFileName = '.pluginToolsConfig.yaml'; -const String _pluginToolsConfigBuildFlagsKey = 'buildFlags'; -const String _pluginToolsConfigGlobalKey = 'global'; - -const String _pluginToolsConfigExample = ''' -$_pluginToolsConfigBuildFlagsKey: - $_pluginToolsConfigGlobalKey: - - "--no-tree-shake-icons" - - "--dart-define=buildmode=testing" -'''; - -const int _exitNoPlatformFlags = 3; -const int _exitInvalidPluginToolsConfig = 4; - -// Flutter build types. These are the values passed to `flutter build `. -const String _flutterBuildTypeAndroid = 'apk'; -const String _flutterBuildTypeIOS = 'ios'; -const String _flutterBuildTypeLinux = 'linux'; -const String _flutterBuildTypeMacOS = 'macos'; -const String _flutterBuildTypeWeb = 'web'; -const String _flutterBuildTypeWindows = 'windows'; - -/// A command to build the example applications for packages. -class BuildExamplesCommand extends PackageLoopingCommand { - /// Creates an instance of the build command. - BuildExamplesCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addFlag(platformLinux); - argParser.addFlag(platformMacOS); - argParser.addFlag(platformWeb); - argParser.addFlag(platformWindows); - argParser.addFlag(platformIOS); - argParser.addFlag(_platformFlagApk); - argParser.addOption( - kEnableExperiment, - defaultsTo: '', - help: 'Enables the given Dart SDK experiments.', - ); - } - - // Maps the switch this command uses to identify a platform to information - // about it. - static final Map _platforms = - { - _platformFlagApk: const _PlatformDetails( - 'Android', - pluginPlatform: platformAndroid, - flutterBuildType: _flutterBuildTypeAndroid, - ), - platformIOS: const _PlatformDetails( - 'iOS', - pluginPlatform: platformIOS, - flutterBuildType: _flutterBuildTypeIOS, - extraBuildFlags: ['--no-codesign'], - ), - platformLinux: const _PlatformDetails( - 'Linux', - pluginPlatform: platformLinux, - flutterBuildType: _flutterBuildTypeLinux, - ), - platformMacOS: const _PlatformDetails( - 'macOS', - pluginPlatform: platformMacOS, - flutterBuildType: _flutterBuildTypeMacOS, - ), - platformWeb: const _PlatformDetails( - 'web', - pluginPlatform: platformWeb, - flutterBuildType: _flutterBuildTypeWeb, - ), - platformWindows: const _PlatformDetails( - 'Windows', - pluginPlatform: platformWindows, - flutterBuildType: _flutterBuildTypeWindows, - ), - }; - - @override - final String name = 'build-examples'; - - @override - final String description = - 'Builds all example apps (IPA for iOS and APK for Android).\n\n' - 'This command requires "flutter" to be in your path.\n\n' - 'A $_pluginToolsConfigFileName file can be placed in an example app ' - 'directory to specify additional build arguments. It should be a YAML ' - 'file with a top-level map containing a single key ' - '"$_pluginToolsConfigBuildFlagsKey" containing a map containing a ' - 'single key "$_pluginToolsConfigGlobalKey" containing a list of build ' - 'arguments.'; - - @override - Future initializeRun() async { - final List platformFlags = _platforms.keys.toList(); - platformFlags.sort(); - if (!platformFlags.any((String platform) => getBoolArg(platform))) { - printError( - 'None of ${platformFlags.map((String platform) => '--$platform').join(', ')} ' - 'were specified. At least one platform must be provided.'); - throw ToolExit(_exitNoPlatformFlags); - } - } - - @override - Future runForPackage(RepositoryPackage package) async { - final List errors = []; - - final bool isPlugin = isFlutterPlugin(package); - final Iterable<_PlatformDetails> requestedPlatforms = _platforms.entries - .where( - (MapEntry entry) => getBoolArg(entry.key)) - .map((MapEntry entry) => entry.value); - - // Platform support is checked at the package level for plugins; there is - // no package-level platform information for non-plugin packages. - final Set<_PlatformDetails> buildPlatforms = isPlugin - ? requestedPlatforms - .where((_PlatformDetails platform) => - pluginSupportsPlatform(platform.pluginPlatform, package)) - .toSet() - : requestedPlatforms.toSet(); - - String platformDisplayList(Iterable<_PlatformDetails> platforms) { - return platforms.map((_PlatformDetails p) => p.label).join(', '); - } - - if (buildPlatforms.isEmpty) { - final String unsupported = requestedPlatforms.length == 1 - ? '${requestedPlatforms.first.label} is not supported' - : 'None of [${platformDisplayList(requestedPlatforms)}] are supported'; - return PackageResult.skip('$unsupported by this plugin'); - } - print('Building for: ${platformDisplayList(buildPlatforms)}'); - - final Set<_PlatformDetails> unsupportedPlatforms = - requestedPlatforms.toSet().difference(buildPlatforms); - if (unsupportedPlatforms.isNotEmpty) { - final List skippedPlatforms = unsupportedPlatforms - .map((_PlatformDetails platform) => platform.label) - .toList(); - skippedPlatforms.sort(); - print('Skipping unsupported platform(s): ' - '${skippedPlatforms.join(', ')}'); - } - print(''); - - bool builtSomething = false; - for (final RepositoryPackage example in package.getExamples()) { - final String packageName = - getRelativePosixPath(example.directory, from: packagesDir); - - for (final _PlatformDetails platform in buildPlatforms) { - // Repo policy is that a plugin must have examples configured for all - // supported platforms. For packages, just log and skip any requested - // platform that a package doesn't have set up. - if (!isPlugin && - !example.directory - .childDirectory(platform.flutterPlatformDirectory) - .existsSync()) { - print('Skipping ${platform.label} for $packageName; not supported.'); - continue; - } - - builtSomething = true; - - String buildPlatform = platform.label; - if (platform.label.toLowerCase() != platform.flutterBuildType) { - buildPlatform += ' (${platform.flutterBuildType})'; - } - print('\nBUILDING $packageName for $buildPlatform'); - if (!await _buildExample(example, platform.flutterBuildType, - extraBuildFlags: platform.extraBuildFlags)) { - errors.add('$packageName (${platform.label})'); - } - } - } - - if (!builtSomething) { - if (isPlugin) { - errors.add('No examples found'); - } else { - return PackageResult.skip( - 'No examples found supporting requested platform(s).'); - } - } - - return errors.isEmpty - ? PackageResult.success() - : PackageResult.fail(errors); - } - - Iterable _readExtraBuildFlagsConfiguration( - Directory directory) sync* { - final File pluginToolsConfig = - directory.childFile(_pluginToolsConfigFileName); - if (pluginToolsConfig.existsSync()) { - final Object? configuration = - loadYaml(pluginToolsConfig.readAsStringSync()); - if (configuration is! YamlMap) { - printError('The $_pluginToolsConfigFileName file must be a YAML map.'); - printError( - 'Currently, the key "$_pluginToolsConfigBuildFlagsKey" is the only one that has an effect.'); - printError( - 'It must itself be a map. Currently, in that map only the key "$_pluginToolsConfigGlobalKey"'); - printError( - 'has any effect; it must contain a list of arguments to pass to the'); - printError('flutter tool.'); - printError(_pluginToolsConfigExample); - throw ToolExit(_exitInvalidPluginToolsConfig); - } - if (configuration.containsKey(_pluginToolsConfigBuildFlagsKey)) { - final Object? buildFlagsConfiguration = - configuration[_pluginToolsConfigBuildFlagsKey]; - if (buildFlagsConfiguration is! YamlMap) { - printError( - 'The $_pluginToolsConfigFileName file\'s "$_pluginToolsConfigBuildFlagsKey" key must be a map.'); - printError( - 'Currently, in that map only the key "$_pluginToolsConfigGlobalKey" has any effect; it must '); - printError( - 'contain a list of arguments to pass to the flutter tool.'); - printError(_pluginToolsConfigExample); - throw ToolExit(_exitInvalidPluginToolsConfig); - } - if (buildFlagsConfiguration.containsKey(_pluginToolsConfigGlobalKey)) { - final Object? globalBuildFlagsConfiguration = - buildFlagsConfiguration[_pluginToolsConfigGlobalKey]; - if (globalBuildFlagsConfiguration is! YamlList) { - printError( - 'The $_pluginToolsConfigFileName file\'s "$_pluginToolsConfigBuildFlagsKey" key must be a map'); - printError('whose "$_pluginToolsConfigGlobalKey" key is a list.'); - printError( - 'That list must contain a list of arguments to pass to the flutter tool.'); - printError( - 'For example, the $_pluginToolsConfigFileName file could look like:'); - printError(_pluginToolsConfigExample); - throw ToolExit(_exitInvalidPluginToolsConfig); - } - yield* globalBuildFlagsConfiguration.cast(); - } - } - } - } - - Future _buildExample( - RepositoryPackage example, - String flutterBuildType, { - List extraBuildFlags = const [], - }) async { - final String enableExperiment = getStringArg(kEnableExperiment); - - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - flutterBuildType, - ...extraBuildFlags, - ..._readExtraBuildFlagsConfiguration(example.directory), - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example.directory, - ); - return exitCode == 0; - } -} - -/// A collection of information related to a specific platform. -class _PlatformDetails { - const _PlatformDetails( - this.label, { - required this.pluginPlatform, - required this.flutterBuildType, - this.extraBuildFlags = const [], - }); - - /// The name to use in output. - final String label; - - /// The key in a pubspec's platform: entry. - final String pluginPlatform; - - /// The `flutter build` build type. - final String flutterBuildType; - - /// The Flutter platform directory name. - // In practice, this is the same as the plugin platform key for all platforms. - // If that changes, this can be adjusted. - String get flutterPlatformDirectory => pluginPlatform; - - /// Any extra flags to pass to `flutter build`. - final List extraBuildFlags; -} diff --git a/script/tool/lib/src/common/cmake.dart b/script/tool/lib/src/common/cmake.dart deleted file mode 100644 index 3f5d8452bd44..000000000000 --- a/script/tool/lib/src/common/cmake.dart +++ /dev/null @@ -1,118 +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 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'core.dart'; -import 'process_runner.dart'; - -const String _cacheCommandKey = 'CMAKE_COMMAND:INTERNAL'; - -/// A utility class for interacting with CMake projects. -class CMakeProject { - /// Creates an instance that runs commands for [project] with the given - /// [processRunner]. - CMakeProject( - this.flutterProject, { - required this.buildMode, - this.processRunner = const ProcessRunner(), - this.platform = const LocalPlatform(), - }); - - /// The directory of a Flutter project to run Gradle commands in. - final Directory flutterProject; - - /// The [ProcessRunner] used to run commands. Overridable for testing. - final ProcessRunner processRunner; - - /// The platform that commands are being run on. - final Platform platform; - - /// The build mode (e.g., Debug, Release). - /// - /// This is a constructor paramater because on Linux many properties depend - /// on the build mode since it uses a single-configuration generator. - final String buildMode; - - late final String _cmakeCommand = _determineCmakeCommand(); - - /// The project's platform directory name. - String get _platformDirName => platform.isWindows ? 'windows' : 'linux'; - - /// The project's 'example' build directory for this instance's platform. - Directory get buildDirectory { - Directory buildDir = - flutterProject.childDirectory('build').childDirectory(_platformDirName); - if (platform.isLinux) { - buildDir = buildDir - // TODO(stuartmorgan): Support arm64 if that ever becomes a supported - // CI configuration for the repository. - .childDirectory('x64') - // Linux uses a single-config generator, so the base build directory - // includes the configuration. - .childDirectory(buildMode.toLowerCase()); - } - return buildDir; - } - - File get _cacheFile => buildDirectory.childFile('CMakeCache.txt'); - - /// Returns the CMake command to run build commands for this project. - /// - /// Assumes the project has been built at least once, such that the CMake - /// generation step has run. - String getCmakeCommand() { - return _cmakeCommand; - } - - /// Returns the CMake command to run build commands for this project. This is - /// used to initialize _cmakeCommand, and should not be called directly. - /// - /// Assumes the project has been built at least once, such that the CMake - /// generation step has run. - String _determineCmakeCommand() { - // On Linux 'cmake' is expected to be in the path, so doesn't need to - // be lookup up and cached. - if (platform.isLinux) { - return 'cmake'; - } - final File cacheFile = _cacheFile; - String? command; - for (String line in cacheFile.readAsLinesSync()) { - line = line.trim(); - if (line.startsWith(_cacheCommandKey)) { - command = line.substring(line.indexOf('=') + 1).trim(); - break; - } - } - if (command == null) { - printError('Unable to find CMake command in ${cacheFile.path}'); - throw ToolExit(100); - } - return command; - } - - /// Whether or not the project is ready to have CMake commands run on it - /// (i.e., whether the `flutter` tool has generated the necessary files). - bool isConfigured() => _cacheFile.existsSync(); - - /// Runs a `cmake` command with the given parameters. - Future runBuild( - String target, { - List arguments = const [], - }) { - return processRunner.runAndStream( - getCmakeCommand(), - [ - '--build', - buildDirectory.path, - '--target', - target, - if (platform.isWindows) ...['--config', buildMode], - ...arguments, - ], - ); - } -} diff --git a/script/tool/lib/src/common/file_utils.dart b/script/tool/lib/src/common/file_utils.dart deleted file mode 100644 index 3c2f2f18f954..000000000000 --- a/script/tool/lib/src/common/file_utils.dart +++ /dev/null @@ -1,20 +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 'package:file/file.dart'; - -/// Returns a [File] created by appending all but the last item in [components] -/// to [base] as subdirectories, then appending the last as a file. -/// -/// Example: -/// childFileWithSubcomponents(rootDir, ['foo', 'bar', 'baz.txt']) -/// creates a File representing /rootDir/foo/bar/baz.txt. -File childFileWithSubcomponents(Directory base, List components) { - Directory dir = base; - final String basename = components.removeLast(); - for (final String directoryName in components) { - dir = dir.childDirectory(directoryName); - } - return dir.childFile(basename); -} diff --git a/script/tool/lib/src/common/git_version_finder.dart b/script/tool/lib/src/common/git_version_finder.dart index b135424827a6..3965ae0ace47 100644 --- a/script/tool/lib/src/common/git_version_finder.dart +++ b/script/tool/lib/src/common/git_version_finder.dart @@ -88,7 +88,8 @@ class GitVersionFinder { if (fileContent.trim().isEmpty) { return null; } - final String? versionString = loadYaml(fileContent)['version'] as String?; + final YamlMap fileYaml = loadYaml(fileContent) as YamlMap; + final String? versionString = fileYaml['version'] as String?; return versionString == null ? null : Version.parse(versionString); } diff --git a/script/tool/lib/src/common/gradle.dart b/script/tool/lib/src/common/gradle.dart deleted file mode 100644 index 746536075014..000000000000 --- a/script/tool/lib/src/common/gradle.dart +++ /dev/null @@ -1,56 +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 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'process_runner.dart'; -import 'repository_package.dart'; - -const String _gradleWrapperWindows = 'gradlew.bat'; -const String _gradleWrapperNonWindows = 'gradlew'; - -/// A utility class for interacting with Gradle projects. -class GradleProject { - /// Creates an instance that runs commands for [project] with the given - /// [processRunner]. - GradleProject( - this.flutterProject, { - this.processRunner = const ProcessRunner(), - this.platform = const LocalPlatform(), - }); - - /// The directory of a Flutter project to run Gradle commands in. - final RepositoryPackage flutterProject; - - /// The [ProcessRunner] used to run commands. Overridable for testing. - final ProcessRunner processRunner; - - /// The platform that commands are being run on. - final Platform platform; - - /// The project's 'android' directory. - Directory get androidDirectory => - flutterProject.platformDirectory(FlutterPlatform.android); - - /// The path to the Gradle wrapper file for the project. - File get gradleWrapper => androidDirectory.childFile( - platform.isWindows ? _gradleWrapperWindows : _gradleWrapperNonWindows); - - /// Whether or not the project is ready to have Gradle commands run on it - /// (i.e., whether the `flutter` tool has generated the necessary files). - bool isConfigured() => gradleWrapper.existsSync(); - - /// Runs a `gradlew` command with the given parameters. - Future runCommand( - String target, { - List arguments = const [], - }) { - return processRunner.runAndStream( - gradleWrapper.path, - [target, ...arguments], - workingDir: androidDirectory, - ); - } -} diff --git a/script/tool/lib/src/common/package_command.dart b/script/tool/lib/src/common/package_command.dart index 0e83d03e9846..8a2bbfc40058 100644 --- a/script/tool/lib/src/common/package_command.dart +++ b/script/tool/lib/src/common/package_command.dart @@ -316,17 +316,28 @@ abstract class PackageCommand extends Command { } else if (getBoolArg(_packagesForBranchArg)) { final String? branch = await _getBranch(); if (branch == null) { - printError('Unabled to determine branch; --$_packagesForBranchArg can ' + printError('Unable to determine branch; --$_packagesForBranchArg can ' 'only be used in a git repository.'); throw ToolExit(exitInvalidArguments); } else { // Configure the change finder the correct mode for the branch. - final bool lastCommitOnly = branch == 'main' || branch == 'master'; + // Log the mode to make it easier to audit logs to see that the + // intended diff was used (or why). + final bool lastCommitOnly; + if (branch == 'main' || branch == 'master') { + print('--$_packagesForBranchArg: running on default branch.'); + lastCommitOnly = true; + } else if (await _isCheckoutFromBranch('main')) { + print( + '--$_packagesForBranchArg: running on a commit from default branch.'); + lastCommitOnly = true; + } else { + print('--$_packagesForBranchArg: running on branch "$branch".'); + lastCommitOnly = false; + } if (lastCommitOnly) { - // Log the mode to make it easier to audit logs to see that the - // intended diff was used. - print('--$_packagesForBranchArg: running on default branch; ' - 'using parent commit as the diff base.'); + print( + '--$_packagesForBranchArg: using parent commit as the diff base.'); changedFileFinder = GitVersionFinder(await gitDir, 'HEAD~'); } else { changedFileFinder = await retrieveVersionFinder(); @@ -522,6 +533,35 @@ abstract class PackageCommand extends Command { return packages; } + // 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, + // in which case branch-name detection won't work. + Future _isCheckoutFromBranch(String branchName) async { + // The target branch may not exist locally; try some common remote names for + // the branch as well. + final List candidateBranchNames = [ + branchName, + 'origin/$branchName', + 'upstream/$branchName', + ]; + for (final String branch in candidateBranchNames) { + final io.ProcessResult result = await (await gitDir).runCommand( + ['merge-base', '--is-ancestor', 'HEAD', branch], + throwOnError: false); + if (result.exitCode == 0) { + return true; + } else if (result.exitCode == 1) { + // 1 indicates that the branch was successfully checked, but it's not + // an ancestor. + return false; + } + // Any other return code is an error, such as `branch` not being a valid + // name in the repository, so try other name variants. + } + return false; + } + Future _getBranch() async { final io.ProcessResult branchResult = await (await gitDir).runCommand( ['rev-parse', '--abbrev-ref', 'HEAD'], diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index d8b1cf001d13..ccfeea0e4732 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -369,7 +369,7 @@ abstract class PackageLoopingCommand extends PackageCommand { } } - return await runForPackage(package); + return runForPackage(package); } void _printSuccess(String message) { diff --git a/script/tool/lib/src/common/package_state_utils.dart b/script/tool/lib/src/common/package_state_utils.dart deleted file mode 100644 index 65f311974f3a..000000000000 --- a/script/tool/lib/src/common/package_state_utils.dart +++ /dev/null @@ -1,222 +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 'package:file/file.dart'; -import 'package:meta/meta.dart'; -import 'package:path/path.dart' as p; - -import 'git_version_finder.dart'; -import 'repository_package.dart'; - -/// The state of a package on disk relative to git state. -@immutable -class PackageChangeState { - /// Creates a new immutable state instance. - const PackageChangeState({ - required this.hasChanges, - required this.hasChangelogChange, - required this.needsChangelogChange, - required this.needsVersionChange, - }); - - /// True if there are any changes to files in the package. - final bool hasChanges; - - /// True if the package's CHANGELOG.md has been changed. - final bool hasChangelogChange; - - /// True if any changes in the package require a version change according - /// to repository policy. - final bool needsVersionChange; - - /// True if any changes in the package require a CHANGELOG change according - /// to repository policy. - final bool needsChangelogChange; -} - -/// Checks [package] against [changedPaths] to determine what changes it has -/// and how those changes relate to repository policy about CHANGELOG and -/// version updates. -/// -/// [changedPaths] should be a list of POSIX-style paths from a common root, -/// and [relativePackagePath] should be the path to [package] from that same -/// root. Commonly these will come from `gitVersionFinder.getChangedFiles()` -/// and `getRelativePosixPath(package.directory, gitDir.path)` respectively; -/// they are arguments mainly to allow for caching the changed paths for an -/// entire command run. -/// -/// If [git] is provided, [changedPaths] must be repository-relative -/// paths, and change type detection can use file diffs in addition to paths. -Future checkPackageChangeState( - RepositoryPackage package, { - required List changedPaths, - required String relativePackagePath, - GitVersionFinder? git, -}) async { - final String packagePrefix = relativePackagePath.endsWith('/') - ? relativePackagePath - : '$relativePackagePath/'; - - bool hasChanges = false; - bool hasChangelogChange = false; - bool needsVersionChange = false; - bool needsChangelogChange = false; - for (final String path in changedPaths) { - // Only consider files within the package. - if (!path.startsWith(packagePrefix)) { - continue; - } - final String packageRelativePath = path.substring(packagePrefix.length); - hasChanges = true; - - final List components = p.posix.split(packageRelativePath); - if (components.isEmpty) { - continue; - } - - if (components.first == 'CHANGELOG.md') { - hasChangelogChange = true; - continue; - } - - if (!needsVersionChange) { - // Developer-only changes don't need version changes or changelog changes. - if (await _isDevChange(components, git: git, repoPath: path)) { - continue; - } - - // Some other changes don't need version changes, but might benefit from - // changelog changes. - needsChangelogChange = true; - if ( - // One of a few special files example will be shown on pub.dev, but - // for anything else in the example publishing has no purpose. - !_isUnpublishedExampleChange(components, package)) { - needsVersionChange = true; - } - } - } - - return PackageChangeState( - hasChanges: hasChanges, - hasChangelogChange: hasChangelogChange, - needsChangelogChange: needsChangelogChange, - needsVersionChange: needsVersionChange); -} - -bool _isTestChange(List pathComponents) { - return pathComponents.contains('test') || - pathComponents.contains('androidTest') || - pathComponents.contains('RunnerTests') || - pathComponents.contains('RunnerUITests'); -} - -// True if the given file is an example file other than the one that will be -// published according to https://dart.dev/tools/pub/package-layout#examples. -// -// This is not exhastive; it currently only handles variations we actually have -// in our repositories. -bool _isUnpublishedExampleChange( - List pathComponents, RepositoryPackage package) { - if (pathComponents.first != 'example') { - return false; - } - final List exampleComponents = pathComponents.sublist(1); - if (exampleComponents.isEmpty) { - return false; - } - - final Directory exampleDirectory = - package.directory.childDirectory('example'); - - // Check for example.md/EXAMPLE.md first, as that has priority. If it's - // present, any other example file is unpublished. - final bool hasExampleMd = - exampleDirectory.childFile('example.md').existsSync() || - exampleDirectory.childFile('EXAMPLE.md').existsSync(); - if (hasExampleMd) { - return !(exampleComponents.length == 1 && - exampleComponents.first.toLowerCase() == 'example.md'); - } - - // Most packages have an example/lib/main.dart (or occasionally - // example/main.dart), so check for that. The other naming variations aren't - // currently used. - const String mainName = 'main.dart'; - final bool hasExampleCode = - exampleDirectory.childDirectory('lib').childFile(mainName).existsSync() || - exampleDirectory.childFile(mainName).existsSync(); - if (hasExampleCode) { - // If there is an example main, only that example file is published. - return !((exampleComponents.length == 1 && - exampleComponents.first == mainName) || - (exampleComponents.length == 2 && - exampleComponents.first == 'lib' && - exampleComponents[1] == mainName)); - } - - // If there's no example code either, the example README.md, if any, is the - // file that will be published. - return exampleComponents.first.toLowerCase() != 'readme.md'; -} - -// True if the change is only relevant to people working on the package. -Future _isDevChange(List pathComponents, - {GitVersionFinder? git, String? repoPath}) async { - return _isTestChange(pathComponents) || - // The top-level "tool" directory is for non-client-facing utility - // code, such as test scripts. - pathComponents.first == 'tool' || - // Entry point for the 'custom-test' command, which is only for CI and - // local testing. - pathComponents.first == 'run_tests.sh' || - // Ignoring lints doesn't affect clients. - pathComponents.contains('lint-baseline.xml') || - // Example build files are very unlikely to be interesting to clients. - _isExampleBuildFile(pathComponents) || - // Test-only gradle depenedencies don't affect clients. - await _isGradleTestDependencyChange(pathComponents, - git: git, repoPath: repoPath); -} - -bool _isExampleBuildFile(List pathComponents) { - if (!pathComponents.contains('example')) { - return false; - } - return pathComponents.contains('gradle-wrapper.properties') || - pathComponents.contains('gradle.properties') || - pathComponents.contains('build.gradle') || - pathComponents.contains('Runner.xcodeproj') || - pathComponents.contains('CMakeLists.txt') || - pathComponents.contains('pubspec.yaml'); -} - -Future _isGradleTestDependencyChange(List pathComponents, - {GitVersionFinder? git, String? repoPath}) async { - if (git == null) { - return false; - } - if (pathComponents.last != 'build.gradle') { - return false; - } - final List diff = await git.getDiffContents(targetPath: repoPath); - final RegExp changeLine = RegExp(r'[+-] '); - final RegExp testDependencyLine = - RegExp(r'[+-]\s*(?:androidT|t)estImplementation\s'); - bool foundTestDependencyChange = false; - for (final String line in diff) { - if (!changeLine.hasMatch(line) || - line.startsWith('--- ') || - line.startsWith('+++ ')) { - continue; - } - if (!testDependencyLine.hasMatch(line)) { - return false; - } - foundTestDependencyChange = true; - } - // Only return true if a test dependency change was found, as a failsafe - // against having the wrong (e.g., incorrectly empty) diff output. - return foundTestDependencyChange; -} diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart deleted file mode 100644 index 94677fe7e5a3..000000000000 --- a/script/tool/lib/src/common/plugin_utils.dart +++ /dev/null @@ -1,119 +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 'package:yaml/yaml.dart'; - -import 'core.dart'; -import 'repository_package.dart'; - -/// Possible plugin support options for a platform. -enum PlatformSupport { - /// The platform has an implementation in the package. - inline, - - /// The platform has an endorsed federated implementation in another package. - federated, -} - -/// Returns true if [package] is a Flutter plugin. -bool isFlutterPlugin(RepositoryPackage package) { - return _readPluginPubspecSection(package) != null; -} - -/// Returns true if [package] is a Flutter [platform] plugin. -/// -/// It checks this by looking for the following pattern in the pubspec: -/// -/// flutter: -/// plugin: -/// platforms: -/// [platform]: -/// -/// If [requiredMode] is provided, the plugin must have the given type of -/// implementation in order to return true. -bool pluginSupportsPlatform( - String platform, - RepositoryPackage plugin, { - PlatformSupport? requiredMode, -}) { - assert(platform == platformIOS || - platform == platformAndroid || - platform == platformWeb || - platform == platformMacOS || - platform == platformWindows || - platform == platformLinux); - - final YamlMap? platformEntry = - _readPlatformPubspecSectionForPlugin(platform, plugin); - if (platformEntry == null) { - return false; - } - - // If the platform entry is present, then it supports the platform. Check - // for required mode if specified. - if (requiredMode != null) { - final bool federated = platformEntry.containsKey('default_package'); - if (federated != (requiredMode == PlatformSupport.federated)) { - return false; - } - } - - return true; -} - -/// Returns true if [plugin] includes native code for [platform], as opposed to -/// being implemented entirely in Dart. -bool pluginHasNativeCodeForPlatform(String platform, RepositoryPackage plugin) { - if (platform == platformWeb) { - // Web plugins are always Dart-only. - return false; - } - final YamlMap? platformEntry = - _readPlatformPubspecSectionForPlugin(platform, plugin); - if (platformEntry == null) { - return false; - } - // All other platforms currently use pluginClass for indicating the native - // code in the plugin. - final String? pluginClass = platformEntry['pluginClass'] as String?; - // TODO(stuartmorgan): Remove the check for 'none' once none of the plugins - // in the repository use that workaround. See - // https://github.com/flutter/flutter/issues/57497 for context. - return pluginClass != null && pluginClass != 'none'; -} - -/// Returns the -/// flutter: -/// plugin: -/// platforms: -/// [platform]: -/// section from [plugin]'s pubspec.yaml, or null if either it is not present, -/// or the pubspec couldn't be read. -YamlMap? _readPlatformPubspecSectionForPlugin( - String platform, RepositoryPackage plugin) { - final YamlMap? pluginSection = _readPluginPubspecSection(plugin); - if (pluginSection == null) { - return null; - } - final YamlMap? platforms = pluginSection['platforms'] as YamlMap?; - if (platforms == null) { - return null; - } - return platforms[platform] as YamlMap?; -} - -/// Returns the -/// flutter: -/// plugin: -/// platforms: -/// section from [plugin]'s pubspec.yaml, or null if either it is not present, -/// or the pubspec couldn't be read. -YamlMap? _readPluginPubspecSection(RepositoryPackage package) { - final Pubspec pubspec = package.parsePubspec(); - final Map? flutterSection = pubspec.flutter; - if (flutterSection == null) { - return null; - } - return flutterSection['plugin'] as YamlMap?; -} diff --git a/script/tool/lib/src/common/pub_version_finder.dart b/script/tool/lib/src/common/pub_version_finder.dart deleted file mode 100644 index 572cb913aa7d..000000000000 --- a/script/tool/lib/src/common/pub_version_finder.dart +++ /dev/null @@ -1,103 +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:convert'; - -import 'package:http/http.dart' as http; -import 'package:pub_semver/pub_semver.dart'; - -/// Finding version of [package] that is published on pub. -class PubVersionFinder { - /// Constructor. - /// - /// Note: you should manually close the [httpClient] when done using the finder. - PubVersionFinder({this.pubHost = defaultPubHost, required this.httpClient}); - - /// The default pub host to use. - static const String defaultPubHost = 'https://pub.dev'; - - /// The pub host url, defaults to `https://pub.dev`. - final String pubHost; - - /// The http client. - /// - /// You should manually close this client when done using this finder. - final http.Client httpClient; - - /// Get the package version on pub. - Future getPackageVersion( - {required String packageName}) async { - assert(packageName.isNotEmpty); - final Uri pubHostUri = Uri.parse(pubHost); - final Uri url = pubHostUri.replace(path: '/packages/$packageName.json'); - final http.Response response = await httpClient.get(url); - - if (response.statusCode == 404) { - return PubVersionFinderResponse( - versions: [], - result: PubVersionFinderResult.noPackageFound, - httpResponse: response); - } else if (response.statusCode != 200) { - return PubVersionFinderResponse( - versions: [], - result: PubVersionFinderResult.fail, - httpResponse: response); - } - final List versions = - (json.decode(response.body)['versions'] as List) - .map((final dynamic versionString) => - Version.parse(versionString as String)) - .toList(); - - return PubVersionFinderResponse( - versions: versions, - result: PubVersionFinderResult.success, - httpResponse: response); - } -} - -/// Represents a response for [PubVersionFinder]. -class PubVersionFinderResponse { - /// Constructor. - PubVersionFinderResponse( - {required this.versions, - required this.result, - required this.httpResponse}) { - if (versions.isNotEmpty) { - versions.sort((Version a, Version b) { - // TODO(cyanglaz): Think about how to handle pre-release version with [Version.prioritize]. - // https://github.com/flutter/flutter/issues/82222 - return b.compareTo(a); - }); - } - } - - /// The versions found in [PubVersionFinder]. - /// - /// This is sorted by largest to smallest, so the first element in the list is the largest version. - /// Might be `null` if the [result] is not [PubVersionFinderResult.success]. - final List versions; - - /// The result of the version finder. - final PubVersionFinderResult result; - - /// The response object of the http request. - final http.Response httpResponse; -} - -/// An enum representing the result of [PubVersionFinder]. -enum PubVersionFinderResult { - /// The version finder successfully found a version. - success, - - /// The version finder failed to find a valid version. - /// - /// This might due to http connection errors or user errors. - fail, - - /// The version finder failed to locate the package. - /// - /// This indicates the package is new. - noPackageFound, -} diff --git a/script/tool/lib/src/common/xcode.dart b/script/tool/lib/src/common/xcode.dart deleted file mode 100644 index 83f681bcb492..000000000000 --- a/script/tool/lib/src/common/xcode.dart +++ /dev/null @@ -1,159 +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:convert'; -import 'dart:io' as io; - -import 'package:file/file.dart'; - -import 'core.dart'; -import 'process_runner.dart'; - -const String _xcodeBuildCommand = 'xcodebuild'; -const String _xcRunCommand = 'xcrun'; - -/// A utility class for interacting with the installed version of Xcode. -class Xcode { - /// Creates an instance that runs commands with the given [processRunner]. - /// - /// If [log] is true, commands run by this instance will long various status - /// messages. - Xcode({ - this.processRunner = const ProcessRunner(), - this.log = false, - }); - - /// The [ProcessRunner] used to run commands. Overridable for testing. - final ProcessRunner processRunner; - - /// Whether or not to log when running commands. - final bool log; - - /// Runs an `xcodebuild` in [directory] with the given parameters. - Future runXcodeBuild( - Directory directory, { - List actions = const ['build'], - required String workspace, - required String scheme, - String? configuration, - List extraFlags = const [], - }) { - final List args = [ - _xcodeBuildCommand, - ...actions, - if (workspace != null) ...['-workspace', workspace], - if (scheme != null) ...['-scheme', scheme], - if (configuration != null) ...['-configuration', configuration], - ...extraFlags, - ]; - final String completeTestCommand = '$_xcRunCommand ${args.join(' ')}'; - if (log) { - print(completeTestCommand); - } - return processRunner.runAndStream(_xcRunCommand, args, - workingDir: directory); - } - - /// Returns true if [project], which should be an .xcodeproj directory, - /// contains a target called [target], false if it does not, and null if the - /// check fails (e.g., if [project] is not an Xcode project). - Future projectHasTarget(Directory project, String target) async { - final io.ProcessResult result = - await processRunner.run(_xcRunCommand, [ - _xcodeBuildCommand, - '-list', - '-json', - '-project', - project.path, - ]); - if (result.exitCode != 0) { - return null; - } - Map? projectInfo; - try { - projectInfo = (jsonDecode(result.stdout as String) - as Map)['project'] as Map?; - } on FormatException { - return null; - } - if (projectInfo == null) { - return null; - } - final List? targets = - (projectInfo['targets'] as List?)?.cast(); - return targets?.contains(target) ?? false; - } - - /// Returns the newest available simulator (highest OS version, with ties - /// broken in favor of newest device), if any. - Future findBestAvailableIphoneSimulator() async { - final List findSimulatorsArguments = [ - 'simctl', - 'list', - 'devices', - 'runtimes', - 'available', - '--json', - ]; - final String findSimulatorCompleteCommand = - '$_xcRunCommand ${findSimulatorsArguments.join(' ')}'; - if (log) { - print('Looking for available simulators...'); - print(findSimulatorCompleteCommand); - } - final io.ProcessResult findSimulatorsResult = - await processRunner.run(_xcRunCommand, findSimulatorsArguments); - if (findSimulatorsResult.exitCode != 0) { - if (log) { - printError( - 'Error occurred while running "$findSimulatorCompleteCommand":\n' - '${findSimulatorsResult.stderr}'); - } - return null; - } - final Map simulatorListJson = - jsonDecode(findSimulatorsResult.stdout as String) - as Map; - final List> runtimes = - (simulatorListJson['runtimes'] as List) - .cast>(); - final Map devices = - (simulatorListJson['devices'] as Map) - .cast(); - if (runtimes.isEmpty || devices.isEmpty) { - return null; - } - String? id; - // Looking for runtimes, trying to find one with highest OS version. - for (final Map rawRuntimeMap in runtimes.reversed) { - final Map runtimeMap = - rawRuntimeMap.cast(); - if ((runtimeMap['name'] as String?)?.contains('iOS') != true) { - continue; - } - final String? runtimeID = runtimeMap['identifier'] as String?; - if (runtimeID == null) { - continue; - } - final List>? devicesForRuntime = - (devices[runtimeID] as List?)?.cast>(); - if (devicesForRuntime == null || devicesForRuntime.isEmpty) { - continue; - } - // Looking for runtimes, trying to find latest version of device. - for (final Map rawDevice in devicesForRuntime.reversed) { - final Map device = rawDevice.cast(); - id = device['udid'] as String?; - if (id == null) { - continue; - } - if (log) { - print('device selected: $device'); - } - return id; - } - } - return null; - } -} diff --git a/script/tool/lib/src/create_all_packages_app_command.dart b/script/tool/lib/src/create_all_packages_app_command.dart deleted file mode 100644 index 142a992972ca..000000000000 --- a/script/tool/lib/src/create_all_packages_app_command.dart +++ /dev/null @@ -1,348 +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:io' as io; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; - -import 'common/core.dart'; -import 'common/package_command.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -const String _outputDirectoryFlag = 'output-dir'; - -const String _projectName = 'all_packages'; - -const int _exitUpdateMacosPodfileFailed = 3; -const int _exitUpdateMacosPbxprojFailed = 4; -const int _exitGenNativeBuildFilesFailed = 5; - -/// A command to create an application that builds all in a single application. -class CreateAllPackagesAppCommand extends PackageCommand { - /// Creates an instance of the builder command. - CreateAllPackagesAppCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Directory? pluginsRoot, - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform) { - final Directory defaultDir = - pluginsRoot ?? packagesDir.fileSystem.currentDirectory; - argParser.addOption(_outputDirectoryFlag, - defaultsTo: defaultDir.path, - help: - 'The path the directory to create the "$_projectName" project in.\n' - 'Defaults to the repository root.'); - } - - /// The location to create the synthesized app project. - Directory get _appDirectory => packagesDir.fileSystem - .directory(getStringArg(_outputDirectoryFlag)) - .childDirectory(_projectName); - - /// The synthesized app project. - RepositoryPackage get app => RepositoryPackage(_appDirectory); - - @override - String get description => - 'Generate Flutter app that includes all target packagas.'; - - @override - String get name => 'create-all-packages-app'; - - @override - Future run() async { - final int exitCode = await _createApp(); - if (exitCode != 0) { - throw ToolExit(exitCode); - } - - final Set excluded = getExcludedPackageNames(); - if (excluded.isNotEmpty) { - print('Exluding the following plugins from the combined build:'); - for (final String plugin in excluded) { - print(' $plugin'); - } - print(''); - } - - await _genPubspecWithAllPlugins(); - - // Run `flutter pub get` to generate all native build files. - // TODO(stuartmorgan): This hangs on Windows for some reason. Since it's - // currently not needed on Windows, skip it there, but we should investigate - // further and/or implement https://github.com/flutter/flutter/issues/93407, - // and remove the need for this conditional. - if (!platform.isWindows) { - if (!await _genNativeBuildFiles()) { - printError( - "Failed to generate native build files via 'flutter pub get'"); - throw ToolExit(_exitGenNativeBuildFilesFailed); - } - } - - await Future.wait(>[ - _updateAppGradle(), - _updateManifest(), - _updateMacosPbxproj(), - // This step requires the native file generation triggered by - // flutter pub get above, so can't currently be run on Windows. - if (!platform.isWindows) _updateMacosPodfile(), - ]); - } - - Future _createApp() async { - final io.ProcessResult result = io.Process.runSync( - flutterCommand, - [ - 'create', - '--template=app', - '--project-name=$_projectName', - '--android-language=java', - _appDirectory.path, - ], - ); - - print(result.stdout); - print(result.stderr); - return result.exitCode; - } - - Future _updateAppGradle() async { - final File gradleFile = app - .platformDirectory(FlutterPlatform.android) - .childDirectory('app') - .childFile('build.gradle'); - if (!gradleFile.existsSync()) { - throw ToolExit(64); - } - - final StringBuffer newGradle = StringBuffer(); - for (final String line in gradleFile.readAsLinesSync()) { - if (line.contains('minSdkVersion')) { - // minSdkVersion 20 is required by Google maps. - // minSdkVersion 19 is required by WebView. - newGradle.writeln('minSdkVersion 20'); - } else if (line.contains('compileSdkVersion')) { - // compileSdkVersion 32 is required by webview_flutter. - newGradle.writeln('compileSdkVersion 32'); - } else { - newGradle.writeln(line); - } - if (line.contains('defaultConfig {')) { - newGradle.writeln(' multiDexEnabled true'); - } else if (line.contains('dependencies {')) { - newGradle.writeln( - " implementation 'com.google.guava:guava:27.0.1-android'\n", - ); - // Tests for https://github.com/flutter/flutter/issues/43383 - newGradle.writeln( - " implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0-rc01'\n", - ); - } - } - gradleFile.writeAsStringSync(newGradle.toString()); - } - - Future _updateManifest() async { - final File manifestFile = app - .platformDirectory(FlutterPlatform.android) - .childDirectory('app') - .childDirectory('src') - .childDirectory('main') - .childFile('AndroidManifest.xml'); - if (!manifestFile.existsSync()) { - throw ToolExit(64); - } - - final StringBuffer newManifest = StringBuffer(); - for (final String line in manifestFile.readAsLinesSync()) { - if (line.contains('package="com.example.$_projectName"')) { - newManifest - ..writeln('package="com.example.$_projectName"') - ..writeln('xmlns:tools="http://schemas.android.com/tools">') - ..writeln() - ..writeln( - '', - ); - } else { - newManifest.writeln(line); - } - } - manifestFile.writeAsStringSync(newManifest.toString()); - } - - Future _genPubspecWithAllPlugins() async { - // Read the old pubspec file's Dart SDK version, in order to preserve it - // in the new file. The template sometimes relies on having opted in to - // specific language features via SDK version, so using a different one - // can cause compilation failures. - final Pubspec originalPubspec = app.parsePubspec(); - const String dartSdkKey = 'sdk'; - final VersionConstraint dartSdkConstraint = - originalPubspec.environment?[dartSdkKey] ?? - VersionConstraint.compatibleWith( - Version.parse('2.12.0'), - ); - - final Map pluginDeps = - await _getValidPathDependencies(); - final Pubspec pubspec = Pubspec( - _projectName, - description: 'Flutter app containing all 1st party plugins.', - version: Version.parse('1.0.0+1'), - environment: { - dartSdkKey: dartSdkConstraint, - }, - dependencies: { - 'flutter': SdkDependency('flutter'), - }..addAll(pluginDeps), - devDependencies: { - 'flutter_test': SdkDependency('flutter'), - }, - dependencyOverrides: pluginDeps, - ); - app.pubspecFile.writeAsStringSync(_pubspecToString(pubspec)); - } - - Future> _getValidPathDependencies() async { - final Map pathDependencies = - {}; - - await for (final PackageEnumerationEntry entry in getTargetPackages()) { - final RepositoryPackage package = entry.package; - final Directory pluginDirectory = package.directory; - final String pluginName = pluginDirectory.basename; - final Pubspec pubspec = package.parsePubspec(); - - if (pubspec.publishTo != 'none') { - pathDependencies[pluginName] = PathDependency(pluginDirectory.path); - } - } - return pathDependencies; - } - - String _pubspecToString(Pubspec pubspec) { - return ''' -### Generated file. Do not edit. Run `pub global run flutter_plugin_tools gen-pubspec` to update. -name: ${pubspec.name} -description: ${pubspec.description} -publish_to: none - -version: ${pubspec.version} - -environment:${_pubspecMapString(pubspec.environment!)} - -dependencies:${_pubspecMapString(pubspec.dependencies)} - -dependency_overrides:${_pubspecMapString(pubspec.dependencyOverrides)} - -dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} -###'''; - } - - String _pubspecMapString(Map values) { - final StringBuffer buffer = StringBuffer(); - - for (final MapEntry entry in values.entries) { - buffer.writeln(); - if (entry.value is VersionConstraint) { - String value = entry.value.toString(); - // Range constraints require quoting. - if (value.startsWith('>') || value.startsWith('<')) { - value = "'$value'"; - } - buffer.write(' ${entry.key}: $value'); - } else if (entry.value is SdkDependency) { - final SdkDependency dep = entry.value as SdkDependency; - buffer.write(' ${entry.key}: \n sdk: ${dep.sdk}'); - } else if (entry.value is PathDependency) { - final PathDependency dep = entry.value as PathDependency; - String depPath = dep.path; - if (path.style == p.Style.windows) { - // Posix-style path separators are preferred in pubspec.yaml (and - // using a consistent format makes unit testing simpler), so convert. - final List components = path.split(depPath); - final String firstComponent = components.first; - // path.split leaves a \ on drive components that isn't necessary, - // and confuses pub, so remove it. - if (firstComponent.endsWith(r':\')) { - components[0] = - firstComponent.substring(0, firstComponent.length - 1); - } - depPath = p.posix.joinAll(components); - } - buffer.write(' ${entry.key}: \n path: $depPath'); - } else { - throw UnimplementedError( - 'Not available for type: ${entry.value.runtimeType}', - ); - } - } - - return buffer.toString(); - } - - Future _genNativeBuildFiles() async { - final int exitCode = await processRunner.runAndStream( - flutterCommand, - ['pub', 'get'], - workingDir: _appDirectory, - ); - return exitCode == 0; - } - - Future _updateMacosPodfile() async { - /// Only change the macOS deployment target if the host platform is macOS. - /// The Podfile is not generated on other platforms. - if (!platform.isMacOS) { - return; - } - - final File podfileFile = - app.platformDirectory(FlutterPlatform.macos).childFile('Podfile'); - if (!podfileFile.existsSync()) { - printError("Can't find Podfile for macOS"); - throw ToolExit(_exitUpdateMacosPodfileFailed); - } - - final StringBuffer newPodfile = StringBuffer(); - for (final String line in podfileFile.readAsLinesSync()) { - if (line.contains('platform :osx')) { - // macOS 10.15 is required by in_app_purchase. - newPodfile.writeln("platform :osx, '10.15'"); - } else { - newPodfile.writeln(line); - } - } - podfileFile.writeAsStringSync(newPodfile.toString()); - } - - Future _updateMacosPbxproj() async { - final File pbxprojFile = app - .platformDirectory(FlutterPlatform.macos) - .childDirectory('Runner.xcodeproj') - .childFile('project.pbxproj'); - if (!pbxprojFile.existsSync()) { - printError("Can't find project.pbxproj for macOS"); - throw ToolExit(_exitUpdateMacosPbxprojFailed); - } - - final StringBuffer newPbxproj = StringBuffer(); - for (final String line in pbxprojFile.readAsLinesSync()) { - if (line.contains('MACOSX_DEPLOYMENT_TARGET')) { - // macOS 10.15 is required by in_app_purchase. - newPbxproj.writeln(' MACOSX_DEPLOYMENT_TARGET = 10.15;'); - } else { - newPbxproj.writeln(line); - } - } - pbxprojFile.writeAsStringSync(newPbxproj.toString()); - } -} diff --git a/script/tool/lib/src/custom_test_command.dart b/script/tool/lib/src/custom_test_command.dart deleted file mode 100644 index 0ef6e602c070..000000000000 --- a/script/tool/lib/src/custom_test_command.dart +++ /dev/null @@ -1,86 +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 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/package_looping_command.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -const String _scriptName = 'run_tests.dart'; -const String _legacyScriptName = 'run_tests.sh'; - -/// A command to run custom, package-local tests on packages. -/// -/// This is an escape hatch for adding tests that this tooling doesn't support. -/// It should be used sparingly; prefer instead to add functionality to this -/// tooling to eliminate the need for bespoke tests. -class CustomTestCommand extends PackageLoopingCommand { - /// Creates a custom test command instance. - CustomTestCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform); - - @override - final String name = 'custom-test'; - - @override - final String description = 'Runs package-specific custom tests defined in ' - "a package's tool/$_scriptName file.\n\n" - 'This command requires "dart" to be in your path.'; - - @override - Future runForPackage(RepositoryPackage package) async { - final File script = - package.directory.childDirectory('tool').childFile(_scriptName); - final File legacyScript = package.directory.childFile(_legacyScriptName); - String? customSkipReason; - bool ranTests = false; - - // Run the custom Dart script if presest. - if (script.existsSync()) { - // Ensure that dependencies are available. - final int pubGetExitCode = await processRunner.runAndStream( - 'dart', ['pub', 'get'], - workingDir: package.directory); - if (pubGetExitCode != 0) { - return PackageResult.fail( - ['Unable to get script dependencies']); - } - - final int testExitCode = await processRunner.runAndStream( - 'dart', ['run', 'tool/$_scriptName'], - workingDir: package.directory); - if (testExitCode != 0) { - return PackageResult.fail(); - } - ranTests = true; - } - - // Run the legacy script if present. - if (legacyScript.existsSync()) { - if (platform.isWindows) { - customSkipReason = '$_legacyScriptName is not supported on Windows. ' - 'Please migrate to $_scriptName.'; - } else { - final int exitCode = await processRunner.runAndStream( - legacyScript.path, [], - workingDir: package.directory); - if (exitCode != 0) { - return PackageResult.fail(); - } - ranTests = true; - } - } - - if (!ranTests) { - return PackageResult.skip(customSkipReason ?? 'No custom tests'); - } - - return PackageResult.success(); - } -} diff --git a/script/tool/lib/src/dependabot_check_command.dart b/script/tool/lib/src/dependabot_check_command.dart deleted file mode 100644 index 5aa762e916e5..000000000000 --- a/script/tool/lib/src/dependabot_check_command.dart +++ /dev/null @@ -1,114 +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:async'; - -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:yaml/yaml.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/repository_package.dart'; - -/// A command to verify Dependabot configuration coverage of packages. -class DependabotCheckCommand extends PackageLoopingCommand { - /// Creates Dependabot check command instance. - DependabotCheckCommand(Directory packagesDir, {GitDir? gitDir}) - : super(packagesDir, gitDir: gitDir) { - argParser.addOption(_configPathFlag, - help: 'Path to the Dependabot configuration file', - defaultsTo: '.github/dependabot.yml'); - } - - static const String _configPathFlag = 'config'; - - late Directory _repoRoot; - - // The set of directories covered by "gradle" entries in the config. - Set _gradleDirs = const {}; - - @override - final String name = 'dependabot-check'; - - @override - final String description = - 'Checks that all packages have Dependabot coverage.'; - - @override - final PackageLoopingType packageLoopingType = - PackageLoopingType.includeAllSubpackages; - - @override - final bool hasLongOutput = false; - - @override - Future initializeRun() async { - _repoRoot = packagesDir.fileSystem.directory((await gitDir).path); - - final YamlMap config = loadYaml(_repoRoot - .childFile(getStringArg(_configPathFlag)) - .readAsStringSync()) as YamlMap; - final dynamic entries = config['updates']; - if (entries is! YamlList) { - return; - } - - const String typeKey = 'package-ecosystem'; - const String dirKey = 'directory'; - _gradleDirs = entries - .where((dynamic entry) => entry[typeKey] == 'gradle') - .map((dynamic entry) => (entry as YamlMap)[dirKey] as String) - .toSet(); - } - - @override - Future runForPackage(RepositoryPackage package) async { - bool skipped = true; - final List errors = []; - - final RunState gradleState = _validateDependabotGradleCoverage(package); - skipped = skipped && gradleState == RunState.skipped; - if (gradleState == RunState.failed) { - printError('${indentation}Missing Gradle coverage.'); - errors.add('Missing Gradle coverage'); - } - - // TODO(stuartmorgan): Add other ecosystem checks here as more are enabled. - - if (skipped) { - return PackageResult.skip('No supported package ecosystems'); - } - return errors.isEmpty - ? PackageResult.success() - : PackageResult.fail(errors); - } - - /// Returns the state for the Dependabot coverage of the Gradle ecosystem for - /// [package]: - /// - succeeded if it includes gradle and is covered. - /// - failed if it includes gradle and is not covered. - /// - skipped if it doesn't include gradle. - RunState _validateDependabotGradleCoverage(RepositoryPackage package) { - final Directory androidDir = - package.platformDirectory(FlutterPlatform.android); - final Directory appDir = androidDir.childDirectory('app'); - if (appDir.existsSync()) { - // It's an app, so only check for the app directory to be covered. - final String dependabotPath = - '/${getRelativePosixPath(appDir, from: _repoRoot)}'; - return _gradleDirs.contains(dependabotPath) - ? RunState.succeeded - : RunState.failed; - } else if (androidDir.existsSync()) { - // It's a library, so only check for the android directory to be covered. - final String dependabotPath = - '/${getRelativePosixPath(androidDir, from: _repoRoot)}'; - return _gradleDirs.contains(dependabotPath) - ? RunState.succeeded - : RunState.failed; - } - return RunState.skipped; - } -} diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart deleted file mode 100644 index e8fb11b5f289..000000000000 --- a/script/tool/lib/src/drive_examples_command.dart +++ /dev/null @@ -1,380 +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:convert'; -import 'dart:io'; - -import 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/plugin_utils.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -const int _exitNoPlatformFlags = 2; -const int _exitNoAvailableDevice = 3; - -/// A command to run the example applications for packages via Flutter driver. -class DriveExamplesCommand extends PackageLoopingCommand { - /// Creates an instance of the drive command. - DriveExamplesCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addFlag(platformAndroid, - help: 'Runs the Android implementation of the examples'); - argParser.addFlag(platformIOS, - help: 'Runs the iOS implementation of the examples'); - argParser.addFlag(platformLinux, - help: 'Runs the Linux implementation of the examples'); - argParser.addFlag(platformMacOS, - help: 'Runs the macOS implementation of the examples'); - argParser.addFlag(platformWeb, - help: 'Runs the web implementation of the examples'); - argParser.addFlag(platformWindows, - help: 'Runs the Windows implementation of the examples'); - argParser.addOption( - kEnableExperiment, - defaultsTo: '', - help: - 'Runs the driver tests in Dart VM with the given experiments enabled.', - ); - } - - @override - final String name = 'drive-examples'; - - @override - final String description = 'Runs driver tests for package example apps.\n\n' - 'For each *_test.dart in test_driver/ it drives an application with ' - 'either the corresponding test in test_driver (for example, ' - 'test_driver/app_test.dart would match test_driver/app.dart), or the ' - '*_test.dart files in integration_test/.\n\n' - 'This command requires "flutter" to be in your path.'; - - Map> _targetDeviceFlags = const >{}; - - @override - Future initializeRun() async { - final List platformSwitches = [ - platformAndroid, - platformIOS, - platformLinux, - platformMacOS, - platformWeb, - platformWindows, - ]; - final int platformCount = platformSwitches - .where((String platform) => getBoolArg(platform)) - .length; - // The flutter tool currently doesn't accept multiple device arguments: - // https://github.com/flutter/flutter/issues/35733 - // If that is implemented, this check can be relaxed. - if (platformCount != 1) { - printError( - 'Exactly one of ${platformSwitches.map((String platform) => '--$platform').join(', ')} ' - 'must be specified.'); - throw ToolExit(_exitNoPlatformFlags); - } - - String? androidDevice; - if (getBoolArg(platformAndroid)) { - final List devices = await _getDevicesForPlatform('android'); - if (devices.isEmpty) { - printError('No Android devices available'); - throw ToolExit(_exitNoAvailableDevice); - } - androidDevice = devices.first; - } - - String? iOSDevice; - if (getBoolArg(platformIOS)) { - final List devices = await _getDevicesForPlatform('ios'); - if (devices.isEmpty) { - printError('No iOS devices available'); - throw ToolExit(_exitNoAvailableDevice); - } - iOSDevice = devices.first; - } - - _targetDeviceFlags = >{ - if (getBoolArg(platformAndroid)) - platformAndroid: ['-d', androidDevice!], - if (getBoolArg(platformIOS)) platformIOS: ['-d', iOSDevice!], - if (getBoolArg(platformLinux)) platformLinux: ['-d', 'linux'], - if (getBoolArg(platformMacOS)) platformMacOS: ['-d', 'macos'], - if (getBoolArg(platformWeb)) - platformWeb: [ - '-d', - 'web-server', - '--web-port=7357', - '--browser-name=chrome', - if (platform.environment.containsKey('CHROME_EXECUTABLE')) - '--chrome-binary=${platform.environment['CHROME_EXECUTABLE']}', - ], - if (getBoolArg(platformWindows)) - platformWindows: ['-d', 'windows'], - }; - } - - @override - Future runForPackage(RepositoryPackage package) async { - final bool isPlugin = isFlutterPlugin(package); - - if (package.isPlatformInterface && package.getExamples().isEmpty) { - // Platform interface packages generally aren't intended to have - // examples, and don't need integration tests, so skip rather than fail. - return PackageResult.skip( - 'Platform interfaces are not expected to have integration tests.'); - } - - // For plugin packages, skip if the plugin itself doesn't support any - // requested platform(s). - if (isPlugin) { - final Iterable requestedPlatforms = _targetDeviceFlags.keys; - final Iterable unsupportedPlatforms = requestedPlatforms.where( - (String platform) => !pluginSupportsPlatform(platform, package)); - for (final String platform in unsupportedPlatforms) { - print('Skipping unsupported platform $platform...'); - } - if (unsupportedPlatforms.length == requestedPlatforms.length) { - return PackageResult.skip( - '${package.displayName} does not support any requested platform.'); - } - } - - int examplesFound = 0; - int supportedExamplesFound = 0; - bool testsRan = false; - final List errors = []; - for (final RepositoryPackage example in package.getExamples()) { - ++examplesFound; - final String exampleName = - getRelativePosixPath(example.directory, from: packagesDir); - - // Skip examples that don't support any requested platform(s). - final List deviceFlags = _deviceFlagsForExample(example); - if (deviceFlags.isEmpty) { - print( - 'Skipping $exampleName; does not support any requested platforms.'); - continue; - } - ++supportedExamplesFound; - - final List drivers = await _getDrivers(example); - if (drivers.isEmpty) { - print('No driver tests found for $exampleName'); - continue; - } - - for (final File driver in drivers) { - final List testTargets = []; - - // Try to find a matching app to drive without the _test.dart - // TODO(stuartmorgan): Migrate all remaining uses of this legacy - // approach (currently only video_player) and remove support for it: - // https://github.com/flutter/flutter/issues/85224. - final File? legacyTestFile = _getLegacyTestFileForTestDriver(driver); - if (legacyTestFile != null) { - testTargets.add(legacyTestFile); - } else { - for (final File testFile in await _getIntegrationTests(example)) { - // Check files for known problematic patterns. - final bool passesValidation = _validateIntegrationTest(testFile); - if (!passesValidation) { - // Report the issue, but continue with the test as the validation - // errors don't prevent running. - errors.add('${testFile.basename} failed validation'); - } - testTargets.add(testFile); - } - } - - if (testTargets.isEmpty) { - final String driverRelativePath = - getRelativePosixPath(driver, from: package.directory); - printError( - 'Found $driverRelativePath, but no integration_test/*_test.dart files.'); - errors.add('No test files for $driverRelativePath'); - continue; - } - - testsRan = true; - final List failingTargets = await _driveTests( - example, driver, testTargets, - deviceFlags: deviceFlags); - for (final File failingTarget in failingTargets) { - errors.add( - getRelativePosixPath(failingTarget, from: package.directory)); - } - } - } - if (!testsRan) { - // It is an error for a plugin not to have integration tests, because that - // is the only way to test the method channel communication. - if (isPlugin) { - printError( - 'No driver tests were run ($examplesFound example(s) found).'); - errors.add('No tests ran (use --exclude if this is intentional).'); - } else { - return PackageResult.skip(supportedExamplesFound == 0 - ? 'No example supports requested platform(s).' - : 'No example is configured for driver tests.'); - } - } - return errors.isEmpty - ? PackageResult.success() - : PackageResult.fail(errors); - } - - /// Returns the device flags for the intersection of the requested platforms - /// and the platforms supported by [example]. - List _deviceFlagsForExample(RepositoryPackage example) { - final List deviceFlags = []; - for (final MapEntry> entry - in _targetDeviceFlags.entries) { - final String platform = entry.key; - if (example.directory.childDirectory(platform).existsSync()) { - deviceFlags.addAll(entry.value); - } else { - final String exampleName = - getRelativePosixPath(example.directory, from: packagesDir); - print('Skipping unsupported platform $platform for $exampleName'); - } - } - return deviceFlags; - } - - Future> _getDevicesForPlatform(String platform) async { - final List deviceIds = []; - - final ProcessResult result = await processRunner.run( - flutterCommand, ['devices', '--machine'], - stdoutEncoding: utf8); - if (result.exitCode != 0) { - return deviceIds; - } - - String output = result.stdout as String; - // --machine doesn't currently prevent the tool from printing banners; - // see https://github.com/flutter/flutter/issues/86055. This workaround - // can be removed once that is fixed. - output = output.substring(output.indexOf('[')); - - final List> devices = - (jsonDecode(output) as List).cast>(); - for (final Map deviceInfo in devices) { - final String targetPlatform = - (deviceInfo['targetPlatform'] as String?) ?? ''; - if (targetPlatform.startsWith(platform)) { - final String? deviceId = deviceInfo['id'] as String?; - if (deviceId != null) { - deviceIds.add(deviceId); - } - } - } - return deviceIds; - } - - Future> _getDrivers(RepositoryPackage example) async { - final List drivers = []; - - final Directory driverDir = example.directory.childDirectory('test_driver'); - if (driverDir.existsSync()) { - await for (final FileSystemEntity driver in driverDir.list()) { - if (driver is File && driver.basename.endsWith('_test.dart')) { - drivers.add(driver); - } - } - } - return drivers; - } - - File? _getLegacyTestFileForTestDriver(File testDriver) { - final String testName = testDriver.basename.replaceAll( - RegExp(r'_test.dart$'), - '.dart', - ); - final File testFile = testDriver.parent.childFile(testName); - - return testFile.existsSync() ? testFile : null; - } - - Future> _getIntegrationTests(RepositoryPackage example) async { - final List tests = []; - final Directory integrationTestDir = - example.directory.childDirectory('integration_test'); - - if (integrationTestDir.existsSync()) { - await for (final FileSystemEntity file in integrationTestDir.list()) { - if (file is File && file.basename.endsWith('_test.dart')) { - tests.add(file); - } - } - } - return tests; - } - - /// Checks [testFile] for known bad patterns in integration tests, logging - /// any issues. - /// - /// Returns true if the file passes validation without issues. - bool _validateIntegrationTest(File testFile) { - final List lines = testFile.readAsLinesSync(); - - final RegExp badTestPattern = RegExp(r'\s*test\('); - if (lines.any((String line) => line.startsWith(badTestPattern))) { - final String filename = testFile.basename; - printError( - '$filename uses "test", which will not report failures correctly. ' - 'Use testWidgets instead.'); - return false; - } - - return true; - } - - /// For each file in [targets], uses - /// `flutter drive --driver [driver] --target ` - /// to drive [example], returning a list of any failing test targets. - /// - /// [deviceFlags] should contain the flags to run the test on a specific - /// target device (plus any supporting device-specific flags). E.g.: - /// - `['-d', 'macos']` for driving for macOS. - /// - `['-d', 'web-server', '--web-port=', '--browser-name=]` - /// for web - Future> _driveTests( - RepositoryPackage example, - File driver, - List targets, { - required List deviceFlags, - }) async { - final List failures = []; - - final String enableExperiment = getStringArg(kEnableExperiment); - - for (final File target in targets) { - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'drive', - ...deviceFlags, - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - '--driver', - getRelativePosixPath(driver, from: example.directory), - '--target', - getRelativePosixPath(target, from: example.directory), - ], - workingDir: example.directory); - if (exitCode != 0) { - failures.add(target); - } - } - return failures; - } -} diff --git a/script/tool/lib/src/federation_safety_check_command.dart b/script/tool/lib/src/federation_safety_check_command.dart deleted file mode 100644 index 93a832eb0e29..000000000000 --- a/script/tool/lib/src/federation_safety_check_command.dart +++ /dev/null @@ -1,199 +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 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:pub_semver/pub_semver.dart'; - -import 'common/core.dart'; -import 'common/file_utils.dart'; -import 'common/git_version_finder.dart'; -import 'common/package_looping_command.dart'; -import 'common/plugin_utils.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -/// A command to check that PRs don't violate repository best practices that -/// have been established to avoid breakages that building and testing won't -/// catch. -class FederationSafetyCheckCommand extends PackageLoopingCommand { - /// Creates an instance of the safety check command. - FederationSafetyCheckCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - GitDir? gitDir, - }) : super( - packagesDir, - processRunner: processRunner, - platform: platform, - gitDir: gitDir, - ); - - // A map of package name (as defined by the directory name of the package) - // to a list of changed Dart files in that package, as Posix paths relative to - // the package root. - // - // This only considers top-level packages, not subpackages such as example/. - final Map> _changedDartFiles = >{}; - - // The set of *_platform_interface packages that will have public code changes - // published. - final Set _modifiedAndPublishedPlatformInterfacePackages = {}; - - // The set of conceptual plugins (not packages) that have changes. - final Set _changedPlugins = {}; - - static const String _platformInterfaceSuffix = '_platform_interface'; - - @override - final String name = 'federation-safety-check'; - - @override - final String description = - 'Checks that the change does not violate repository rules around changes ' - 'to federated plugin packages.'; - - @override - bool get hasLongOutput => false; - - @override - Future initializeRun() async { - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - final String baseSha = await gitVersionFinder.getBaseSha(); - print('Validating changes relative to "$baseSha"\n'); - for (final String path in await gitVersionFinder.getChangedFiles()) { - // Git output always uses Posix paths. - final List allComponents = p.posix.split(path); - final int packageIndex = allComponents.indexOf('packages'); - if (packageIndex == -1) { - continue; - } - final List relativeComponents = - allComponents.sublist(packageIndex + 1); - // The package name is either the directory directly under packages/, or - // the directory under that in the case of a federated plugin. - String packageName = relativeComponents.removeAt(0); - // Count the top-level plugin as changed. - _changedPlugins.add(packageName); - if (relativeComponents[0] == packageName || - (relativeComponents.length > 1 && - relativeComponents[0].startsWith('${packageName}_'))) { - packageName = relativeComponents.removeAt(0); - } - - if (relativeComponents.last.endsWith('.dart')) { - _changedDartFiles[packageName] ??= []; - _changedDartFiles[packageName]! - .add(p.posix.joinAll(relativeComponents)); - } - - if (packageName.endsWith(_platformInterfaceSuffix) && - relativeComponents.first == 'pubspec.yaml' && - await _packageWillBePublished(path)) { - _modifiedAndPublishedPlatformInterfacePackages.add(packageName); - } - } - } - - @override - Future runForPackage(RepositoryPackage package) async { - if (!isFlutterPlugin(package)) { - return PackageResult.skip('Not a plugin.'); - } - - if (!package.isFederated) { - return PackageResult.skip('Not a federated plugin.'); - } - - if (package.isPlatformInterface) { - // As the leaf nodes in the graph, a published package interface change is - // assumed to be correct, and other changes are validated against that. - return PackageResult.skip( - 'Platform interface changes are not validated.'); - } - - // Uses basename to match _changedPackageFiles. - final String basePackageName = package.directory.parent.basename; - final String platformInterfacePackageName = - '$basePackageName$_platformInterfaceSuffix'; - final List changedPlatformInterfaceFiles = - _changedDartFiles[platformInterfacePackageName] ?? []; - - if (!_modifiedAndPublishedPlatformInterfacePackages - .contains(platformInterfacePackageName)) { - print('No published changes for $platformInterfacePackageName.'); - return PackageResult.success(); - } - - if (!changedPlatformInterfaceFiles - .any((String path) => path.startsWith('lib/'))) { - print('No public code changes for $platformInterfacePackageName.'); - return PackageResult.success(); - } - - final List changedPackageFiles = - _changedDartFiles[package.directory.basename] ?? []; - if (changedPackageFiles.isEmpty) { - print('No Dart changes.'); - return PackageResult.success(); - } - - // If the change would be flagged, but it appears to be a mass change - // rather than a plugin-specific change, allow it with a warning. - // - // This is a tradeoff between safety and convenience; forcing mass changes - // to be split apart is not ideal, and the assumption is that reviewers are - // unlikely to accidentally approve a PR that is supposed to be changing a - // single plugin, but touches other plugins (vs accidentally approving a - // PR that changes multiple parts of a single plugin, which is a relatively - // easy mistake to make). - // - // 3 is chosen to minimize the chances of accidentally letting something - // through (vs 2, which could be a single-plugin change with one stray - // change to another file accidentally included), while not setting too - // high a bar for detecting mass changes. This can be tuned if there are - // issues with false positives or false negatives. - const int massChangePluginThreshold = 3; - if (_changedPlugins.length >= massChangePluginThreshold) { - logWarning('Ignoring potentially dangerous change, as this appears ' - 'to be a mass change.'); - return PackageResult.success(); - } - - printError('Dart changes are not allowed to other packages in ' - '$basePackageName in the same PR as changes to public Dart code in ' - '$platformInterfacePackageName, as this can cause accidental breaking ' - 'changes to be missed by automated checks. Please split the changes to ' - 'these two packages into separate PRs.\n\n' - 'If you believe that this is a false positive, please file a bug.'); - return PackageResult.fail( - ['$platformInterfacePackageName changed.']); - } - - Future _packageWillBePublished( - String pubspecRepoRelativePosixPath) async { - final File pubspecFile = childFileWithSubcomponents( - packagesDir.parent, p.posix.split(pubspecRepoRelativePosixPath)); - if (!pubspecFile.existsSync()) { - // If the package was deleted, nothing will be published. - return false; - } - final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); - if (pubspec.publishTo == 'none') { - return false; - } - - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - final Version? previousVersion = - await gitVersionFinder.getPackageVersion(pubspecRepoRelativePosixPath); - if (previousVersion == null) { - // The plugin is new, so it will be published. - return true; - } - return pubspec.version != previousVersion; - } -} diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart deleted file mode 100644 index a11284411908..000000000000 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ /dev/null @@ -1,358 +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:async'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:platform/platform.dart'; -import 'package:uuid/uuid.dart'; - -import 'common/core.dart'; -import 'common/gradle.dart'; -import 'common/package_looping_command.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -const int _exitGcloudAuthFailed = 2; - -/// A command to run tests via Firebase test lab. -class FirebaseTestLabCommand extends PackageLoopingCommand { - /// Creates an instance of the test runner command. - FirebaseTestLabCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addOption( - 'project', - defaultsTo: 'flutter-cirrus', - help: 'The Firebase project name.', - ); - final String? homeDir = io.Platform.environment['HOME']; - argParser.addOption('service-key', - defaultsTo: homeDir == null - ? null - : path.join(homeDir, 'gcloud-service-key.json'), - help: 'The path to the service key for gcloud authentication.\n' - r'If not provided, \$HOME/gcloud-service-key.json will be ' - r'assumed if $HOME is set.'); - argParser.addOption('test-run-id', - defaultsTo: const Uuid().v4(), - help: - 'Optional string to append to the results path, to avoid conflicts. ' - 'Randomly chosen on each invocation if none is provided. ' - 'The default shown here is just an example.'); - argParser.addOption('build-id', - defaultsTo: - io.Platform.environment['CIRRUS_BUILD_ID'] ?? 'unknown_build', - help: - 'Optional string to append to the results path, to avoid conflicts. ' - r'Defaults to $CIRRUS_BUILD_ID if that is set.'); - argParser.addMultiOption('device', - splitCommas: false, - defaultsTo: [ - 'model=walleye,version=26', - 'model=redfin,version=30' - ], - help: - 'Device model(s) to test. See https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run for more info'); - argParser.addOption('results-bucket', - defaultsTo: 'gs://flutter_cirrus_testlab'); - argParser.addOption( - kEnableExperiment, - defaultsTo: '', - help: 'Enables the given Dart SDK experiments.', - ); - } - - @override - final String name = 'firebase-test-lab'; - - @override - final String description = 'Runs the instrumentation tests of the example ' - 'apps on Firebase Test Lab.\n\n' - 'Runs tests in test_instrumentation folder using the ' - 'instrumentation_test package.'; - - bool _firebaseProjectConfigured = false; - - Future _configureFirebaseProject() async { - if (_firebaseProjectConfigured) { - return; - } - - final String serviceKey = getStringArg('service-key'); - if (serviceKey.isEmpty) { - print('No --service-key provided; skipping gcloud authorization'); - } else { - final io.ProcessResult result = await processRunner.run( - 'gcloud', - [ - 'auth', - 'activate-service-account', - '--key-file=$serviceKey', - ], - logOnError: true, - ); - if (result.exitCode != 0) { - printError('Unable to activate gcloud account.'); - throw ToolExit(_exitGcloudAuthFailed); - } - final int exitCode = await processRunner.runAndStream('gcloud', [ - 'config', - 'set', - 'project', - getStringArg('project'), - ]); - print(''); - if (exitCode == 0) { - print('Firebase project configured.'); - } else { - logWarning( - 'Warning: gcloud config set returned a non-zero exit code. Continuing anyway.'); - } - } - _firebaseProjectConfigured = true; - } - - @override - Future runForPackage(RepositoryPackage package) async { - final List results = []; - for (final RepositoryPackage example in package.getExamples()) { - results.add(await _runForExample(example, package: package)); - } - - // If all results skipped, report skip overall. - if (results - .every((PackageResult result) => result.state == RunState.skipped)) { - return PackageResult.skip('No examples support Android.'); - } - // Otherwise, report failure if there were any failures. - final List allErrors = results - .map((PackageResult result) => - result.state == RunState.failed ? result.details : []) - .expand((List list) => list) - .toList(); - return allErrors.isEmpty - ? PackageResult.success() - : PackageResult.fail(allErrors); - } - - /// Runs the test for the given example of [package]. - Future _runForExample( - RepositoryPackage example, { - required RepositoryPackage package, - }) async { - final Directory androidDirectory = - example.platformDirectory(FlutterPlatform.android); - if (!androidDirectory.existsSync()) { - return PackageResult.skip( - '${example.displayName} does not support Android.'); - } - - final Directory uiTestDirectory = androidDirectory - .childDirectory('app') - .childDirectory('src') - .childDirectory('androidTest'); - if (!uiTestDirectory.existsSync()) { - printError('No androidTest directory found.'); - return PackageResult.fail( - ['No tests ran (use --exclude if this is intentional).']); - } - - // Ensure that the Dart integration tests will be run, not just native UI - // tests. - if (!await _testsContainDartIntegrationTestRunner(uiTestDirectory)) { - printError('No integration_test runner found. ' - 'See the integration_test package README for setup instructions.'); - return PackageResult.fail(['No integration_test runner.']); - } - - // Ensures that gradle wrapper exists - final GradleProject project = GradleProject(example, - processRunner: processRunner, platform: platform); - if (!await _ensureGradleWrapperExists(project)) { - return PackageResult.fail(['Unable to build example apk']); - } - - await _configureFirebaseProject(); - - if (!await _runGradle(project, 'app:assembleAndroidTest')) { - return PackageResult.fail(['Unable to assemble androidTest']); - } - - final List errors = []; - - // Used within the loop to ensure a unique GCS output location for each - // test file's run. - int resultsCounter = 0; - for (final File test in _findIntegrationTestFiles(example)) { - final String testName = - getRelativePosixPath(test, from: package.directory); - print('Testing $testName...'); - if (!await _runGradle(project, 'app:assembleDebug', testFile: test)) { - printError('Could not build $testName'); - errors.add('$testName failed to build'); - continue; - } - final String buildId = getStringArg('build-id'); - final String testRunId = getStringArg('test-run-id'); - final String resultsDir = - 'plugins_android_test/${package.displayName}/$buildId/$testRunId/' - '${example.directory.basename}/${resultsCounter++}/'; - - // Automatically retry failures; there is significant flake with these - // tests whose cause isn't yet understood, and having to re-run the - // entire shard for a flake in any one test is extremely slow. This should - // be removed once the root cause of the flake is understood. - // See https://github.com/flutter/flutter/issues/95063 - const int maxRetries = 2; - bool passing = false; - for (int i = 1; i <= maxRetries && !passing; ++i) { - if (i > 1) { - logWarning('$testName failed on attempt ${i - 1}. Retrying...'); - } - passing = await _runFirebaseTest(example, test, resultsDir: resultsDir); - } - if (!passing) { - printError('Test failure for $testName after $maxRetries attempts'); - errors.add('$testName failed tests'); - } - } - - if (errors.isEmpty && resultsCounter == 0) { - printError('No integration tests were run.'); - errors.add('No tests ran (use --exclude if this is intentional).'); - } - - return errors.isEmpty - ? PackageResult.success() - : PackageResult.fail(errors); - } - - /// Checks that Gradle has been configured for [project], and if not runs a - /// Flutter build to generate it. - /// - /// Returns true if either gradlew was already present, or the build succeeds. - Future _ensureGradleWrapperExists(GradleProject project) async { - if (!project.isConfigured()) { - print('Running flutter build apk...'); - final String experiment = getStringArg(kEnableExperiment); - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - 'apk', - if (experiment.isNotEmpty) '--enable-experiment=$experiment', - ], - workingDir: project.androidDirectory); - - if (exitCode != 0) { - return false; - } - } - return true; - } - - /// Runs [test] from [example] as a Firebase Test Lab test, returning true if - /// the test passed. - /// - /// [resultsDir] should be a unique-to-the-test-run directory to store the - /// results on the server. - Future _runFirebaseTest( - RepositoryPackage example, - File test, { - required String resultsDir, - }) async { - final List args = [ - 'firebase', - 'test', - 'android', - 'run', - '--type', - 'instrumentation', - '--app', - 'build/app/outputs/apk/debug/app-debug.apk', - '--test', - 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', - '--timeout', - '7m', - '--results-bucket=${getStringArg('results-bucket')}', - '--results-dir=$resultsDir', - for (final String device in getStringListArg('device')) ...[ - '--device', - device - ], - ]; - final int exitCode = await processRunner.runAndStream('gcloud', args, - workingDir: example.directory); - - return exitCode == 0; - } - - /// Builds [target] using Gradle in the given [project]. Assumes Gradle is - /// already configured. - /// - /// [testFile] optionally does the Flutter build with the given test file as - /// the build target. - /// - /// Returns true if the command succeeds. - Future _runGradle( - GradleProject project, - String target, { - File? testFile, - }) async { - final String experiment = getStringArg(kEnableExperiment); - final String? extraOptions = experiment.isNotEmpty - ? Uri.encodeComponent('--enable-experiment=$experiment') - : null; - - final int exitCode = await project.runCommand( - target, - arguments: [ - '-Pverbose=true', - if (testFile != null) '-Ptarget=${testFile.path}', - if (extraOptions != null) '-Pextra-front-end-options=$extraOptions', - if (extraOptions != null) '-Pextra-gen-snapshot-options=$extraOptions', - ], - ); - - if (exitCode != 0) { - return false; - } - return true; - } - - /// Finds and returns all integration test files for [example]. - Iterable _findIntegrationTestFiles(RepositoryPackage example) sync* { - final Directory integrationTestDir = - example.directory.childDirectory('integration_test'); - - if (!integrationTestDir.existsSync()) { - return; - } - - yield* integrationTestDir - .listSync(recursive: true) - .where((FileSystemEntity file) => - file is File && file.basename.endsWith('_test.dart')) - .cast(); - } - - /// Returns true if any of the test files in [uiTestDirectory] contain the - /// annotation that means that the test will reports the results of running - /// the Dart integration tests. - Future _testsContainDartIntegrationTestRunner( - Directory uiTestDirectory) async { - return uiTestDirectory - .list(recursive: true, followLinks: false) - .where((FileSystemEntity entity) => entity is File) - .cast() - .any((File file) { - return file.basename.endsWith('.java') && - file.readAsStringSync().contains('@RunWith(FlutterTestRunner.class)'); - }); - } -} diff --git a/script/tool/lib/src/fix_command.dart b/script/tool/lib/src/fix_command.dart deleted file mode 100644 index 2819609eabbd..000000000000 --- a/script/tool/lib/src/fix_command.dart +++ /dev/null @@ -1,51 +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:async'; - -import 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -/// A command to run Dart's "fix" command on packages. -class FixCommand extends PackageLoopingCommand { - /// Creates a fix command instance. - FixCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform); - - @override - final String name = 'fix'; - - @override - final String description = 'Fixes packages using dart fix.\n\n' - 'This command requires "dart" and "flutter" to be in your path, and ' - 'assumes that dependencies have already been fetched (e.g., by running ' - 'the analyze command first).'; - - @override - final bool hasLongOutput = false; - - @override - PackageLoopingType get packageLoopingType => - PackageLoopingType.includeAllSubpackages; - - @override - Future runForPackage(RepositoryPackage package) async { - final int exitCode = await processRunner.runAndStream( - 'dart', ['fix', '--apply'], - workingDir: package.directory); - if (exitCode != 0) { - printError('Unable to automatically fix package.'); - return PackageResult.fail(); - } - return PackageResult.success(); - } -} diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart deleted file mode 100644 index cc6936c566e1..000000000000 --- a/script/tool/lib/src/format_command.dart +++ /dev/null @@ -1,336 +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:convert'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:http/http.dart' as http; -import 'package:meta/meta.dart'; -import 'package:platform/platform.dart'; - -import 'common/core.dart'; -import 'common/package_command.dart'; -import 'common/process_runner.dart'; - -/// In theory this should be 8191, but in practice that was still resulting in -/// "The input line is too long" errors. This was chosen as a value that worked -/// in practice in testing with flutter/plugins, but may need to be adjusted -/// based on further experience. -@visibleForTesting -const int windowsCommandLineMax = 8000; - -/// This value is picked somewhat arbitrarily based on checking `ARG_MAX` on a -/// macOS and Linux machine. If anyone encounters a lower limit in pratice, it -/// can be lowered accordingly. -@visibleForTesting -const int nonWindowsCommandLineMax = 1000000; - -const int _exitClangFormatFailed = 3; -const int _exitFlutterFormatFailed = 4; -const int _exitJavaFormatFailed = 5; -const int _exitGitFailed = 6; -const int _exitDependencyMissing = 7; - -final Uri _googleFormatterUrl = Uri.https('github.com', - '/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar'); - -/// A command to format all package code. -class FormatCommand extends PackageCommand { - /// Creates an instance of the format command. - FormatCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addFlag('fail-on-change', hide: true); - argParser.addOption('clang-format', - defaultsTo: 'clang-format', help: 'Path to "clang-format" executable.'); - argParser.addOption('java', - defaultsTo: 'java', help: 'Path to "java" executable.'); - } - - @override - final String name = 'format'; - - @override - final String description = - 'Formats the code of all packages (Java, Objective-C, C++, and Dart).\n\n' - 'This command requires "git", "flutter" and "clang-format" v5 to be in ' - 'your path.'; - - @override - Future run() async { - final String googleFormatterPath = await _getGoogleFormatterPath(); - - // This class is not based on PackageLoopingCommand because running the - // formatters separately for each package is an order of magnitude slower, - // due to the startup overhead of the formatters. - final Iterable files = - await _getFilteredFilePaths(getFiles(), relativeTo: packagesDir); - await _formatDart(files); - await _formatJava(files, googleFormatterPath); - await _formatCppAndObjectiveC(files); - - if (getBoolArg('fail-on-change')) { - final bool modified = await _didModifyAnything(); - if (modified) { - throw ToolExit(exitCommandFoundErrors); - } - } - } - - Future _didModifyAnything() async { - final io.ProcessResult modifiedFiles = await processRunner.run( - 'git', - ['ls-files', '--modified'], - workingDir: packagesDir, - logOnError: true, - ); - if (modifiedFiles.exitCode != 0) { - printError('Unable to determine changed files.'); - throw ToolExit(_exitGitFailed); - } - - print('\n\n'); - - final String stdout = modifiedFiles.stdout as String; - if (stdout.isEmpty) { - print('All files formatted correctly.'); - return false; - } - - print('These files are not formatted correctly (see diff below):'); - LineSplitter.split(stdout).map((String line) => ' $line').forEach(print); - - print('\nTo fix run "pub global activate flutter_plugin_tools && ' - 'pub global run flutter_plugin_tools format" or copy-paste ' - 'this command into your terminal:'); - - final io.ProcessResult diff = await processRunner.run( - 'git', - ['diff'], - workingDir: packagesDir, - logOnError: true, - ); - if (diff.exitCode != 0) { - printError('Unable to determine diff.'); - throw ToolExit(_exitGitFailed); - } - print('patch -p1 < _formatCppAndObjectiveC(Iterable files) async { - final Iterable clangFiles = _getPathsWithExtensions( - files, {'.h', '.m', '.mm', '.cc', '.cpp'}); - if (clangFiles.isNotEmpty) { - final String clangFormat = getStringArg('clang-format'); - if (!await _hasDependency(clangFormat)) { - printError('Unable to run "clang-format". Make sure that it is in your ' - 'path, or provide a full path with --clang-format.'); - throw ToolExit(_exitDependencyMissing); - } - - print('Formatting .cc, .cpp, .h, .m, and .mm files...'); - final int exitCode = await _runBatched( - getStringArg('clang-format'), ['-i', '--style=file'], - files: clangFiles); - if (exitCode != 0) { - printError( - 'Failed to format C, C++, and Objective-C files: exit code $exitCode.'); - throw ToolExit(_exitClangFormatFailed); - } - } - } - - Future _formatJava( - Iterable files, String googleFormatterPath) async { - final Iterable javaFiles = - _getPathsWithExtensions(files, {'.java'}); - if (javaFiles.isNotEmpty) { - final String java = getStringArg('java'); - if (!await _hasDependency(java)) { - printError( - 'Unable to run "java". Make sure that it is in your path, or ' - 'provide a full path with --java.'); - throw ToolExit(_exitDependencyMissing); - } - - print('Formatting .java files...'); - final int exitCode = await _runBatched( - java, ['-jar', googleFormatterPath, '--replace'], - files: javaFiles); - if (exitCode != 0) { - printError('Failed to format Java files: exit code $exitCode.'); - throw ToolExit(_exitJavaFormatFailed); - } - } - } - - Future _formatDart(Iterable files) async { - final Iterable dartFiles = - _getPathsWithExtensions(files, {'.dart'}); - if (dartFiles.isNotEmpty) { - print('Formatting .dart files...'); - // `flutter format` doesn't require the project to actually be a Flutter - // project. - final int exitCode = await _runBatched(flutterCommand, ['format'], - files: dartFiles); - if (exitCode != 0) { - printError('Failed to format Dart files: exit code $exitCode.'); - throw ToolExit(_exitFlutterFormatFailed); - } - } - } - - /// Given a stream of [files], returns the paths of any that are not in known - /// locations to ignore, relative to [relativeTo]. - Future> _getFilteredFilePaths( - Stream files, { - required Directory relativeTo, - }) async { - // Returns a pattern to check for [directories] as a subset of a file path. - RegExp pathFragmentForDirectories(List directories) { - String s = path.separator; - // Escape the separator for use in the regex. - if (s == r'\') { - s = r'\\'; - } - return RegExp('(?:^|$s)${path.joinAll(directories)}$s'); - } - - final String fromPath = relativeTo.path; - - // Dart files are allowed to have a pragma to disable auto-formatting. This - // was added because Hixie hurts when dealing with what dartfmt does to - // artisanally-formatted Dart, while Stuart gets really frustrated when - // dealing with PRs from newer contributors who don't know how to make Dart - // readable. After much discussion, it was decided that files in the plugins - // and packages repos that really benefit from hand-formatting (e.g. files - // with large blobs of hex literals) could be opted-out of the requirement - // that they be autoformatted, so long as the code's owner was willing to - // bear the cost of this during code reviews. - // In the event that code ownership moves to someone who does not hold the - // same views as the original owner, the pragma can be removed and the file - // auto-formatted. - const String handFormattedExtension = '.dart'; - const String handFormattedPragma = '// This file is hand-formatted.'; - - return files - .where((File file) { - // See comment above near [handFormattedPragma]. - return path.extension(file.path) != handFormattedExtension || - !file.readAsLinesSync().contains(handFormattedPragma); - }) - .map((File file) => path.relative(file.path, from: fromPath)) - .where((String path) => - // Ignore files in build/ directories (e.g., headers of frameworks) - // to avoid useless extra work in local repositories. - !path.contains( - pathFragmentForDirectories(['example', 'build'])) && - // Ignore files in Pods, which are not part of the repository. - !path.contains(pathFragmentForDirectories(['Pods'])) && - // Ignore .dart_tool/, which can have various intermediate files. - !path.contains(pathFragmentForDirectories(['.dart_tool']))) - .toList(); - } - - Iterable _getPathsWithExtensions( - Iterable files, Set extensions) { - return files.where( - (String filePath) => extensions.contains(path.extension(filePath))); - } - - Future _getGoogleFormatterPath() async { - final String javaFormatterPath = path.join( - path.dirname(path.fromUri(platform.script)), - 'google-java-format-1.3-all-deps.jar'); - final File javaFormatterFile = - packagesDir.fileSystem.file(javaFormatterPath); - - if (!javaFormatterFile.existsSync()) { - print('Downloading Google Java Format...'); - final http.Response response = await http.get(_googleFormatterUrl); - javaFormatterFile.writeAsBytesSync(response.bodyBytes); - } - - return javaFormatterPath; - } - - /// Returns true if [command] can be run successfully. - Future _hasDependency(String command) async { - // Some versions of Java accept both -version and --version, but some only - // accept -version. - final String versionFlag = command == 'java' ? '-version' : '--version'; - try { - final io.ProcessResult result = - await processRunner.run(command, [versionFlag]); - if (result.exitCode != 0) { - return false; - } - } on io.ProcessException { - // Thrown when the binary is missing entirely. - return false; - } - return true; - } - - /// Runs [command] on [arguments] on all of the files in [files], batched as - /// necessary to avoid OS command-line length limits. - /// - /// Returns the exit code of the first failure, which stops the run, or 0 - /// on success. - Future _runBatched( - String command, - List arguments, { - required Iterable files, - }) async { - final int commandLineMax = - platform.isWindows ? windowsCommandLineMax : nonWindowsCommandLineMax; - - // Compute the max length of the file argument portion of a batch. - // Add one to each argument's length for the space before it. - final int argumentTotalLength = - arguments.fold(0, (int sum, String arg) => sum + arg.length + 1); - final int batchMaxTotalLength = - commandLineMax - command.length - argumentTotalLength; - - // Run the command in batches. - final List> batches = - _partitionFileList(files, maxStringLength: batchMaxTotalLength); - for (final List batch in batches) { - batch.sort(); // For ease of testing. - final int exitCode = await processRunner.runAndStream( - command, [...arguments, ...batch], - workingDir: packagesDir); - if (exitCode != 0) { - return exitCode; - } - } - return 0; - } - - /// Partitions [files] into batches whose max string length as parameters to - /// a command (including the spaces between them, and between the list and - /// the command itself) is no longer than [maxStringLength]. - List> _partitionFileList(Iterable files, - {required int maxStringLength}) { - final List> batches = >[[]]; - int currentBatchTotalLength = 0; - for (final String file in files) { - final int length = file.length + 1 /* for the space */; - if (currentBatchTotalLength + length > maxStringLength) { - // Start a new batch. - batches.add([]); - currentBatchTotalLength = 0; - } - batches.last.add(file); - currentBatchTotalLength += length; - } - return batches; - } -} diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart deleted file mode 100644 index 0517bcf43298..000000000000 --- a/script/tool/lib/src/license_check_command.dart +++ /dev/null @@ -1,308 +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 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; - -import 'common/core.dart'; -import 'common/package_command.dart'; - -const Set _codeFileExtensions = { - '.c', - '.cc', - '.cpp', - '.dart', - '.h', - '.html', - '.java', - '.kt', - '.m', - '.mm', - '.swift', - '.sh', -}; - -// Basenames without extensions of files to ignore. -const Set _ignoreBasenameList = { - 'flutter_export_environment', - 'GeneratedPluginRegistrant', - 'generated_plugin_registrant', -}; - -// File suffixes that otherwise match _codeFileExtensions to ignore. -const Set _ignoreSuffixList = { - '.g.dart', // Generated API code. - '.mocks.dart', // Generated by Mockito. -}; - -// Full basenames of files to ignore. -const Set _ignoredFullBasenameList = { - 'resource.h', // Generated by VS. -}; - -// Copyright and license regexes for third-party code. -// -// These are intentionally very simple, since there is very little third-party -// code in this repository. Complexity can be added as-needed on a case-by-case -// basis. -// -// When adding license regexes here, include the copyright info to ensure that -// any new additions are flagged for added scrutiny in review. -final List _thirdPartyLicenseBlockRegexes = [ - // Third-party code used in url_launcher_web. - RegExp( - r'^// Copyright 2017 Workiva Inc\..*' - r'^// Licensed under the Apache License, Version 2\.0', - multiLine: true, - dotAll: true, - ), - // Third-party code used in google_maps_flutter_web. - RegExp( - r'^// The MIT License [^C]+ Copyright \(c\) 2008 Krasimir Tsonev', - multiLine: true, - ), - // bsdiff in flutter/packages. - RegExp( - r'// Copyright 2003-2005 Colin Percival\. All rights reserved\.\n' - r'// Use of this source code is governed by a BSD-style license that can be\n' - r'// found in the LICENSE file\.\n', - ), -]; - -// The exact format of the BSD license that our license files should contain. -// Slight variants are not accepted because they may prevent consolidation in -// tools that assemble all licenses used in distributed applications. -// standardized. -const String _fullBsdLicenseText = ''' -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. -'''; - -/// Validates that code files have copyright and license blocks. -class LicenseCheckCommand extends PackageCommand { - /// Creates a new license check command for [packagesDir]. - LicenseCheckCommand(Directory packagesDir, - {Platform platform = const LocalPlatform(), GitDir? gitDir}) - : super(packagesDir, platform: platform, gitDir: gitDir); - - @override - final String name = 'license-check'; - - @override - final String description = - 'Ensures that all code files have copyright/license blocks.'; - - @override - Future run() async { - // Create a set of absolute paths to submodule directories, with trailing - // separator, to do prefix matching with to test directory inclusion. - final Iterable submodulePaths = (await _getSubmoduleDirectories()) - .map( - (Directory dir) => '${dir.absolute.path}${platform.pathSeparator}'); - - final Iterable allFiles = (await _getAllFiles()).where( - (File file) => !submodulePaths.any(file.absolute.path.startsWith)); - - final Iterable codeFiles = allFiles.where((File file) => - _codeFileExtensions.contains(p.extension(file.path)) && - !_shouldIgnoreFile(file)); - final Iterable firstPartyLicenseFiles = allFiles.where((File file) => - path.basename(file.basename) == 'LICENSE' && !_isThirdParty(file)); - - final List licenseFileFailures = - await _checkLicenseFiles(firstPartyLicenseFiles); - final Map<_LicenseFailureType, List> codeFileFailures = - await _checkCodeLicenses(codeFiles); - - bool passed = true; - - print('\n=======================================\n'); - - if (licenseFileFailures.isNotEmpty) { - passed = false; - printError( - 'The following LICENSE files do not follow the expected format:'); - for (final File file in licenseFileFailures) { - printError(' ${file.path}'); - } - printError('Please ensure that they use the exact format used in this ' - 'repository".\n'); - } - - if (codeFileFailures[_LicenseFailureType.incorrectFirstParty]!.isNotEmpty) { - passed = false; - printError('The license block for these files is missing or incorrect:'); - for (final File file - in codeFileFailures[_LicenseFailureType.incorrectFirstParty]!) { - printError(' ${file.path}'); - } - printError( - 'If this third-party code, move it to a "third_party/" directory, ' - 'otherwise ensure that you are using the exact copyright and license ' - 'text used by all first-party files in this repository.\n'); - } - - if (codeFileFailures[_LicenseFailureType.unknownThirdParty]!.isNotEmpty) { - passed = false; - printError( - 'No recognized license was found for the following third-party files:'); - for (final File file - in codeFileFailures[_LicenseFailureType.unknownThirdParty]!) { - printError(' ${file.path}'); - } - print('Please check that they have a license at the top of the file. ' - 'If they do, the license check needs to be updated to recognize ' - 'the new third-party license block.\n'); - } - - if (!passed) { - throw ToolExit(1); - } - - printSuccess('All files passed validation!'); - } - - // Creates the expected copyright+license block for first-party code. - String _generateLicenseBlock( - String comment, { - String prefix = '', - String suffix = '', - }) { - return '$prefix${comment}Copyright 2013 The Flutter Authors. All rights reserved.\n' - '${comment}Use of this source code is governed by a BSD-style license that can be\n' - '${comment}found in the LICENSE file.$suffix\n'; - } - - /// Checks all license blocks for [codeFiles], returning any that fail - /// validation. - Future>> _checkCodeLicenses( - Iterable codeFiles) async { - final List incorrectFirstPartyFiles = []; - final List unrecognizedThirdPartyFiles = []; - - // Most code file types in the repository use '//' comments. - final String defaultFirstParyLicenseBlock = _generateLicenseBlock('// '); - // A few file types have a different comment structure. - final Map firstPartyLicenseBlockByExtension = - { - '.sh': _generateLicenseBlock('# '), - '.html': _generateLicenseBlock('', prefix: ''), - }; - - for (final File file in codeFiles) { - print('Checking ${file.path}'); - // On Windows, git may auto-convert line endings on checkout; this should - // still pass since they will be converted back on commit. - final String content = - (await file.readAsString()).replaceAll('\r\n', '\n'); - - final String firstParyLicense = - firstPartyLicenseBlockByExtension[p.extension(file.path)] ?? - defaultFirstParyLicenseBlock; - if (_isThirdParty(file)) { - // Third-party directories allow either known third-party licenses, our - // the first-party license, as there may be local additions. - if (!_thirdPartyLicenseBlockRegexes - .any((RegExp regex) => regex.hasMatch(content)) && - !content.contains(firstParyLicense)) { - unrecognizedThirdPartyFiles.add(file); - } - } else { - if (!content.contains(firstParyLicense)) { - incorrectFirstPartyFiles.add(file); - } - } - } - - // Sort by path for more usable output. - int pathCompare(File a, File b) => a.path.compareTo(b.path); - incorrectFirstPartyFiles.sort(pathCompare); - unrecognizedThirdPartyFiles.sort(pathCompare); - - return <_LicenseFailureType, List>{ - _LicenseFailureType.incorrectFirstParty: incorrectFirstPartyFiles, - _LicenseFailureType.unknownThirdParty: unrecognizedThirdPartyFiles, - }; - } - - /// Checks all provided LICENSE [files], returning any that fail validation. - Future> _checkLicenseFiles(Iterable files) async { - final List incorrectLicenseFiles = []; - - for (final File file in files) { - print('Checking ${file.path}'); - // On Windows, git may auto-convert line endings on checkout; this should - // still pass since they will be converted back on commit. - final String contents = file.readAsStringSync().replaceAll('\r\n', '\n'); - if (!contents.contains(_fullBsdLicenseText)) { - incorrectLicenseFiles.add(file); - } - } - - return incorrectLicenseFiles; - } - - bool _shouldIgnoreFile(File file) { - final String path = file.path; - return _ignoreBasenameList.contains(p.basenameWithoutExtension(path)) || - _ignoreSuffixList.any((String suffix) => - path.endsWith(suffix) || - _ignoredFullBasenameList.contains(p.basename(path))); - } - - bool _isThirdParty(File file) { - return path.split(file.path).contains('third_party'); - } - - Future> _getAllFiles() => packagesDir.parent - .list(recursive: true, followLinks: false) - .where((FileSystemEntity entity) => entity is File) - .map((FileSystemEntity file) => file as File) - .toList(); - - // Returns the directories containing mapped submodules, if any. - Future> _getSubmoduleDirectories() async { - final List submodulePaths = []; - final Directory repoRoot = - packagesDir.fileSystem.directory((await gitDir).path); - final File submoduleSpec = repoRoot.childFile('.gitmodules'); - if (submoduleSpec.existsSync()) { - final RegExp pathLine = RegExp(r'path\s*=\s*(.*)'); - for (final String line in submoduleSpec.readAsLinesSync()) { - final RegExpMatch? match = pathLine.firstMatch(line); - if (match != null) { - submodulePaths.add(repoRoot.childDirectory(match.group(1)!.trim())); - } - } - } - return submodulePaths; - } -} - -enum _LicenseFailureType { incorrectFirstParty, unknownThirdParty } diff --git a/script/tool/lib/src/lint_android_command.dart b/script/tool/lib/src/lint_android_command.dart deleted file mode 100644 index eb78ce891685..000000000000 --- a/script/tool/lib/src/lint_android_command.dart +++ /dev/null @@ -1,67 +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 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/core.dart'; -import 'common/gradle.dart'; -import 'common/package_looping_command.dart'; -import 'common/plugin_utils.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -/// Run 'gradlew lint'. -/// -/// See https://developer.android.com/studio/write/lint. -class LintAndroidCommand extends PackageLoopingCommand { - /// Creates an instance of the linter command. - LintAndroidCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform); - - @override - final String name = 'lint-android'; - - @override - final String description = 'Runs "gradlew lint" on Android plugins.\n\n' - 'Requires the examples to have been build at least once before running.'; - - @override - Future runForPackage(RepositoryPackage package) async { - if (!pluginSupportsPlatform(platformAndroid, package, - requiredMode: PlatformSupport.inline)) { - return PackageResult.skip( - 'Plugin does not have an Android implementation.'); - } - - bool failed = false; - for (final RepositoryPackage example in package.getExamples()) { - final GradleProject project = GradleProject(example, - processRunner: processRunner, platform: platform); - - if (!project.isConfigured()) { - return PackageResult.fail(['Build examples before linting']); - } - - final String packageName = package.directory.basename; - - // Only lint one build mode to avoid extra work. - // Only lint the plugin project itself, to avoid failing due to errors in - // dependencies. - // - // TODO(stuartmorgan): Consider adding an XML parser to read and summarize - // all results. Currently, only the first three errors will be shown - // inline, and the rest have to be checked via the CI-uploaded artifact. - final int exitCode = await project.runCommand('$packageName:lintDebug'); - if (exitCode != 0) { - failed = true; - } - } - - return failed ? PackageResult.fail() : PackageResult.success(); - } -} diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart deleted file mode 100644 index 198dd9472115..000000000000 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ /dev/null @@ -1,129 +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:convert'; -import 'dart:io'; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -const int _exitUnsupportedPlatform = 2; -const int _exitPodNotInstalled = 3; - -/// Lint the CocoaPod podspecs and run unit tests. -/// -/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. -class LintPodspecsCommand extends PackageLoopingCommand { - /// Creates an instance of the linter command. - LintPodspecsCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform); - - @override - final String name = 'podspecs'; - - @override - List get aliases => ['podspec']; - - @override - final String description = - 'Runs "pod lib lint" on all iOS and macOS plugin podspecs.\n\n' - 'This command requires "pod" and "flutter" to be in your path. Runs on macOS only.'; - - @override - Future initializeRun() async { - if (!platform.isMacOS) { - printError('This command is only supported on macOS'); - throw ToolExit(_exitUnsupportedPlatform); - } - - final ProcessResult result = await processRunner.run( - 'which', - ['pod'], - workingDir: packagesDir, - logOnError: true, - ); - if (result.exitCode != 0) { - printError('Unable to find "pod". Make sure it is in your path.'); - throw ToolExit(_exitPodNotInstalled); - } - } - - @override - Future runForPackage(RepositoryPackage package) async { - final List errors = []; - - final List podspecs = await _podspecsToLint(package); - if (podspecs.isEmpty) { - return PackageResult.skip('No podspecs.'); - } - - for (final File podspec in podspecs) { - if (!await _lintPodspec(podspec)) { - errors.add(p.basename(podspec.path)); - } - } - return errors.isEmpty - ? PackageResult.success() - : PackageResult.fail(errors); - } - - Future> _podspecsToLint(RepositoryPackage package) async { - final List podspecs = - await getFilesForPackage(package).where((File entity) { - final String filePath = entity.path; - return path.extension(filePath) == '.podspec'; - }).toList(); - - podspecs.sort((File a, File b) => a.basename.compareTo(b.basename)); - return podspecs; - } - - Future _lintPodspec(File podspec) async { - // Do not run the static analyzer on plugins with known analyzer issues. - final String podspecPath = podspec.path; - - final String podspecBasename = p.basename(podspecPath); - print('Linting $podspecBasename'); - - // Lint plugin as framework (use_frameworks!). - final ProcessResult frameworkResult = - await _runPodLint(podspecPath, libraryLint: true); - print(frameworkResult.stdout); - print(frameworkResult.stderr); - - // Lint plugin as library. - final ProcessResult libraryResult = - await _runPodLint(podspecPath, libraryLint: false); - print(libraryResult.stdout); - print(libraryResult.stderr); - - return frameworkResult.exitCode == 0 && libraryResult.exitCode == 0; - } - - Future _runPodLint(String podspecPath, - {required bool libraryLint}) async { - final List arguments = [ - 'lib', - 'lint', - podspecPath, - '--configuration=Debug', // Release targets unsupported arm64 simulators. Use Debug to only build against targeted x86_64 simulator devices. - '--skip-tests', - '--use-modular-headers', // Flutter sets use_modular_headers! in its templates. - if (libraryLint) '--use-libraries' - ]; - - print('Running "pod ${arguments.join(' ')}"'); - return processRunner.run('pod', arguments, - workingDir: packagesDir, stdoutEncoding: utf8, stderrEncoding: utf8); - } -} diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart deleted file mode 100644 index b47657e47eff..000000000000 --- a/script/tool/lib/src/list_command.dart +++ /dev/null @@ -1,68 +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 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/package_command.dart'; -import 'common/repository_package.dart'; - -/// A command to list different types of repository content. -class ListCommand extends PackageCommand { - /// Creates an instance of the list command, whose behavior depends on the - /// 'type' argument it provides. - ListCommand( - Directory packagesDir, { - Platform platform = const LocalPlatform(), - }) : super(packagesDir, platform: platform) { - argParser.addOption( - _type, - defaultsTo: _package, - allowed: [_package, _example, _allPackage, _file], - help: 'What type of file system content to list.', - ); - } - - static const String _type = 'type'; - static const String _allPackage = 'package-or-subpackage'; - static const String _example = 'example'; - static const String _package = 'package'; - static const String _file = 'file'; - - @override - final String name = 'list'; - - @override - final String description = 'Lists packages or files'; - - @override - Future run() async { - switch (getStringArg(_type)) { - case _package: - await for (final PackageEnumerationEntry entry in getTargetPackages()) { - print(entry.package.path); - } - break; - case _example: - final Stream examples = getTargetPackages() - .expand( - (PackageEnumerationEntry entry) => entry.package.getExamples()); - await for (final RepositoryPackage package in examples) { - print(package.path); - } - break; - case _allPackage: - await for (final PackageEnumerationEntry entry - in getTargetPackagesAndSubpackages()) { - print(entry.package.path); - } - break; - case _file: - await for (final File file in getFiles()) { - print(file.path); - } - break; - } - } -} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 2d48f079b306..6b421ebaebc0 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -9,34 +9,19 @@ import 'package:file/file.dart'; import 'package:file/local.dart'; import 'analyze_command.dart'; -import 'build_examples_command.dart'; import 'common/core.dart'; -import 'create_all_packages_app_command.dart'; -import 'custom_test_command.dart'; -import 'dependabot_check_command.dart'; -import 'drive_examples_command.dart'; -import 'federation_safety_check_command.dart'; -import 'firebase_test_lab_command.dart'; -import 'fix_command.dart'; -import 'format_command.dart'; -import 'license_check_command.dart'; -import 'lint_android_command.dart'; -import 'lint_podspecs_command.dart'; -import 'list_command.dart'; -import 'make_deps_path_based_command.dart'; -import 'native_test_command.dart'; -import 'publish_check_command.dart'; -import 'publish_command.dart'; -import 'pubspec_check_command.dart'; -import 'readme_check_command.dart'; -import 'remove_dev_dependencies.dart'; -import 'test_command.dart'; -import 'update_excerpts_command.dart'; -import 'update_release_info_command.dart'; -import 'version_check_command.dart'; -import 'xcode_analyze_command.dart'; void main(List args) { + print(''' +*** WARNING *** +This copy of the tooling is now only here as a shim for scripts in other +repositories that have not yet been updated, and can only run 'analyze'. For +full tooling in this repository, see the updated instructions: +https://github.com/flutter/packages/blob/main/script/tool/README.md +to switch to running the published version. + +'''); + const FileSystem fileSystem = LocalFileSystem(); Directory packagesDir = @@ -52,34 +37,9 @@ void main(List args) { } final CommandRunner commandRunner = CommandRunner( - 'pub global run flutter_plugin_tools', + 'dart pub global run flutter_plugin_tools', 'Productivity utils for hosting multiple plugins within one repository.') - ..addCommand(AnalyzeCommand(packagesDir)) - ..addCommand(BuildExamplesCommand(packagesDir)) - ..addCommand(CreateAllPackagesAppCommand(packagesDir)) - ..addCommand(CustomTestCommand(packagesDir)) - ..addCommand(DependabotCheckCommand(packagesDir)) - ..addCommand(DriveExamplesCommand(packagesDir)) - ..addCommand(FederationSafetyCheckCommand(packagesDir)) - ..addCommand(FirebaseTestLabCommand(packagesDir)) - ..addCommand(FixCommand(packagesDir)) - ..addCommand(FormatCommand(packagesDir)) - ..addCommand(LicenseCheckCommand(packagesDir)) - ..addCommand(LintAndroidCommand(packagesDir)) - ..addCommand(LintPodspecsCommand(packagesDir)) - ..addCommand(ListCommand(packagesDir)) - ..addCommand(NativeTestCommand(packagesDir)) - ..addCommand(MakeDepsPathBasedCommand(packagesDir)) - ..addCommand(PublishCheckCommand(packagesDir)) - ..addCommand(PublishCommand(packagesDir)) - ..addCommand(PubspecCheckCommand(packagesDir)) - ..addCommand(ReadmeCheckCommand(packagesDir)) - ..addCommand(RemoveDevDependenciesCommand(packagesDir)) - ..addCommand(TestCommand(packagesDir)) - ..addCommand(UpdateExcerptsCommand(packagesDir)) - ..addCommand(UpdateReleaseInfoCommand(packagesDir)) - ..addCommand(VersionCheckCommand(packagesDir)) - ..addCommand(XcodeAnalyzeCommand(packagesDir)); + ..addCommand(AnalyzeCommand(packagesDir)); commandRunner.run(args).catchError((Object e) { final ToolExit toolExit = e as ToolExit; diff --git a/script/tool/lib/src/make_deps_path_based_command.dart b/script/tool/lib/src/make_deps_path_based_command.dart deleted file mode 100644 index 10abcd44ae6e..000000000000 --- a/script/tool/lib/src/make_deps_path_based_command.dart +++ /dev/null @@ -1,283 +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 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:path/path.dart' as p; -import 'package:pub_semver/pub_semver.dart'; - -import 'common/core.dart'; -import 'common/git_version_finder.dart'; -import 'common/package_command.dart'; -import 'common/repository_package.dart'; - -const int _exitPackageNotFound = 3; -const int _exitCannotUpdatePubspec = 4; - -enum _RewriteOutcome { changed, noChangesNeeded, alreadyChanged } - -/// Converts all dependencies on target packages to path-based dependencies. -/// -/// This is to allow for pre-publish testing of changes that could affect other -/// packages in the repository. For instance, this allows for catching cases -/// where a non-breaking change to a platform interface package of a federated -/// plugin would cause post-publish analyzer failures in another package of that -/// plugin. -class MakeDepsPathBasedCommand extends PackageCommand { - /// Creates an instance of the command to convert selected dependencies to - /// path-based. - MakeDepsPathBasedCommand( - Directory packagesDir, { - GitDir? gitDir, - }) : super(packagesDir, gitDir: gitDir) { - argParser.addMultiOption(_targetDependenciesArg, - help: - 'The names of the packages to convert to path-based dependencies.\n' - 'Ignored if --$_targetDependenciesWithNonBreakingUpdatesArg is ' - 'passed.', - valueHelp: 'some_package'); - argParser.addFlag( - _targetDependenciesWithNonBreakingUpdatesArg, - help: 'Causes all packages that have non-breaking version changes ' - 'when compared against the git base to be treated as target ' - 'packages.', - ); - } - - static const String _targetDependenciesArg = 'target-dependencies'; - static const String _targetDependenciesWithNonBreakingUpdatesArg = - 'target-dependencies-with-non-breaking-updates'; - - // The comment to add to temporary dependency overrides. - static const String _dependencyOverrideWarningComment = - '# FOR TESTING ONLY. DO NOT MERGE.'; - - @override - final String name = 'make-deps-path-based'; - - @override - final String description = - 'Converts package dependencies to path-based references.'; - - @override - Future run() async { - final Set targetDependencies = - getBoolArg(_targetDependenciesWithNonBreakingUpdatesArg) - ? await _getNonBreakingUpdatePackages() - : getStringListArg(_targetDependenciesArg).toSet(); - - if (targetDependencies.isEmpty) { - print('No target dependencies; nothing to do.'); - return; - } - print('Rewriting references to: ${targetDependencies.join(', ')}...'); - - final Map localDependencyPackages = - _findLocalPackages(targetDependencies); - - final String repoRootPath = (await gitDir).path; - for (final File pubspec in await _getAllPubspecs()) { - final String displayPath = p.posix.joinAll( - path.split(path.relative(pubspec.absolute.path, from: repoRootPath))); - final _RewriteOutcome outcome = await _addDependencyOverridesIfNecessary( - pubspec, localDependencyPackages); - switch (outcome) { - case _RewriteOutcome.changed: - print(' Modified $displayPath'); - break; - case _RewriteOutcome.alreadyChanged: - print(' Skipped $displayPath - Already rewritten'); - break; - case _RewriteOutcome.noChangesNeeded: - break; - } - } - } - - Map _findLocalPackages(Set packageNames) { - final Map targets = - {}; - for (final String packageName in packageNames) { - final Directory topLevelCandidate = - packagesDir.childDirectory(packageName); - // If packages// exists, then either that directory is the - // package, or packages/// exists and is the - // package (in the case of a federated plugin). - if (topLevelCandidate.existsSync()) { - final Directory appFacingCandidate = - topLevelCandidate.childDirectory(packageName); - targets[packageName] = RepositoryPackage(appFacingCandidate.existsSync() - ? appFacingCandidate - : topLevelCandidate); - continue; - } - // If there is no packages/ directory, then either the - // packages doesn't exist, or it is a sub-package of a federated plugin. - // If it's the latter, it will be a directory whose name is a prefix. - for (final FileSystemEntity entity in packagesDir.listSync()) { - if (entity is Directory && packageName.startsWith(entity.basename)) { - final Directory subPackageCandidate = - entity.childDirectory(packageName); - if (subPackageCandidate.existsSync()) { - targets[packageName] = RepositoryPackage(subPackageCandidate); - break; - } - } - } - - if (!targets.containsKey(packageName)) { - printError('Unable to find package "$packageName"'); - throw ToolExit(_exitPackageNotFound); - } - } - return targets; - } - - /// If [pubspecFile] has any dependencies on packages in [localDependencies], - /// adds dependency_overrides entries to redirect them to the local version - /// using path-based dependencies. - Future<_RewriteOutcome> _addDependencyOverridesIfNecessary(File pubspecFile, - Map localDependencies) async { - final String pubspecContents = pubspecFile.readAsStringSync(); - final Pubspec pubspec = Pubspec.parse(pubspecContents); - // Fail if there are any dependency overrides already, other than ones - // created by this script. If support for that is needed at some point, it - // can be added, but currently it's not and relying on that makes the logic - // here much simpler. - if (pubspec.dependencyOverrides.isNotEmpty) { - if (pubspecContents.contains(_dependencyOverrideWarningComment)) { - return _RewriteOutcome.alreadyChanged; - } - printError( - 'Packages with dependency overrides are not currently supported.'); - throw ToolExit(_exitCannotUpdatePubspec); - } - - final Iterable combinedDependencies = [ - ...pubspec.dependencies.keys, - ...pubspec.devDependencies.keys, - ]; - final List packagesToOverride = combinedDependencies - .where( - (String packageName) => localDependencies.containsKey(packageName)) - .toList(); - // Sort the combined list to avoid sort_pub_dependencies lint violations. - packagesToOverride.sort(); - if (packagesToOverride.isNotEmpty) { - final String commonBasePath = packagesDir.path; - // Find the relative path to the common base. - final int packageDepth = path - .split(path.relative(pubspecFile.parent.absolute.path, - from: commonBasePath)) - .length; - final List relativeBasePathComponents = - List.filled(packageDepth, '..'); - // This is done via strings rather than by manipulating the Pubspec and - // then re-serialiazing so that it's a localized change, rather than - // rewriting the whole file (e.g., destroying comments), which could be - // more disruptive for local use. - String newPubspecContents = ''' -$pubspecContents - -$_dependencyOverrideWarningComment -dependency_overrides: -'''; - for (final String packageName in packagesToOverride) { - // Find the relative path from the common base to the local package. - final List repoRelativePathComponents = path.split( - path.relative(localDependencies[packageName]!.path, - from: commonBasePath)); - newPubspecContents += ''' - $packageName: - path: ${p.posix.joinAll([ - ...relativeBasePathComponents, - ...repoRelativePathComponents, - ])} -'''; - } - pubspecFile.writeAsStringSync(newPubspecContents); - return _RewriteOutcome.changed; - } - return _RewriteOutcome.noChangesNeeded; - } - - /// Returns all pubspecs anywhere under the packages directory. - Future> _getAllPubspecs() => packagesDir.parent - .list(recursive: true, followLinks: false) - .where((FileSystemEntity entity) => - entity is File && p.basename(entity.path) == 'pubspec.yaml') - .map((FileSystemEntity file) => file as File) - .toList(); - - /// Returns all packages that have non-breaking published changes (i.e., a - /// minor or bugfix version change) relative to the git comparison base. - /// - /// Prints status information about what was checked for ease of auditing logs - /// in CI. - Future> _getNonBreakingUpdatePackages() async { - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - final String baseSha = await gitVersionFinder.getBaseSha(); - print('Finding changed packages relative to "$baseSha"...'); - - final Set changedPackages = {}; - for (final String changedPath in await gitVersionFinder.getChangedFiles()) { - // Git output always uses Posix paths. - final List allComponents = p.posix.split(changedPath); - // Only pubspec changes are potential publishing events. - if (allComponents.last != 'pubspec.yaml' || - allComponents.contains('example')) { - continue; - } - if (!allComponents.contains(packagesDir.basename)) { - print(' Skipping $changedPath; not in packages directory.'); - continue; - } - final RepositoryPackage package = - RepositoryPackage(packagesDir.fileSystem.file(changedPath).parent); - // Ignored deleted packages, as they won't be published. - if (!package.pubspecFile.existsSync()) { - final String directoryName = p.posix.joinAll(path.split(path.relative( - package.directory.absolute.path, - from: packagesDir.path))); - print(' Skipping $directoryName; deleted.'); - continue; - } - final String packageName = package.parsePubspec().name; - if (!await _hasNonBreakingVersionChange(package)) { - // Log packages that had pubspec changes but weren't included for ease - // of auditing CI. - print(' Skipping $packageName; no non-breaking version change.'); - continue; - } - changedPackages.add(packageName); - } - return changedPackages; - } - - Future _hasNonBreakingVersionChange(RepositoryPackage package) async { - final Pubspec pubspec = package.parsePubspec(); - if (pubspec.publishTo == 'none') { - return false; - } - - final String pubspecGitPath = p.posix.joinAll(path.split(path.relative( - package.pubspecFile.absolute.path, - from: (await gitDir).path))); - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - final Version? previousVersion = - await gitVersionFinder.getPackageVersion(pubspecGitPath); - if (previousVersion == null) { - // The plugin is new, so nothing can be depending on it yet. - return false; - } - final Version newVersion = pubspec.version!; - if ((newVersion.major > 0 && newVersion.major != previousVersion.major) || - (newVersion.major == 0 && newVersion.minor != previousVersion.minor)) { - // Breaking changes aren't targetted since they won't be picked up - // automatically. - return false; - } - return newVersion != previousVersion; - } -} diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart deleted file mode 100644 index af5f4df98e86..000000000000 --- a/script/tool/lib/src/native_test_command.dart +++ /dev/null @@ -1,624 +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 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/cmake.dart'; -import 'common/core.dart'; -import 'common/gradle.dart'; -import 'common/package_looping_command.dart'; -import 'common/plugin_utils.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; -import 'common/xcode.dart'; - -const String _unitTestFlag = 'unit'; -const String _integrationTestFlag = 'integration'; - -const String _iOSDestinationFlag = 'ios-destination'; - -const int _exitNoIOSSimulators = 3; - -/// The command to run native tests for plugins: -/// - iOS and macOS: XCTests (XCUnitTest and XCUITest) -/// - Android: JUnit tests -/// - Windows and Linux: GoogleTest tests -class NativeTestCommand extends PackageLoopingCommand { - /// Creates an instance of the test command. - NativeTestCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : _xcode = Xcode(processRunner: processRunner, log: true), - super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addOption( - _iOSDestinationFlag, - help: 'Specify the destination when running iOS tests.\n' - 'This is passed to the `-destination` argument in the xcodebuild command.\n' - 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT ' - 'for details on how to specify the destination.', - ); - argParser.addFlag(platformAndroid, help: 'Runs Android tests'); - argParser.addFlag(platformIOS, help: 'Runs iOS tests'); - argParser.addFlag(platformLinux, help: 'Runs Linux tests'); - argParser.addFlag(platformMacOS, help: 'Runs macOS tests'); - argParser.addFlag(platformWindows, help: 'Runs Windows tests'); - - // By default, both unit tests and integration tests are run, but provide - // flags to disable one or the other. - argParser.addFlag(_unitTestFlag, - help: 'Runs native unit tests', defaultsTo: true); - argParser.addFlag(_integrationTestFlag, - help: 'Runs native integration (UI) tests', defaultsTo: true); - } - - // The device destination flags for iOS tests. - List _iOSDestinationFlags = []; - - final Xcode _xcode; - - @override - final String name = 'native-test'; - - @override - final String description = ''' -Runs native unit tests and native integration tests. - -Currently supported platforms: -- Android -- iOS: requires 'xcrun' to be in your path. -- Linux (unit tests only) -- macOS: requires 'xcrun' to be in your path. -- Windows (unit tests only) - -The example app(s) must be built for all targeted platforms before running -this command. -'''; - - Map _platforms = {}; - - List _requestedPlatforms = []; - - @override - Future initializeRun() async { - _platforms = { - platformAndroid: _PlatformDetails('Android', _testAndroid), - platformIOS: _PlatformDetails('iOS', _testIOS), - platformLinux: _PlatformDetails('Linux', _testLinux), - platformMacOS: _PlatformDetails('macOS', _testMacOS), - platformWindows: _PlatformDetails('Windows', _testWindows), - }; - _requestedPlatforms = _platforms.keys - .where((String platform) => getBoolArg(platform)) - .toList(); - _requestedPlatforms.sort(); - - if (_requestedPlatforms.isEmpty) { - printError('At least one platform flag must be provided.'); - throw ToolExit(exitInvalidArguments); - } - - if (!(getBoolArg(_unitTestFlag) || getBoolArg(_integrationTestFlag))) { - printError('At least one test type must be enabled.'); - throw ToolExit(exitInvalidArguments); - } - - if (getBoolArg(platformWindows) && getBoolArg(_integrationTestFlag)) { - logWarning('This command currently only supports unit tests for Windows. ' - 'See https://github.com/flutter/flutter/issues/70233.'); - } - - if (getBoolArg(platformLinux) && getBoolArg(_integrationTestFlag)) { - logWarning('This command currently only supports unit tests for Linux. ' - 'See https://github.com/flutter/flutter/issues/70235.'); - } - - // iOS-specific run-level state. - if (_requestedPlatforms.contains('ios')) { - String destination = getStringArg(_iOSDestinationFlag); - if (destination.isEmpty) { - final String? simulatorId = - await _xcode.findBestAvailableIphoneSimulator(); - if (simulatorId == null) { - printError('Cannot find any available iOS simulators.'); - throw ToolExit(_exitNoIOSSimulators); - } - destination = 'id=$simulatorId'; - } - _iOSDestinationFlags = [ - '-destination', - destination, - ]; - } - } - - @override - Future runForPackage(RepositoryPackage package) async { - final List testPlatforms = []; - for (final String platform in _requestedPlatforms) { - if (!pluginSupportsPlatform(platform, package, - requiredMode: PlatformSupport.inline)) { - print('No implementation for ${_platforms[platform]!.label}.'); - continue; - } - if (!pluginHasNativeCodeForPlatform(platform, package)) { - print('No native code for ${_platforms[platform]!.label}.'); - continue; - } - testPlatforms.add(platform); - } - - if (testPlatforms.isEmpty) { - return PackageResult.skip('Nothing to test for target platform(s).'); - } - - final _TestMode mode = _TestMode( - unit: getBoolArg(_unitTestFlag), - integration: getBoolArg(_integrationTestFlag), - ); - - bool ranTests = false; - bool failed = false; - final List failureMessages = []; - for (final String platform in testPlatforms) { - final _PlatformDetails platformInfo = _platforms[platform]!; - print('Running tests for ${platformInfo.label}...'); - print('----------------------------------------'); - final _PlatformResult result = - await platformInfo.testFunction(package, mode); - ranTests |= result.state != RunState.skipped; - if (result.state == RunState.failed) { - failed = true; - - final String? error = result.error; - // Only provide the failing platforms in the failure details if testing - // multiple platforms, otherwise it's just noise. - if (_requestedPlatforms.length > 1) { - failureMessages.add(error != null - ? '${platformInfo.label}: $error' - : platformInfo.label); - } else if (error != null) { - // If there's only one platform, only provide error details in the - // summary if the platform returned a message. - failureMessages.add(error); - } - } - } - - if (!ranTests) { - return PackageResult.skip('No tests found.'); - } - return failed - ? PackageResult.fail(failureMessages) - : PackageResult.success(); - } - - Future<_PlatformResult> _testAndroid( - RepositoryPackage plugin, _TestMode mode) async { - bool exampleHasUnitTests(RepositoryPackage example) { - return example - .platformDirectory(FlutterPlatform.android) - .childDirectory('app') - .childDirectory('src') - .childDirectory('test') - .existsSync() || - plugin - .platformDirectory(FlutterPlatform.android) - .childDirectory('src') - .childDirectory('test') - .existsSync(); - } - - bool exampleHasNativeIntegrationTests(RepositoryPackage example) { - final Directory integrationTestDirectory = example - .platformDirectory(FlutterPlatform.android) - .childDirectory('app') - .childDirectory('src') - .childDirectory('androidTest'); - // There are two types of integration tests that can be in the androidTest - // directory: - // - FlutterTestRunner.class tests, which bridge to Dart integration tests - // - Purely native tests - // Only the latter is supported by this command; the former will hang if - // run here because they will wait for a Dart call that will never come. - // - // This repository uses a convention of putting the former in a - // *ActivityTest.java file, so ignore that file when checking for tests. - // Also ignore DartIntegrationTest.java, which defines the annotation used - // below for filtering the former out when running tests. - // - // If those are the only files, then there are no tests to run here. - return integrationTestDirectory.existsSync() && - integrationTestDirectory - .listSync(recursive: true) - .whereType() - .any((File file) { - final String basename = file.basename; - return !basename.endsWith('ActivityTest.java') && - basename != 'DartIntegrationTest.java'; - }); - } - - final Iterable examples = plugin.getExamples(); - - bool ranUnitTests = false; - bool ranAnyTests = false; - bool failed = false; - bool hasMissingBuild = false; - for (final RepositoryPackage example in examples) { - final bool hasUnitTests = exampleHasUnitTests(example); - final bool hasIntegrationTests = - exampleHasNativeIntegrationTests(example); - - if (mode.unit && !hasUnitTests) { - _printNoExampleTestsMessage(example, 'Android unit'); - } - if (mode.integration && !hasIntegrationTests) { - _printNoExampleTestsMessage(example, 'Android integration'); - } - - final bool runUnitTests = mode.unit && hasUnitTests; - final bool runIntegrationTests = mode.integration && hasIntegrationTests; - if (!runUnitTests && !runIntegrationTests) { - continue; - } - - final String exampleName = example.displayName; - _printRunningExampleTestsMessage(example, 'Android'); - - final GradleProject project = GradleProject( - example, - processRunner: processRunner, - platform: platform, - ); - if (!project.isConfigured()) { - printError('ERROR: Run "flutter build apk" on $exampleName, or run ' - 'this tool\'s "build-examples --apk" command, ' - 'before executing tests.'); - failed = true; - hasMissingBuild = true; - continue; - } - - if (runUnitTests) { - print('Running unit tests...'); - final int exitCode = await project.runCommand('testDebugUnitTest'); - if (exitCode != 0) { - printError('$exampleName unit tests failed.'); - failed = true; - } - ranUnitTests = true; - ranAnyTests = true; - } - - if (runIntegrationTests) { - // FlutterTestRunner-based tests will hang forever if run in a normal - // app build, since they wait for a Dart call from integration_test that - // will never come. Those tests have an extra annotation to allow - // filtering them out. - const String filter = - 'notAnnotation=io.flutter.plugins.DartIntegrationTest'; - - print('Running integration tests...'); - final int exitCode = await project.runCommand( - 'app:connectedAndroidTest', - arguments: [ - '-Pandroid.testInstrumentationRunnerArguments.$filter', - ], - ); - if (exitCode != 0) { - printError('$exampleName integration tests failed.'); - failed = true; - } - ranAnyTests = true; - } - } - - if (failed) { - return _PlatformResult(RunState.failed, - error: hasMissingBuild - ? 'Examples must be built before testing.' - : null); - } - if (!mode.integrationOnly && !ranUnitTests) { - printError('No unit tests ran. Plugins are required to have unit tests.'); - return _PlatformResult(RunState.failed, - error: 'No unit tests ran (use --exclude if this is intentional).'); - } - if (!ranAnyTests) { - return _PlatformResult(RunState.skipped); - } - return _PlatformResult(RunState.succeeded); - } - - Future<_PlatformResult> _testIOS(RepositoryPackage plugin, _TestMode mode) { - return _runXcodeTests(plugin, 'iOS', mode, - extraFlags: _iOSDestinationFlags); - } - - Future<_PlatformResult> _testMacOS(RepositoryPackage plugin, _TestMode mode) { - return _runXcodeTests(plugin, 'macOS', mode); - } - - /// Runs all applicable tests for [plugin], printing status and returning - /// the test result. - /// - /// The tests targets must be added to the Xcode project of the example app, - /// usually at "example/{ios,macos}/Runner.xcworkspace". - Future<_PlatformResult> _runXcodeTests( - RepositoryPackage plugin, - String platform, - _TestMode mode, { - List extraFlags = const [], - }) async { - String? testTarget; - const String unitTestTarget = 'RunnerTests'; - if (mode.unitOnly) { - testTarget = unitTestTarget; - } else if (mode.integrationOnly) { - testTarget = 'RunnerUITests'; - } - - bool ranUnitTests = false; - // Assume skipped until at least one test has run. - RunState overallResult = RunState.skipped; - for (final RepositoryPackage example in plugin.getExamples()) { - final String exampleName = example.displayName; - - // If running a specific target, check that. Otherwise, check if there - // are unit tests, since having no unit tests for a plugin is fatal - // (by repo policy) even if there are integration tests. - bool exampleHasUnitTests = false; - final String? targetToCheck = - testTarget ?? (mode.unit ? unitTestTarget : null); - final Directory xcodeProject = example.directory - .childDirectory(platform.toLowerCase()) - .childDirectory('Runner.xcodeproj'); - if (targetToCheck != null) { - final bool? hasTarget = - await _xcode.projectHasTarget(xcodeProject, targetToCheck); - if (hasTarget == null) { - printError('Unable to check targets for $exampleName.'); - overallResult = RunState.failed; - continue; - } else if (!hasTarget) { - print('No "$targetToCheck" target in $exampleName; skipping.'); - continue; - } else if (targetToCheck == unitTestTarget) { - exampleHasUnitTests = true; - } - } - - _printRunningExampleTestsMessage(example, platform); - final int exitCode = await _xcode.runXcodeBuild( - example.directory, - actions: ['test'], - workspace: '${platform.toLowerCase()}/Runner.xcworkspace', - scheme: 'Runner', - configuration: 'Debug', - extraFlags: [ - if (testTarget != null) '-only-testing:$testTarget', - ...extraFlags, - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - ); - - // The exit code from 'xcodebuild test' when there are no tests. - const int xcodebuildNoTestExitCode = 66; - switch (exitCode) { - case xcodebuildNoTestExitCode: - _printNoExampleTestsMessage(example, platform); - break; - case 0: - printSuccess('Successfully ran $platform xctest for $exampleName'); - // If this is the first test, assume success until something fails. - if (overallResult == RunState.skipped) { - overallResult = RunState.succeeded; - } - if (exampleHasUnitTests) { - ranUnitTests = true; - } - break; - default: - // Any failure means a failure overall. - overallResult = RunState.failed; - // If unit tests ran, note that even if they failed. - if (exampleHasUnitTests) { - ranUnitTests = true; - } - break; - } - } - - if (!mode.integrationOnly && !ranUnitTests) { - printError('No unit tests ran. Plugins are required to have unit tests.'); - // Only return a specific summary error message about the missing unit - // tests if there weren't also failures, to avoid having a misleadingly - // specific message. - if (overallResult != RunState.failed) { - return _PlatformResult(RunState.failed, - error: 'No unit tests ran (use --exclude if this is intentional).'); - } - } - - return _PlatformResult(overallResult); - } - - Future<_PlatformResult> _testWindows( - RepositoryPackage plugin, _TestMode mode) async { - if (mode.integrationOnly) { - return _PlatformResult(RunState.skipped); - } - - bool isTestBinary(File file) { - return file.basename.endsWith('_test.exe') || - file.basename.endsWith('_tests.exe'); - } - - return _runGoogleTestTests(plugin, 'Windows', 'Debug', - isTestBinary: isTestBinary); - } - - Future<_PlatformResult> _testLinux( - RepositoryPackage plugin, _TestMode mode) async { - if (mode.integrationOnly) { - return _PlatformResult(RunState.skipped); - } - - bool isTestBinary(File file) { - return file.basename.endsWith('_test') || - file.basename.endsWith('_tests'); - } - - // Since Linux uses a single-config generator, building-examples only - // generates the build files for release, so the tests have to be run in - // release mode as well. - // - // TODO(stuartmorgan): Consider adding a command to `flutter` that would - // generate build files without doing a build, and using that instead of - // relying on running build-examples. See - // https://github.com/flutter/flutter/issues/93407. - return _runGoogleTestTests(plugin, 'Linux', 'Release', - isTestBinary: isTestBinary); - } - - /// Finds every file in the [buildDirectoryName] subdirectory of [plugin]'s - /// build directory for which [isTestBinary] is true, and runs all of them, - /// returning the overall result. - /// - /// The binaries are assumed to be Google Test test binaries, thus returning - /// zero for success and non-zero for failure. - Future<_PlatformResult> _runGoogleTestTests( - RepositoryPackage plugin, - String platformName, - String buildMode, { - required bool Function(File) isTestBinary, - }) async { - final List testBinaries = []; - bool hasMissingBuild = false; - bool buildFailed = false; - for (final RepositoryPackage example in plugin.getExamples()) { - final CMakeProject project = CMakeProject(example.directory, - buildMode: buildMode, - processRunner: processRunner, - platform: platform); - if (!project.isConfigured()) { - printError('ERROR: Run "flutter build" on ${example.displayName}, ' - 'or run this tool\'s "build-examples" command, for the target ' - 'platform before executing tests.'); - hasMissingBuild = true; - continue; - } - - // By repository convention, example projects create an aggregate target - // called 'unit_tests' that builds all unit tests (usually just an alias - // for a specific test target). - final int exitCode = await project.runBuild('unit_tests'); - if (exitCode != 0) { - printError('${example.displayName} unit tests failed to build.'); - buildFailed = true; - } - - testBinaries.addAll(project.buildDirectory - .listSync(recursive: true) - .whereType() - .where(isTestBinary) - .where((File file) { - // Only run the `buildMode` build of the unit tests, to avoid running - // the same tests multiple times. - final List components = path.split(file.path); - return components.contains(buildMode) || - components.contains(buildMode.toLowerCase()); - })); - } - - if (hasMissingBuild) { - return _PlatformResult(RunState.failed, - error: 'Examples must be built before testing.'); - } - - if (buildFailed) { - return _PlatformResult(RunState.failed, - error: 'Failed to build $platformName unit tests.'); - } - - if (testBinaries.isEmpty) { - final String binaryExtension = platform.isWindows ? '.exe' : ''; - printError( - 'No test binaries found. At least one *_test(s)$binaryExtension ' - 'binary should be built by the example(s)'); - return _PlatformResult(RunState.failed, - error: 'No $platformName unit tests found'); - } - - bool passing = true; - for (final File test in testBinaries) { - print('Running ${test.basename}...'); - final int exitCode = - await processRunner.runAndStream(test.path, []); - passing &= exitCode == 0; - } - return _PlatformResult(passing ? RunState.succeeded : RunState.failed); - } - - /// Prints a standard format message indicating that [platform] tests for - /// [plugin]'s [example] are about to be run. - void _printRunningExampleTestsMessage( - RepositoryPackage example, String platform) { - print('Running $platform tests for ${example.displayName}...'); - } - - /// Prints a standard format message indicating that no tests were found for - /// [plugin]'s [example] for [platform]. - void _printNoExampleTestsMessage(RepositoryPackage example, String platform) { - print('No $platform tests found for ${example.displayName}'); - } -} - -// The type for a function that takes a plugin directory and runs its native -// tests for a specific platform. -typedef _TestFunction = Future<_PlatformResult> Function( - RepositoryPackage, _TestMode); - -/// A collection of information related to a specific platform. -class _PlatformDetails { - const _PlatformDetails( - this.label, - this.testFunction, - ); - - /// The name to use in output. - final String label; - - /// The function to call to run tests. - final _TestFunction testFunction; -} - -/// Enabled state for different test types. -class _TestMode { - const _TestMode({required this.unit, required this.integration}); - - final bool unit; - final bool integration; - - bool get integrationOnly => integration && !unit; - bool get unitOnly => unit && !integration; -} - -/// The result of running a single platform's tests. -class _PlatformResult { - _PlatformResult(this.state, {this.error}); - - /// The overall state of the platform's tests. This should be: - /// - failed if any tests failed. - /// - succeeded if at least one test ran, and all tests passed. - /// - skipped if no tests ran. - final RunState state; - - /// An optional error string to include in the summary for this platform. - /// - /// Ignored unless [state] is `failed`. - final String? error; -} diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart deleted file mode 100644 index 14b240dc04c2..000000000000 --- a/script/tool/lib/src/publish_check_command.dart +++ /dev/null @@ -1,289 +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:async'; -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:http/http.dart' as http; -import 'package:platform/platform.dart'; -import 'package:pub_semver/pub_semver.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/process_runner.dart'; -import 'common/pub_version_finder.dart'; -import 'common/repository_package.dart'; - -/// A command to check that packages are publishable via 'dart publish'. -class PublishCheckCommand extends PackageLoopingCommand { - /// Creates an instance of the publish command. - PublishCheckCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - http.Client? httpClient, - }) : _pubVersionFinder = - PubVersionFinder(httpClient: httpClient ?? http.Client()), - super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addFlag( - _allowPrereleaseFlag, - help: 'Allows the pre-release SDK warning to pass.\n' - 'When enabled, a pub warning, which asks to publish the package as a pre-release version when ' - 'the SDK constraint is a pre-release version, is ignored.', - ); - argParser.addFlag(_machineFlag, - help: 'Switch outputs to a machine readable JSON. \n' - 'The JSON contains a "status" field indicating the final status of the command, the possible values are:\n' - ' $_statusNeedsPublish: There is at least one package need to be published. They also passed all publish checks.\n' - ' $_statusMessageNoPublish: There are no packages needs to be published. Either no pubspec change detected or all versions have already been published.\n' - ' $_statusMessageError: Some error has occurred.'); - } - - static const String _allowPrereleaseFlag = 'allow-pre-release'; - static const String _machineFlag = 'machine'; - static const String _statusNeedsPublish = 'needs-publish'; - static const String _statusMessageNoPublish = 'no-publish'; - static const String _statusMessageError = 'error'; - static const String _statusKey = 'status'; - static const String _humanMessageKey = 'humanMessage'; - - @override - final String name = 'publish-check'; - - @override - final String description = - 'Checks to make sure that a package *could* be published.'; - - final PubVersionFinder _pubVersionFinder; - - /// The overall result of the run for machine-readable output. This is the - /// highest value that occurs during the run. - _PublishCheckResult _overallResult = _PublishCheckResult.nothingToPublish; - - @override - bool get captureOutput => getBoolArg(_machineFlag); - - @override - Future initializeRun() async { - _overallResult = _PublishCheckResult.nothingToPublish; - } - - @override - Future runForPackage(RepositoryPackage package) async { - _PublishCheckResult? result = await _passesPublishCheck(package); - if (result == null) { - return PackageResult.skip('Package is marked as unpublishable.'); - } - if (!_passesAuthorsCheck(package)) { - _printImportantStatusMessage( - 'No AUTHORS file found. Packages must include an AUTHORS file.', - isError: true); - result = _PublishCheckResult.error; - } - - if (result.index > _overallResult.index) { - _overallResult = result; - } - return result == _PublishCheckResult.error - ? PackageResult.fail() - : PackageResult.success(); - } - - @override - Future completeRun() async { - _pubVersionFinder.httpClient.close(); - } - - @override - Future handleCapturedOutput(List output) async { - final Map machineOutput = { - _statusKey: _statusStringForResult(_overallResult), - _humanMessageKey: output, - }; - - print(const JsonEncoder.withIndent(' ').convert(machineOutput)); - } - - String _statusStringForResult(_PublishCheckResult result) { - switch (result) { - case _PublishCheckResult.nothingToPublish: - return _statusMessageNoPublish; - case _PublishCheckResult.needsPublishing: - return _statusNeedsPublish; - case _PublishCheckResult.error: - return _statusMessageError; - } - } - - Pubspec? _tryParsePubspec(RepositoryPackage package) { - try { - return package.parsePubspec(); - } on Exception catch (exception) { - print( - 'Failed to parse `pubspec.yaml` at ${package.pubspecFile.path}: ' - '$exception', - ); - return null; - } - } - - // Run `dart pub get` on the examples of [package]. - Future _fetchExampleDeps(RepositoryPackage package) async { - for (final RepositoryPackage example in package.getExamples()) { - await processRunner.runAndStream( - 'dart', - ['pub', 'get'], - workingDir: example.directory, - ); - } - } - - Future _hasValidPublishCheckRun(RepositoryPackage package) async { - // `pub publish` does not do `dart pub get` inside `example` directories - // of a package (but they're part of the analysis output!). - // Issue: https://github.com/flutter/flutter/issues/113788 - await _fetchExampleDeps(package); - - print('Running pub publish --dry-run:'); - final io.Process process = await processRunner.start( - flutterCommand, - ['pub', 'publish', '--', '--dry-run'], - workingDirectory: package.directory, - ); - - final StringBuffer outputBuffer = StringBuffer(); - - final Completer stdOutCompleter = Completer(); - process.stdout.listen( - (List event) { - final String output = String.fromCharCodes(event); - if (output.isNotEmpty) { - print(output); - outputBuffer.write(output); - } - }, - onDone: () => stdOutCompleter.complete(), - ); - - final Completer stdInCompleter = Completer(); - process.stderr.listen( - (List event) { - final String output = String.fromCharCodes(event); - if (output.isNotEmpty) { - // The final result is always printed on stderr, whether success or - // failure. - final bool isError = !output.contains('has 0 warnings'); - _printImportantStatusMessage(output, isError: isError); - outputBuffer.write(output); - } - }, - onDone: () => stdInCompleter.complete(), - ); - - if (await process.exitCode == 0) { - return true; - } - - if (!getBoolArg(_allowPrereleaseFlag)) { - return false; - } - - await stdOutCompleter.future; - await stdInCompleter.future; - - final String output = outputBuffer.toString(); - return output.contains('Package has 1 warning') && - output.contains( - 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'); - } - - /// Returns the result of the publish check, or null if the package is marked - /// as unpublishable. - Future<_PublishCheckResult?> _passesPublishCheck( - RepositoryPackage package) async { - final String packageName = package.directory.basename; - final Pubspec? pubspec = _tryParsePubspec(package); - if (pubspec == null) { - print('No valid pubspec found.'); - return _PublishCheckResult.error; - } else if (pubspec.publishTo == 'none') { - return null; - } - - final Version? version = pubspec.version; - final _PublishCheckResult alreadyPublishedResult = - await _checkPublishingStatus( - packageName: packageName, version: version); - if (alreadyPublishedResult == _PublishCheckResult.nothingToPublish) { - print( - 'Package $packageName version: $version has already be published on pub.'); - return alreadyPublishedResult; - } else if (alreadyPublishedResult == _PublishCheckResult.error) { - print('Check pub version failed $packageName'); - return _PublishCheckResult.error; - } - - if (await _hasValidPublishCheckRun(package)) { - print('Package $packageName is able to be published.'); - return _PublishCheckResult.needsPublishing; - } else { - print('Unable to publish $packageName'); - return _PublishCheckResult.error; - } - } - - // Check if `packageName` already has `version` published on pub. - Future<_PublishCheckResult> _checkPublishingStatus( - {required String packageName, required Version? version}) async { - final PubVersionFinderResponse pubVersionFinderResponse = - await _pubVersionFinder.getPackageVersion(packageName: packageName); - switch (pubVersionFinderResponse.result) { - case PubVersionFinderResult.success: - return pubVersionFinderResponse.versions.contains(version) - ? _PublishCheckResult.nothingToPublish - : _PublishCheckResult.needsPublishing; - case PubVersionFinderResult.fail: - print(''' -Error fetching version on pub for $packageName. -HTTP Status ${pubVersionFinderResponse.httpResponse.statusCode} -HTTP response: ${pubVersionFinderResponse.httpResponse.body} -'''); - return _PublishCheckResult.error; - case PubVersionFinderResult.noPackageFound: - return _PublishCheckResult.needsPublishing; - } - } - - bool _passesAuthorsCheck(RepositoryPackage package) { - final List pathComponents = - package.directory.fileSystem.path.split(package.path); - if (pathComponents.contains('third_party')) { - // Third-party packages aren't required to have an AUTHORS file. - return true; - } - return package.authorsFile.existsSync(); - } - - void _printImportantStatusMessage(String message, {required bool isError}) { - final String statusMessage = '${isError ? 'ERROR' : 'SUCCESS'}: $message'; - if (getBoolArg(_machineFlag)) { - print(statusMessage); - } else { - if (isError) { - printError(statusMessage); - } else { - printSuccess(statusMessage); - } - } - } -} - -/// Possible outcomes of of a publishing check. -enum _PublishCheckResult { - nothingToPublish, - needsPublishing, - error, -} diff --git a/script/tool/lib/src/publish_command.dart b/script/tool/lib/src/publish_command.dart deleted file mode 100644 index e7b3d110c5fa..000000000000 --- a/script/tool/lib/src/publish_command.dart +++ /dev/null @@ -1,456 +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:async'; -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:http/http.dart' as http; -import 'package:meta/meta.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:pub_semver/pub_semver.dart'; -import 'package:yaml/yaml.dart'; - -import 'common/core.dart'; -import 'common/file_utils.dart'; -import 'common/git_version_finder.dart'; -import 'common/package_command.dart'; -import 'common/package_looping_command.dart'; -import 'common/process_runner.dart'; -import 'common/pub_version_finder.dart'; -import 'common/repository_package.dart'; - -@immutable -class _RemoteInfo { - const _RemoteInfo({required this.name, required this.url}); - - /// The git name for the remote. - final String name; - - /// The remote's URL. - final String url; -} - -/// Wraps pub publish with a few niceties used by the flutter/plugin team. -/// -/// 1. Checks for any modified files in git and refuses to publish if there's an -/// issue. -/// 2. Tags the release with the format -v. -/// 3. Pushes the release to a remote. -/// -/// Both 2 and 3 are optional, see `plugin_tools help publish` for full -/// usage information. -/// -/// [processRunner], [print], and [stdin] can be overriden for easier testing. -class PublishCommand extends PackageLoopingCommand { - /// Creates an instance of the publish command. - PublishCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - io.Stdin? stdinput, - GitDir? gitDir, - http.Client? httpClient, - }) : _pubVersionFinder = - PubVersionFinder(httpClient: httpClient ?? http.Client()), - _stdin = stdinput ?? io.stdin, - super(packagesDir, - platform: platform, processRunner: processRunner, gitDir: gitDir) { - argParser.addMultiOption(_pubFlagsOption, - help: - 'A list of options that will be forwarded on to pub. Separate multiple flags with commas.'); - argParser.addOption( - _remoteOption, - help: 'The name of the remote to push the tags to.', - // Flutter convention is to use "upstream" for the single source of truth, and "origin" for personal forks. - defaultsTo: 'upstream', - ); - argParser.addFlag( - _allChangedFlag, - help: - 'Release all packages that contains pubspec changes at the current commit compares to the base-sha.\n' - 'The --packages option is ignored if this is on.', - ); - argParser.addFlag( - _dryRunFlag, - help: - 'Skips the real `pub publish` and `git tag` commands and assumes both commands are successful.\n' - 'This does not run `pub publish --dry-run`.\n' - 'If you want to run the command with `pub publish --dry-run`, use `pub-publish-flags=--dry-run`', - ); - argParser.addFlag(_skipConfirmationFlag, - help: 'Run the command without asking for Y/N inputs.\n' - 'This command will add a `--force` flag to the `pub publish` command if it is not added with $_pubFlagsOption\n'); - } - - static const String _pubFlagsOption = 'pub-publish-flags'; - static const String _remoteOption = 'remote'; - static const String _allChangedFlag = 'all-changed'; - static const String _dryRunFlag = 'dry-run'; - static const String _skipConfirmationFlag = 'skip-confirmation'; - - static const String _pubCredentialName = 'PUB_CREDENTIALS'; - - // Version tags should follow -v. For example, - // `flutter_plugin_tools-v0.0.24`. - static const String _tagFormat = '%PACKAGE%-v%VERSION%'; - - @override - final String name = 'publish'; - - @override - final String description = - 'Attempts to publish the given packages and tag the release(s) on GitHub.\n' - 'If running this on CI, an environment variable named $_pubCredentialName must be set to a String that represents the pub credential JSON.\n' - 'WARNING: Do not check in the content of pub credential JSON, it should only come from secure sources.'; - - final io.Stdin _stdin; - StreamSubscription? _stdinSubscription; - final PubVersionFinder _pubVersionFinder; - - // Tags that already exist in the repository. - List _existingGitTags = []; - // The remote to push tags to. - late _RemoteInfo _remote; - // Flags to pass to `pub publish`. - late List _publishFlags; - - @override - String get successSummaryMessage => 'published'; - - @override - String get failureListHeader => - 'The following packages had failures during publishing:'; - - @override - Future initializeRun() async { - print('Checking local repo...'); - - // Ensure that the requested remote is present. - final String remoteName = getStringArg(_remoteOption); - final String? remoteUrl = await _verifyRemote(remoteName); - if (remoteUrl == null) { - printError('Unable to find URL for remote $remoteName; cannot push tags'); - throw ToolExit(1); - } - _remote = _RemoteInfo(name: remoteName, url: remoteUrl); - - // Pre-fetch all the repository's tags, to check against when publishing. - final GitDir repository = await gitDir; - final io.ProcessResult existingTagsResult = - await repository.runCommand(['tag', '--sort=-committerdate']); - _existingGitTags = (existingTagsResult.stdout as String).split('\n') - ..removeWhere((String element) => element.isEmpty); - - _publishFlags = [ - ...getStringListArg(_pubFlagsOption), - if (getBoolArg(_skipConfirmationFlag)) '--force', - ]; - - if (getBoolArg(_dryRunFlag)) { - print('=============== DRY RUN ==============='); - } - } - - @override - Stream getPackagesToProcess() async* { - if (getBoolArg(_allChangedFlag)) { - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - final String baseSha = await gitVersionFinder.getBaseSha(); - print( - 'Publishing all packages that have changed relative to "$baseSha"\n'); - final List changedPubspecs = - await gitVersionFinder.getChangedPubSpecs(); - - for (final String pubspecPath in changedPubspecs) { - // git outputs a relativa, Posix-style path. - final File pubspecFile = childFileWithSubcomponents( - packagesDir.fileSystem.directory((await gitDir).path), - p.posix.split(pubspecPath)); - yield PackageEnumerationEntry(RepositoryPackage(pubspecFile.parent), - excluded: false); - } - } else { - yield* getTargetPackages(filterExcluded: false); - } - } - - @override - Future runForPackage(RepositoryPackage package) async { - final PackageResult? checkResult = await _checkNeedsRelease(package); - if (checkResult != null) { - return checkResult; - } - - if (!await _checkGitStatus(package)) { - return PackageResult.fail(['uncommitted changes']); - } - - if (!await _publish(package)) { - return PackageResult.fail(['publish failed']); - } - - if (!await _tagRelease(package)) { - return PackageResult.fail(['tagging failed']); - } - - print('\nPublished ${package.directory.basename} successfully!'); - return PackageResult.success(); - } - - @override - Future completeRun() async { - _pubVersionFinder.httpClient.close(); - await _stdinSubscription?.cancel(); - _stdinSubscription = null; - } - - /// Checks whether [package] needs to be released, printing check status and - /// returning one of: - /// - PackageResult.fail if the check could not be completed - /// - PackageResult.skip if no release is necessary - /// - null if releasing should proceed - /// - /// In cases where a non-null result is returned, that should be returned - /// as the final result for the package, without further processing. - Future _checkNeedsRelease(RepositoryPackage package) async { - if (!package.pubspecFile.existsSync()) { - logWarning(''' -The pubspec file for ${package.displayName} does not exist, so no publishing will happen. -Safe to ignore if the package is deleted in this commit. -'''); - return PackageResult.skip('package deleted'); - } - - final Pubspec pubspec = package.parsePubspec(); - - if (pubspec.name == 'flutter_plugin_tools') { - // Ignore flutter_plugin_tools package when running publishing through flutter_plugin_tools. - // TODO(cyanglaz): Make the tool also auto publish flutter_plugin_tools package. - // https://github.com/flutter/flutter/issues/85430 - return PackageResult.skip( - 'publishing flutter_plugin_tools via the tool is not supported'); - } - - if (pubspec.publishTo == 'none') { - return PackageResult.skip('publish_to: none'); - } - - if (pubspec.version == null) { - printError( - 'No version found. A package that intentionally has no version should be marked "publish_to: none"'); - return PackageResult.fail(['no version']); - } - - // Check if the package named `packageName` with `version` has already - // been published. - final Version version = pubspec.version!; - final PubVersionFinderResponse pubVersionFinderResponse = - await _pubVersionFinder.getPackageVersion(packageName: pubspec.name); - if (pubVersionFinderResponse.versions.contains(version)) { - final String tagsForPackageWithSameVersion = _existingGitTags.firstWhere( - (String tag) => - tag.split('-v').first == pubspec.name && - tag.split('-v').last == version.toString(), - orElse: () => ''); - if (tagsForPackageWithSameVersion.isEmpty) { - printError( - '${pubspec.name} $version has already been published, however ' - 'the git release tag (${pubspec.name}-v$version) was not found. ' - 'Please manually fix the tag then run the command again.'); - return PackageResult.fail(['published but untagged']); - } else { - print('${pubspec.name} $version has already been published.'); - return PackageResult.skip('already published'); - } - } - return null; - } - - // Tag the release with -v, and push it to the remote. - // - // Return `true` if successful, `false` otherwise. - Future _tagRelease(RepositoryPackage package) async { - final String tag = _getTag(package); - print('Tagging release $tag...'); - if (!getBoolArg(_dryRunFlag)) { - final io.ProcessResult result = await (await gitDir).runCommand( - ['tag', tag], - throwOnError: false, - ); - if (result.exitCode != 0) { - return false; - } - } - - print('Pushing tag to ${_remote.name}...'); - final bool success = await _pushTagToRemote( - tag: tag, - remote: _remote, - ); - if (success) { - print('Release tagged!'); - } - return success; - } - - Future _checkGitStatus(RepositoryPackage package) async { - final io.ProcessResult statusResult = await (await gitDir).runCommand( - [ - 'status', - '--porcelain', - '--ignored', - package.directory.absolute.path - ], - throwOnError: false, - ); - if (statusResult.exitCode != 0) { - return false; - } - - final String statusOutput = statusResult.stdout as String; - if (statusOutput.isNotEmpty) { - printError( - "There are files in the package directory that haven't been saved in git. Refusing to publish these files:\n\n" - '$statusOutput\n' - 'If the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.'); - } - return statusOutput.isEmpty; - } - - Future _verifyRemote(String remote) async { - final io.ProcessResult getRemoteUrlResult = await (await gitDir).runCommand( - ['remote', 'get-url', remote], - throwOnError: false, - ); - if (getRemoteUrlResult.exitCode != 0) { - return null; - } - return getRemoteUrlResult.stdout as String?; - } - - Future _publish(RepositoryPackage package) async { - print('Publishing...'); - print('Running `pub publish ${_publishFlags.join(' ')}` in ' - '${package.directory.absolute.path}...\n'); - if (getBoolArg(_dryRunFlag)) { - return true; - } - - if (_publishFlags.contains('--force')) { - _ensureValidPubCredential(); - } - - final io.Process publish = await processRunner.start( - flutterCommand, ['pub', 'publish', ..._publishFlags], - workingDirectory: package.directory); - publish.stdout.transform(utf8.decoder).listen((String data) => print(data)); - publish.stderr.transform(utf8.decoder).listen((String data) => print(data)); - _stdinSubscription ??= _stdin - .transform(utf8.decoder) - .listen((String data) => publish.stdin.writeln(data)); - final int result = await publish.exitCode; - if (result != 0) { - printError('Publishing ${package.directory.basename} failed.'); - return false; - } - - print('Package published!'); - return true; - } - - String _getTag(RepositoryPackage package) { - final File pubspecFile = package.pubspecFile; - final YamlMap pubspecYaml = - loadYaml(pubspecFile.readAsStringSync()) as YamlMap; - final String name = pubspecYaml['name'] as String; - final String version = pubspecYaml['version'] as String; - // We should have failed to publish if these were unset. - assert(name.isNotEmpty && version.isNotEmpty); - return _tagFormat - .replaceAll('%PACKAGE%', name) - .replaceAll('%VERSION%', version); - } - - // Pushes the `tag` to `remote` - // - // Return `true` if successful, `false` otherwise. - Future _pushTagToRemote({ - required String tag, - required _RemoteInfo remote, - }) async { - assert(remote != null && tag != null); - if (!getBoolArg(_dryRunFlag)) { - final io.ProcessResult result = await (await gitDir).runCommand( - ['push', remote.name, tag], - throwOnError: false, - ); - if (result.exitCode != 0) { - return false; - } - } - return true; - } - - void _ensureValidPubCredential() { - final String credentialsPath = _credentialsPath; - final File credentialFile = packagesDir.fileSystem.file(credentialsPath); - if (credentialFile.existsSync() && - credentialFile.readAsStringSync().isNotEmpty) { - return; - } - final String? credential = io.Platform.environment[_pubCredentialName]; - if (credential == null) { - printError(''' -No pub credential available. Please check if `$credentialsPath` is valid. -If running this command on CI, you can set the pub credential content in the $_pubCredentialName environment variable. -'''); - throw ToolExit(1); - } - credentialFile.openSync(mode: FileMode.writeOnlyAppend) - ..writeStringSync(credential) - ..closeSync(); - } - - /// Returns the correct path where the pub credential is stored. - @visibleForTesting - static String getCredentialPath() { - return _credentialsPath; - } -} - -/// The path in which pub expects to find its credentials file. -final String _credentialsPath = () { - // This follows the same logic as pub: - // https://github.com/dart-lang/pub/blob/d99b0d58f4059d7bb4ac4616fd3d54ec00a2b5d4/lib/src/system_cache.dart#L34-L43 - String? cacheDir; - final String? pubCache = io.Platform.environment['PUB_CACHE']; - if (pubCache != null) { - cacheDir = pubCache; - } else if (io.Platform.isWindows) { - final String? appData = io.Platform.environment['APPDATA']; - if (appData == null) { - printError('"APPDATA" environment variable is not set.'); - } else { - cacheDir = p.join(appData, 'Pub', 'Cache'); - } - } else { - final String? home = io.Platform.environment['HOME']; - if (home == null) { - printError('"HOME" environment variable is not set.'); - } else { - cacheDir = p.join(home, '.pub-cache'); - } - } - - if (cacheDir == null) { - printError('Unable to determine pub cache location'); - throw ToolExit(1); - } - - return p.join(cacheDir, 'credentials.json'); -}(); diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart deleted file mode 100644 index 79ef1e1d3e5e..000000000000 --- a/script/tool/lib/src/pubspec_check_command.dart +++ /dev/null @@ -1,322 +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 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:platform/platform.dart'; -import 'package:yaml/yaml.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -/// A command to enforce pubspec conventions across the repository. -/// -/// This both ensures that repo best practices for which optional fields are -/// used are followed, and that the structure is consistent to make edits -/// across multiple pubspec files easier. -class PubspecCheckCommand extends PackageLoopingCommand { - /// Creates an instance of the version check command. - PubspecCheckCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - GitDir? gitDir, - }) : super( - packagesDir, - processRunner: processRunner, - platform: platform, - gitDir: gitDir, - ); - - // Section order for plugins. Because the 'flutter' section is critical - // information for plugins, and usually small, it goes near the top unlike in - // a normal app or package. - static const List _majorPluginSections = [ - 'environment:', - 'flutter:', - 'dependencies:', - 'dev_dependencies:', - 'false_secrets:', - ]; - - static const List _majorPackageSections = [ - 'environment:', - 'dependencies:', - 'dev_dependencies:', - 'flutter:', - 'false_secrets:', - ]; - - static const String _expectedIssueLinkFormat = - 'https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A'; - - @override - final String name = 'pubspec-check'; - - @override - final String description = - 'Checks that pubspecs follow repository conventions.'; - - @override - bool get hasLongOutput => false; - - @override - PackageLoopingType get packageLoopingType => - PackageLoopingType.includeAllSubpackages; - - @override - Future runForPackage(RepositoryPackage package) async { - final File pubspec = package.pubspecFile; - final bool passesCheck = - !pubspec.existsSync() || await _checkPubspec(pubspec, package: package); - if (!passesCheck) { - return PackageResult.fail(); - } - return PackageResult.success(); - } - - Future _checkPubspec( - File pubspecFile, { - required RepositoryPackage package, - }) async { - final String contents = pubspecFile.readAsStringSync(); - final Pubspec? pubspec = _tryParsePubspec(contents); - if (pubspec == null) { - return false; - } - - final List pubspecLines = contents.split('\n'); - final bool isPlugin = pubspec.flutter?.containsKey('plugin') ?? false; - final List sectionOrder = - isPlugin ? _majorPluginSections : _majorPackageSections; - bool passing = _checkSectionOrder(pubspecLines, sectionOrder); - if (!passing) { - printError('${indentation}Major sections should follow standard ' - 'repository ordering:'); - final String listIndentation = indentation * 2; - printError('$listIndentation${sectionOrder.join('\n$listIndentation')}'); - } - - if (isPlugin) { - final String? implementsError = - _checkForImplementsError(pubspec, package: package); - if (implementsError != null) { - printError('$indentation$implementsError'); - passing = false; - } - - final String? defaultPackageError = - _checkForDefaultPackageError(pubspec, package: package); - if (defaultPackageError != null) { - printError('$indentation$defaultPackageError'); - passing = false; - } - } - - // Ignore metadata that's only relevant for published packages if the - // packages is not intended for publishing. - if (pubspec.publishTo != 'none') { - final List repositoryErrors = - _checkForRepositoryLinkErrors(pubspec, package: package); - if (repositoryErrors.isNotEmpty) { - for (final String error in repositoryErrors) { - printError('$indentation$error'); - } - passing = false; - } - - if (!_checkIssueLink(pubspec)) { - printError( - '${indentation}A package should have an "issue_tracker" link to a ' - 'search for open flutter/flutter bugs with the relevant label:\n' - '${indentation * 2}$_expectedIssueLinkFormat'); - passing = false; - } - - // Don't check descriptions for federated package components other than - // the app-facing package, since they are unlisted, and are expected to - // have short descriptions. - if (!package.isPlatformInterface && !package.isPlatformImplementation) { - final String? descriptionError = - _checkDescription(pubspec, package: package); - if (descriptionError != null) { - printError('$indentation$descriptionError'); - passing = false; - } - } - } - - return passing; - } - - Pubspec? _tryParsePubspec(String pubspecContents) { - try { - return Pubspec.parse(pubspecContents); - } on Exception catch (exception) { - print(' Cannot parse pubspec.yaml: $exception'); - } - return null; - } - - bool _checkSectionOrder( - List pubspecLines, List sectionOrder) { - int previousSectionIndex = 0; - for (final String line in pubspecLines) { - final int index = sectionOrder.indexOf(line); - if (index == -1) { - continue; - } - if (index < previousSectionIndex) { - return false; - } - previousSectionIndex = index; - } - return true; - } - - List _checkForRepositoryLinkErrors( - Pubspec pubspec, { - required RepositoryPackage package, - }) { - final List errorMessages = []; - if (pubspec.repository == null) { - errorMessages.add('Missing "repository"'); - } else { - final String relativePackagePath = - getRelativePosixPath(package.directory, from: packagesDir.parent); - if (!pubspec.repository!.path.endsWith(relativePackagePath)) { - errorMessages - .add('The "repository" link should end with the package path.'); - } - - if (pubspec.repository!.path.contains('/master/')) { - errorMessages - .add('The "repository" link should use "main", not "master".'); - } - } - - if (pubspec.homepage != null) { - errorMessages - .add('Found a "homepage" entry; only "repository" should be used.'); - } - - return errorMessages; - } - - // Validates the "description" field for a package, returning an error - // string if there are any issues. - String? _checkDescription( - Pubspec pubspec, { - required RepositoryPackage package, - }) { - final String? description = pubspec.description; - if (description == null) { - return 'Missing "description"'; - } - - if (description.length < 60) { - return '"description" is too short. pub.dev recommends package ' - 'descriptions of 60-180 characters.'; - } - if (description.length > 180) { - return '"description" is too long. pub.dev recommends package ' - 'descriptions of 60-180 characters.'; - } - return null; - } - - bool _checkIssueLink(Pubspec pubspec) { - return pubspec.issueTracker - ?.toString() - .startsWith(_expectedIssueLinkFormat) ?? - false; - } - - // Validates the "implements" keyword for a plugin, returning an error - // string if there are any issues. - // - // Should only be called on plugin packages. - String? _checkForImplementsError( - Pubspec pubspec, { - required RepositoryPackage package, - }) { - if (_isImplementationPackage(package)) { - final String? implements = - pubspec.flutter!['plugin']!['implements'] as String?; - final String expectedImplements = package.directory.parent.basename; - if (implements == null) { - return 'Missing "implements: $expectedImplements" in "plugin" section.'; - } else if (implements != expectedImplements) { - return 'Expecetd "implements: $expectedImplements"; ' - 'found "implements: $implements".'; - } - } - return null; - } - - // Validates any "default_package" entries a plugin, returning an error - // string if there are any issues. - // - // Should only be called on plugin packages. - String? _checkForDefaultPackageError( - Pubspec pubspec, { - required RepositoryPackage package, - }) { - final dynamic platformsEntry = pubspec.flutter!['plugin']!['platforms']; - if (platformsEntry == null) { - logWarning('Does not implement any platforms'); - return null; - } - final YamlMap platforms = platformsEntry as YamlMap; - final String packageName = package.directory.basename; - - // Validate that the default_package entries look correct (e.g., no typos). - final Set defaultPackages = {}; - for (final MapEntry platformEntry in platforms.entries) { - final String? defaultPackage = - platformEntry.value['default_package'] as String?; - if (defaultPackage != null) { - defaultPackages.add(defaultPackage); - if (!defaultPackage.startsWith('${packageName}_')) { - return '"$defaultPackage" is not an expected implementation name ' - 'for "$packageName"'; - } - } - } - - // Validate that all default_packages are also dependencies. - final Iterable dependencies = pubspec.dependencies.keys; - final Iterable missingPackages = defaultPackages - .where((String package) => !dependencies.contains(package)); - if (missingPackages.isNotEmpty) { - return 'The following default_packages are missing ' - 'corresponding dependencies:\n' - ' ${missingPackages.join('\n ')}'; - } - - return null; - } - - // Returns true if [packageName] appears to be an implementation package - // according to repository conventions. - bool _isImplementationPackage(RepositoryPackage package) { - if (!package.isFederated) { - return false; - } - final String packageName = package.directory.basename; - final String parentName = package.directory.parent.basename; - // A few known package names are not implementation packages; assume - // anything else is. (This is done instead of listing known implementation - // suffixes to allow for non-standard suffixes; e.g., to put several - // platforms in one package for code-sharing purposes.) - const Set nonImplementationSuffixes = { - '', // App-facing package. - '_platform_interface', // Platform interface package. - }; - final String suffix = packageName.substring(parentName.length); - return !nonImplementationSuffixes.contains(suffix); - } -} diff --git a/script/tool/lib/src/readme_check_command.dart b/script/tool/lib/src/readme_check_command.dart deleted file mode 100644 index e3fbc7bc454d..000000000000 --- a/script/tool/lib/src/readme_check_command.dart +++ /dev/null @@ -1,343 +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 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:platform/platform.dart'; -import 'package:yaml/yaml.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -const String _instructionWikiUrl = - 'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages'; - -/// A command to enforce README conventions across the repository. -class ReadmeCheckCommand extends PackageLoopingCommand { - /// Creates an instance of the README check command. - ReadmeCheckCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - GitDir? gitDir, - }) : super( - packagesDir, - processRunner: processRunner, - platform: platform, - gitDir: gitDir, - ) { - argParser.addFlag(_requireExcerptsArg, - help: 'Require that Dart code blocks be managed by code-excerpt.'); - } - - static const String _requireExcerptsArg = 'require-excerpts'; - - // Standardized capitalizations for platforms that a plugin can support. - static const Map _standardPlatformNames = { - 'android': 'Android', - 'ios': 'iOS', - 'linux': 'Linux', - 'macos': 'macOS', - 'web': 'Web', - 'windows': 'Windows', - }; - - @override - final String name = 'readme-check'; - - @override - final String description = - 'Checks that READMEs follow repository conventions.'; - - @override - bool get hasLongOutput => false; - - @override - Future runForPackage(RepositoryPackage package) async { - final List errors = _validateReadme(package.readmeFile, - mainPackage: package, isExample: false); - for (final RepositoryPackage packageToCheck in package.getExamples()) { - errors.addAll(_validateReadme(packageToCheck.readmeFile, - mainPackage: package, isExample: true)); - } - - // If there's an example/README.md for a multi-example package, validate - // that as well, as it will be shown on pub.dev. - final Directory exampleDir = package.directory.childDirectory('example'); - final File exampleDirReadme = exampleDir.childFile('README.md'); - if (exampleDir.existsSync() && !isPackage(exampleDir)) { - errors.addAll(_validateReadme(exampleDirReadme, - mainPackage: package, isExample: true)); - } - - return errors.isEmpty - ? PackageResult.success() - : PackageResult.fail(errors); - } - - List _validateReadme(File readme, - {required RepositoryPackage mainPackage, required bool isExample}) { - if (!readme.existsSync()) { - if (isExample) { - print('${indentation}No README for ' - '${getRelativePosixPath(readme.parent, from: mainPackage.directory)}'); - return []; - } else { - printError('${indentation}No README found at ' - '${getRelativePosixPath(readme, from: mainPackage.directory)}'); - return ['Missing README.md']; - } - } - - print('${indentation}Checking ' - '${getRelativePosixPath(readme, from: mainPackage.directory)}...'); - - final List readmeLines = readme.readAsLinesSync(); - final List errors = []; - - final String? blockValidationError = - _validateCodeBlocks(readmeLines, mainPackage: mainPackage); - if (blockValidationError != null) { - errors.add(blockValidationError); - } - - errors.addAll(_validateBoilerplate(readmeLines, - mainPackage: mainPackage, isExample: isExample)); - - // Check if this is the main readme for a plugin, and if so enforce extra - // checks. - if (!isExample) { - final Pubspec pubspec = mainPackage.parsePubspec(); - final bool isPlugin = pubspec.flutter?['plugin'] != null; - if (isPlugin && (!mainPackage.isFederated || mainPackage.isAppFacing)) { - final String? error = _validateSupportedPlatforms(readmeLines, pubspec); - if (error != null) { - errors.add(error); - } - } - } - - return errors; - } - - /// Validates that code blocks (``` ... ```) follow repository standards. - String? _validateCodeBlocks( - List readmeLines, { - required RepositoryPackage mainPackage, - }) { - final RegExp codeBlockDelimiterPattern = RegExp(r'^\s*```\s*([^ ]*)\s*'); - const String excerptTagStart = ' missingLanguageLines = []; - final List missingExcerptLines = []; - bool inBlock = false; - for (int i = 0; i < readmeLines.length; ++i) { - final RegExpMatch? match = - codeBlockDelimiterPattern.firstMatch(readmeLines[i]); - if (match == null) { - continue; - } - if (inBlock) { - inBlock = false; - continue; - } - inBlock = true; - - final int humanReadableLineNumber = i + 1; - - // Ensure that there's a language tag. - final String infoString = match[1] ?? ''; - if (infoString.isEmpty) { - missingLanguageLines.add(humanReadableLineNumber); - continue; - } - - // Check for code-excerpt usage if requested. - if (getBoolArg(_requireExcerptsArg) && infoString == 'dart') { - if (i == 0 || !readmeLines[i - 1].trim().startsWith(excerptTagStart)) { - missingExcerptLines.add(humanReadableLineNumber); - } - } - } - - String? errorSummary; - - if (missingLanguageLines.isNotEmpty) { - for (final int lineNumber in missingLanguageLines) { - printError('${indentation}Code block at line $lineNumber is missing ' - 'a language identifier.'); - } - printError( - '\n${indentation}For each block listed above, add a language tag to ' - 'the opening block. For instance, for Dart code, use:\n' - '${indentation * 2}```dart\n'); - errorSummary = 'Missing language identifier for code block'; - } - - // If any blocks use code excerpts, make sure excerpting is configured - // for the package. - if (readmeLines.any((String line) => line.startsWith(excerptTagStart))) { - const String buildRunnerConfigFile = 'build.excerpt.yaml'; - if (!mainPackage.getExamples().any((RepositoryPackage example) => - example.directory.childFile(buildRunnerConfigFile).existsSync())) { - printError('code-excerpt tag found, but the package is not configured ' - 'for excerpting. Follow the instructions at\n' - '$_instructionWikiUrl\n' - 'for setting up a build.excerpt.yaml file.'); - errorSummary ??= 'Missing code-excerpt configuration'; - } - } - - if (missingExcerptLines.isNotEmpty) { - for (final int lineNumber in missingExcerptLines) { - printError('${indentation}Dart code block at line $lineNumber is not ' - 'managed by code-excerpt.'); - } - printError( - '\n${indentation}For each block listed above, add ' - 'tag on the previous line, and ensure that a build.excerpt.yaml is ' - 'configured for the source example as explained at\n' - '$_instructionWikiUrl'); - errorSummary ??= 'Missing code-excerpt management for code block'; - } - - return errorSummary; - } - - /// Validates that the plugin has a supported platforms table following the - /// expected format, returning an error string if any issues are found. - String? _validateSupportedPlatforms( - List readmeLines, Pubspec pubspec) { - // Example table following expected format: - // | | Android | iOS | Web | - // |----------------|---------|----------|------------------------| - // | **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] | - final int detailsLineNumber = readmeLines - .indexWhere((String line) => line.startsWith('| **Support**')); - if (detailsLineNumber == -1) { - return 'No OS support table found'; - } - final int osLineNumber = detailsLineNumber - 2; - if (osLineNumber < 0 || !readmeLines[osLineNumber].startsWith('|')) { - return 'OS support table does not have the expected header format'; - } - - // Utility method to convert an iterable of strings to a case-insensitive - // sorted, comma-separated string of its elements. - String sortedListString(Iterable entries) { - final List entryList = entries.toList(); - entryList.sort( - (String a, String b) => a.toLowerCase().compareTo(b.toLowerCase())); - return entryList.join(', '); - } - - // Validate that the supported OS lists match. - final dynamic platformsEntry = pubspec.flutter!['plugin']!['platforms']; - if (platformsEntry == null) { - logWarning('Plugin not support any platforms'); - return null; - } - final YamlMap platformSupportMaps = platformsEntry as YamlMap; - final Set actuallySupportedPlatform = - platformSupportMaps.keys.toSet().cast(); - final Iterable documentedPlatforms = readmeLines[osLineNumber] - .split('|') - .map((String entry) => entry.trim()) - .where((String entry) => entry.isNotEmpty); - final Set documentedPlatformsLowercase = - documentedPlatforms.map((String entry) => entry.toLowerCase()).toSet(); - if (actuallySupportedPlatform.length != documentedPlatforms.length || - actuallySupportedPlatform - .intersection(documentedPlatformsLowercase) - .length != - actuallySupportedPlatform.length) { - printError(''' -${indentation}OS support table does not match supported platforms: -${indentation * 2}Actual: ${sortedListString(actuallySupportedPlatform)} -${indentation * 2}Documented: ${sortedListString(documentedPlatformsLowercase)} -'''); - return 'Incorrect OS support table'; - } - - // Enforce a standard set of capitalizations for the OS headings. - final Iterable incorrectCapitalizations = documentedPlatforms - .toSet() - .difference(_standardPlatformNames.values.toSet()); - if (incorrectCapitalizations.isNotEmpty) { - final Iterable expectedVersions = incorrectCapitalizations - .map((String name) => _standardPlatformNames[name.toLowerCase()]!); - printError(''' -${indentation}Incorrect OS capitalization: ${sortedListString(incorrectCapitalizations)} -${indentation * 2}Please use standard capitalizations: ${sortedListString(expectedVersions)} -'''); - return 'Incorrect OS support formatting'; - } - - // TODO(stuartmorgan): Add validation that the minimums in the table are - // consistent with what the current implementations require. See - // https://github.com/flutter/flutter/issues/84200 - return null; - } - - /// Validates [readmeLines], outputing error messages for any issue and - /// returning an array of error summaries (if any). - /// - /// Returns an empty array if validation passes. - List _validateBoilerplate( - List readmeLines, { - required RepositoryPackage mainPackage, - required bool isExample, - }) { - final List errors = []; - - if (_containsTemplateFlutterBoilerplate(readmeLines)) { - printError('${indentation}The boilerplate section about getting started ' - 'with Flutter should not be left in.'); - errors.add('Contains template boilerplate'); - } - - // Enforce a repository-standard message in implementation plugin examples, - // since they aren't typical examples, which has been a source of - // confusion for plugin clients who find them. - if (isExample && mainPackage.isPlatformImplementation) { - if (_containsExampleBoilerplate(readmeLines)) { - printError('${indentation}The boilerplate should not be left in for a ' - "federated plugin implementation package's example."); - errors.add('Contains template boilerplate'); - } - if (!_containsImplementationExampleExplanation(readmeLines)) { - printError('${indentation}The example README for a platform ' - 'implementation package should warn readers about its intended ' - 'use. Please copy the example README from another implementation ' - 'package in this repository.'); - errors.add('Missing implementation package example warning'); - } - } - - return errors; - } - - /// Returns true if the README still has unwanted parts of the boilerplate - /// from the `flutter create` templates. - bool _containsTemplateFlutterBoilerplate(List readmeLines) { - return readmeLines.any((String line) => - line.contains('For help getting started with Flutter')); - } - - /// Returns true if the README still has the generic description of an - /// example from the `flutter create` templates. - bool _containsExampleBoilerplate(List readmeLines) { - return readmeLines - .any((String line) => line.contains('Demonstrates how to use the')); - } - - /// Returns true if the README contains the repository-standard explanation of - /// the purpose of a federated plugin implementation's example. - bool _containsImplementationExampleExplanation(List readmeLines) { - return readmeLines.contains('# Platform Implementation Test App') && - readmeLines - .any((String line) => line.contains('This is a test app for')); - } -} diff --git a/script/tool/lib/src/remove_dev_dependencies.dart b/script/tool/lib/src/remove_dev_dependencies.dart deleted file mode 100644 index 3085e0df85e0..000000000000 --- a/script/tool/lib/src/remove_dev_dependencies.dart +++ /dev/null @@ -1,58 +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 'package:file/file.dart'; -import 'package:yaml/yaml.dart'; -import 'package:yaml_edit/yaml_edit.dart'; - -import 'common/package_looping_command.dart'; -import 'common/repository_package.dart'; - -/// A command to remove dev_dependencies, which are not used by package clients. -/// -/// This is intended for use with legacy Flutter version testing, to allow -/// running analysis (with --lib-only) with versions that are supported for -/// clients of the library, but not for development of the library. -class RemoveDevDependenciesCommand extends PackageLoopingCommand { - /// Creates a publish metadata updater command instance. - RemoveDevDependenciesCommand(Directory packagesDir) : super(packagesDir); - - @override - final String name = 'remove-dev-dependencies'; - - @override - final String description = 'Removes any dev_dependencies section from a ' - 'package, to allow more legacy testing.'; - - @override - bool get hasLongOutput => false; - - @override - PackageLoopingType get packageLoopingType => - PackageLoopingType.includeAllSubpackages; - - @override - Future runForPackage(RepositoryPackage package) async { - bool changed = false; - final YamlEditor editablePubspec = - YamlEditor(package.pubspecFile.readAsStringSync()); - const String devDependenciesKey = 'dev_dependencies'; - final YamlNode root = editablePubspec.parseAt([]); - final YamlMap? devDependencies = - (root as YamlMap)[devDependenciesKey] as YamlMap?; - if (devDependencies != null) { - changed = true; - print('${indentation}Removed dev_dependencies'); - editablePubspec.remove([devDependenciesKey]); - } - - if (changed) { - package.pubspecFile.writeAsStringSync(editablePubspec.toString()); - } - - return changed - ? PackageResult.success() - : PackageResult.skip('Nothing to remove.'); - } -} diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart deleted file mode 100644 index 5101b8f19e7e..000000000000 --- a/script/tool/lib/src/test_command.dart +++ /dev/null @@ -1,104 +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 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/plugin_utils.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -/// A command to run Dart unit tests for packages. -class TestCommand extends PackageLoopingCommand { - /// Creates an instance of the test command. - TestCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addOption( - kEnableExperiment, - defaultsTo: '', - help: - 'Runs Dart unit tests in Dart VM with the given experiments enabled. ' - 'See https://github.com/dart-lang/sdk/blob/main/docs/process/experimental-flags.md ' - 'for details.', - ); - } - - @override - final String name = 'test'; - - @override - final String description = 'Runs the Dart tests for all packages.\n\n' - 'This command requires "flutter" to be in your path.'; - - @override - PackageLoopingType get packageLoopingType => - PackageLoopingType.includeAllSubpackages; - - @override - Future runForPackage(RepositoryPackage package) async { - if (!package.testDirectory.existsSync()) { - return PackageResult.skip('No test/ directory.'); - } - - bool passed; - if (package.requiresFlutter()) { - passed = await _runFlutterTests(package); - } else { - passed = await _runDartTests(package); - } - return passed ? PackageResult.success() : PackageResult.fail(); - } - - /// Runs the Dart tests for a Flutter package, returning true on success. - Future _runFlutterTests(RepositoryPackage package) async { - final String experiment = getStringArg(kEnableExperiment); - - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'test', - '--color', - if (experiment.isNotEmpty) '--enable-experiment=$experiment', - // TODO(ditman): Remove this once all plugins are migrated to 'drive'. - if (pluginSupportsPlatform(platformWeb, package)) '--platform=chrome', - ], - workingDir: package.directory, - ); - return exitCode == 0; - } - - /// Runs the Dart tests for a non-Flutter package, returning true on success. - Future _runDartTests(RepositoryPackage package) async { - // Unlike `flutter test`, `pub run test` does not automatically get - // packages - int exitCode = await processRunner.runAndStream( - 'dart', - ['pub', 'get'], - workingDir: package.directory, - ); - if (exitCode != 0) { - printError('Unable to fetch dependencies.'); - return false; - } - - final String experiment = getStringArg(kEnableExperiment); - - exitCode = await processRunner.runAndStream( - 'dart', - [ - 'run', - if (experiment.isNotEmpty) '--enable-experiment=$experiment', - 'test', - ], - workingDir: package.directory, - ); - - return exitCode == 0; - } -} diff --git a/script/tool/lib/src/update_excerpts_command.dart b/script/tool/lib/src/update_excerpts_command.dart deleted file mode 100644 index 5a59104d4e7f..000000000000 --- a/script/tool/lib/src/update_excerpts_command.dart +++ /dev/null @@ -1,225 +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:io' as io; - -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:platform/platform.dart'; -import 'package:yaml/yaml.dart'; -import 'package:yaml_edit/yaml_edit.dart'; - -import 'common/core.dart'; -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. -class UpdateExcerptsCommand extends PackageLoopingCommand { - /// Creates a excerpt updater command instance. - UpdateExcerptsCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - GitDir? gitDir, - }) : super( - packagesDir, - processRunner: processRunner, - platform: platform, - gitDir: gitDir, - ) { - argParser.addFlag(_failOnChangeFlag, hide: true); - } - - static const String _failOnChangeFlag = 'fail-on-change'; - - static const String _buildRunnerConfigName = 'excerpt'; - // The name of the build_runner configuration file that will be in an example - // directory if the package is set up to use `code-excerpt`. - static const String _buildRunnerConfigFile = - 'build.$_buildRunnerConfigName.yaml'; - - // The relative directory path to put the extracted excerpt yaml files. - static const String _excerptOutputDir = 'excerpts'; - - // The filename to store the pre-modification copy of the pubspec. - static const String _originalPubspecFilename = - 'pubspec.plugin_tools_original.yaml'; - - @override - final String name = 'update-excerpts'; - - @override - final String description = 'Updates code excerpts in README.md files, based ' - 'on code from code files, via code-excerpt'; - - @override - Future runForPackage(RepositoryPackage package) async { - final Iterable configuredExamples = package - .getExamples() - .where((RepositoryPackage example) => - example.directory.childFile(_buildRunnerConfigFile).existsSync()); - - if (configuredExamples.isEmpty) { - return PackageResult.skip( - 'No $_buildRunnerConfigFile found in example(s).'); - } - - final Directory repoRoot = - packagesDir.fileSystem.directory((await gitDir).path); - - for (final RepositoryPackage example in configuredExamples) { - _addSubmoduleDependencies(example, repoRoot: repoRoot); - - try { - // Ensure that dependencies are available. - final int pubGetExitCode = await processRunner.runAndStream( - 'dart', ['pub', 'get'], - workingDir: example.directory); - if (pubGetExitCode != 0) { - return PackageResult.fail( - ['Unable to get script dependencies']); - } - - // Update the excerpts. - if (!await _extractSnippets(example)) { - return PackageResult.fail(['Unable to extract excerpts']); - } - if (!await _injectSnippets(example, targetPackage: package)) { - return PackageResult.fail(['Unable to inject excerpts']); - } - } finally { - // Clean up the pubspec changes and extracted excerpts directory. - _undoPubspecChanges(example); - final Directory excerptDirectory = - example.directory.childDirectory(_excerptOutputDir); - if (excerptDirectory.existsSync()) { - excerptDirectory.deleteSync(recursive: true); - } - } - } - - if (getBoolArg(_failOnChangeFlag)) { - final String? stateError = await _validateRepositoryState(); - 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.'); - return PackageResult.fail([stateError]); - } - } - - return PackageResult.success(); - } - - /// Runs the extraction step to create the excerpt files for the given - /// example, returning true on success. - Future _extractSnippets(RepositoryPackage example) async { - final int exitCode = await processRunner.runAndStream( - 'dart', - [ - 'run', - 'build_runner', - 'build', - '--config', - _buildRunnerConfigName, - '--output', - _excerptOutputDir, - '--delete-conflicting-outputs', - ], - workingDir: example.directory); - return exitCode == 0; - } - - /// Runs the injection step to update [targetPackage]'s README 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 int exitCode = await processRunner.runAndStream( - 'dart', - [ - 'run', - 'code_excerpt_updater', - '--write-in-place', - '--yaml', - '--no-escape-ng-interpolation', - relativeReadmePath, - ], - workingDir: example.directory); - return exitCode == 0; - } - - /// Adds `code_excerpter` and `code_excerpt_updater` to [package]'s - /// `dev_dependencies` using path-based references to the submodule copies. - /// - /// This is done on the fly rather than being checked in so that: - /// - Just building examples don't require everyone to check out submodules. - /// - Examples can be analyzed/built even on versions of Flutter that these - /// submodules do not support. - void _addSubmoduleDependencies(RepositoryPackage package, - {required Directory repoRoot}) { - final String pubspecContents = package.pubspecFile.readAsStringSync(); - // Save aside a copy of the current pubspec state. This allows restoration - // to the previous state regardless of its git status at the time the script - // ran. - package.directory - .childFile(_originalPubspecFilename) - .writeAsStringSync(pubspecContents); - - // Update the actual pubspec. - final YamlEditor editablePubspec = YamlEditor(pubspecContents); - const String devDependenciesKey = 'dev_dependencies'; - final YamlNode root = editablePubspec.parseAt([]); - // Ensure that there's a `dev_dependencies` entry to update. - if ((root as YamlMap)[devDependenciesKey] == null) { - editablePubspec.update(['dev_dependencies'], YamlMap()); - } - final Set submoduleDependencies = { - 'code_excerpter', - 'code_excerpt_updater', - }; - final String relativeRootPath = - getRelativePosixPath(repoRoot, from: package.directory); - for (final String dependency in submoduleDependencies) { - editablePubspec.update([ - devDependenciesKey, - dependency - ], { - 'path': '$relativeRootPath/site-shared/packages/$dependency' - }); - } - package.pubspecFile.writeAsStringSync(editablePubspec.toString()); - } - - /// Restores the version of the pubspec that was present before running - /// [_addSubmoduleDependencies]. - void _undoPubspecChanges(RepositoryPackage package) { - package.directory - .childFile(_originalPubspecFilename) - .renameSync(package.pubspecFile.path); - } - - /// Checks the git state, returning an error string unless nothing has - /// changed. - Future _validateRepositoryState() async { - final io.ProcessResult modifiedFiles = await processRunner.run( - 'git', - ['ls-files', '--modified'], - workingDir: packagesDir, - logOnError: true, - ); - if (modifiedFiles.exitCode != 0) { - return 'Unable to determine local file state'; - } - - final String stdout = modifiedFiles.stdout as String; - return stdout.trim().isEmpty ? null : 'Snippets are out of sync'; - } -} diff --git a/script/tool/lib/src/update_release_info_command.dart b/script/tool/lib/src/update_release_info_command.dart deleted file mode 100644 index 67aa994d963c..000000000000 --- a/script/tool/lib/src/update_release_info_command.dart +++ /dev/null @@ -1,310 +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 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:pub_semver/pub_semver.dart'; -import 'package:yaml_edit/yaml_edit.dart'; - -import 'common/core.dart'; -import 'common/git_version_finder.dart'; -import 'common/package_looping_command.dart'; -import 'common/package_state_utils.dart'; -import 'common/repository_package.dart'; - -/// Supported version change types, from smallest to largest component. -enum _VersionIncrementType { build, bugfix, minor } - -/// Possible results of attempting to update a CHANGELOG.md file. -enum _ChangelogUpdateOutcome { addedSection, updatedSection, failed } - -/// A state machine for the process of updating a CHANGELOG.md. -enum _ChangelogUpdateState { - /// Looking for the first version section. - findingFirstSection, - - /// Looking for the first list entry in an existing section. - findingFirstListItem, - - /// Finished with updates. - finishedUpdating, -} - -/// A command to update the changelog, and optionally version, of packages. -class UpdateReleaseInfoCommand extends PackageLoopingCommand { - /// Creates a publish metadata updater command instance. - UpdateReleaseInfoCommand( - Directory packagesDir, { - GitDir? gitDir, - }) : super(packagesDir, gitDir: gitDir) { - argParser.addOption(_changelogFlag, - mandatory: true, - help: 'The changelog entry to add. ' - 'Each line will be a separate list entry.'); - argParser.addOption(_versionTypeFlag, - mandatory: true, - help: 'The version change level', - allowed: [ - _versionNext, - _versionMinimal, - _versionBugfix, - _versionMinor, - ], - allowedHelp: { - _versionNext: - 'No version change; just adds a NEXT entry to the changelog.', - _versionBugfix: 'Increments the bugfix version.', - _versionMinor: 'Increments the minor version.', - _versionMinimal: 'Depending on the changes to each package: ' - 'increments the bugfix version (for publishable changes), ' - "uses NEXT (for changes that don't need to be published), " - 'or skips (if no changes).', - }); - } - - static const String _changelogFlag = 'changelog'; - static const String _versionTypeFlag = 'version'; - - static const String _versionNext = 'next'; - static const String _versionBugfix = 'bugfix'; - static const String _versionMinor = 'minor'; - static const String _versionMinimal = 'minimal'; - - // The version change type, if there is a set type for all platforms. - // - // If null, either there is no version change, or it is dynamic (`minimal`). - _VersionIncrementType? _versionChange; - - // The cache of changed files, for dynamic version change determination. - // - // Only set for `minimal` version change. - late final List _changedFiles; - - @override - final String name = 'update-release-info'; - - @override - final String description = 'Updates CHANGELOG.md files, and optionally the ' - 'version in pubspec.yaml, in a way that is consistent with version-check ' - 'enforcement.'; - - @override - bool get hasLongOutput => false; - - @override - Future initializeRun() async { - if (getStringArg(_changelogFlag).trim().isEmpty) { - throw UsageException('Changelog message must not be empty.', usage); - } - switch (getStringArg(_versionTypeFlag)) { - case _versionMinor: - _versionChange = _VersionIncrementType.minor; - break; - case _versionBugfix: - _versionChange = _VersionIncrementType.bugfix; - break; - case _versionMinimal: - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - _changedFiles = await gitVersionFinder.getChangedFiles(); - // Anothing other than a fixed change is null. - _versionChange = null; - break; - case _versionNext: - _versionChange = null; - break; - default: - throw UnimplementedError('Unimplemented version change type'); - } - } - - @override - Future runForPackage(RepositoryPackage package) async { - String nextVersionString; - - _VersionIncrementType? versionChange = _versionChange; - - // If the change type is `minimal` determine what changes, if any, are - // needed. - if (versionChange == null && - getStringArg(_versionTypeFlag) == _versionMinimal) { - final Directory gitRoot = - packagesDir.fileSystem.directory((await gitDir).path); - final String relativePackagePath = - getRelativePosixPath(package.directory, from: gitRoot); - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: _changedFiles, - relativePackagePath: relativePackagePath); - - if (!state.hasChanges) { - return PackageResult.skip('No changes to package'); - } - if (state.needsVersionChange) { - versionChange = _VersionIncrementType.bugfix; - } - } - - if (versionChange != null) { - final Version? updatedVersion = - _updatePubspecVersion(package, versionChange); - if (updatedVersion == null) { - return PackageResult.fail( - ['Could not determine current version.']); - } - nextVersionString = updatedVersion.toString(); - print('${indentation}Incremented version to $nextVersionString.'); - } else { - nextVersionString = 'NEXT'; - } - - final _ChangelogUpdateOutcome updateOutcome = - _updateChangelog(package, nextVersionString); - switch (updateOutcome) { - case _ChangelogUpdateOutcome.addedSection: - print('${indentation}Added a $nextVersionString section.'); - break; - case _ChangelogUpdateOutcome.updatedSection: - print('${indentation}Updated NEXT section.'); - break; - case _ChangelogUpdateOutcome.failed: - return PackageResult.fail(['Could not update CHANGELOG.md.']); - } - - return PackageResult.success(); - } - - _ChangelogUpdateOutcome _updateChangelog( - RepositoryPackage package, String version) { - if (!package.changelogFile.existsSync()) { - printError('${indentation}Missing CHANGELOG.md.'); - return _ChangelogUpdateOutcome.failed; - } - - final String newHeader = '## $version'; - final RegExp listItemPattern = RegExp(r'^(\s*[-*])'); - - final StringBuffer newChangelog = StringBuffer(); - _ChangelogUpdateState state = _ChangelogUpdateState.findingFirstSection; - bool updatedExistingSection = false; - - for (final String line in package.changelogFile.readAsLinesSync()) { - switch (state) { - case _ChangelogUpdateState.findingFirstSection: - final String trimmedLine = line.trim(); - if (trimmedLine.isEmpty) { - // Discard any whitespace at the top of the file. - } else if (trimmedLine == '## NEXT') { - // Replace the header with the new version (which may also be NEXT). - newChangelog.writeln(newHeader); - // Find the existing list to add to. - state = _ChangelogUpdateState.findingFirstListItem; - } else { - // The first content in the file isn't a NEXT section, so just add - // the new section. - [ - newHeader, - '', - ..._changelogAdditionsAsList(), - '', - line, // Don't drop the current line. - ].forEach(newChangelog.writeln); - state = _ChangelogUpdateState.finishedUpdating; - } - break; - case _ChangelogUpdateState.findingFirstListItem: - final RegExpMatch? match = listItemPattern.firstMatch(line); - if (match != null) { - final String listMarker = match[1]!; - // Add the new items on top. If the new change is changing the - // version, then the new item should be more relevant to package - // clients than anything that was already there. If it's still - // NEXT, the order doesn't matter. - [ - ..._changelogAdditionsAsList(listMarker: listMarker), - line, // Don't drop the current line. - ].forEach(newChangelog.writeln); - state = _ChangelogUpdateState.finishedUpdating; - updatedExistingSection = true; - } else if (line.trim().isEmpty) { - // Scan past empty lines, but keep them. - newChangelog.writeln(line); - } else { - printError(' Existing NEXT section has unrecognized format.'); - return _ChangelogUpdateOutcome.failed; - } - break; - case _ChangelogUpdateState.finishedUpdating: - // Once changes are done, add the rest of the lines as-is. - newChangelog.writeln(line); - break; - } - } - - package.changelogFile.writeAsStringSync(newChangelog.toString()); - - return updatedExistingSection - ? _ChangelogUpdateOutcome.updatedSection - : _ChangelogUpdateOutcome.addedSection; - } - - /// Returns the changelog to add as a Markdown list, using the given list - /// bullet style (default to the repository standard of '*'), and adding - /// any missing periods. - /// - /// E.g., 'A line\nAnother line.' will become: - /// ``` - /// [ '* A line.', '* Another line.' ] - /// ``` - Iterable _changelogAdditionsAsList({String listMarker = '*'}) { - return getStringArg(_changelogFlag).split('\n').map((String entry) { - String standardizedEntry = entry.trim(); - if (!standardizedEntry.endsWith('.')) { - standardizedEntry = '$standardizedEntry.'; - } - return '$listMarker $standardizedEntry'; - }); - } - - /// Updates the version in [package]'s pubspec according to [type], returning - /// the new version, or null if there was an error updating the version. - Version? _updatePubspecVersion( - RepositoryPackage package, _VersionIncrementType type) { - final Pubspec pubspec = package.parsePubspec(); - final Version? currentVersion = pubspec.version; - if (currentVersion == null) { - printError('${indentation}No version in pubspec.yaml'); - return null; - } - - // For versions less than 1.0, shift the change down one component per - // Dart versioning conventions. - final _VersionIncrementType adjustedType = currentVersion.major > 0 - ? type - : _VersionIncrementType.values[type.index - 1]; - - final Version newVersion = _nextVersion(currentVersion, adjustedType); - - // Write the new version to the pubspec. - final YamlEditor editablePubspec = - YamlEditor(package.pubspecFile.readAsStringSync()); - editablePubspec.update(['version'], newVersion.toString()); - package.pubspecFile.writeAsStringSync(editablePubspec.toString()); - - return newVersion; - } - - Version _nextVersion(Version version, _VersionIncrementType type) { - switch (type) { - case _VersionIncrementType.minor: - return version.nextMinor; - case _VersionIncrementType.bugfix: - return version.nextPatch; - case _VersionIncrementType.build: - final int buildNumber = - version.build.isEmpty ? 0 : version.build.first as int; - return Version(version.major, version.minor, version.patch, - build: '${buildNumber + 1}'); - } - } -} diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart deleted file mode 100644 index b3be672066d9..000000000000 --- a/script/tool/lib/src/version_check_command.dart +++ /dev/null @@ -1,592 +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 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:http/http.dart' as http; -import 'package:meta/meta.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:pub_semver/pub_semver.dart'; - -import 'common/core.dart'; -import 'common/git_version_finder.dart'; -import 'common/package_looping_command.dart'; -import 'common/package_state_utils.dart'; -import 'common/process_runner.dart'; -import 'common/pub_version_finder.dart'; -import 'common/repository_package.dart'; - -/// Categories of version change types. -enum NextVersionType { - /// A breaking change. - BREAKING_MAJOR, - - /// A minor change (e.g., added feature). - MINOR, - - /// A bugfix change. - PATCH, - - /// The release of an existing pre-1.0 version. - V1_RELEASE, -} - -/// The state of a package's version relative to the comparison base. -enum _CurrentVersionState { - /// The version is unchanged. - unchanged, - - /// The version has increased, and the transition is valid. - validIncrease, - - /// The version has decrease, and the transition is a valid revert. - validRevert, - - /// The version has changed, and the transition is invalid. - invalidChange, - - /// There was an error determining the version state. - unknown, -} - -/// Returns the set of allowed next non-prerelease versions, with their change -/// type, for [version]. -/// -/// [newVersion] is used to check whether this is a pre-1.0 version bump, as -/// those have different semver rules. -@visibleForTesting -Map getAllowedNextVersions( - Version version, { - required Version newVersion, -}) { - final Map allowedNextVersions = - { - version.nextMajor: NextVersionType.BREAKING_MAJOR, - version.nextMinor: NextVersionType.MINOR, - version.nextPatch: NextVersionType.PATCH, - }; - - if (version.major < 1 && newVersion.major < 1) { - int nextBuildNumber = -1; - if (version.build.isEmpty) { - nextBuildNumber = 1; - } else { - final int currentBuildNumber = version.build.first as int; - nextBuildNumber = currentBuildNumber + 1; - } - final Version nextBuildVersion = Version( - version.major, - version.minor, - version.patch, - build: nextBuildNumber.toString(), - ); - allowedNextVersions.clear(); - allowedNextVersions[version.nextMajor] = NextVersionType.V1_RELEASE; - allowedNextVersions[version.nextMinor] = NextVersionType.BREAKING_MAJOR; - allowedNextVersions[version.nextPatch] = NextVersionType.MINOR; - allowedNextVersions[nextBuildVersion] = NextVersionType.PATCH; - } - return allowedNextVersions; -} - -/// A command to validate version changes to packages. -class VersionCheckCommand extends PackageLoopingCommand { - /// Creates an instance of the version check command. - VersionCheckCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - GitDir? gitDir, - http.Client? httpClient, - }) : _pubVersionFinder = - PubVersionFinder(httpClient: httpClient ?? http.Client()), - super( - packagesDir, - processRunner: processRunner, - platform: platform, - gitDir: gitDir, - ) { - argParser.addFlag( - _againstPubFlag, - help: 'Whether the version check should run against the version on pub.\n' - 'Defaults to false, which means the version check only run against ' - 'the previous version in code.', - ); - argParser.addOption(_prLabelsArg, - help: 'A comma-separated list of labels associated with this PR, ' - 'if applicable.\n\n' - 'If supplied, this may be to allow overrides to some version ' - 'checks.'); - argParser.addFlag(_checkForMissingChanges, - help: 'Validates that changes to packages include CHANGELOG and ' - 'version changes unless they meet an established exemption.\n\n' - 'If used with --$_prLabelsArg, this is should only be ' - 'used in pre-submit CI checks, to prevent post-submit breakage ' - 'when labels are no longer applicable.', - hide: true); - argParser.addFlag(_ignorePlatformInterfaceBreaks, - help: 'Bypasses the check that platform interfaces do not contain ' - 'breaking changes.\n\n' - 'This is only intended for use in post-submit CI checks, to ' - 'prevent post-submit breakage when overriding the check with ' - 'labels. Pre-submit checks should always use ' - '--$_prLabelsArg instead.', - hide: true); - } - - static const String _againstPubFlag = 'against-pub'; - static const String _prLabelsArg = 'pr-labels'; - static const String _checkForMissingChanges = 'check-for-missing-changes'; - static const String _ignorePlatformInterfaceBreaks = - 'ignore-platform-interface-breaks'; - - /// The label that must be on a PR to allow a breaking - /// change to a platform interface. - static const String _breakingChangeOverrideLabel = - 'override: allow breaking change'; - - /// The label that must be on a PR to allow skipping a version change for a PR - /// that would normally require one. - static const String _missingVersionChangeOverrideLabel = - 'override: no versioning needed'; - - /// The label that must be on a PR to allow skipping a CHANGELOG change for a - /// PR that would normally require one. - static const String _missingChangelogChangeOverrideLabel = - 'override: no changelog needed'; - - final PubVersionFinder _pubVersionFinder; - - late final GitVersionFinder _gitVersionFinder; - late final String _mergeBase; - late final List _changedFiles; - - late final Set _prLabels = _getPRLabels(); - - @override - final String name = 'version-check'; - - @override - final String description = - 'Checks if the versions of packages have been incremented per pub specification.\n' - 'Also checks if the latest version in CHANGELOG matches the version in pubspec.\n\n' - 'This command requires "pub" and "flutter" to be in your path.'; - - @override - bool get hasLongOutput => false; - - @override - Future initializeRun() async { - _gitVersionFinder = await retrieveVersionFinder(); - _mergeBase = await _gitVersionFinder.getBaseSha(); - _changedFiles = await _gitVersionFinder.getChangedFiles(); - } - - @override - Future runForPackage(RepositoryPackage package) async { - final Pubspec? pubspec = _tryParsePubspec(package); - if (pubspec == null) { - // No remaining checks make sense, so fail immediately. - return PackageResult.fail(['Invalid pubspec.yaml.']); - } - - if (pubspec.publishTo == 'none') { - return PackageResult.skip('Found "publish_to: none".'); - } - - final Version? currentPubspecVersion = pubspec.version; - if (currentPubspecVersion == null) { - printError('${indentation}No version found in pubspec.yaml. A package ' - 'that intentionally has no version should be marked ' - '"publish_to: none".'); - // No remaining checks make sense, so fail immediately. - return PackageResult.fail(['No pubspec.yaml version.']); - } - - final List errors = []; - - bool versionChanged; - final _CurrentVersionState versionState = - await _getVersionState(package, pubspec: pubspec); - switch (versionState) { - case _CurrentVersionState.unchanged: - versionChanged = false; - break; - case _CurrentVersionState.validIncrease: - case _CurrentVersionState.validRevert: - versionChanged = true; - break; - case _CurrentVersionState.invalidChange: - versionChanged = true; - errors.add('Disallowed version change.'); - break; - case _CurrentVersionState.unknown: - versionChanged = false; - errors.add('Unable to determine previous version.'); - break; - } - - if (!(await _validateChangelogVersion(package, - pubspec: pubspec, pubspecVersionState: versionState))) { - errors.add('CHANGELOG.md failed validation.'); - } - - // If there are no other issues, make sure that there isn't a missing - // change to the version and/or CHANGELOG. - if (getBoolArg(_checkForMissingChanges) && - !versionChanged && - errors.isEmpty) { - final String? error = await _checkForMissingChangeError(package); - if (error != null) { - errors.add(error); - } - } - - return errors.isEmpty - ? PackageResult.success() - : PackageResult.fail(errors); - } - - @override - Future completeRun() async { - _pubVersionFinder.httpClient.close(); - } - - /// Returns the previous published version of [package]. - /// - /// [packageName] must be the actual name of the package as published (i.e., - /// the name from pubspec.yaml, not the on disk name if different.) - Future _fetchPreviousVersionFromPub(String packageName) async { - final PubVersionFinderResponse pubVersionFinderResponse = - await _pubVersionFinder.getPackageVersion(packageName: packageName); - switch (pubVersionFinderResponse.result) { - case PubVersionFinderResult.success: - return pubVersionFinderResponse.versions.first; - case PubVersionFinderResult.fail: - printError(''' -${indentation}Error fetching version on pub for $packageName. -${indentation}HTTP Status ${pubVersionFinderResponse.httpResponse.statusCode} -${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} -'''); - return null; - case PubVersionFinderResult.noPackageFound: - return Version.none; - } - } - - /// Returns the version of [package] from git at the base comparison hash. - Future _getPreviousVersionFromGit(RepositoryPackage package) async { - final File pubspecFile = package.pubspecFile; - final String relativePath = - path.relative(pubspecFile.absolute.path, from: (await gitDir).path); - // Use Posix-style paths for git. - final String gitPath = path.style == p.Style.windows - ? p.posix.joinAll(path.split(relativePath)) - : relativePath; - return await _gitVersionFinder.getPackageVersion(gitPath, - gitRef: _mergeBase); - } - - /// Returns the state of the verison of [package] relative to the comparison - /// base (git or pub, depending on flags). - Future<_CurrentVersionState> _getVersionState( - RepositoryPackage package, { - required Pubspec pubspec, - }) async { - // This method isn't called unless `version` is non-null. - final Version currentVersion = pubspec.version!; - Version? previousVersion; - String previousVersionSource; - if (getBoolArg(_againstPubFlag)) { - previousVersionSource = 'pub'; - previousVersion = await _fetchPreviousVersionFromPub(pubspec.name); - if (previousVersion == null) { - return _CurrentVersionState.unknown; - } - if (previousVersion != Version.none) { - print( - '$indentation${pubspec.name}: Current largest version on pub: $previousVersion'); - } - } else { - previousVersionSource = _mergeBase; - previousVersion = - await _getPreviousVersionFromGit(package) ?? Version.none; - } - if (previousVersion == Version.none) { - print('${indentation}Unable to find previous version ' - '${getBoolArg(_againstPubFlag) ? 'on pub server' : 'at git base'}.'); - logWarning( - '${indentation}If this package is not new, something has gone wrong.'); - return _CurrentVersionState.validIncrease; // Assume new, thus valid. - } - - if (previousVersion == currentVersion) { - print('${indentation}No version change.'); - return _CurrentVersionState.unchanged; - } - - // Check for reverts when doing local validation. - if (!getBoolArg(_againstPubFlag) && currentVersion < previousVersion) { - // Since this skips validation, try to ensure that it really is likely - // to be a revert rather than a typo by checking that the transition - // from the lower version to the new version would have been valid. - if (_shouldAllowVersionChange( - oldVersion: currentVersion, newVersion: previousVersion)) { - logWarning('${indentation}New version is lower than previous version. ' - 'This is assumed to be a revert.'); - return _CurrentVersionState.validRevert; - } - } - - final Map allowedNextVersions = - getAllowedNextVersions(previousVersion, newVersion: currentVersion); - - if (_shouldAllowVersionChange( - oldVersion: previousVersion, newVersion: currentVersion)) { - print('$indentation$previousVersion -> $currentVersion'); - } else { - printError('${indentation}Incorrectly updated version.\n' - '${indentation}HEAD: $currentVersion, $previousVersionSource: $previousVersion.\n' - '${indentation}Allowed versions: $allowedNextVersions'); - return _CurrentVersionState.invalidChange; - } - - // Check whether the version (or for a pre-release, the version that - // pre-release would eventually be released as) is a breaking change, and - // if so, validate it. - final Version targetReleaseVersion = - currentVersion.isPreRelease ? currentVersion.nextPatch : currentVersion; - if (allowedNextVersions[targetReleaseVersion] == - NextVersionType.BREAKING_MAJOR && - !_validateBreakingChange(package)) { - printError('${indentation}Breaking change detected.\n' - '${indentation}Breaking changes to platform interfaces are not ' - 'allowed without explicit justification.\n' - '${indentation}See ' - 'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages ' - 'for more information.'); - return _CurrentVersionState.invalidChange; - } - - return _CurrentVersionState.validIncrease; - } - - /// Checks whether or not [package]'s CHANGELOG's versioning is correct, - /// both that it matches [pubspec] and that NEXT is used correctly, printing - /// the results of its checks. - /// - /// Returns false if the CHANGELOG fails validation. - Future _validateChangelogVersion( - RepositoryPackage package, { - required Pubspec pubspec, - required _CurrentVersionState pubspecVersionState, - }) async { - // This method isn't called unless `version` is non-null. - final Version fromPubspec = pubspec.version!; - - // get first version from CHANGELOG - final File changelog = package.changelogFile; - final List lines = changelog.readAsLinesSync(); - String? firstLineWithText; - final Iterator iterator = lines.iterator; - while (iterator.moveNext()) { - if (iterator.current.trim().isNotEmpty) { - firstLineWithText = iterator.current.trim(); - break; - } - } - // Remove all leading mark down syntax from the version line. - String? versionString = firstLineWithText?.split(' ').last; - - final String badNextErrorMessage = '${indentation}When bumping the version ' - 'for release, the NEXT section should be incorporated into the new ' - "version's release notes."; - - // Skip validation for the special NEXT version that's used to accumulate - // changes that don't warrant publishing on their own. - final bool hasNextSection = versionString == 'NEXT'; - if (hasNextSection) { - // NEXT should not be present in a commit that increases the version. - if (pubspecVersionState == _CurrentVersionState.validIncrease || - pubspecVersionState == _CurrentVersionState.invalidChange) { - printError(badNextErrorMessage); - return false; - } - print( - '${indentation}Found NEXT; validating next version in the CHANGELOG.'); - // Ensure that the version in pubspec hasn't changed without updating - // CHANGELOG. That means the next version entry in the CHANGELOG should - // pass the normal validation. - versionString = null; - while (iterator.moveNext()) { - if (iterator.current.trim().startsWith('## ')) { - versionString = iterator.current.trim().split(' ').last; - break; - } - } - } - - if (versionString == null) { - printError('${indentation}Unable to find a version in CHANGELOG.md'); - print('${indentation}The current version should be on a line starting ' - 'with "## ", either on the first non-empty line or after a "## NEXT" ' - 'section.'); - return false; - } - - final Version fromChangeLog; - try { - fromChangeLog = Version.parse(versionString); - } on FormatException { - printError('"$versionString" could not be parsed as a version.'); - return false; - } - - if (fromPubspec != fromChangeLog) { - printError(''' -${indentation}Versions in CHANGELOG.md and pubspec.yaml do not match. -${indentation}The version in pubspec.yaml is $fromPubspec. -${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. -'''); - return false; - } - - // If NEXT wasn't the first section, it should not exist at all. - if (!hasNextSection) { - final RegExp nextRegex = RegExp(r'^#+\s*NEXT\s*$'); - if (lines.any((String line) => nextRegex.hasMatch(line))) { - printError(badNextErrorMessage); - return false; - } - } - - return true; - } - - Pubspec? _tryParsePubspec(RepositoryPackage package) { - try { - final Pubspec pubspec = package.parsePubspec(); - return pubspec; - } on Exception catch (exception) { - printError('${indentation}Failed to parse `pubspec.yaml`: $exception}'); - return null; - } - } - - /// Checks whether the current breaking change to [package] should be allowed, - /// logging extra information for auditing when allowing unusual cases. - bool _validateBreakingChange(RepositoryPackage package) { - // Only platform interfaces have breaking change restrictions. - if (!package.isPlatformInterface) { - return true; - } - - if (getBoolArg(_ignorePlatformInterfaceBreaks)) { - logWarning( - '${indentation}Allowing breaking change to ${package.displayName} ' - 'due to --$_ignorePlatformInterfaceBreaks'); - return true; - } - - if (_prLabels.contains(_breakingChangeOverrideLabel)) { - logWarning( - '${indentation}Allowing breaking change to ${package.displayName} ' - 'due to the "$_breakingChangeOverrideLabel" label.'); - return true; - } - - return false; - } - - /// Returns the labels associated with this PR, if any, or an empty set - /// if that flag is not provided. - Set _getPRLabels() { - final String labels = getStringArg(_prLabelsArg); - if (labels.isEmpty) { - return {}; - } - return labels.split(',').map((String label) => label.trim()).toSet(); - } - - /// Returns true if the given version transition should be allowed. - bool _shouldAllowVersionChange( - {required Version oldVersion, required Version newVersion}) { - // Get the non-pre-release next version mapping. - final Map allowedNextVersions = - getAllowedNextVersions(oldVersion, newVersion: newVersion); - - if (allowedNextVersions.containsKey(newVersion)) { - return true; - } - // Allow a pre-release version of a version that would be a valid - // transition. - if (newVersion.isPreRelease) { - final Version targetReleaseVersion = newVersion.nextPatch; - if (allowedNextVersions.containsKey(targetReleaseVersion)) { - return true; - } - } - return false; - } - - /// Returns an error string if the changes to this package should have - /// resulted in a version change, or shoud have resulted in a CHANGELOG change - /// but didn't. - /// - /// This should only be called if the version did not change. - Future _checkForMissingChangeError(RepositoryPackage package) async { - // Find the relative path to the current package, as it would appear at the - // beginning of a path reported by getChangedFiles() (which always uses - // Posix paths). - final Directory gitRoot = - packagesDir.fileSystem.directory((await gitDir).path); - final String relativePackagePath = - getRelativePosixPath(package.directory, from: gitRoot); - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: _changedFiles, - relativePackagePath: relativePackagePath, - git: await retrieveVersionFinder()); - - if (!state.hasChanges) { - return null; - } - - if (state.needsVersionChange) { - if (_prLabels.contains(_missingVersionChangeOverrideLabel)) { - logWarning('Ignoring lack of version change due to the ' - '"$_missingVersionChangeOverrideLabel" label.'); - } else { - printError( - 'No version change found, but the change to this package could ' - 'not be verified to be exempt from version changes according to ' - 'repository policy. If this is a false positive, please comment in ' - 'the PR to explain why the PR is exempt, and add (or ask your ' - 'reviewer to add) the "$_missingVersionChangeOverrideLabel" ' - 'label.'); - return 'Missing version change'; - } - } - - if (!state.hasChangelogChange && state.needsChangelogChange) { - if (_prLabels.contains(_missingChangelogChangeOverrideLabel)) { - logWarning('Ignoring lack of CHANGELOG update due to the ' - '"$_missingChangelogChangeOverrideLabel" label.'); - } else { - printError( - 'No CHANGELOG change found. If this PR needs an exemption from ' - 'the standard policy of listing all changes in the CHANGELOG, ' - 'comment in the PR to explain why the PR is exempt, and add (or ' - 'ask your reviewer to add) the ' - '"$_missingChangelogChangeOverrideLabel" label. Otherwise, ' - 'please add a NEXT entry in the CHANGELOG as described in ' - 'the contributing guide.'); - return 'Missing CHANGELOG change'; - } - } - - return null; - } -} diff --git a/script/tool/lib/src/xcode_analyze_command.dart b/script/tool/lib/src/xcode_analyze_command.dart deleted file mode 100644 index a81bf15477af..000000000000 --- a/script/tool/lib/src/xcode_analyze_command.dart +++ /dev/null @@ -1,133 +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 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/plugin_utils.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; -import 'common/xcode.dart'; - -/// The command to run Xcode's static analyzer on plugins. -class XcodeAnalyzeCommand extends PackageLoopingCommand { - /// Creates an instance of the test command. - XcodeAnalyzeCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : _xcode = Xcode(processRunner: processRunner, log: true), - super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addFlag(platformIOS, help: 'Analyze iOS'); - argParser.addFlag(platformMacOS, help: 'Analyze macOS'); - argParser.addOption(_minIOSVersionArg, - help: 'Sets the minimum iOS deployment version to use when compiling, ' - 'overriding the default minimum version. This can be used to find ' - 'deprecation warnings that will affect the plugin in the future.'); - argParser.addOption(_minMacOSVersionArg, - help: - 'Sets the minimum macOS deployment version to use when compiling, ' - 'overriding the default minimum version. This can be used to find ' - 'deprecation warnings that will affect the plugin in the future.'); - } - - static const String _minIOSVersionArg = 'ios-min-version'; - static const String _minMacOSVersionArg = 'macos-min-version'; - - final Xcode _xcode; - - @override - final String name = 'xcode-analyze'; - - @override - final String description = - 'Runs Xcode analysis on the iOS and/or macOS example apps.'; - - @override - Future initializeRun() async { - if (!(getBoolArg(platformIOS) || getBoolArg(platformMacOS))) { - printError('At least one platform flag must be provided.'); - throw ToolExit(exitInvalidArguments); - } - } - - @override - Future runForPackage(RepositoryPackage package) async { - final bool testIOS = getBoolArg(platformIOS) && - pluginSupportsPlatform(platformIOS, package, - requiredMode: PlatformSupport.inline); - final bool testMacOS = getBoolArg(platformMacOS) && - pluginSupportsPlatform(platformMacOS, package, - requiredMode: PlatformSupport.inline); - - final bool multiplePlatformsRequested = - getBoolArg(platformIOS) && getBoolArg(platformMacOS); - if (!(testIOS || testMacOS)) { - return PackageResult.skip('Not implemented for target platform(s).'); - } - - final String minIOSVersion = getStringArg(_minIOSVersionArg); - final String minMacOSVersion = getStringArg(_minMacOSVersionArg); - - final List failures = []; - if (testIOS && - !await _analyzePlugin(package, 'iOS', extraFlags: [ - '-destination', - 'generic/platform=iOS Simulator', - if (minIOSVersion.isNotEmpty) - 'IPHONEOS_DEPLOYMENT_TARGET=$minIOSVersion', - ])) { - failures.add('iOS'); - } - if (testMacOS && - !await _analyzePlugin(package, 'macOS', extraFlags: [ - if (minMacOSVersion.isNotEmpty) - 'MACOSX_DEPLOYMENT_TARGET=$minMacOSVersion', - ])) { - failures.add('macOS'); - } - - // Only provide the failing platform in the failure details if testing - // multiple platforms, otherwise it's just noise. - return failures.isEmpty - ? PackageResult.success() - : PackageResult.fail( - multiplePlatformsRequested ? failures : []); - } - - /// Analyzes [plugin] for [platform], returning true if it passed analysis. - Future _analyzePlugin( - RepositoryPackage plugin, - String platform, { - List extraFlags = const [], - }) async { - bool passing = true; - for (final RepositoryPackage example in plugin.getExamples()) { - // Running tests and static analyzer. - final String examplePath = getRelativePosixPath(example.directory, - from: plugin.directory.parent); - print('Running $platform tests and analyzer for $examplePath...'); - final int exitCode = await _xcode.runXcodeBuild( - example.directory, - actions: ['analyze'], - workspace: '${platform.toLowerCase()}/Runner.xcworkspace', - scheme: 'Runner', - configuration: 'Debug', - extraFlags: [ - ...extraFlags, - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - ); - if (exitCode == 0) { - printSuccess('$examplePath ($platform) passed analysis.'); - } else { - printError('$examplePath ($platform) failed analysis.'); - passing = false; - } - } - return passing; - } -} diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index e450a1114e87..60884fdeb8ae 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,8 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.13.0 +version: 0.13.4+2 +publish_to: none # See README.md dependencies: args: ^2.1.0 diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart deleted file mode 100644 index e6b910960846..000000000000 --- a/script/tool/test/analyze_command_test.dart +++ /dev/null @@ -1,425 +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:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/analyze_command.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late RecordingProcessRunner processRunner; - late CommandRunner runner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final AnalyzeCommand analyzeCommand = AnalyzeCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner('analyze_command', 'Test for analyze_command'); - runner.addCommand(analyzeCommand); - }); - - test('analyzes all packages', () async { - final RepositoryPackage package1 = createFakePackage('a', packagesDir); - final RepositoryPackage plugin2 = createFakePlugin('b', packagesDir); - - await runCapturingPrint(runner, ['analyze']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', const ['pub', 'get'], package1.path), - ProcessCall('dart', const ['analyze', '--fatal-infos'], - package1.path), - ProcessCall('flutter', const ['pub', 'get'], plugin2.path), - ProcessCall( - 'dart', const ['analyze', '--fatal-infos'], plugin2.path), - ])); - }); - - test('skips flutter pub get for examples', () async { - final RepositoryPackage plugin1 = createFakePlugin('a', packagesDir); - - await runCapturingPrint(runner, ['analyze']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', const ['pub', 'get'], plugin1.path), - ProcessCall( - 'dart', const ['analyze', '--fatal-infos'], plugin1.path), - ])); - }); - - test('runs flutter pub get for non-example subpackages', () async { - final RepositoryPackage mainPackage = createFakePackage('a', packagesDir); - final Directory otherPackagesDir = - mainPackage.directory.childDirectory('other_packages'); - final RepositoryPackage subpackage1 = - createFakePackage('subpackage1', otherPackagesDir); - final RepositoryPackage subpackage2 = - createFakePackage('subpackage2', otherPackagesDir); - - await runCapturingPrint(runner, ['analyze']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'flutter', const ['pub', 'get'], mainPackage.path), - ProcessCall( - 'flutter', const ['pub', 'get'], subpackage1.path), - ProcessCall( - 'flutter', const ['pub', 'get'], subpackage2.path), - ProcessCall('dart', const ['analyze', '--fatal-infos'], - mainPackage.path), - ])); - }); - - test('passes lib/ directory with --lib-only', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - await runCapturingPrint(runner, ['analyze', '--lib-only']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', const ['pub', 'get'], package.path), - ProcessCall('dart', const ['analyze', '--fatal-infos', 'lib'], - package.path), - ])); - }); - - test('skips when missing lib/ directory with --lib-only', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - package.libDirectory.deleteSync(); - - final List output = - await runCapturingPrint(runner, ['analyze', '--lib-only']); - - expect(processRunner.recordedCalls, isEmpty); - expect( - output, - containsAllInOrder([ - contains('SKIPPING: No lib/ directory'), - ]), - ); - }); - - test( - 'does not run flutter pub get for non-example subpackages with --lib-only', - () async { - final RepositoryPackage mainPackage = createFakePackage('a', packagesDir); - final Directory otherPackagesDir = - mainPackage.directory.childDirectory('other_packages'); - createFakePackage('subpackage1', otherPackagesDir); - createFakePackage('subpackage2', otherPackagesDir); - - await runCapturingPrint(runner, ['analyze', '--lib-only']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'flutter', const ['pub', 'get'], mainPackage.path), - ProcessCall('dart', const ['analyze', '--fatal-infos', 'lib'], - mainPackage.path), - ])); - }); - - test("don't elide a non-contained example package", () async { - final RepositoryPackage plugin1 = createFakePlugin('a', packagesDir); - final RepositoryPackage plugin2 = createFakePlugin('example', packagesDir); - - await runCapturingPrint(runner, ['analyze']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', const ['pub', 'get'], plugin1.path), - ProcessCall( - 'dart', const ['analyze', '--fatal-infos'], plugin1.path), - ProcessCall('flutter', const ['pub', 'get'], plugin2.path), - ProcessCall( - 'dart', const ['analyze', '--fatal-infos'], plugin2.path), - ])); - }); - - test('uses a separate analysis sdk', () async { - final RepositoryPackage plugin = createFakePlugin('a', packagesDir); - - await runCapturingPrint( - runner, ['analyze', '--analysis-sdk', 'foo/bar/baz']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'flutter', - const ['pub', 'get'], - plugin.path, - ), - ProcessCall( - 'foo/bar/baz/bin/dart', - const ['analyze', '--fatal-infos'], - plugin.path, - ), - ]), - ); - }); - - test('downgrades first when requested', () async { - final RepositoryPackage plugin = createFakePlugin('a', packagesDir); - - await runCapturingPrint(runner, ['analyze', '--downgrade']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'flutter', - const ['pub', 'downgrade'], - plugin.path, - ), - ProcessCall( - 'flutter', - const ['pub', 'get'], - plugin.path, - ), - ProcessCall( - 'dart', - const ['analyze', '--fatal-infos'], - plugin.path, - ), - ]), - ); - }); - - group('verifies analysis settings', () { - test('fails analysis_options.yaml', () async { - createFakePlugin('foo', packagesDir, - extraFiles: ['analysis_options.yaml']); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['analyze'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Found an extra analysis_options.yaml at /packages/foo/analysis_options.yaml'), - contains(' foo:\n' - ' Unexpected local analysis options'), - ]), - ); - }); - - test('fails .analysis_options', () async { - createFakePlugin('foo', packagesDir, - extraFiles: ['.analysis_options']); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['analyze'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Found an extra analysis_options.yaml at /packages/foo/.analysis_options'), - contains(' foo:\n' - ' Unexpected local analysis options'), - ]), - ); - }); - - test('takes an allow list', () async { - final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, - extraFiles: ['analysis_options.yaml']); - - await runCapturingPrint( - runner, ['analyze', '--custom-analysis', 'foo']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', const ['pub', 'get'], plugin.path), - ProcessCall('dart', const ['analyze', '--fatal-infos'], - plugin.path), - ])); - }); - - test('takes an allow config file', () async { - final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, - extraFiles: ['analysis_options.yaml']); - final File allowFile = packagesDir.childFile('custom.yaml'); - allowFile.writeAsStringSync('- foo'); - - await runCapturingPrint( - runner, ['analyze', '--custom-analysis', allowFile.path]); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', const ['pub', 'get'], plugin.path), - ProcessCall('dart', const ['analyze', '--fatal-infos'], - plugin.path), - ])); - }); - - test('allows an empty config file', () async { - createFakePlugin('foo', packagesDir, - extraFiles: ['analysis_options.yaml']); - final File allowFile = packagesDir.childFile('custom.yaml'); - allowFile.createSync(); - - await expectLater( - () => runCapturingPrint( - runner, ['analyze', '--custom-analysis', allowFile.path]), - throwsA(isA())); - }); - - // See: https://github.com/flutter/flutter/issues/78994 - test('takes an empty allow list', () async { - createFakePlugin('foo', packagesDir, - extraFiles: ['analysis_options.yaml']); - - await expectLater( - () => runCapturingPrint( - runner, ['analyze', '--custom-analysis', '']), - throwsA(isA())); - }); - }); - - test('fails if "pub get" fails', () async { - createFakePlugin('foo', packagesDir); - - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(exitCode: 1) // flutter pub get - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['analyze'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to get dependencies'), - ]), - ); - }); - - test('fails if "pub downgrade" fails', () async { - createFakePlugin('foo', packagesDir); - - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(exitCode: 1) // flutter pub downgrade - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['analyze', '--downgrade'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to downgrade dependencies'), - ]), - ); - }); - - test('fails if "analyze" fails', () async { - createFakePlugin('foo', packagesDir); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(exitCode: 1) // dart analyze - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['analyze'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains(' foo'), - ]), - ); - }); - - // Ensure that the command used to analyze flutter/plugins in the Dart repo: - // https://github.com/dart-lang/sdk/blob/main/tools/bots/flutter/analyze_flutter_plugins.sh - // continues to work. - // - // DO NOT remove or modify this test without a coordination plan in place to - // modify the script above, as it is run from source, but out-of-repo. - // Contact stuartmorgan or devoncarew for assistance. - test('Dart repo analyze command works', () async { - final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, - extraFiles: ['analysis_options.yaml']); - final File allowFile = packagesDir.childFile('custom.yaml'); - allowFile.writeAsStringSync('- foo'); - - await runCapturingPrint(runner, [ - // DO NOT change this call; see comment above. - 'analyze', - '--analysis-sdk', - 'foo/bar/baz', - '--custom-analysis', - allowFile.path - ]); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'flutter', - const ['pub', 'get'], - plugin.path, - ), - ProcessCall( - 'foo/bar/baz/bin/dart', - const ['analyze', '--fatal-infos'], - plugin.path, - ), - ]), - ); - }); -} diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart deleted file mode 100644 index a819e7a12674..000000000000 --- a/script/tool/test/build_examples_command_test.dart +++ /dev/null @@ -1,634 +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:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/build_examples_command.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('build-example', () { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final BuildExamplesCommand command = BuildExamplesCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'build_examples_command', 'Test for build_example_command'); - runner.addCommand(command); - }); - - test('fails if no plaform flags are passed', () async { - Error? commandError; - final List output = await runCapturingPrint( - runner, ['build-examples'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('At least one platform must be provided'), - ])); - }); - - test('fails if building fails', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline), - }); - - processRunner - .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = - [ - MockProcess(exitCode: 1) // flutter pub get - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['build-examples', '--ios'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains(' plugin:\n' - ' plugin/example (iOS)'), - ])); - }); - - test('fails if a plugin has no examples', () async { - createFakePlugin('plugin', packagesDir, - examples: [], - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); - - processRunner - .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = - [ - MockProcess(exitCode: 1) // flutter pub get - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['build-examples', '--ios'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains(' plugin:\n' - ' No examples found'), - ])); - }); - - test('building for iOS when plugin is not set up for iOS results in no-op', - () async { - mockPlatform.isMacOS = true; - createFakePlugin('plugin', packagesDir); - - final List output = - await runCapturingPrint(runner, ['build-examples', '--ios']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('iOS is not supported by this plugin'), - ]), - ); - - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('building for iOS', () async { - mockPlatform.isMacOS = true; - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, - ['build-examples', '--ios', '--enable-experiment=exp1']); - - expect( - output, - containsAllInOrder([ - '\nBUILDING plugin/example for iOS', - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'build', - 'ios', - '--no-codesign', - '--enable-experiment=exp1' - ], - pluginExampleDirectory.path), - ])); - }); - - test( - 'building for Linux when plugin is not set up for Linux results in no-op', - () async { - mockPlatform.isLinux = true; - createFakePlugin('plugin', packagesDir); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--linux']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Linux is not supported by this plugin'), - ]), - ); - - // Output should be empty since running build-examples --linux with no - // Linux implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('building for Linux', () async { - mockPlatform.isLinux = true; - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--linux']); - - expect( - output, - containsAllInOrder([ - '\nBUILDING plugin/example for Linux', - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['build', 'linux'], pluginExampleDirectory.path), - ])); - }); - - test('building for macOS with no implementation results in no-op', - () async { - mockPlatform.isMacOS = true; - createFakePlugin('plugin', packagesDir); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--macos']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('macOS is not supported by this plugin'), - ]), - ); - - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('building for macOS', () async { - mockPlatform.isMacOS = true; - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--macos']); - - expect( - output, - containsAllInOrder([ - '\nBUILDING plugin/example for macOS', - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['build', 'macos'], pluginExampleDirectory.path), - ])); - }); - - test('building for web with no implementation results in no-op', () async { - createFakePlugin('plugin', packagesDir); - - final List output = - await runCapturingPrint(runner, ['build-examples', '--web']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('web is not supported by this plugin'), - ]), - ); - - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('building for web', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformWeb: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = - await runCapturingPrint(runner, ['build-examples', '--web']); - - expect( - output, - containsAllInOrder([ - '\nBUILDING plugin/example for web', - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['build', 'web'], pluginExampleDirectory.path), - ])); - }); - - test( - 'building for Windows when plugin is not set up for Windows results in no-op', - () async { - mockPlatform.isWindows = true; - createFakePlugin('plugin', packagesDir); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--windows']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Windows is not supported by this plugin'), - ]), - ); - - // Output should be empty since running build-examples --windows with no - // Windows implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('building for Windows', () async { - mockPlatform.isWindows = true; - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--windows']); - - expect( - output, - containsAllInOrder([ - '\nBUILDING plugin/example for Windows', - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const ['build', 'windows'], - pluginExampleDirectory.path), - ])); - }); - - test( - 'building for Android when plugin is not set up for Android results in no-op', - () async { - createFakePlugin('plugin', packagesDir); - - final List output = - await runCapturingPrint(runner, ['build-examples', '--apk']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Android is not supported by this plugin'), - ]), - ); - - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('building for Android', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, [ - 'build-examples', - '--apk', - ]); - - expect( - output, - containsAllInOrder([ - '\nBUILDING plugin/example for Android (apk)', - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['build', 'apk'], pluginExampleDirectory.path), - ])); - }); - - test('enable-experiment flag for Android', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - await runCapturingPrint(runner, - ['build-examples', '--apk', '--enable-experiment=exp1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const ['build', 'apk', '--enable-experiment=exp1'], - pluginExampleDirectory.path), - ])); - }); - - test('enable-experiment flag for ios', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - await runCapturingPrint(runner, - ['build-examples', '--ios', '--enable-experiment=exp1']); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'build', - 'ios', - '--no-codesign', - '--enable-experiment=exp1' - ], - pluginExampleDirectory.path), - ])); - }); - - test('logs skipped platforms', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - }); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--apk', '--ios', '--macos']); - - expect( - output, - containsAllInOrder([ - contains('Skipping unsupported platform(s): iOS, macOS'), - ]), - ); - }); - - group('packages', () { - test('builds when requested platform is supported by example', () async { - final RepositoryPackage package = createFakePackage( - 'package', packagesDir, isFlutter: true, extraFiles: [ - 'example/ios/Runner.xcodeproj/project.pbxproj' - ]); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--ios']); - - expect( - output, - containsAllInOrder([ - contains('Running for package'), - contains('BUILDING package/example for iOS'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'build', - 'ios', - '--no-codesign', - ], - getExampleDir(package).path), - ])); - }); - - test('skips non-Flutter examples', () async { - createFakePackage('package', packagesDir); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--ios']); - - expect( - output, - containsAllInOrder([ - contains('Running for package'), - contains('No examples found supporting requested platform(s).'), - ]), - ); - - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('skips when there is no example', () async { - createFakePackage('package', packagesDir, - isFlutter: true, examples: []); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--ios']); - - expect( - output, - containsAllInOrder([ - contains('Running for package'), - contains('No examples found supporting requested platform(s).'), - ]), - ); - - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('skip when example does not support requested platform', () async { - createFakePackage('package', packagesDir, - isFlutter: true, - extraFiles: ['example/linux/CMakeLists.txt']); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--ios']); - - expect( - output, - containsAllInOrder([ - contains('Running for package'), - contains('Skipping iOS for package/example; not supported.'), - contains('No examples found supporting requested platform(s).'), - ]), - ); - - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('logs skipped platforms when only some are supported', () async { - final RepositoryPackage package = createFakePackage( - 'package', packagesDir, - isFlutter: true, - extraFiles: ['example/linux/CMakeLists.txt']); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--apk', '--linux']); - - expect( - output, - containsAllInOrder([ - contains('Running for package'), - contains('Building for: Android, Linux'), - contains('Skipping Android for package/example; not supported.'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const ['build', 'linux'], - getExampleDir(package).path), - ])); - }); - }); - - test('The .pluginToolsConfig.yaml file', () async { - mockPlatform.isLinux = true; - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline), - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final File pluginExampleConfigFile = - pluginExampleDirectory.childFile('.pluginToolsConfig.yaml'); - pluginExampleConfigFile - .writeAsStringSync('buildFlags:\n global:\n - "test argument"'); - - final List output = [ - ...await runCapturingPrint( - runner, ['build-examples', '--linux']), - ...await runCapturingPrint( - runner, ['build-examples', '--macos']), - ]; - - expect( - output, - containsAllInOrder([ - '\nBUILDING plugin/example for Linux', - '\nBUILDING plugin/example for macOS', - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const ['build', 'linux', 'test argument'], - pluginExampleDirectory.path), - ProcessCall( - getFlutterCommand(mockPlatform), - const ['build', 'macos', 'test argument'], - pluginExampleDirectory.path), - ])); - }); - }); -} diff --git a/script/tool/test/common/file_utils_test.dart b/script/tool/test/common/file_utils_test.dart deleted file mode 100644 index 79b804e31ea5..000000000000 --- a/script/tool/test/common/file_utils_test.dart +++ /dev/null @@ -1,32 +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 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/file_utils.dart'; -import 'package:test/test.dart'; - -void main() { - test('works on Posix', () async { - final FileSystem fileSystem = - MemoryFileSystem(); - - final Directory base = fileSystem.directory('/').childDirectory('base'); - final File file = - childFileWithSubcomponents(base, ['foo', 'bar', 'baz.txt']); - - expect(file.absolute.path, '/base/foo/bar/baz.txt'); - }); - - test('works on Windows', () async { - final FileSystem fileSystem = - MemoryFileSystem(style: FileSystemStyle.windows); - - final Directory base = fileSystem.directory(r'C:\').childDirectory('base'); - final File file = - childFileWithSubcomponents(base, ['foo', 'bar', 'baz.txt']); - - expect(file.absolute.path, r'C:\base\foo\bar\baz.txt'); - }); -} diff --git a/script/tool/test/common/git_version_finder_test.dart b/script/tool/test/common/git_version_finder_test.dart deleted file mode 100644 index d5a5dd4fe876..000000000000 --- a/script/tool/test/common/git_version_finder_test.dart +++ /dev/null @@ -1,105 +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:io'; - -import 'package:flutter_plugin_tools/src/common/git_version_finder.dart'; -import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; - -import 'package_command_test.mocks.dart'; - -void main() { - late List?> gitDirCommands; - late String gitDiffResponse; - late MockGitDir gitDir; - String mergeBaseResponse = ''; - - setUp(() { - gitDirCommands = ?>[]; - gitDiffResponse = ''; - gitDir = MockGitDir(); - when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) - .thenAnswer((Invocation invocation) { - gitDirCommands.add(invocation.positionalArguments[0] as List?); - final MockProcessResult mockProcessResult = MockProcessResult(); - if (invocation.positionalArguments[0][0] == 'diff') { - when(mockProcessResult.stdout as String?) - .thenReturn(gitDiffResponse); - } else if (invocation.positionalArguments[0][0] == 'merge-base') { - when(mockProcessResult.stdout as String?) - .thenReturn(mergeBaseResponse); - } - return Future.value(mockProcessResult); - }); - }); - - test('No git diff should result no files changed', () async { - final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); - final List changedFiles = await finder.getChangedFiles(); - - expect(changedFiles, isEmpty); - }); - - test('get correct files changed based on git diff', () async { - gitDiffResponse = ''' -file1/file1.cc -file2/file2.cc -'''; - final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); - final List changedFiles = await finder.getChangedFiles(); - - expect(changedFiles, equals(['file1/file1.cc', 'file2/file2.cc'])); - }); - - test('get correct pubspec change based on git diff', () async { - gitDiffResponse = ''' -file1/pubspec.yaml -file2/file2.cc -'''; - final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); - final List changedFiles = await finder.getChangedPubSpecs(); - - expect(changedFiles, equals(['file1/pubspec.yaml'])); - }); - - test('use correct base sha if not specified', () async { - mergeBaseResponse = 'shaqwiueroaaidf12312jnadf123nd'; - gitDiffResponse = ''' -file1/pubspec.yaml -file2/file2.cc -'''; - - final GitVersionFinder finder = GitVersionFinder(gitDir, null); - await finder.getChangedFiles(); - verify(gitDir.runCommand( - ['diff', '--name-only', mergeBaseResponse, 'HEAD'])); - }); - - test('use correct base sha if specified', () async { - const String customBaseSha = 'aklsjdcaskf12312'; - gitDiffResponse = ''' -file1/pubspec.yaml -file2/file2.cc -'''; - final GitVersionFinder finder = GitVersionFinder(gitDir, customBaseSha); - await finder.getChangedFiles(); - verify(gitDir - .runCommand(['diff', '--name-only', customBaseSha, 'HEAD'])); - }); - - test('include uncommitted files if requested', () async { - const String customBaseSha = 'aklsjdcaskf12312'; - gitDiffResponse = ''' -file1/pubspec.yaml -file2/file2.cc -'''; - final GitVersionFinder finder = GitVersionFinder(gitDir, customBaseSha); - await finder.getChangedFiles(includeUncommitted: true); - // The call should not have HEAD as a final argument like the default diff. - verify(gitDir.runCommand(['diff', '--name-only', customBaseSha])); - }); -} - -class MockProcessResult extends Mock implements ProcessResult {} diff --git a/script/tool/test/common/gradle_test.dart b/script/tool/test/common/gradle_test.dart deleted file mode 100644 index 8df4a65b93a5..000000000000 --- a/script/tool/test/common/gradle_test.dart +++ /dev/null @@ -1,188 +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:io' as io; - -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/gradle.dart'; -import 'package:test/test.dart'; - -import '../mocks.dart'; -import '../util.dart'; - -void main() { - late FileSystem fileSystem; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - processRunner = RecordingProcessRunner(); - }); - - group('isConfigured', () { - test('reports true when configured on Windows', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', fileSystem.directory('/'), - extraFiles: ['android/gradlew.bat']); - final GradleProject project = GradleProject( - plugin, - processRunner: processRunner, - platform: MockPlatform(isWindows: true), - ); - - expect(project.isConfigured(), true); - }); - - test('reports true when configured on non-Windows', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', fileSystem.directory('/'), - extraFiles: ['android/gradlew']); - final GradleProject project = GradleProject( - plugin, - processRunner: processRunner, - platform: MockPlatform(isMacOS: true), - ); - - expect(project.isConfigured(), true); - }); - - test('reports false when not configured on Windows', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', fileSystem.directory('/'), - extraFiles: ['android/foo']); - final GradleProject project = GradleProject( - plugin, - processRunner: processRunner, - platform: MockPlatform(isWindows: true), - ); - - expect(project.isConfigured(), false); - }); - - test('reports true when configured on non-Windows', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', fileSystem.directory('/'), - extraFiles: ['android/foo']); - final GradleProject project = GradleProject( - plugin, - processRunner: processRunner, - platform: MockPlatform(isMacOS: true), - ); - - expect(project.isConfigured(), false); - }); - }); - - group('runCommand', () { - test('runs without arguments', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', fileSystem.directory('/'), - extraFiles: ['android/gradlew']); - final GradleProject project = GradleProject( - plugin, - processRunner: processRunner, - platform: MockPlatform(isMacOS: true), - ); - - final int exitCode = await project.runCommand('foo'); - - expect(exitCode, 0); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - plugin - .platformDirectory(FlutterPlatform.android) - .childFile('gradlew') - .path, - const [ - 'foo', - ], - plugin.platformDirectory(FlutterPlatform.android).path), - ])); - }); - - test('runs with arguments', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', fileSystem.directory('/'), - extraFiles: ['android/gradlew']); - final GradleProject project = GradleProject( - plugin, - processRunner: processRunner, - platform: MockPlatform(isMacOS: true), - ); - - final int exitCode = await project.runCommand( - 'foo', - arguments: ['--bar', '--baz'], - ); - - expect(exitCode, 0); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - plugin - .platformDirectory(FlutterPlatform.android) - .childFile('gradlew') - .path, - const [ - 'foo', - '--bar', - '--baz', - ], - plugin.platformDirectory(FlutterPlatform.android).path), - ])); - }); - - test('runs with the correct wrapper on Windows', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', fileSystem.directory('/'), - extraFiles: ['android/gradlew.bat']); - final GradleProject project = GradleProject( - plugin, - processRunner: processRunner, - platform: MockPlatform(isWindows: true), - ); - - final int exitCode = await project.runCommand('foo'); - - expect(exitCode, 0); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - plugin - .platformDirectory(FlutterPlatform.android) - .childFile('gradlew.bat') - .path, - const [ - 'foo', - ], - plugin.platformDirectory(FlutterPlatform.android).path), - ])); - }); - - test('returns error codes', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', fileSystem.directory('/'), - extraFiles: ['android/gradlew.bat']); - final GradleProject project = GradleProject( - plugin, - processRunner: processRunner, - platform: MockPlatform(isWindows: true), - ); - - processRunner.mockProcessesForExecutable[project.gradleWrapper.path] = - [ - MockProcess(exitCode: 1), - ]; - - final int exitCode = await project.runCommand('foo'); - - expect(exitCode, 1); - }); - }); -} diff --git a/script/tool/test/common/package_command_test.dart b/script/tool/test/common/package_command_test.dart deleted file mode 100644 index aa0a20253955..000000000000 --- a/script/tool/test/common/package_command_test.dart +++ /dev/null @@ -1,1069 +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:io'; - -import 'package:args/command_runner.dart'; -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/package_command.dart'; -import 'package:flutter_plugin_tools/src/common/process_runner.dart'; -import 'package:git/git.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import '../mocks.dart'; -import '../util.dart'; -import 'package_command_test.mocks.dart'; - -@GenerateMocks([GitDir]) -void main() { - late RecordingProcessRunner processRunner; - late SamplePackageCommand command; - late CommandRunner runner; - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late Directory thirdPartyPackagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - thirdPartyPackagesDir = packagesDir.parent - .childDirectory('third_party') - .childDirectory('packages'); - - final MockGitDir gitDir = MockGitDir(); - when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) - .thenAnswer((Invocation invocation) { - final List arguments = - invocation.positionalArguments[0]! as List; - // Attach the first argument to the command to make targeting the mock - // results easier. - final String gitCommand = arguments.removeAt(0); - return processRunner.run('git-$gitCommand', arguments); - }); - processRunner = RecordingProcessRunner(); - command = SamplePackageCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - gitDir: gitDir, - ); - runner = - CommandRunner('common_command', 'Test for common functionality'); - runner.addCommand(command); - }); - - group('plugin iteration', () { - test('all plugins from file system', () async { - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, ['sample']); - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - }); - - test('includes both plugins and packages', () async { - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - final RepositoryPackage package3 = - createFakePackage('package3', packagesDir); - final RepositoryPackage package4 = - createFakePackage('package4', packagesDir); - await runCapturingPrint(runner, ['sample']); - expect( - command.plugins, - unorderedEquals([ - plugin1.path, - plugin2.path, - package3.path, - package4.path, - ])); - }); - - test('includes packages without source', () async { - final RepositoryPackage package = - createFakePackage('package', packagesDir); - package.libDirectory.deleteSync(recursive: true); - - await runCapturingPrint(runner, ['sample']); - expect( - command.plugins, - unorderedEquals([ - package.path, - ])); - }); - - test('all plugins includes third_party/packages', () async { - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - final RepositoryPackage plugin3 = - createFakePlugin('plugin3', thirdPartyPackagesDir); - await runCapturingPrint(runner, ['sample']); - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path, plugin3.path])); - }); - - test('--packages limits packages', () async { - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir); - createFakePackage('package3', packagesDir); - final RepositoryPackage package4 = - createFakePackage('package4', packagesDir); - await runCapturingPrint( - runner, ['sample', '--packages=plugin1,package4']); - expect( - command.plugins, - unorderedEquals([ - plugin1.path, - package4.path, - ])); - }); - - test('--plugins acts as an alias to --packages', () async { - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir); - createFakePackage('package3', packagesDir); - final RepositoryPackage package4 = - createFakePackage('package4', packagesDir); - await runCapturingPrint( - runner, ['sample', '--plugins=plugin1,package4']); - expect( - command.plugins, - unorderedEquals([ - plugin1.path, - package4.path, - ])); - }); - - test('exclude packages when packages flag is specified', () async { - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--packages=plugin1,plugin2', - '--exclude=plugin1' - ]); - expect(command.plugins, unorderedEquals([plugin2.path])); - }); - - test("exclude packages when packages flag isn't specified", () async { - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir); - await runCapturingPrint( - runner, ['sample', '--exclude=plugin1,plugin2']); - expect(command.plugins, unorderedEquals([])); - }); - - test('exclude federated plugins when packages flag is specified', () async { - createFakePlugin('plugin1', packagesDir.childDirectory('federated')); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--packages=federated/plugin1,plugin2', - '--exclude=federated/plugin1' - ]); - expect(command.plugins, unorderedEquals([plugin2.path])); - }); - - test('exclude entire federated plugins when packages flag is specified', - () async { - createFakePlugin('plugin1', packagesDir.childDirectory('federated')); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--packages=federated/plugin1,plugin2', - '--exclude=federated' - ]); - expect(command.plugins, unorderedEquals([plugin2.path])); - }); - - test('exclude accepts config files', () async { - createFakePlugin('plugin1', packagesDir); - final File configFile = packagesDir.childFile('exclude.yaml'); - configFile.writeAsStringSync('- plugin1'); - - await runCapturingPrint(runner, [ - 'sample', - '--packages=plugin1', - '--exclude=${configFile.path}' - ]); - expect(command.plugins, unorderedEquals([])); - }); - - test( - 'explicitly specifying the plugin (group) name of a federated plugin ' - 'should include all plugins in the group', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin1/plugin1/plugin1.dart -'''), - ]; - final Directory pluginGroup = packagesDir.childDirectory('plugin1'); - final RepositoryPackage appFacingPackage = - createFakePlugin('plugin1', pluginGroup); - final RepositoryPackage platformInterfacePackage = - createFakePlugin('plugin1_platform_interface', pluginGroup); - final RepositoryPackage implementationPackage = - createFakePlugin('plugin1_web', pluginGroup); - - await runCapturingPrint( - runner, ['sample', '--base-sha=main', '--packages=plugin1']); - - expect( - command.plugins, - unorderedEquals([ - appFacingPackage.path, - platformInterfacePackage.path, - implementationPackage.path - ])); - }); - - test( - 'specifying the app-facing package of a federated plugin using its ' - 'fully qualified name should include only that package', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin1/plugin1/plugin1.dart -'''), - ]; - final Directory pluginGroup = packagesDir.childDirectory('plugin1'); - final RepositoryPackage appFacingPackage = - createFakePlugin('plugin1', pluginGroup); - createFakePlugin('plugin1_platform_interface', pluginGroup); - createFakePlugin('plugin1_web', pluginGroup); - - await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--packages=plugin1/plugin1']); - - expect(command.plugins, unorderedEquals([appFacingPackage.path])); - }); - - test( - 'specifying a package of a federated plugin by its name should ' - 'include only that package', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin1/plugin1/plugin1.dart -'''), - ]; - final Directory pluginGroup = packagesDir.childDirectory('plugin1'); - - createFakePlugin('plugin1', pluginGroup); - final RepositoryPackage platformInterfacePackage = - createFakePlugin('plugin1_platform_interface', pluginGroup); - createFakePlugin('plugin1_web', pluginGroup); - - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=main', - '--packages=plugin1_platform_interface' - ]); - - expect(command.plugins, - unorderedEquals([platformInterfacePackage.path])); - }); - - test('returns subpackages after the enclosing package', () async { - final SamplePackageCommand localCommand = SamplePackageCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - gitDir: MockGitDir(), - includeSubpackages: true, - ); - final CommandRunner localRunner = - CommandRunner('common_command', 'subpackage testing'); - localRunner.addCommand(localCommand); - - final RepositoryPackage package = - createFakePackage('apackage', packagesDir); - - await runCapturingPrint(localRunner, ['sample']); - expect( - localCommand.plugins, - containsAllInOrder([ - package.path, - getExampleDir(package).path, - ])); - }); - - group('conflicting package selection', () { - test('does not allow --packages with --run-on-changed-packages', - () async { - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'sample', - '--run-on-changed-packages', - '--packages=plugin1', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Only one of --packages, --run-on-changed-packages, or ' - '--packages-for-branch can be provided.') - ])); - }); - - test('does not allow --packages with --packages-for-branch', () async { - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'sample', - '--packages-for-branch', - '--packages=plugin1', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Only one of --packages, --run-on-changed-packages, or ' - '--packages-for-branch can be provided.') - ])); - }); - - test( - 'does not allow --run-on-changed-packages with --packages-for-branch', - () async { - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'sample', - '--packages-for-branch', - '--packages=plugin1', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Only one of --packages, --run-on-changed-packages, or ' - '--packages-for-branch can be provided.') - ])); - }); - }); - - group('test run-on-changed-packages', () { - test('all plugins should be tested if there are no changes.', () async { - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - }); - - test( - 'all plugins should be tested if there are no plugin related changes.', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: 'AUTHORS'), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - }); - - test('all plugins should be tested if .cirrus.yml changes.', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -.cirrus.yml -packages/plugin1/CHANGELOG -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - - final List output = await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - expect( - output, - containsAllInOrder([ - contains('Running for all packages, since a file has changed ' - 'that could affect the entire repository.') - ])); - }); - - test('all plugins should be tested if .ci.yaml changes', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -.ci.yaml -packages/plugin1/CHANGELOG -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - final List output = await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - expect( - output, - containsAllInOrder([ - contains('Running for all packages, since a file has changed ' - 'that could affect the entire repository.') - ])); - }); - - test('all plugins should be tested if anything in .ci/ changes', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -.ci/Dockerfile -packages/plugin1/CHANGELOG -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - final List output = await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - expect( - output, - containsAllInOrder([ - contains('Running for all packages, since a file has changed ' - 'that could affect the entire repository.') - ])); - }); - - test('all plugins should be tested if anything in script/ changes.', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -script/tool_runner.sh -packages/plugin1/CHANGELOG -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - final List output = await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - expect( - output, - containsAllInOrder([ - contains('Running for all packages, since a file has changed ' - 'that could affect the entire repository.') - ])); - }); - - test('all plugins should be tested if the root analysis options change.', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -analysis_options.yaml -packages/plugin1/CHANGELOG -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - final List output = await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - expect( - output, - containsAllInOrder([ - contains('Running for all packages, since a file has changed ' - 'that could affect the entire repository.') - ])); - }); - - test('all plugins should be tested if formatting options change.', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -.clang-format -packages/plugin1/CHANGELOG -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - final List output = await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - expect( - output, - containsAllInOrder([ - contains('Running for all packages, since a file has changed ' - 'that could affect the entire repository.') - ])); - }); - - test('Only changed plugin should be tested.', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: 'packages/plugin1/plugin1.dart'), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir); - final List output = await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect( - output, - containsAllInOrder([ - contains( - 'Running for all packages that have diffs relative to "main"'), - ])); - - expect(command.plugins, unorderedEquals([plugin1.path])); - }); - - test('multiple files in one plugin should also test the plugin', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin1/plugin1.dart -packages/plugin1/ios/plugin1.m -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, unorderedEquals([plugin1.path])); - }); - - test('multiple plugins changed should test all the changed plugins', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin1/plugin1.dart -packages/plugin2/ios/plugin2.m -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - createFakePlugin('plugin3', packagesDir); - await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - }); - - test( - 'multiple plugins inside the same plugin group changed should output the plugin group name', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin1/plugin1/plugin1.dart -packages/plugin1/plugin1_platform_interface/plugin1_platform_interface.dart -packages/plugin1/plugin1_web/plugin1_web.dart -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); - createFakePlugin('plugin2', packagesDir); - createFakePlugin('plugin3', packagesDir); - await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, unorderedEquals([plugin1.path])); - }); - - test( - 'changing one plugin in a federated group should only include that plugin', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin1/plugin1/plugin1.dart -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); - createFakePlugin('plugin1_platform_interface', - packagesDir.childDirectory('plugin1')); - createFakePlugin('plugin1_web', packagesDir.childDirectory('plugin1')); - await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, unorderedEquals([plugin1.path])); - }); - - test('--exclude flag works with --run-on-changed-packages', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin1/plugin1.dart -packages/plugin2/ios/plugin2.m -packages/plugin3/plugin3.dart -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); - createFakePlugin('plugin2', packagesDir); - createFakePlugin('plugin3', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--exclude=plugin2,plugin3', - '--base-sha=main', - '--run-on-changed-packages' - ]); - - expect(command.plugins, unorderedEquals([plugin1.path])); - }); - }); - - group('test run-on-dirty-packages', () { - test('no packages should be tested if there are no changes.', () async { - createFakePackage('a_package', packagesDir); - await runCapturingPrint( - runner, ['sample', '--run-on-dirty-packages']); - - expect(command.plugins, unorderedEquals([])); - }); - - test( - 'no packages should be tested if there are no plugin related changes.', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: 'AUTHORS'), - ]; - createFakePackage('a_package', packagesDir); - await runCapturingPrint( - runner, ['sample', '--run-on-dirty-packages']); - - expect(command.plugins, unorderedEquals([])); - }); - - test('no packages should be tested even if special repo files change.', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -.cirrus.yml -.ci.yaml -.ci/Dockerfile -.clang-format -analysis_options.yaml -script/tool_runner.sh -'''), - ]; - createFakePackage('a_package', packagesDir); - await runCapturingPrint( - runner, ['sample', '--run-on-dirty-packages']); - - expect(command.plugins, unorderedEquals([])); - }); - - test('Only changed packages should be tested.', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: 'packages/a_package/lib/a_package.dart'), - ]; - final RepositoryPackage packageA = - createFakePackage('a_package', packagesDir); - createFakePlugin('b_package', packagesDir); - final List output = await runCapturingPrint( - runner, ['sample', '--run-on-dirty-packages']); - - expect( - output, - containsAllInOrder([ - contains( - 'Running for all packages that have uncommitted changes'), - ])); - - expect(command.plugins, unorderedEquals([packageA.path])); - }); - - test('multiple packages changed should test all the changed packages', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/a_package/lib/a_package.dart -packages/b_package/lib/src/foo.dart -'''), - ]; - final RepositoryPackage packageA = - createFakePackage('a_package', packagesDir); - final RepositoryPackage packageB = - createFakePackage('b_package', packagesDir); - createFakePackage('c_package', packagesDir); - await runCapturingPrint( - runner, ['sample', '--run-on-dirty-packages']); - - expect(command.plugins, - unorderedEquals([packageA.path, packageB.path])); - }); - - test('honors --exclude flag', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/a_package/lib/a_package.dart -packages/b_package/lib/src/foo.dart -'''), - ]; - final RepositoryPackage packageA = - createFakePackage('a_package', packagesDir); - createFakePackage('b_package', packagesDir); - createFakePackage('c_package', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--exclude=b_package', - '--run-on-dirty-packages' - ]); - - expect(command.plugins, unorderedEquals([packageA.path])); - }); - }); - }); - - group('--packages-for-branch', () { - test('only tests changed packages relative to the merge base on a branch', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: 'packages/plugin1/plugin1.dart'), - ]; - processRunner.mockProcessesForExecutable['git-rev-parse'] = [ - MockProcess(stdout: 'a-branch'), - ]; - processRunner.mockProcessesForExecutable['git-merge-base'] = [ - MockProcess(stdout: 'abc123'), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir); - - final List output = await runCapturingPrint( - runner, ['sample', '--packages-for-branch']); - - expect(command.plugins, unorderedEquals([plugin1.path])); - expect( - output, - containsAllInOrder([ - contains( - 'Running for all packages that have diffs relative to "abc123"'), - ])); - // Ensure that it's diffing against the merge-base. - expect( - processRunner.recordedCalls, - contains( - const ProcessCall( - 'git-diff', ['--name-only', 'abc123', 'HEAD'], null), - )); - }); - - test('only tests changed packages relative to the previous commit on main', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: 'packages/plugin1/plugin1.dart'), - ]; - processRunner.mockProcessesForExecutable['git-rev-parse'] = [ - MockProcess(stdout: 'main'), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir); - - final List output = await runCapturingPrint( - runner, ['sample', '--packages-for-branch']); - - expect(command.plugins, unorderedEquals([plugin1.path])); - expect( - output, - containsAllInOrder([ - contains('--packages-for-branch: running on default branch; ' - 'using parent commit as the diff base'), - contains( - 'Running for all packages that have diffs relative to "HEAD~"'), - ])); - // Ensure that it's diffing against the prior commit. - expect( - processRunner.recordedCalls, - contains( - const ProcessCall( - 'git-diff', ['--name-only', 'HEAD~', 'HEAD'], null), - )); - }); - - test('tests all packages on master', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: 'packages/plugin1/plugin1.dart'), - ]; - processRunner.mockProcessesForExecutable['git-rev-parse'] = [ - MockProcess(stdout: 'master'), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir); - - final List output = await runCapturingPrint( - runner, ['sample', '--packages-for-branch']); - - expect(command.plugins, unorderedEquals([plugin1.path])); - expect( - output, - containsAllInOrder([ - contains('--packages-for-branch: running on default branch; ' - 'using parent commit as the diff base'), - contains( - 'Running for all packages that have diffs relative to "HEAD~"'), - ])); - // Ensure that it's diffing against the prior commit. - expect( - processRunner.recordedCalls, - contains( - const ProcessCall( - 'git-diff', ['--name-only', 'HEAD~', 'HEAD'], null), - )); - }); - - test('throws if getting the branch fails', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: 'packages/plugin1/plugin1.dart'), - ]; - processRunner.mockProcessesForExecutable['git-rev-parse'] = [ - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['sample', '--packages-for-branch'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unabled to determine branch'), - ])); - }); - }); - - group('sharding', () { - test('distributes evenly when evenly divisible', () async { - final List> expectedShards = - >[ - [ - createFakePackage('package1', packagesDir), - createFakePackage('package2', packagesDir), - createFakePackage('package3', packagesDir), - ], - [ - createFakePackage('package4', packagesDir), - createFakePackage('package5', packagesDir), - createFakePackage('package6', packagesDir), - ], - [ - createFakePackage('package7', packagesDir), - createFakePackage('package8', packagesDir), - createFakePackage('package9', packagesDir), - ], - ]; - - for (int i = 0; i < expectedShards.length; ++i) { - final SamplePackageCommand localCommand = SamplePackageCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - gitDir: MockGitDir(), - ); - final CommandRunner localRunner = - CommandRunner('common_command', 'Shard testing'); - localRunner.addCommand(localCommand); - - await runCapturingPrint(localRunner, [ - 'sample', - '--shardIndex=$i', - '--shardCount=3', - ]); - expect( - localCommand.plugins, - unorderedEquals(expectedShards[i] - .map((RepositoryPackage package) => package.path) - .toList())); - } - }); - - test('distributes as evenly as possible when not evenly divisible', - () async { - final List> expectedShards = - >[ - [ - createFakePackage('package1', packagesDir), - createFakePackage('package2', packagesDir), - createFakePackage('package3', packagesDir), - ], - [ - createFakePackage('package4', packagesDir), - createFakePackage('package5', packagesDir), - createFakePackage('package6', packagesDir), - ], - [ - createFakePackage('package7', packagesDir), - createFakePackage('package8', packagesDir), - ], - ]; - - for (int i = 0; i < expectedShards.length; ++i) { - final SamplePackageCommand localCommand = SamplePackageCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - gitDir: MockGitDir(), - ); - final CommandRunner localRunner = - CommandRunner('common_command', 'Shard testing'); - localRunner.addCommand(localCommand); - - await runCapturingPrint(localRunner, [ - 'sample', - '--shardIndex=$i', - '--shardCount=3', - ]); - expect( - localCommand.plugins, - unorderedEquals(expectedShards[i] - .map((RepositoryPackage package) => package.path) - .toList())); - } - }); - - // In CI (which is the use case for sharding) we often want to run muliple - // commands on the same set of packages, but the exclusion lists for those - // commands may be different. In those cases we still want all the commands - // to operate on a consistent set of plugins. - // - // E.g., some commands require running build-examples in a previous step; - // excluding some plugins from the later step shouldn't change what's tested - // in each shard, as it may no longer align with what was built. - test('counts excluded plugins when sharding', () async { - final List> expectedShards = - >[ - [ - createFakePackage('package1', packagesDir), - createFakePackage('package2', packagesDir), - createFakePackage('package3', packagesDir), - ], - [ - createFakePackage('package4', packagesDir), - createFakePackage('package5', packagesDir), - createFakePackage('package6', packagesDir), - ], - [ - createFakePackage('package7', packagesDir), - ], - ]; - // These would be in the last shard, but are excluded. - createFakePackage('package8', packagesDir); - createFakePackage('package9', packagesDir); - - for (int i = 0; i < expectedShards.length; ++i) { - final SamplePackageCommand localCommand = SamplePackageCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - gitDir: MockGitDir(), - ); - final CommandRunner localRunner = - CommandRunner('common_command', 'Shard testing'); - localRunner.addCommand(localCommand); - - await runCapturingPrint(localRunner, [ - 'sample', - '--shardIndex=$i', - '--shardCount=3', - '--exclude=package8,package9', - ]); - expect( - localCommand.plugins, - unorderedEquals(expectedShards[i] - .map((RepositoryPackage package) => package.path) - .toList())); - } - }); - }); -} - -class SamplePackageCommand extends PackageCommand { - SamplePackageCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - GitDir? gitDir, - this.includeSubpackages = false, - }) : super(packagesDir, - processRunner: processRunner, platform: platform, gitDir: gitDir); - - final List plugins = []; - - final bool includeSubpackages; - - @override - final String name = 'sample'; - - @override - final String description = 'sample command'; - - @override - Future run() async { - final Stream packages = includeSubpackages - ? getTargetPackagesAndSubpackages() - : getTargetPackages(); - await for (final PackageEnumerationEntry entry in packages) { - plugins.add(entry.package.path); - } - } -} diff --git a/script/tool/test/common/package_command_test.mocks.dart b/script/tool/test/common/package_command_test.mocks.dart deleted file mode 100644 index 79c5d4df1a8c..000000000000 --- a/script/tool/test/common/package_command_test.mocks.dart +++ /dev/null @@ -1,286 +0,0 @@ -// Mocks generated by Mockito 5.3.2 from annotations -// in flutter_plugin_tools/test/common/package_command_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i6; -import 'dart:io' as _i4; - -import 'package:git/src/branch_reference.dart' as _i3; -import 'package:git/src/commit.dart' as _i2; -import 'package:git/src/commit_reference.dart' as _i8; -import 'package:git/src/git_dir.dart' as _i5; -import 'package:git/src/tag.dart' as _i7; -import 'package:git/src/tree_entry.dart' as _i9; -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 - -class _FakeCommit_0 extends _i1.SmartFake implements _i2.Commit { - _FakeCommit_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeBranchReference_1 extends _i1.SmartFake - implements _i3.BranchReference { - _FakeBranchReference_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeProcessResult_2 extends _i1.SmartFake implements _i4.ProcessResult { - _FakeProcessResult_2( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -/// A class which mocks [GitDir]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockGitDir extends _i1.Mock implements _i5.GitDir { - MockGitDir() { - _i1.throwOnMissingStub(this); - } - - @override - String get path => (super.noSuchMethod( - Invocation.getter(#path), - returnValue: '', - ) as String); - @override - _i6.Future commitCount([String? branchName = r'HEAD']) => - (super.noSuchMethod( - Invocation.method( - #commitCount, - [branchName], - ), - returnValue: _i6.Future.value(0), - ) as _i6.Future); - @override - _i6.Future<_i2.Commit> commitFromRevision(String? revision) => - (super.noSuchMethod( - Invocation.method( - #commitFromRevision, - [revision], - ), - returnValue: _i6.Future<_i2.Commit>.value(_FakeCommit_0( - this, - Invocation.method( - #commitFromRevision, - [revision], - ), - )), - ) as _i6.Future<_i2.Commit>); - @override - _i6.Future> commits([String? branchName = r'HEAD']) => - (super.noSuchMethod( - Invocation.method( - #commits, - [branchName], - ), - returnValue: - _i6.Future>.value({}), - ) as _i6.Future>); - @override - _i6.Future<_i3.BranchReference?> branchReference(String? branchName) => - (super.noSuchMethod( - Invocation.method( - #branchReference, - [branchName], - ), - returnValue: _i6.Future<_i3.BranchReference?>.value(), - ) as _i6.Future<_i3.BranchReference?>); - @override - _i6.Future> branches() => (super.noSuchMethod( - Invocation.method( - #branches, - [], - ), - returnValue: _i6.Future>.value( - <_i3.BranchReference>[]), - ) as _i6.Future>); - @override - _i6.Stream<_i7.Tag> tags() => (super.noSuchMethod( - Invocation.method( - #tags, - [], - ), - returnValue: _i6.Stream<_i7.Tag>.empty(), - ) as _i6.Stream<_i7.Tag>); - @override - _i6.Future> showRef({ - bool? heads = false, - bool? tags = false, - }) => - (super.noSuchMethod( - Invocation.method( - #showRef, - [], - { - #heads: heads, - #tags: tags, - }, - ), - returnValue: _i6.Future>.value( - <_i8.CommitReference>[]), - ) as _i6.Future>); - @override - _i6.Future<_i3.BranchReference> currentBranch() => (super.noSuchMethod( - Invocation.method( - #currentBranch, - [], - ), - returnValue: - _i6.Future<_i3.BranchReference>.value(_FakeBranchReference_1( - this, - Invocation.method( - #currentBranch, - [], - ), - )), - ) as _i6.Future<_i3.BranchReference>); - @override - _i6.Future> lsTree( - String? treeish, { - bool? subTreesOnly = false, - String? path, - }) => - (super.noSuchMethod( - Invocation.method( - #lsTree, - [treeish], - { - #subTreesOnly: subTreesOnly, - #path: path, - }, - ), - returnValue: _i6.Future>.value(<_i9.TreeEntry>[]), - ) as _i6.Future>); - @override - _i6.Future createOrUpdateBranch( - String? branchName, - String? treeSha, - String? commitMessage, - ) => - (super.noSuchMethod( - Invocation.method( - #createOrUpdateBranch, - [ - branchName, - treeSha, - commitMessage, - ], - ), - returnValue: _i6.Future.value(), - ) as _i6.Future); - @override - _i6.Future commitTree( - String? treeSha, - String? commitMessage, { - List? parentCommitShas, - }) => - (super.noSuchMethod( - Invocation.method( - #commitTree, - [ - treeSha, - commitMessage, - ], - {#parentCommitShas: parentCommitShas}, - ), - returnValue: _i6.Future.value(''), - ) as _i6.Future); - @override - _i6.Future> writeObjects(List? paths) => - (super.noSuchMethod( - Invocation.method( - #writeObjects, - [paths], - ), - returnValue: _i6.Future>.value({}), - ) as _i6.Future>); - @override - _i6.Future<_i4.ProcessResult> runCommand( - Iterable? args, { - bool? throwOnError = true, - }) => - (super.noSuchMethod( - Invocation.method( - #runCommand, - [args], - {#throwOnError: throwOnError}, - ), - returnValue: _i6.Future<_i4.ProcessResult>.value(_FakeProcessResult_2( - this, - Invocation.method( - #runCommand, - [args], - {#throwOnError: throwOnError}, - ), - )), - ) as _i6.Future<_i4.ProcessResult>); - @override - _i6.Future isWorkingTreeClean() => (super.noSuchMethod( - Invocation.method( - #isWorkingTreeClean, - [], - ), - returnValue: _i6.Future.value(false), - ) as _i6.Future); - @override - _i6.Future<_i2.Commit?> updateBranch( - String? branchName, - _i6.Future Function(_i4.Directory)? populater, - String? commitMessage, - ) => - (super.noSuchMethod( - Invocation.method( - #updateBranch, - [ - branchName, - populater, - commitMessage, - ], - ), - returnValue: _i6.Future<_i2.Commit?>.value(), - ) as _i6.Future<_i2.Commit?>); - @override - _i6.Future<_i2.Commit?> updateBranchWithDirectoryContents( - String? branchName, - String? sourceDirectoryPath, - String? commitMessage, - ) => - (super.noSuchMethod( - Invocation.method( - #updateBranchWithDirectoryContents, - [ - branchName, - sourceDirectoryPath, - commitMessage, - ], - ), - returnValue: _i6.Future<_i2.Commit?>.value(), - ) as _i6.Future<_i2.Commit?>); -} diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart deleted file mode 100644 index c858df0022cc..000000000000 --- a/script/tool/test/common/package_looping_command_test.dart +++ /dev/null @@ -1,947 +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:async'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -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/package_looping_command.dart'; -import 'package:flutter_plugin_tools/src/common/process_runner.dart'; -import 'package:git/git.dart'; -import 'package:mockito/mockito.dart'; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import '../mocks.dart'; -import '../util.dart'; -import 'package_command_test.mocks.dart'; - -// Constants for colorized output start and end. -const String _startElapsedTimeColor = '\x1B[90m'; -const String _startErrorColor = '\x1B[31m'; -const String _startHeadingColor = '\x1B[36m'; -const String _startSkipColor = '\x1B[90m'; -const String _startSkipWithWarningColor = '\x1B[93m'; -const String _startSuccessColor = '\x1B[32m'; -const String _startWarningColor = '\x1B[33m'; -const String _endColor = '\x1B[0m'; - -// The filename within a package containing warnings to log during runForPackage. -enum _ResultFileType { - /// A file containing errors to return. - errors, - - /// A file containing warnings that should be logged. - warns, - - /// A file indicating that the package should be skipped, and why. - skips, - - /// A file indicating that the package should throw. - throws, -} - -// The filename within a package containing errors to return from runForPackage. -const String _errorFile = 'errors'; -// The filename within a package indicating that it should be skipped. -const String _skipFile = 'skip'; -// The filename within a package containing warnings to log during runForPackage. -const String _warningFile = 'warnings'; -// The filename within a package indicating that it should throw. -const String _throwFile = 'throw'; - -/// Writes a file to [package] to control the behavior of -/// [TestPackageLoopingCommand] for that package. -void _addResultFile(RepositoryPackage package, _ResultFileType type, - {String? contents}) { - final File file = package.directory.childFile(_filenameForType(type)); - file.createSync(); - if (contents != null) { - file.writeAsStringSync(contents); - } -} - -String _filenameForType(_ResultFileType type) { - switch (type) { - case _ResultFileType.errors: - return _errorFile; - case _ResultFileType.warns: - return _warningFile; - case _ResultFileType.skips: - return _skipFile; - case _ResultFileType.throws: - return _throwFile; - } -} - -void main() { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late Directory thirdPartyPackagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - thirdPartyPackagesDir = packagesDir.parent - .childDirectory('third_party') - .childDirectory('packages'); - }); - - /// Creates a TestPackageLoopingCommand instance that uses [gitDiffResponse] - /// for git diffs, and logs output to [printOutput]. - TestPackageLoopingCommand createTestCommand({ - String gitDiffResponse = '', - bool hasLongOutput = true, - PackageLoopingType packageLoopingType = PackageLoopingType.topLevelOnly, - bool failsDuringInit = false, - bool warnsDuringInit = false, - bool warnsDuringCleanup = false, - bool captureOutput = false, - String? customFailureListHeader, - String? customFailureListFooter, - }) { - // Set up the git diff response. - final MockGitDir gitDir = MockGitDir(); - when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) - .thenAnswer((Invocation invocation) { - final MockProcessResult mockProcessResult = MockProcessResult(); - if (invocation.positionalArguments[0][0] == 'diff') { - when(mockProcessResult.stdout as String?) - .thenReturn(gitDiffResponse); - } - return Future.value(mockProcessResult); - }); - - return TestPackageLoopingCommand( - packagesDir, - platform: mockPlatform, - hasLongOutput: hasLongOutput, - packageLoopingType: packageLoopingType, - failsDuringInit: failsDuringInit, - warnsDuringInit: warnsDuringInit, - warnsDuringCleanup: warnsDuringCleanup, - customFailureListHeader: customFailureListHeader, - customFailureListFooter: customFailureListFooter, - captureOutput: captureOutput, - gitDir: gitDir, - ); - } - - /// Runs [command] with the given [arguments], and returns its output. - Future> runCommand( - TestPackageLoopingCommand command, { - List arguments = const [], - void Function(Error error)? errorHandler, - }) async { - late CommandRunner runner; - runner = CommandRunner('test_package_looping_command', - 'Test for base package looping functionality'); - runner.addCommand(command); - return await runCapturingPrint( - runner, - [command.name, ...arguments], - errorHandler: errorHandler, - ); - } - - group('tool exit', () { - test('is handled during initializeRun', () async { - final TestPackageLoopingCommand command = - createTestCommand(failsDuringInit: true); - - expect(() => runCommand(command), throwsA(isA())); - }); - - test('does not stop looping on error', () async { - createFakePackage('package_a', packagesDir); - final RepositoryPackage failingPackage = - createFakePlugin('package_b', packagesDir); - createFakePackage('package_c', packagesDir); - _addResultFile(failingPackage, _ResultFileType.errors); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - Error? commandError; - final List output = - await runCommand(command, errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - '${_startHeadingColor}Running for package_a...$_endColor', - '${_startHeadingColor}Running for package_b...$_endColor', - '${_startHeadingColor}Running for package_c...$_endColor', - ])); - }); - - test('does not stop looping on exceptions', () async { - createFakePackage('package_a', packagesDir); - final RepositoryPackage failingPackage = - createFakePlugin('package_b', packagesDir); - createFakePackage('package_c', packagesDir); - _addResultFile(failingPackage, _ResultFileType.throws); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - Error? commandError; - final List output = - await runCommand(command, errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - '${_startHeadingColor}Running for package_a...$_endColor', - '${_startHeadingColor}Running for package_b...$_endColor', - '${_startHeadingColor}Running for package_c...$_endColor', - ])); - }); - }); - - group('package iteration', () { - test('includes plugins and packages', () async { - final RepositoryPackage plugin = - createFakePlugin('a_plugin', packagesDir); - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - final TestPackageLoopingCommand command = createTestCommand(); - await runCommand(command); - - expect(command.checkedPackages, - unorderedEquals([plugin.path, package.path])); - }); - - test('includes third_party/packages', () async { - final RepositoryPackage package1 = - createFakePackage('a_package', packagesDir); - final RepositoryPackage package2 = - createFakePackage('another_package', thirdPartyPackagesDir); - - final TestPackageLoopingCommand command = createTestCommand(); - await runCommand(command); - - expect(command.checkedPackages, - unorderedEquals([package1.path, package2.path])); - }); - - test('includes all subpackages when requested', () async { - final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, - examples: ['example1', 'example2']); - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - final RepositoryPackage subPackage = createFakePackage( - 'sub_package', package.directory, - examples: []); - - final TestPackageLoopingCommand command = createTestCommand( - packageLoopingType: PackageLoopingType.includeAllSubpackages); - await runCommand(command); - - expect( - command.checkedPackages, - unorderedEquals([ - plugin.path, - getExampleDir(plugin).childDirectory('example1').path, - getExampleDir(plugin).childDirectory('example2').path, - package.path, - getExampleDir(package).path, - subPackage.path, - ])); - }); - - test('includes examples when requested', () async { - final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, - examples: ['example1', 'example2']); - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - final RepositoryPackage subPackage = - createFakePackage('sub_package', package.directory); - - final TestPackageLoopingCommand command = createTestCommand( - packageLoopingType: PackageLoopingType.includeExamples); - await runCommand(command); - - expect( - command.checkedPackages, - unorderedEquals([ - plugin.path, - getExampleDir(plugin).childDirectory('example1').path, - getExampleDir(plugin).childDirectory('example2').path, - package.path, - getExampleDir(package).path, - ])); - expect(command.checkedPackages, isNot(contains(subPackage.path))); - }); - - test('excludes subpackages when main package is excluded', () async { - final RepositoryPackage excluded = createFakePlugin( - 'a_plugin', packagesDir, - examples: ['example1', 'example2']); - final RepositoryPackage included = - createFakePackage('a_package', packagesDir); - final RepositoryPackage subpackage = - createFakePackage('sub_package', excluded.directory); - - final TestPackageLoopingCommand command = createTestCommand( - packageLoopingType: PackageLoopingType.includeAllSubpackages); - await runCommand(command, arguments: ['--exclude=a_plugin']); - - final Iterable examples = excluded.getExamples(); - - expect( - command.checkedPackages, - unorderedEquals([ - included.path, - getExampleDir(included).path, - ])); - expect(command.checkedPackages, isNot(contains(excluded.path))); - expect(examples.length, 2); - for (final RepositoryPackage example in examples) { - expect(command.checkedPackages, isNot(contains(example.path))); - } - expect(command.checkedPackages, isNot(contains(subpackage.path))); - }); - - test('excludes examples when main package is excluded', () async { - final RepositoryPackage excluded = createFakePlugin( - 'a_plugin', packagesDir, - examples: ['example1', 'example2']); - final RepositoryPackage included = - createFakePackage('a_package', packagesDir); - - final TestPackageLoopingCommand command = createTestCommand( - packageLoopingType: PackageLoopingType.includeExamples); - await runCommand(command, arguments: ['--exclude=a_plugin']); - - final Iterable examples = excluded.getExamples(); - - expect( - command.checkedPackages, - unorderedEquals([ - included.path, - getExampleDir(included).path, - ])); - expect(command.checkedPackages, isNot(contains(excluded.path))); - expect(examples.length, 2); - for (final RepositoryPackage example in examples) { - expect(command.checkedPackages, isNot(contains(example.path))); - } - }); - - test('skips unsupported Flutter versions when requested', () async { - final RepositoryPackage excluded = createFakePlugin( - 'a_plugin', packagesDir, - flutterConstraint: '>=2.10.0'); - final RepositoryPackage included = - createFakePackage('a_package', packagesDir); - - final TestPackageLoopingCommand command = createTestCommand( - packageLoopingType: PackageLoopingType.includeAllSubpackages, - hasLongOutput: false); - final List output = await runCommand(command, arguments: [ - '--skip-if-not-supporting-flutter-version=2.5.0' - ]); - - expect( - command.checkedPackages, - unorderedEquals([ - included.path, - getExampleDir(included).path, - ])); - expect(command.checkedPackages, isNot(contains(excluded.path))); - - expect( - output, - containsAllInOrder([ - '${_startHeadingColor}Running for a_package...$_endColor', - '${_startHeadingColor}Running for a_plugin...$_endColor', - '$_startSkipColor SKIPPING: Does not support Flutter 2.5.0$_endColor', - ])); - }); - - test('skips unsupported Dart versions when requested', () async { - final RepositoryPackage excluded = createFakePackage( - 'excluded_package', packagesDir, - dartConstraint: '>=2.17.0 <3.0.0'); - final RepositoryPackage included = - createFakePackage('a_package', packagesDir); - - final TestPackageLoopingCommand command = createTestCommand( - packageLoopingType: PackageLoopingType.includeAllSubpackages, - hasLongOutput: false); - final List output = await runCommand(command, - arguments: ['--skip-if-not-supporting-dart-version=2.14.0']); - - expect( - command.checkedPackages, - unorderedEquals([ - included.path, - getExampleDir(included).path, - ])); - expect(command.checkedPackages, isNot(contains(excluded.path))); - - expect( - output, - containsAllInOrder([ - '${_startHeadingColor}Running for a_package...$_endColor', - '${_startHeadingColor}Running for excluded_package...$_endColor', - '$_startSkipColor SKIPPING: Does not support Dart 2.14.0$_endColor', - ])); - }); - }); - - group('output', () { - test('has the expected package headers for long-form output', () async { - createFakePlugin('package_a', packagesDir); - createFakePackage('package_b', packagesDir); - - final TestPackageLoopingCommand command = createTestCommand(); - final List output = await runCommand(command); - - const String separator = - '============================================================'; - expect( - output, - containsAllInOrder([ - '$_startHeadingColor\n$separator\n|| Running for package_a\n$separator\n$_endColor', - '$_startHeadingColor\n$separator\n|| Running for package_b\n$separator\n$_endColor', - ])); - }); - - test('has the expected package headers for short-form output', () async { - createFakePlugin('package_a', packagesDir); - createFakePackage('package_b', packagesDir); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - final List output = await runCommand(command); - - expect( - output, - containsAllInOrder([ - '${_startHeadingColor}Running for package_a...$_endColor', - '${_startHeadingColor}Running for package_b...$_endColor', - ])); - }); - - test('prints timing info in long-form output when requested', () async { - createFakePlugin('package_a', packagesDir); - createFakePackage('package_b', packagesDir); - - final TestPackageLoopingCommand command = createTestCommand(); - final List output = - await runCommand(command, arguments: ['--log-timing']); - - const String separator = - '============================================================'; - expect( - output, - containsAllInOrder([ - '$_startHeadingColor\n$separator\n|| Running for package_a [@0:00]\n$separator\n$_endColor', - '$_startElapsedTimeColor\n[package_a completed in 0m 0s]$_endColor', - '$_startHeadingColor\n$separator\n|| Running for package_b [@0:00]\n$separator\n$_endColor', - '$_startElapsedTimeColor\n[package_b completed in 0m 0s]$_endColor', - ])); - }); - - test('prints timing info in short-form output when requested', () async { - createFakePlugin('package_a', packagesDir); - createFakePackage('package_b', packagesDir); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - final List output = - await runCommand(command, arguments: ['--log-timing']); - - expect( - output, - containsAllInOrder([ - '$_startHeadingColor[0:00] Running for package_a...$_endColor', - '$_startHeadingColor[0:00] Running for package_b...$_endColor', - ])); - // Short-form output should not include elapsed time. - expect(output, isNot(contains('[package_a completed in 0m 0s]'))); - }); - - test('shows the success message when nothing fails', () async { - createFakePackage('package_a', packagesDir); - createFakePackage('package_b', packagesDir); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - final List output = await runCommand(command); - - expect( - output, - containsAllInOrder([ - '\n', - '${_startSuccessColor}No issues found!$_endColor', - ])); - }); - - test('shows failure summaries when something fails without extra details', - () async { - createFakePackage('package_a', packagesDir); - final RepositoryPackage failingPackage1 = - createFakePlugin('package_b', packagesDir); - createFakePackage('package_c', packagesDir); - final RepositoryPackage failingPackage2 = - createFakePlugin('package_d', packagesDir); - _addResultFile(failingPackage1, _ResultFileType.errors); - _addResultFile(failingPackage2, _ResultFileType.errors); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - Error? commandError; - final List output = - await runCommand(command, errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - '\n', - '${_startErrorColor}The following packages had errors:$_endColor', - '$_startErrorColor package_b$_endColor', - '$_startErrorColor package_d$_endColor', - '${_startErrorColor}See above for full details.$_endColor', - ])); - }); - - test('uses custom summary header and footer if provided', () async { - createFakePackage('package_a', packagesDir); - final RepositoryPackage failingPackage1 = - createFakePlugin('package_b', packagesDir); - createFakePackage('package_c', packagesDir); - final RepositoryPackage failingPackage2 = - createFakePlugin('package_d', packagesDir); - _addResultFile(failingPackage1, _ResultFileType.errors); - _addResultFile(failingPackage2, _ResultFileType.errors); - - final TestPackageLoopingCommand command = createTestCommand( - hasLongOutput: false, - customFailureListHeader: 'This is a custom header', - customFailureListFooter: 'And a custom footer!'); - Error? commandError; - final List output = - await runCommand(command, errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - '\n', - '${_startErrorColor}This is a custom header$_endColor', - '$_startErrorColor package_b$_endColor', - '$_startErrorColor package_d$_endColor', - '${_startErrorColor}And a custom footer!$_endColor', - ])); - }); - - test('shows failure summaries when something fails with extra details', - () async { - createFakePackage('package_a', packagesDir); - final RepositoryPackage failingPackage1 = - createFakePlugin('package_b', packagesDir); - createFakePackage('package_c', packagesDir); - final RepositoryPackage failingPackage2 = - createFakePlugin('package_d', packagesDir); - _addResultFile(failingPackage1, _ResultFileType.errors, - contents: 'just one detail'); - _addResultFile(failingPackage2, _ResultFileType.errors, - contents: 'first detail\nsecond detail'); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - Error? commandError; - final List output = - await runCommand(command, errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - '\n', - '${_startErrorColor}The following packages had errors:$_endColor', - '$_startErrorColor package_b:\n just one detail$_endColor', - '$_startErrorColor package_d:\n first detail\n second detail$_endColor', - '${_startErrorColor}See above for full details.$_endColor', - ])); - }); - - test('is captured, not printed, when requested', () async { - createFakePlugin('package_a', packagesDir); - createFakePackage('package_b', packagesDir); - - final TestPackageLoopingCommand command = - createTestCommand(captureOutput: true); - final List output = await runCommand(command); - - expect(output, isEmpty); - - // None of the output should be colorized when captured. - const String separator = - '============================================================'; - expect( - command.capturedOutput, - containsAllInOrder([ - '\n$separator\n|| Running for package_a\n$separator\n', - '\n$separator\n|| Running for package_b\n$separator\n', - 'No issues found!', - ])); - }); - - test('logs skips', () async { - createFakePackage('package_a', packagesDir); - final RepositoryPackage skipPackage = - createFakePackage('package_b', packagesDir); - _addResultFile(skipPackage, _ResultFileType.skips, - contents: 'For a reason'); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - final List output = await runCommand(command); - - expect( - output, - containsAllInOrder([ - '${_startHeadingColor}Running for package_a...$_endColor', - '${_startHeadingColor}Running for package_b...$_endColor', - '$_startSkipColor SKIPPING: For a reason$_endColor', - ])); - }); - - test('logs exclusions', () async { - createFakePackage('package_a', packagesDir); - createFakePackage('package_b', packagesDir); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - final List output = - await runCommand(command, arguments: ['--exclude=package_b']); - - expect( - output, - containsAllInOrder([ - '${_startHeadingColor}Running for package_a...$_endColor', - '${_startSkipColor}Not running for package_b; excluded$_endColor', - ])); - }); - - test('logs warnings', () async { - final RepositoryPackage warnPackage = - createFakePackage('package_a', packagesDir); - _addResultFile(warnPackage, _ResultFileType.warns, - contents: 'Warning 1\nWarning 2'); - createFakePackage('package_b', packagesDir); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - final List output = await runCommand(command); - - expect( - output, - containsAllInOrder([ - '${_startHeadingColor}Running for package_a...$_endColor', - '${_startWarningColor}Warning 1$_endColor', - '${_startWarningColor}Warning 2$_endColor', - '${_startHeadingColor}Running for package_b...$_endColor', - ])); - }); - - test('logs unhandled exceptions as errors', () async { - createFakePackage('package_a', packagesDir); - final RepositoryPackage failingPackage = - createFakePlugin('package_b', packagesDir); - createFakePackage('package_c', packagesDir); - _addResultFile(failingPackage, _ResultFileType.throws); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - Error? commandError; - final List output = - await runCommand(command, errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - '${_startErrorColor}Exception: Uh-oh$_endColor', - '${_startErrorColor}The following packages had errors:$_endColor', - '$_startErrorColor package_b:\n Unhandled exception$_endColor', - ])); - }); - - test('prints run summary on success', () async { - final RepositoryPackage warnPackage1 = - createFakePackage('package_a', packagesDir); - _addResultFile(warnPackage1, _ResultFileType.warns, - contents: 'Warning 1\nWarning 2'); - - createFakePackage('package_b', packagesDir); - - final RepositoryPackage skipPackage = - createFakePackage('package_c', packagesDir); - _addResultFile(skipPackage, _ResultFileType.skips, - contents: 'For a reason'); - - final RepositoryPackage skipAndWarnPackage = - createFakePackage('package_d', packagesDir); - _addResultFile(skipAndWarnPackage, _ResultFileType.warns, - contents: 'Warning'); - _addResultFile(skipAndWarnPackage, _ResultFileType.skips, - contents: 'See warning'); - - final RepositoryPackage warnPackage2 = - createFakePackage('package_e', packagesDir); - _addResultFile(warnPackage2, _ResultFileType.warns, - contents: 'Warning 1\nWarning 2'); - - createFakePackage('package_f', packagesDir); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - final List output = await runCommand(command); - - expect( - output, - containsAllInOrder([ - '------------------------------------------------------------', - 'Ran for 4 package(s) (2 with warnings)', - 'Skipped 2 package(s) (1 with warnings)', - '\n', - '${_startSuccessColor}No issues found!$_endColor', - ])); - // The long-form summary should not be printed for short-form commands. - expect(output, isNot(contains('Run summary:'))); - expect(output, isNot(contains(contains('package a - ran')))); - }); - - test('counts exclusions as skips in run summary', () async { - createFakePackage('package_a', packagesDir); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - final List output = - await runCommand(command, arguments: ['--exclude=package_a']); - - expect( - output, - containsAllInOrder([ - '------------------------------------------------------------', - 'Skipped 1 package(s)', - '\n', - '${_startSuccessColor}No issues found!$_endColor', - ])); - }); - - test('prints long-form run summary for long-output commands', () async { - final RepositoryPackage warnPackage1 = - createFakePackage('package_a', packagesDir); - _addResultFile(warnPackage1, _ResultFileType.warns, - contents: 'Warning 1\nWarning 2'); - - createFakePackage('package_b', packagesDir); - - final RepositoryPackage skipPackage = - createFakePackage('package_c', packagesDir); - _addResultFile(skipPackage, _ResultFileType.skips, - contents: 'For a reason'); - - final RepositoryPackage skipAndWarnPackage = - createFakePackage('package_d', packagesDir); - _addResultFile(skipAndWarnPackage, _ResultFileType.warns, - contents: 'Warning'); - _addResultFile(skipAndWarnPackage, _ResultFileType.skips, - contents: 'See warning'); - - final RepositoryPackage warnPackage2 = - createFakePackage('package_e', packagesDir); - _addResultFile(warnPackage2, _ResultFileType.warns, - contents: 'Warning 1\nWarning 2'); - - createFakePackage('package_f', packagesDir); - - final TestPackageLoopingCommand command = createTestCommand(); - final List output = await runCommand(command); - - expect( - output, - containsAllInOrder([ - '------------------------------------------------------------', - 'Run overview:', - ' package_a - ${_startWarningColor}ran (with warning)$_endColor', - ' package_b - ${_startSuccessColor}ran$_endColor', - ' package_c - ${_startSkipColor}skipped$_endColor', - ' package_d - ${_startSkipWithWarningColor}skipped (with warning)$_endColor', - ' package_e - ${_startWarningColor}ran (with warning)$_endColor', - ' package_f - ${_startSuccessColor}ran$_endColor', - '', - 'Ran for 4 package(s) (2 with warnings)', - 'Skipped 2 package(s) (1 with warnings)', - '\n', - '${_startSuccessColor}No issues found!$_endColor', - ])); - }); - - test('prints exclusions as skips in long-form run summary', () async { - createFakePackage('package_a', packagesDir); - - final TestPackageLoopingCommand command = createTestCommand(); - final List output = - await runCommand(command, arguments: ['--exclude=package_a']); - - expect( - output, - containsAllInOrder([ - ' package_a - ${_startSkipColor}excluded$_endColor', - '', - 'Skipped 1 package(s)', - '\n', - '${_startSuccessColor}No issues found!$_endColor', - ])); - }); - - test('handles warnings outside of runForPackage', () async { - createFakePackage('package_a', packagesDir); - - final TestPackageLoopingCommand command = createTestCommand( - hasLongOutput: false, - warnsDuringCleanup: true, - warnsDuringInit: true, - ); - final List output = await runCommand(command); - - expect( - output, - containsAllInOrder([ - '${_startWarningColor}Warning during initializeRun$_endColor', - '${_startHeadingColor}Running for package_a...$_endColor', - '${_startWarningColor}Warning during completeRun$_endColor', - '------------------------------------------------------------', - 'Ran for 1 package(s)', - '2 warnings not associated with a package', - '\n', - '${_startSuccessColor}No issues found!$_endColor', - ])); - }); - }); -} - -class TestPackageLoopingCommand extends PackageLoopingCommand { - TestPackageLoopingCommand( - Directory packagesDir, { - required Platform platform, - this.hasLongOutput = true, - this.packageLoopingType = PackageLoopingType.topLevelOnly, - this.customFailureListHeader, - this.customFailureListFooter, - this.failsDuringInit = false, - this.warnsDuringInit = false, - this.warnsDuringCleanup = false, - this.captureOutput = false, - ProcessRunner processRunner = const ProcessRunner(), - GitDir? gitDir, - }) : super(packagesDir, - processRunner: processRunner, platform: platform, gitDir: gitDir); - - final List checkedPackages = []; - final List capturedOutput = []; - - final String? customFailureListHeader; - final String? customFailureListFooter; - - final bool failsDuringInit; - final bool warnsDuringInit; - final bool warnsDuringCleanup; - - @override - bool hasLongOutput; - - @override - PackageLoopingType packageLoopingType; - - @override - String get failureListHeader => - customFailureListHeader ?? super.failureListHeader; - - @override - String get failureListFooter => - customFailureListFooter ?? super.failureListFooter; - - @override - bool captureOutput; - - @override - final String name = 'loop-test'; - - @override - final String description = 'sample package looping command'; - - @override - Future initializeRun() async { - if (warnsDuringInit) { - logWarning('Warning during initializeRun'); - } - if (failsDuringInit) { - throw ToolExit(2); - } - } - - @override - Future runForPackage(RepositoryPackage package) async { - checkedPackages.add(package.path); - final File warningFile = package.directory.childFile(_warningFile); - if (warningFile.existsSync()) { - final List warnings = warningFile.readAsLinesSync(); - warnings.forEach(logWarning); - } - final File skipFile = package.directory.childFile(_skipFile); - if (skipFile.existsSync()) { - return PackageResult.skip(skipFile.readAsStringSync()); - } - final File errorFile = package.directory.childFile(_errorFile); - if (errorFile.existsSync()) { - return PackageResult.fail(errorFile.readAsLinesSync()); - } - final File throwFile = package.directory.childFile(_throwFile); - if (throwFile.existsSync()) { - throw Exception('Uh-oh'); - } - return PackageResult.success(); - } - - @override - Future completeRun() async { - if (warnsDuringInit) { - logWarning('Warning during completeRun'); - } - } - - @override - Future handleCapturedOutput(List output) async { - capturedOutput.addAll(output); - } -} - -class MockProcessResult extends Mock implements io.ProcessResult {} diff --git a/script/tool/test/common/package_state_utils_test.dart b/script/tool/test/common/package_state_utils_test.dart deleted file mode 100644 index c9ae5ba4c742..000000000000 --- a/script/tool/test/common/package_state_utils_test.dart +++ /dev/null @@ -1,341 +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 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/git_version_finder.dart'; -import 'package:flutter_plugin_tools/src/common/package_state_utils.dart'; -import 'package:test/fake.dart'; -import 'package:test/test.dart'; - -import '../util.dart'; - -void main() { - late FileSystem fileSystem; - late Directory packagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - }); - - group('checkPackageChangeState', () { - test('reports version change needed for code changes', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - const List changedFiles = [ - 'packages/a_package/lib/plugin.dart', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_package'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, true); - expect(state.needsChangelogChange, true); - }); - - test('handles trailing slash on package path', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - const List changedFiles = [ - 'packages/a_package/lib/plugin.dart', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_package/'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, true); - expect(state.needsChangelogChange, true); - expect(state.hasChangelogChange, false); - }); - - test('does not flag version- and changelog-change-exempt changes', - () async { - final RepositoryPackage package = - createFakePlugin('a_plugin', packagesDir); - - const List changedFiles = [ - 'packages/a_plugin/CHANGELOG.md', - // Analysis. - 'packages/a_plugin/example/android/lint-baseline.xml', - // Tests. - 'packages/a_plugin/example/android/src/androidTest/foo/bar/FooTest.java', - 'packages/a_plugin/example/ios/RunnerTests/Foo.m', - 'packages/a_plugin/example/ios/RunnerUITests/info.plist', - // Test scripts. - 'packages/a_plugin/run_tests.sh', - // Tools. - 'packages/a_plugin/tool/a_development_tool.dart', - // Example build files. - 'packages/a_plugin/example/android/build.gradle', - 'packages/a_plugin/example/android/gradle/wrapper/gradle-wrapper.properties', - 'packages/a_plugin/example/ios/Runner.xcodeproj/project.pbxproj', - 'packages/a_plugin/example/linux/flutter/CMakeLists.txt', - 'packages/a_plugin/example/macos/Runner.xcodeproj/project.pbxproj', - 'packages/a_plugin/example/windows/CMakeLists.txt', - 'packages/a_plugin/example/pubspec.yaml', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, false); - expect(state.needsChangelogChange, false); - expect(state.hasChangelogChange, true); - }); - - test('only considers a root "tool" folder to be special', () async { - final RepositoryPackage package = - createFakePlugin('a_plugin', packagesDir); - - const List changedFiles = [ - 'packages/a_plugin/lib/foo/tool/tool_thing.dart', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, true); - expect(state.needsChangelogChange, true); - }); - - test('requires a version change for example/lib/main.dart', () async { - final RepositoryPackage package = createFakePlugin( - 'a_plugin', packagesDir, - extraFiles: ['example/lib/main.dart']); - - const List changedFiles = [ - 'packages/a_plugin/example/lib/main.dart', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, true); - expect(state.needsChangelogChange, true); - }); - - test('requires a version change for example/main.dart', () async { - final RepositoryPackage package = createFakePlugin( - 'a_plugin', packagesDir, - extraFiles: ['example/main.dart']); - - const List changedFiles = [ - 'packages/a_plugin/example/main.dart', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, true); - expect(state.needsChangelogChange, true); - }); - - test('requires a version change for example readme.md', () async { - final RepositoryPackage package = - createFakePlugin('a_plugin', packagesDir); - - const List changedFiles = [ - 'packages/a_plugin/example/README.md', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, true); - expect(state.needsChangelogChange, true); - }); - - test('requires a version change for example/example.md', () async { - final RepositoryPackage package = createFakePlugin( - 'a_plugin', packagesDir, - extraFiles: ['example/example.md']); - - const List changedFiles = [ - 'packages/a_plugin/example/example.md', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, true); - expect(state.needsChangelogChange, true); - }); - - test( - 'requires a changelog change but no version change for ' - 'lower-priority examples when example.md is present', () async { - final RepositoryPackage package = createFakePlugin( - 'a_plugin', packagesDir, - extraFiles: ['example/example.md']); - - const List changedFiles = [ - 'packages/a_plugin/example/lib/main.dart', - 'packages/a_plugin/example/main.dart', - 'packages/a_plugin/example/README.md', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, false); - expect(state.needsChangelogChange, true); - }); - - test( - 'requires a changelog change but no version change for README.md when ' - 'code example is present', () async { - final RepositoryPackage package = createFakePlugin( - 'a_plugin', packagesDir, - extraFiles: ['example/lib/main.dart']); - - const List changedFiles = [ - 'packages/a_plugin/example/README.md', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, false); - expect(state.needsChangelogChange, true); - }); - - test( - 'does not requires changelog or version change for build.gradle ' - 'test-dependency-only changes', () async { - final RepositoryPackage package = - createFakePlugin('a_plugin', packagesDir); - - const List changedFiles = [ - 'packages/a_plugin/android/build.gradle', - ]; - - final GitVersionFinder git = FakeGitVersionFinder(>{ - 'packages/a_plugin/android/build.gradle': [ - "- androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'", - "- testImplementation 'junit:junit:4.10.0'", - "+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'", - "+ testImplementation 'junit:junit:4.13.2'", - ] - }); - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/', - git: git); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, false); - expect(state.needsChangelogChange, false); - }); - - test('requires changelog or version change for other build.gradle changes', - () async { - final RepositoryPackage package = - createFakePlugin('a_plugin', packagesDir); - - const List changedFiles = [ - 'packages/a_plugin/android/build.gradle', - ]; - - final GitVersionFinder git = FakeGitVersionFinder(>{ - 'packages/a_plugin/android/build.gradle': [ - "- androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'", - "- testImplementation 'junit:junit:4.10.0'", - "+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'", - "+ testImplementation 'junit:junit:4.13.2'", - "- implementation 'com.google.android.gms:play-services-maps:18.0.0'", - "+ implementation 'com.google.android.gms:play-services-maps:18.0.2'", - ] - }); - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/', - git: git); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, true); - expect(state.needsChangelogChange, true); - }); - - test( - 'requires changelog or version change if build.gradle diffs cannot ' - 'be checked', () async { - final RepositoryPackage package = - createFakePlugin('a_plugin', packagesDir); - - const List changedFiles = [ - 'packages/a_plugin/android/build.gradle', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, true); - expect(state.needsChangelogChange, true); - }); - - test( - 'requires changelog or version change if build.gradle diffs cannot ' - 'be determined', () async { - final RepositoryPackage package = - createFakePlugin('a_plugin', packagesDir); - - const List changedFiles = [ - 'packages/a_plugin/android/build.gradle', - ]; - - final GitVersionFinder git = FakeGitVersionFinder(>{ - 'packages/a_plugin/android/build.gradle': [] - }); - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/', - git: git); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, true); - expect(state.needsChangelogChange, true); - }); - }); -} - -class FakeGitVersionFinder extends Fake implements GitVersionFinder { - FakeGitVersionFinder(this.fileDiffs); - - final Map> fileDiffs; - - @override - Future> getDiffContents({ - String? targetPath, - bool includeUncommitted = false, - }) async { - return fileDiffs[targetPath]!; - } -} diff --git a/script/tool/test/common/plugin_utils_test.dart b/script/tool/test/common/plugin_utils_test.dart deleted file mode 100644 index 415b1db8932a..000000000000 --- a/script/tool/test/common/plugin_utils_test.dart +++ /dev/null @@ -1,256 +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 '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:test/test.dart'; - -import '../util.dart'; - -void main() { - late FileSystem fileSystem; - late Directory packagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - }); - - group('pluginSupportsPlatform', () { - test('no platforms', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir); - - expect(pluginSupportsPlatform(platformAndroid, plugin), isFalse); - expect(pluginSupportsPlatform(platformIOS, plugin), isFalse); - expect(pluginSupportsPlatform(platformLinux, plugin), isFalse); - expect(pluginSupportsPlatform(platformMacOS, plugin), isFalse); - expect(pluginSupportsPlatform(platformWeb, plugin), isFalse); - expect(pluginSupportsPlatform(platformWindows, plugin), isFalse); - }); - - test('all platforms', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - platformLinux: const PlatformDetails(PlatformSupport.inline), - platformMacOS: const PlatformDetails(PlatformSupport.inline), - platformWeb: const PlatformDetails(PlatformSupport.inline), - platformWindows: const PlatformDetails(PlatformSupport.inline), - }); - - expect(pluginSupportsPlatform(platformAndroid, plugin), isTrue); - expect(pluginSupportsPlatform(platformIOS, plugin), isTrue); - expect(pluginSupportsPlatform(platformLinux, plugin), isTrue); - expect(pluginSupportsPlatform(platformMacOS, plugin), isTrue); - expect(pluginSupportsPlatform(platformWeb, plugin), isTrue); - expect(pluginSupportsPlatform(platformWindows, plugin), isTrue); - }); - - test('some platforms', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformLinux: const PlatformDetails(PlatformSupport.inline), - platformWeb: const PlatformDetails(PlatformSupport.inline), - }); - - expect(pluginSupportsPlatform(platformAndroid, plugin), isTrue); - expect(pluginSupportsPlatform(platformIOS, plugin), isFalse); - expect(pluginSupportsPlatform(platformLinux, plugin), isTrue); - expect(pluginSupportsPlatform(platformMacOS, plugin), isFalse); - expect(pluginSupportsPlatform(platformWeb, plugin), isTrue); - expect(pluginSupportsPlatform(platformWindows, plugin), isFalse); - }); - - test('inline plugins are only detected as inline', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - platformLinux: const PlatformDetails(PlatformSupport.inline), - platformMacOS: const PlatformDetails(PlatformSupport.inline), - platformWeb: const PlatformDetails(PlatformSupport.inline), - platformWindows: const PlatformDetails(PlatformSupport.inline), - }); - - expect( - pluginSupportsPlatform(platformAndroid, plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform(platformAndroid, plugin, - requiredMode: PlatformSupport.federated), - isFalse); - expect( - pluginSupportsPlatform(platformIOS, plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform(platformIOS, plugin, - requiredMode: PlatformSupport.federated), - isFalse); - expect( - pluginSupportsPlatform(platformLinux, plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform(platformLinux, plugin, - requiredMode: PlatformSupport.federated), - isFalse); - expect( - pluginSupportsPlatform(platformMacOS, plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform(platformMacOS, plugin, - requiredMode: PlatformSupport.federated), - isFalse); - expect( - pluginSupportsPlatform(platformWeb, plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform(platformWeb, plugin, - requiredMode: PlatformSupport.federated), - isFalse); - expect( - pluginSupportsPlatform(platformWindows, plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform(platformWindows, plugin, - requiredMode: PlatformSupport.federated), - isFalse); - }); - - test('federated plugins are only detected as federated', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.federated), - platformIOS: const PlatformDetails(PlatformSupport.federated), - platformLinux: const PlatformDetails(PlatformSupport.federated), - platformMacOS: const PlatformDetails(PlatformSupport.federated), - platformWeb: const PlatformDetails(PlatformSupport.federated), - platformWindows: const PlatformDetails(PlatformSupport.federated), - }); - - expect( - pluginSupportsPlatform(platformAndroid, plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform(platformAndroid, plugin, - requiredMode: PlatformSupport.inline), - isFalse); - expect( - pluginSupportsPlatform(platformIOS, plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform(platformIOS, plugin, - requiredMode: PlatformSupport.inline), - isFalse); - expect( - pluginSupportsPlatform(platformLinux, plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform(platformLinux, plugin, - requiredMode: PlatformSupport.inline), - isFalse); - expect( - pluginSupportsPlatform(platformMacOS, plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform(platformMacOS, plugin, - requiredMode: PlatformSupport.inline), - isFalse); - expect( - pluginSupportsPlatform(platformWeb, plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform(platformWeb, plugin, - requiredMode: PlatformSupport.inline), - isFalse); - expect( - pluginSupportsPlatform(platformWindows, plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform(platformWindows, plugin, - requiredMode: PlatformSupport.inline), - isFalse); - }); - }); - - group('pluginHasNativeCodeForPlatform', () { - test('returns false for web', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformWeb: const PlatformDetails(PlatformSupport.inline), - }, - ); - - expect(pluginHasNativeCodeForPlatform(platformWeb, plugin), isFalse); - }); - - test('returns false for a native-only plugin', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline), - platformMacOS: const PlatformDetails(PlatformSupport.inline), - platformWindows: const PlatformDetails(PlatformSupport.inline), - }, - ); - - expect(pluginHasNativeCodeForPlatform(platformLinux, plugin), isTrue); - expect(pluginHasNativeCodeForPlatform(platformMacOS, plugin), isTrue); - expect(pluginHasNativeCodeForPlatform(platformWindows, plugin), isTrue); - }); - - test('returns true for a native+Dart plugin', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline, hasDartCode: true), - platformMacOS: const PlatformDetails(PlatformSupport.inline, hasDartCode: true), - platformWindows: const PlatformDetails(PlatformSupport.inline, hasDartCode: true), - }, - ); - - expect(pluginHasNativeCodeForPlatform(platformLinux, plugin), isTrue); - expect(pluginHasNativeCodeForPlatform(platformMacOS, plugin), isTrue); - expect(pluginHasNativeCodeForPlatform(platformWindows, plugin), isTrue); - }); - - test('returns false for a Dart-only plugin', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline, - hasNativeCode: false, hasDartCode: true), - platformMacOS: const PlatformDetails(PlatformSupport.inline, - hasNativeCode: false, hasDartCode: true), - platformWindows: const PlatformDetails(PlatformSupport.inline, - hasNativeCode: false, hasDartCode: true), - }, - ); - - expect(pluginHasNativeCodeForPlatform(platformLinux, plugin), isFalse); - expect(pluginHasNativeCodeForPlatform(platformMacOS, plugin), isFalse); - expect(pluginHasNativeCodeForPlatform(platformWindows, plugin), isFalse); - }); - }); -} diff --git a/script/tool/test/common/pub_version_finder_test.dart b/script/tool/test/common/pub_version_finder_test.dart deleted file mode 100644 index 1692cf214abe..000000000000 --- a/script/tool/test/common/pub_version_finder_test.dart +++ /dev/null @@ -1,89 +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:convert'; -import 'dart:io'; - -import 'package:flutter_plugin_tools/src/common/pub_version_finder.dart'; -import 'package:http/http.dart' as http; -import 'package:http/testing.dart'; -import 'package:mockito/mockito.dart'; -import 'package:pub_semver/pub_semver.dart'; -import 'package:test/test.dart'; - -void main() { - test('Package does not exist.', () async { - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response('', 404); - }); - final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); - final PubVersionFinderResponse response = - await finder.getPackageVersion(packageName: 'some_package'); - - expect(response.versions, isEmpty); - expect(response.result, PubVersionFinderResult.noPackageFound); - expect(response.httpResponse.statusCode, 404); - expect(response.httpResponse.body, ''); - }); - - test('HTTP error when getting versions from pub', () async { - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response('', 400); - }); - final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); - final PubVersionFinderResponse response = - await finder.getPackageVersion(packageName: 'some_package'); - - expect(response.versions, isEmpty); - expect(response.result, PubVersionFinderResult.fail); - expect(response.httpResponse.statusCode, 400); - expect(response.httpResponse.body, ''); - }); - - test('Get a correct list of versions when http response is OK.', () async { - const Map httpResponse = { - 'name': 'some_package', - 'versions': [ - '0.0.1', - '0.0.2', - '0.0.2+2', - '0.1.1', - '0.0.1+1', - '0.1.0', - '0.2.0', - '0.1.0+1', - '0.0.2+1', - '2.0.0', - '1.2.0', - '1.0.0', - ], - }; - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response(json.encode(httpResponse), 200); - }); - final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); - final PubVersionFinderResponse response = - await finder.getPackageVersion(packageName: 'some_package'); - - expect(response.versions, [ - Version.parse('2.0.0'), - Version.parse('1.2.0'), - Version.parse('1.0.0'), - Version.parse('0.2.0'), - Version.parse('0.1.1'), - Version.parse('0.1.0+1'), - Version.parse('0.1.0'), - Version.parse('0.0.2+2'), - Version.parse('0.0.2+1'), - Version.parse('0.0.2'), - Version.parse('0.0.1+1'), - Version.parse('0.0.1'), - ]); - expect(response.result, PubVersionFinderResult.success); - expect(response.httpResponse.statusCode, 200); - expect(response.httpResponse.body, json.encode(httpResponse)); - }); -} - -class MockProcessResult extends Mock implements ProcessResult {} diff --git a/script/tool/test/common/repository_package_test.dart b/script/tool/test/common/repository_package_test.dart deleted file mode 100644 index db519c008233..000000000000 --- a/script/tool/test/common/repository_package_test.dart +++ /dev/null @@ -1,220 +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 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:test/test.dart'; - -import '../util.dart'; - -void main() { - late FileSystem fileSystem; - late Directory packagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - }); - - group('displayName', () { - test('prints packageDir-relative paths by default', () async { - expect( - RepositoryPackage(packagesDir.childDirectory('foo')).displayName, - 'foo', - ); - expect( - RepositoryPackage(packagesDir - .childDirectory('foo') - .childDirectory('bar') - .childDirectory('baz')) - .displayName, - 'foo/bar/baz', - ); - }); - - test('handles third_party/packages/', () async { - expect( - RepositoryPackage(packagesDir.parent - .childDirectory('third_party') - .childDirectory('packages') - .childDirectory('foo') - .childDirectory('bar') - .childDirectory('baz')) - .displayName, - 'foo/bar/baz', - ); - }); - - test('always uses Posix-style paths', () async { - final Directory windowsPackagesDir = createPackagesDirectory( - fileSystem: MemoryFileSystem(style: FileSystemStyle.windows)); - - expect( - RepositoryPackage(windowsPackagesDir.childDirectory('foo')).displayName, - 'foo', - ); - expect( - RepositoryPackage(windowsPackagesDir - .childDirectory('foo') - .childDirectory('bar') - .childDirectory('baz')) - .displayName, - 'foo/bar/baz', - ); - }); - - test('elides group name in grouped federated plugin structure', () async { - expect( - RepositoryPackage(packagesDir - .childDirectory('a_plugin') - .childDirectory('a_plugin_platform_interface')) - .displayName, - 'a_plugin_platform_interface', - ); - expect( - RepositoryPackage(packagesDir - .childDirectory('a_plugin') - .childDirectory('a_plugin_platform_web')) - .displayName, - 'a_plugin_platform_web', - ); - }); - - // The app-facing package doesn't get elided to avoid potential confusion - // with the group folder itself. - test('does not elide group name for app-facing packages', () async { - expect( - RepositoryPackage(packagesDir - .childDirectory('a_plugin') - .childDirectory('a_plugin')) - .displayName, - 'a_plugin/a_plugin', - ); - }); - }); - - group('getExamples', () { - test('handles a single Flutter example', () async { - final RepositoryPackage plugin = - createFakePlugin('a_plugin', packagesDir); - - final List examples = plugin.getExamples().toList(); - - expect(examples.length, 1); - expect(examples[0].path, getExampleDir(plugin).path); - }); - - test('handles multiple Flutter examples', () async { - final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, - examples: ['example1', 'example2']); - - final List examples = plugin.getExamples().toList(); - - expect(examples.length, 2); - expect(examples[0].path, - getExampleDir(plugin).childDirectory('example1').path); - expect(examples[1].path, - getExampleDir(plugin).childDirectory('example2').path); - }); - - test('handles a single non-Flutter example', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - final List examples = package.getExamples().toList(); - - expect(examples.length, 1); - expect(examples[0].path, getExampleDir(package).path); - }); - - test('handles multiple non-Flutter examples', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', packagesDir, - examples: ['example1', 'example2']); - - final List examples = package.getExamples().toList(); - - expect(examples.length, 2); - expect(examples[0].path, - getExampleDir(package).childDirectory('example1').path); - expect(examples[1].path, - getExampleDir(package).childDirectory('example2').path); - }); - }); - - group('federated plugin queries', () { - test('all return false for a simple plugin', () { - final RepositoryPackage plugin = - createFakePlugin('a_plugin', packagesDir); - expect(plugin.isFederated, false); - expect(plugin.isAppFacing, false); - expect(plugin.isPlatformInterface, false); - expect(plugin.isFederated, false); - }); - - test('handle app-facing packages', () { - final RepositoryPackage plugin = - createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); - expect(plugin.isFederated, true); - expect(plugin.isAppFacing, true); - expect(plugin.isPlatformInterface, false); - expect(plugin.isPlatformImplementation, false); - }); - - test('handle platform interface packages', () { - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin_platform_interface', - packagesDir.childDirectory('a_plugin')); - expect(plugin.isFederated, true); - expect(plugin.isAppFacing, false); - expect(plugin.isPlatformInterface, true); - expect(plugin.isPlatformImplementation, false); - }); - - test('handle platform implementation packages', () { - // A platform interface can end with anything, not just one of the known - // platform names, because of cases like webview_flutter_wkwebview. - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin_foo', packagesDir.childDirectory('a_plugin')); - expect(plugin.isFederated, true); - expect(plugin.isAppFacing, false); - expect(plugin.isPlatformInterface, false); - expect(plugin.isPlatformImplementation, true); - }); - }); - - group('pubspec', () { - test('file', () async { - final RepositoryPackage plugin = - createFakePlugin('a_plugin', packagesDir); - - final File pubspecFile = plugin.pubspecFile; - - expect(pubspecFile.path, plugin.directory.childFile('pubspec.yaml').path); - }); - - test('parsing', () async { - final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, - examples: ['example1', 'example2']); - - final Pubspec pubspec = plugin.parsePubspec(); - - expect(pubspec.name, 'a_plugin'); - }); - }); - - group('requiresFlutter', () { - test('returns true for Flutter package', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, isFlutter: true); - expect(package.requiresFlutter(), true); - }); - - test('returns false for non-Flutter package', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - expect(package.requiresFlutter(), false); - }); - }); -} diff --git a/script/tool/test/common/xcode_test.dart b/script/tool/test/common/xcode_test.dart deleted file mode 100644 index 259d8ea36cd2..000000000000 --- a/script/tool/test/common/xcode_test.dart +++ /dev/null @@ -1,406 +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:convert'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:file/local.dart'; -import 'package:flutter_plugin_tools/src/common/xcode.dart'; -import 'package:test/test.dart'; - -import '../mocks.dart'; -import '../util.dart'; - -void main() { - late RecordingProcessRunner processRunner; - late Xcode xcode; - - setUp(() { - processRunner = RecordingProcessRunner(); - xcode = Xcode(processRunner: processRunner); - }); - - group('findBestAvailableIphoneSimulator', () { - test('finds the newest device', () async { - const String expectedDeviceId = '1E76A0FD-38AC-4537-A989-EA639D7D012A'; - // Note: This uses `dynamic` deliberately, and should not be updated to - // Object, in order to ensure that the code correctly handles this return - // type from JSON decoding. - final Map devices = { - 'runtimes': >[ - { - 'bundlePath': - '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime', - 'buildversion': '17A577', - 'runtimeRoot': - '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime/Contents/Resources/RuntimeRoot', - 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-0', - 'version': '13.0', - 'isAvailable': true, - 'name': 'iOS 13.0' - }, - { - 'bundlePath': - '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime', - 'buildversion': '17L255', - 'runtimeRoot': - '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot', - 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-4', - 'version': '13.4', - 'isAvailable': true, - 'name': 'iOS 13.4' - }, - { - 'bundlePath': - '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime', - 'buildversion': '17T531', - 'runtimeRoot': - '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot', - 'identifier': 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2', - 'version': '6.2.1', - 'isAvailable': true, - 'name': 'watchOS 6.2' - } - ], - 'devices': { - 'com.apple.CoreSimulator.SimRuntime.iOS-13-4': >[ - { - 'dataPath': - '/Users/xxx/Library/Developer/CoreSimulator/Devices/2706BBEB-1E01-403E-A8E9-70E8E5A24774/data', - 'logPath': - '/Users/xxx/Library/Logs/CoreSimulator/2706BBEB-1E01-403E-A8E9-70E8E5A24774', - 'udid': '2706BBEB-1E01-403E-A8E9-70E8E5A24774', - 'isAvailable': true, - 'deviceTypeIdentifier': - 'com.apple.CoreSimulator.SimDeviceType.iPhone-8', - 'state': 'Shutdown', - 'name': 'iPhone 8' - }, - { - 'dataPath': - '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', - 'logPath': - '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'udid': expectedDeviceId, - 'isAvailable': true, - 'deviceTypeIdentifier': - 'com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus', - 'state': 'Shutdown', - 'name': 'iPhone 8 Plus' - } - ] - } - }; - - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(stdout: jsonEncode(devices)), - ]; - - expect(await xcode.findBestAvailableIphoneSimulator(), expectedDeviceId); - }); - - test('ignores non-iOS runtimes', () async { - // Note: This uses `dynamic` deliberately, and should not be updated to - // Object, in order to ensure that the code correctly handles this return - // type from JSON decoding. - final Map devices = { - 'runtimes': >[ - { - 'bundlePath': - '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime', - 'buildversion': '17T531', - 'runtimeRoot': - '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot', - 'identifier': 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2', - 'version': '6.2.1', - 'isAvailable': true, - 'name': 'watchOS 6.2' - } - ], - 'devices': { - 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2': - >[ - { - 'dataPath': - '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', - 'logPath': - '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'udid': '1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'isAvailable': true, - 'deviceTypeIdentifier': - 'com.apple.CoreSimulator.SimDeviceType.Apple-Watch-38mm', - 'state': 'Shutdown', - 'name': 'Apple Watch' - } - ] - } - }; - - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(stdout: jsonEncode(devices)), - ]; - - expect(await xcode.findBestAvailableIphoneSimulator(), null); - }); - - test('returns null if simctl fails', () async { - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(exitCode: 1), - ]; - - expect(await xcode.findBestAvailableIphoneSimulator(), null); - }); - }); - - group('runXcodeBuild', () { - test('handles minimal arguments', () async { - final Directory directory = const LocalFileSystem().currentDirectory; - - final int exitCode = await xcode.runXcodeBuild( - directory, - workspace: 'A.xcworkspace', - scheme: 'AScheme', - ); - - expect(exitCode, 0); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'build', - '-workspace', - 'A.xcworkspace', - '-scheme', - 'AScheme', - ], - directory.path), - ])); - }); - - test('handles all arguments', () async { - final Directory directory = const LocalFileSystem().currentDirectory; - - final int exitCode = await xcode.runXcodeBuild(directory, - actions: ['action1', 'action2'], - workspace: 'A.xcworkspace', - scheme: 'AScheme', - configuration: 'Debug', - extraFlags: ['-a', '-b', 'c=d']); - - expect(exitCode, 0); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'action1', - 'action2', - '-workspace', - 'A.xcworkspace', - '-scheme', - 'AScheme', - '-configuration', - 'Debug', - '-a', - '-b', - 'c=d', - ], - directory.path), - ])); - }); - - test('returns error codes', () async { - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(exitCode: 1), - ]; - final Directory directory = const LocalFileSystem().currentDirectory; - - final int exitCode = await xcode.runXcodeBuild( - directory, - workspace: 'A.xcworkspace', - scheme: 'AScheme', - ); - - expect(exitCode, 1); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'build', - '-workspace', - 'A.xcworkspace', - '-scheme', - 'AScheme', - ], - directory.path), - ])); - }); - }); - - group('projectHasTarget', () { - test('returns true when present', () async { - const String stdout = ''' -{ - "project" : { - "configurations" : [ - "Debug", - "Release" - ], - "name" : "Runner", - "schemes" : [ - "Runner" - ], - "targets" : [ - "Runner", - "RunnerTests", - "RunnerUITests" - ] - } -}'''; - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(stdout: stdout), - ]; - - final Directory project = - const LocalFileSystem().directory('/foo.xcodeproj'); - expect(await xcode.projectHasTarget(project, 'RunnerTests'), true); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - project.path, - ], - null), - ])); - }); - - test('returns false when not present', () async { - const String stdout = ''' -{ - "project" : { - "configurations" : [ - "Debug", - "Release" - ], - "name" : "Runner", - "schemes" : [ - "Runner" - ], - "targets" : [ - "Runner", - "RunnerUITests" - ] - } -}'''; - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(stdout: stdout), - ]; - - final Directory project = - const LocalFileSystem().directory('/foo.xcodeproj'); - expect(await xcode.projectHasTarget(project, 'RunnerTests'), false); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - project.path, - ], - null), - ])); - }); - - test('returns null for unexpected output', () async { - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(stdout: '{}'), - ]; - - final Directory project = - const LocalFileSystem().directory('/foo.xcodeproj'); - expect(await xcode.projectHasTarget(project, 'RunnerTests'), null); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - project.path, - ], - null), - ])); - }); - - test('returns null for invalid output', () async { - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(stdout: ':)'), - ]; - - final Directory project = - const LocalFileSystem().directory('/foo.xcodeproj'); - expect(await xcode.projectHasTarget(project, 'RunnerTests'), null); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - project.path, - ], - null), - ])); - }); - - test('returns null for failure', () async { - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(exitCode: 1), // xcodebuild -list - ]; - - final Directory project = - const LocalFileSystem().directory('/foo.xcodeproj'); - expect(await xcode.projectHasTarget(project, 'RunnerTests'), null); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - project.path, - ], - null), - ])); - }); - }); -} diff --git a/script/tool/test/create_all_packages_app_command_test.dart b/script/tool/test/create_all_packages_app_command_test.dart deleted file mode 100644 index 54551cbc3712..000000000000 --- a/script/tool/test/create_all_packages_app_command_test.dart +++ /dev/null @@ -1,256 +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:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/local.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/create_all_packages_app_command.dart'; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - late CommandRunner runner; - late CreateAllPackagesAppCommand command; - late FileSystem fileSystem; - late Directory testRoot; - late Directory packagesDir; - late RecordingProcessRunner processRunner; - - setUp(() { - // Since the core of this command is a call to 'flutter create', the test - // has to use the real filesystem. Put everything possible in a unique - // temporary to minimize effect on the host system. - fileSystem = const LocalFileSystem(); - testRoot = fileSystem.systemTempDirectory.createTempSync(); - packagesDir = testRoot.childDirectory('packages'); - processRunner = RecordingProcessRunner(); - - command = CreateAllPackagesAppCommand( - packagesDir, - processRunner: processRunner, - pluginsRoot: testRoot, - ); - runner = CommandRunner( - 'create_all_test', 'Test for $CreateAllPackagesAppCommand'); - runner.addCommand(command); - }); - - tearDown(() { - testRoot.deleteSync(recursive: true); - }); - - group('non-macOS host', () { - setUp(() { - command = CreateAllPackagesAppCommand( - packagesDir, - processRunner: processRunner, - // Set isWindows or not based on the actual host, so that - // `flutterCommand` works, since these tests actually call 'flutter'. - // The important thing is that isMacOS always returns false. - platform: MockPlatform(isWindows: const LocalPlatform().isWindows), - pluginsRoot: testRoot, - ); - runner = CommandRunner( - 'create_all_test', 'Test for $CreateAllPackagesAppCommand'); - runner.addCommand(command); - }); - - test('pubspec includes all plugins', () async { - createFakePlugin('plugina', packagesDir); - createFakePlugin('pluginb', packagesDir); - createFakePlugin('pluginc', packagesDir); - - await runCapturingPrint(runner, ['create-all-packages-app']); - final List pubspec = command.app.pubspecFile.readAsLinesSync(); - - expect( - pubspec, - containsAll([ - contains(RegExp('path: .*/packages/plugina')), - contains(RegExp('path: .*/packages/pluginb')), - contains(RegExp('path: .*/packages/pluginc')), - ])); - }); - - test('pubspec has overrides for all plugins', () async { - createFakePlugin('plugina', packagesDir); - createFakePlugin('pluginb', packagesDir); - createFakePlugin('pluginc', packagesDir); - - await runCapturingPrint(runner, ['create-all-packages-app']); - final List pubspec = command.app.pubspecFile.readAsLinesSync(); - - expect( - pubspec, - containsAllInOrder([ - contains('dependency_overrides:'), - contains(RegExp('path: .*/packages/plugina')), - contains(RegExp('path: .*/packages/pluginb')), - contains(RegExp('path: .*/packages/pluginc')), - ])); - }); - - test('pubspec preserves existing Dart SDK version', () async { - const String baselineProjectName = 'baseline'; - final Directory baselineProjectDirectory = - testRoot.childDirectory(baselineProjectName); - io.Process.runSync( - getFlutterCommand(const LocalPlatform()), - [ - 'create', - '--template=app', - '--project-name=$baselineProjectName', - baselineProjectDirectory.path, - ], - ); - final Pubspec baselinePubspec = - RepositoryPackage(baselineProjectDirectory).parsePubspec(); - - createFakePlugin('plugina', packagesDir); - - await runCapturingPrint(runner, ['create-all-packages-app']); - final Pubspec generatedPubspec = command.app.parsePubspec(); - - const String dartSdkKey = 'sdk'; - expect(generatedPubspec.environment?[dartSdkKey], - baselinePubspec.environment?[dartSdkKey]); - }); - - test('macOS deployment target is modified in pbxproj', () async { - createFakePlugin('plugina', packagesDir); - - await runCapturingPrint(runner, ['create-all-packages-app']); - final List pbxproj = command.app - .platformDirectory(FlutterPlatform.macos) - .childDirectory('Runner.xcodeproj') - .childFile('project.pbxproj') - .readAsLinesSync(); - - expect( - pbxproj, - everyElement((String line) => - !line.contains('MACOSX_DEPLOYMENT_TARGET') || - line.contains('10.15'))); - }); - - test('calls flutter pub get', () async { - createFakePlugin('plugina', packagesDir); - - await runCapturingPrint(runner, ['create-all-packages-app']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(const LocalPlatform()), - const ['pub', 'get'], - testRoot.childDirectory('all_packages').path), - ])); - }, - // See comment about Windows in create_all_packages_app_command.dart - skip: io.Platform.isWindows); - - test('fails if flutter pub get fails', () async { - createFakePlugin('plugina', packagesDir); - - processRunner.mockProcessesForExecutable[ - getFlutterCommand(const LocalPlatform())] = [ - MockProcess(exitCode: 1) - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['create-all-packages-app'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - "Failed to generate native build files via 'flutter pub get'"), - ])); - }, - // See comment about Windows in create_all_packages_app_command.dart - skip: io.Platform.isWindows); - - test('handles --output-dir', () async { - createFakePlugin('plugina', packagesDir); - - final Directory customOutputDir = - fileSystem.systemTempDirectory.createTempSync(); - await runCapturingPrint(runner, [ - 'create-all-packages-app', - '--output-dir=${customOutputDir.path}' - ]); - - expect(command.app.path, - customOutputDir.childDirectory('all_packages').path); - }); - - test('logs exclusions', () async { - createFakePlugin('plugina', packagesDir); - createFakePlugin('pluginb', packagesDir); - createFakePlugin('pluginc', packagesDir); - - final List output = await runCapturingPrint(runner, - ['create-all-packages-app', '--exclude=pluginb,pluginc']); - - expect( - output, - containsAllInOrder([ - 'Exluding the following plugins from the combined build:', - ' pluginb', - ' pluginc', - ])); - }); - }); - - group('macOS host', () { - setUp(() { - command = CreateAllPackagesAppCommand( - packagesDir, - processRunner: processRunner, - platform: MockPlatform(isMacOS: true), - pluginsRoot: testRoot, - ); - runner = CommandRunner( - 'create_all_test', 'Test for $CreateAllPackagesAppCommand'); - runner.addCommand(command); - }); - - test('macOS deployment target is modified in Podfile', () async { - createFakePlugin('plugina', packagesDir); - - final File podfileFile = RepositoryPackage( - command.packagesDir.parent.childDirectory('all_packages')) - .platformDirectory(FlutterPlatform.macos) - .childFile('Podfile'); - podfileFile.createSync(recursive: true); - podfileFile.writeAsStringSync(""" -platform :osx, '10.11' -# some other line -"""); - - await runCapturingPrint(runner, ['create-all-packages-app']); - final List podfile = command.app - .platformDirectory(FlutterPlatform.macos) - .childFile('Podfile') - .readAsLinesSync(); - - expect( - podfile, - everyElement((String line) => - !line.contains('platform :osx') || line.contains("'10.15'"))); - }, - // Podfile is only generated (and thus only edited) on macOS. - skip: !io.Platform.isMacOS); - }); -} diff --git a/script/tool/test/custom_test_command_test.dart b/script/tool/test/custom_test_command_test.dart deleted file mode 100644 index 8b0c021b1255..000000000000 --- a/script/tool/test/custom_test_command_test.dart +++ /dev/null @@ -1,328 +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:io' as io; - -import 'package:args/command_runner.dart'; -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/custom_test_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late RecordingProcessRunner processRunner; - late CommandRunner runner; - - group('posix', () { - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final CustomTestCommand analyzeCommand = CustomTestCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'custom_test_command', 'Test for custom_test_command'); - runner.addCommand(analyzeCommand); - }); - - test('runs both new and legacy when both are present', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, extraFiles: [ - 'tool/run_tests.dart', - 'run_tests.sh', - ]); - - final List output = - await runCapturingPrint(runner, ['custom-test']); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall(package.directory.childFile('run_tests.sh').path, - const [], package.path), - ProcessCall('dart', const ['run', 'tool/run_tests.dart'], - package.path), - ])); - - expect( - output, - containsAllInOrder([ - contains('Ran for 1 package(s)'), - ])); - }); - - test('runs when only new is present', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', packagesDir, - extraFiles: ['tool/run_tests.dart']); - - final List output = - await runCapturingPrint(runner, ['custom-test']); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall('dart', const ['run', 'tool/run_tests.dart'], - package.path), - ])); - - expect( - output, - containsAllInOrder([ - contains('Ran for 1 package(s)'), - ])); - }); - - test('runs pub get before running Dart test script', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', packagesDir, - extraFiles: ['tool/run_tests.dart']); - - await runCapturingPrint(runner, ['custom-test']); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall('dart', const ['pub', 'get'], package.path), - ProcessCall('dart', const ['run', 'tool/run_tests.dart'], - package.path), - ])); - }); - - test('runs when only legacy is present', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', packagesDir, - extraFiles: ['run_tests.sh']); - - final List output = - await runCapturingPrint(runner, ['custom-test']); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall(package.directory.childFile('run_tests.sh').path, - const [], package.path), - ])); - - expect( - output, - containsAllInOrder([ - contains('Ran for 1 package(s)'), - ])); - }); - - test('skips when neither is present', () async { - createFakePackage('a_package', packagesDir); - - final List output = - await runCapturingPrint(runner, ['custom-test']); - - expect(processRunner.recordedCalls, isEmpty); - - expect( - output, - containsAllInOrder([ - contains('Skipped 1 package(s)'), - ])); - }); - - test('fails if new fails', () async { - createFakePackage('a_package', packagesDir, extraFiles: [ - 'tool/run_tests.dart', - 'run_tests.sh', - ]); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(), // pub get - MockProcess(exitCode: 1), // test script - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['custom-test'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains('a_package') - ])); - }); - - test('fails if pub get fails', () async { - createFakePackage('a_package', packagesDir, extraFiles: [ - 'tool/run_tests.dart', - 'run_tests.sh', - ]); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['custom-test'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains('a_package:\n' - ' Unable to get script dependencies') - ])); - }); - - test('fails if legacy fails', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, extraFiles: [ - 'tool/run_tests.dart', - 'run_tests.sh', - ]); - - processRunner.mockProcessesForExecutable[ - package.directory.childFile('run_tests.sh').path] = [ - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['custom-test'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains('a_package') - ])); - }); - }); - - group('Windows', () { - setUp(() { - fileSystem = MemoryFileSystem(style: FileSystemStyle.windows); - mockPlatform = MockPlatform(isWindows: true); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final CustomTestCommand analyzeCommand = CustomTestCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'custom_test_command', 'Test for custom_test_command'); - runner.addCommand(analyzeCommand); - }); - - test('runs new and skips old when both are present', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, extraFiles: [ - 'tool/run_tests.dart', - 'run_tests.sh', - ]); - - final List output = - await runCapturingPrint(runner, ['custom-test']); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall('dart', const ['run', 'tool/run_tests.dart'], - package.path), - ])); - - expect( - output, - containsAllInOrder([ - contains('Ran for 1 package(s)'), - ])); - }); - - test('runs when only new is present', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', packagesDir, - extraFiles: ['tool/run_tests.dart']); - - final List output = - await runCapturingPrint(runner, ['custom-test']); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall('dart', const ['run', 'tool/run_tests.dart'], - package.path), - ])); - - expect( - output, - containsAllInOrder([ - contains('Ran for 1 package(s)'), - ])); - }); - - test('skips package when only legacy is present', () async { - createFakePackage('a_package', packagesDir, - extraFiles: ['run_tests.sh']); - - final List output = - await runCapturingPrint(runner, ['custom-test']); - - expect(processRunner.recordedCalls, isEmpty); - - expect( - output, - containsAllInOrder([ - contains('run_tests.sh is not supported on Windows'), - contains('Skipped 1 package(s)'), - ])); - }); - - test('fails if new fails', () async { - createFakePackage('a_package', packagesDir, extraFiles: [ - 'tool/run_tests.dart', - 'run_tests.sh', - ]); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(), // pub get - MockProcess(exitCode: 1), // test script - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['custom-test'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains('a_package') - ])); - }); - }); -} diff --git a/script/tool/test/dependabot_check_command_test.dart b/script/tool/test/dependabot_check_command_test.dart deleted file mode 100644 index 39dd8f4fcb92..000000000000 --- a/script/tool/test/dependabot_check_command_test.dart +++ /dev/null @@ -1,141 +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 'package:args/command_runner.dart'; -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/dependabot_check_command.dart'; -import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; - -import 'common/package_command_test.mocks.dart'; -import 'util.dart'; - -void main() { - late CommandRunner runner; - late FileSystem fileSystem; - late Directory root; - late Directory packagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - root = fileSystem.currentDirectory; - packagesDir = root.childDirectory('packages'); - - final MockGitDir gitDir = MockGitDir(); - when(gitDir.path).thenReturn(root.path); - - final DependabotCheckCommand command = DependabotCheckCommand( - packagesDir, - gitDir: gitDir, - ); - runner = CommandRunner( - 'dependabot_test', 'Test for $DependabotCheckCommand'); - runner.addCommand(command); - }); - - void setDependabotCoverage({ - Iterable gradleDirs = const [], - }) { - final Iterable gradleEntries = - gradleDirs.map((String directory) => ''' - - package-ecosystem: "gradle" - directory: "/$directory" - schedule: - interval: "daily" -'''); - final File configFile = - root.childDirectory('.github').childFile('dependabot.yml'); - configFile.createSync(recursive: true); - configFile.writeAsStringSync(''' -version: 2 -updates: -${gradleEntries.join('\n')} -'''); - } - - test('skips with no supported ecosystems', () async { - setDependabotCoverage(); - createFakePackage('a_package', packagesDir); - - final List output = - await runCapturingPrint(runner, ['dependabot-check']); - - expect( - output, - containsAllInOrder([ - contains('SKIPPING: No supported package ecosystems'), - ])); - }); - - test('fails for app missing Gradle coverage', () async { - setDependabotCoverage(); - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - package.directory - .childDirectory('example') - .childDirectory('android') - .childDirectory('app') - .createSync(recursive: true); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['dependabot-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Missing Gradle coverage.'), - contains('a_package/example:\n' - ' Missing Gradle coverage') - ])); - }); - - test('fails for plugin missing Gradle coverage', () async { - setDependabotCoverage(); - final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir); - plugin.directory.childDirectory('android').createSync(recursive: true); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['dependabot-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Missing Gradle coverage.'), - contains('a_plugin:\n' - ' Missing Gradle coverage') - ])); - }); - - test('passes for correct Gradle coverage', () async { - setDependabotCoverage(gradleDirs: [ - 'packages/a_plugin/android', - 'packages/a_plugin/example/android/app', - ]); - final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir); - // Test the plugin. - plugin.directory.childDirectory('android').createSync(recursive: true); - // And its example app. - plugin.directory - .childDirectory('example') - .childDirectory('android') - .childDirectory('app') - .createSync(recursive: true); - - final List output = - await runCapturingPrint(runner, ['dependabot-check']); - - expect(output, - containsAllInOrder([contains('Ran for 2 package(s)')])); - }); -} diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart deleted file mode 100644 index 0b6082098ae8..000000000000 --- a/script/tool/test/drive_examples_command_test.dart +++ /dev/null @@ -1,1257 +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:convert'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -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/drive_examples_command.dart'; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -const String _fakeIOSDevice = '67d5c3d1-8bdf-46ad-8f6b-b00e2a972dda'; -const String _fakeAndroidDevice = 'emulator-1234'; - -void main() { - group('test drive_example_command', () { - late FileSystem fileSystem; - late Platform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final DriveExamplesCommand command = DriveExamplesCommand(packagesDir, - processRunner: processRunner, platform: mockPlatform); - - runner = CommandRunner( - 'drive_examples_command', 'Test for drive_example_command'); - runner.addCommand(command); - }); - - void setMockFlutterDevicesOutput({ - bool hasIOSDevice = true, - bool hasAndroidDevice = true, - bool includeBanner = false, - }) { - const String updateBanner = ''' -╔════════════════════════════════════════════════════════════════════════════╗ -║ A new version of Flutter is available! ║ -║ ║ -║ To update to the latest version, run "flutter upgrade". ║ -╚════════════════════════════════════════════════════════════════════════════╝ -'''; - final List devices = [ - if (hasIOSDevice) '{"id": "$_fakeIOSDevice", "targetPlatform": "ios"}', - if (hasAndroidDevice) - '{"id": "$_fakeAndroidDevice", "targetPlatform": "android-x86"}', - ]; - final String output = - '''${includeBanner ? updateBanner : ''}[${devices.join(',')}]'''; - - final MockProcess mockDevicesProcess = - MockProcess(stdout: output, stdoutEncoding: utf8); - processRunner - .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = - [mockDevicesProcess]; - } - - test('fails if no platforms are provided', () async { - setMockFlutterDevicesOutput(); - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Exactly one of'), - ]), - ); - }); - - test('fails if multiple platforms are provided', () async { - setMockFlutterDevicesOutput(); - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples', '--ios', '--macos'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Exactly one of'), - ]), - ); - }); - - test('fails for iOS if no iOS devices are present', () async { - setMockFlutterDevicesOutput(hasIOSDevice: false); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples', '--ios'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No iOS devices'), - ]), - ); - }); - - test('handles flutter tool banners when checking devices', () async { - createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/integration_test.dart', - 'example/integration_test/foo_test.dart', - 'example/ios/ios.m', - ], - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - setMockFlutterDevicesOutput(includeBanner: true); - final List output = - await runCapturingPrint(runner, ['drive-examples', '--ios']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No issues found!'), - ]), - ); - }); - - test('fails for iOS if getting devices fails', () async { - // Simulate failure from `flutter devices`. - processRunner - .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = - [MockProcess(exitCode: 1)]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples', '--ios'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No iOS devices'), - ]), - ); - }); - - test('fails for Android if no Android devices are present', () async { - setMockFlutterDevicesOutput(hasAndroidDevice: false); - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples', '--android'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No Android devices'), - ]), - ); - }); - - test('driving under folder "test_driver"', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - 'example/android/android.java', - 'example/ios/ios.m', - ], - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - setMockFlutterDevicesOutput(); - final List output = - await runCapturingPrint(runner, ['drive-examples', '--ios']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['devices', '--machine'], null), - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - _fakeIOSDevice, - '--driver', - 'test_driver/plugin_test.dart', - '--target', - 'test_driver/plugin.dart' - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving under folder "test_driver" when test files are missing"', - () async { - setMockFlutterDevicesOutput(); - createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/android/android.java', - 'example/ios/ios.m', - ], - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples', '--android'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No driver tests were run (1 example(s) found).'), - contains('No test files for example/test_driver/plugin_test.dart'), - ]), - ); - }); - - test('a plugin without any integration test files is reported as an error', - () async { - setMockFlutterDevicesOutput(); - createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/lib/main.dart', - 'example/android/android.java', - 'example/ios/ios.m', - ], - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples', '--android'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No driver tests were run (1 example(s) found).'), - contains('No tests ran'), - ]), - ); - }); - - test('integration tests using test(...) fail validation', () async { - setMockFlutterDevicesOutput(); - final RepositoryPackage package = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/integration_test.dart', - 'example/integration_test/foo_test.dart', - 'example/android/android.java', - ], - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - package.directory - .childDirectory('example') - .childDirectory('integration_test') - .childFile('foo_test.dart') - .writeAsStringSync(''' - test('this is the wrong kind of test!'), () { - ... - } -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples', '--android'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('foo_test.dart failed validation'), - ]), - ); - }); - - test( - 'driving under folder "test_driver" when targets are under "integration_test"', - () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/integration_test.dart', - 'example/integration_test/bar_test.dart', - 'example/integration_test/foo_test.dart', - 'example/integration_test/ignore_me.dart', - 'example/android/android.java', - 'example/ios/ios.m', - ], - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - setMockFlutterDevicesOutput(); - final List output = - await runCapturingPrint(runner, ['drive-examples', '--ios']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['devices', '--machine'], null), - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - _fakeIOSDevice, - '--driver', - 'test_driver/integration_test.dart', - '--target', - 'integration_test/bar_test.dart', - ], - pluginExampleDirectory.path), - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - _fakeIOSDevice, - '--driver', - 'test_driver/integration_test.dart', - '--target', - 'integration_test/foo_test.dart', - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not support Linux is a no-op', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - ]); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--linux', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Skipping unsupported platform linux...'), - contains('No issues found!'), - ]), - ); - - // Output should be empty since running drive-examples --linux on a non-Linux - // plugin is a no-op. - expect(processRunner.recordedCalls, []); - }); - - test('driving on a Linux plugin', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - 'example/linux/linux.cc', - ], - platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline), - }, - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--linux', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - 'linux', - '--driver', - 'test_driver/plugin_test.dart', - '--target', - 'test_driver/plugin.dart' - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not suppport macOS is a no-op', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - ]); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--macos', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Skipping unsupported platform macos...'), - contains('No issues found!'), - ]), - ); - - // Output should be empty since running drive-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, []); - }); - - test('driving on a macOS plugin', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - 'example/macos/macos.swift', - ], - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--macos', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - 'macos', - '--driver', - 'test_driver/plugin_test.dart', - '--target', - 'test_driver/plugin.dart' - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not suppport web is a no-op', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - ]); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--web', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No issues found!'), - ]), - ); - - // Output should be empty since running drive-examples --web on a non-web - // plugin is a no-op. - expect(processRunner.recordedCalls, []); - }); - - test('driving a web plugin', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - 'example/web/index.html', - ], - platformSupport: { - platformWeb: const PlatformDetails(PlatformSupport.inline), - }, - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--web', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - 'web-server', - '--web-port=7357', - '--browser-name=chrome', - '--driver', - 'test_driver/plugin_test.dart', - '--target', - 'test_driver/plugin.dart' - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving a web plugin with CHROME_EXECUTABLE', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - 'example/web/index.html', - ], - platformSupport: { - platformWeb: const PlatformDetails(PlatformSupport.inline), - }, - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - mockPlatform.environment['CHROME_EXECUTABLE'] = '/path/to/chrome'; - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--web', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - 'web-server', - '--web-port=7357', - '--browser-name=chrome', - '--chrome-binary=/path/to/chrome', - '--driver', - 'test_driver/plugin_test.dart', - '--target', - 'test_driver/plugin.dart' - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not suppport Windows is a no-op', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - ]); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--windows', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Skipping unsupported platform windows...'), - contains('No issues found!'), - ]), - ); - - // Output should be empty since running drive-examples --windows on a - // non-Windows plugin is a no-op. - expect(processRunner.recordedCalls, []); - }); - - test('driving on a Windows plugin', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - 'example/windows/windows.cpp', - ], - platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.inline), - }, - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--windows', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - 'windows', - '--driver', - 'test_driver/plugin_test.dart', - '--target', - 'test_driver/plugin.dart' - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving on an Android plugin', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - 'example/android/android.java', - ], - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - }, - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - setMockFlutterDevicesOutput(); - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--android', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['devices', '--machine'], null), - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - _fakeAndroidDevice, - '--driver', - 'test_driver/plugin_test.dart', - '--target', - 'test_driver/plugin.dart' - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not support Android is no-op', () async { - createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - ], - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - setMockFlutterDevicesOutput(); - final List output = await runCapturingPrint( - runner, ['drive-examples', '--android']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Skipping unsupported platform android...'), - contains('No issues found!'), - ]), - ); - - // Output should be empty other than the device query. - expect(processRunner.recordedCalls, [ - ProcessCall(getFlutterCommand(mockPlatform), - const ['devices', '--machine'], null), - ]); - }); - - test('driving when plugin does not support iOS is no-op', () async { - createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - ], - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - setMockFlutterDevicesOutput(); - final List output = - await runCapturingPrint(runner, ['drive-examples', '--ios']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Skipping unsupported platform ios...'), - contains('No issues found!'), - ]), - ); - - // Output should be empty other than the device query. - expect(processRunner.recordedCalls, [ - ProcessCall(getFlutterCommand(mockPlatform), - const ['devices', '--machine'], null), - ]); - }); - - test('platform interface plugins are silently skipped', () async { - createFakePlugin('aplugin_platform_interface', packagesDir, - examples: []); - - setMockFlutterDevicesOutput(); - final List output = await runCapturingPrint( - runner, ['drive-examples', '--macos']); - - expect( - output, - containsAllInOrder([ - contains('Running for aplugin_platform_interface'), - contains( - 'SKIPPING: Platform interfaces are not expected to have integration tests.'), - contains('No issues found!'), - ]), - ); - - // Output should be empty since it's skipped. - expect(processRunner.recordedCalls, []); - }); - - test('enable-experiment flag', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - 'example/android/android.java', - 'example/ios/ios.m', - ], - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - setMockFlutterDevicesOutput(); - await runCapturingPrint(runner, [ - 'drive-examples', - '--ios', - '--enable-experiment=exp1', - ]); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['devices', '--machine'], null), - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - _fakeIOSDevice, - '--enable-experiment=exp1', - '--driver', - 'test_driver/plugin_test.dart', - '--target', - 'test_driver/plugin.dart' - ], - pluginExampleDirectory.path), - ])); - }); - - test('fails when no example is present', () async { - createFakePlugin( - 'plugin', - packagesDir, - examples: [], - platformSupport: { - platformWeb: const PlatformDetails(PlatformSupport.inline), - }, - ); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples', '--web'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No driver tests were run (0 example(s) found).'), - contains('The following packages had errors:'), - contains(' plugin:\n' - ' No tests ran (use --exclude if this is intentional)'), - ]), - ); - }); - - test('fails when no driver is present', () async { - createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/integration_test/bar_test.dart', - 'example/integration_test/foo_test.dart', - 'example/web/index.html', - ], - platformSupport: { - platformWeb: const PlatformDetails(PlatformSupport.inline), - }, - ); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples', '--web'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No driver tests found for plugin/example'), - contains('No driver tests were run (1 example(s) found).'), - contains('The following packages had errors:'), - contains(' plugin:\n' - ' No tests ran (use --exclude if this is intentional)'), - ]), - ); - }); - - test('fails when no integration tests are present', () async { - createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/integration_test.dart', - 'example/web/index.html', - ], - platformSupport: { - platformWeb: const PlatformDetails(PlatformSupport.inline), - }, - ); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples', '--web'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Found example/test_driver/integration_test.dart, but no ' - 'integration_test/*_test.dart files.'), - contains('No driver tests were run (1 example(s) found).'), - contains('The following packages had errors:'), - contains(' plugin:\n' - ' No test files for example/test_driver/integration_test.dart\n' - ' No tests ran (use --exclude if this is intentional)'), - ]), - ); - }); - - test('reports test failures', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/integration_test.dart', - 'example/integration_test/bar_test.dart', - 'example/integration_test/foo_test.dart', - 'example/macos/macos.swift', - ], - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - // Simulate failure from `flutter drive`. - processRunner - .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = - [ - // No mock for 'devices', since it's running for macOS. - MockProcess(exitCode: 1), // 'drive' #1 - MockProcess(exitCode: 1), // 'drive' #2 - ]; - - Error? commandError; - final List output = - await runCapturingPrint(runner, ['drive-examples', '--macos'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('The following packages had errors:'), - contains(' plugin:\n' - ' example/integration_test/bar_test.dart\n' - ' example/integration_test/foo_test.dart'), - ]), - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - 'macos', - '--driver', - 'test_driver/integration_test.dart', - '--target', - 'integration_test/bar_test.dart', - ], - pluginExampleDirectory.path), - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - 'macos', - '--driver', - 'test_driver/integration_test.dart', - '--target', - 'integration_test/foo_test.dart', - ], - pluginExampleDirectory.path), - ])); - }); - - group('packages', () { - test('can be driven', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, extraFiles: [ - 'example/integration_test/foo_test.dart', - 'example/test_driver/integration_test.dart', - 'example/web/index.html', - ]); - final Directory exampleDirectory = getExampleDir(package); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--web', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for a_package'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - 'web-server', - '--web-port=7357', - '--browser-name=chrome', - '--driver', - 'test_driver/integration_test.dart', - '--target', - 'integration_test/foo_test.dart' - ], - exampleDirectory.path), - ])); - }); - - test('are skipped when example does not support platform', () async { - createFakePackage('a_package', packagesDir, - isFlutter: true, - extraFiles: [ - 'example/integration_test/foo_test.dart', - 'example/test_driver/integration_test.dart', - ]); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--web', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for a_package'), - contains('Skipping a_package/example; does not support any ' - 'requested platforms'), - contains('SKIPPING: No example supports requested platform(s).'), - ]), - ); - - expect(processRunner.recordedCalls.isEmpty, true); - }); - - test('drive only supported examples if there is more than one', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', packagesDir, - isFlutter: true, - examples: [ - 'with_web', - 'without_web' - ], - extraFiles: [ - 'example/with_web/integration_test/foo_test.dart', - 'example/with_web/test_driver/integration_test.dart', - 'example/with_web/web/index.html', - 'example/without_web/integration_test/foo_test.dart', - 'example/without_web/test_driver/integration_test.dart', - ]); - final Directory supportedExampleDirectory = - getExampleDir(package).childDirectory('with_web'); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--web', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for a_package'), - contains( - 'Skipping a_package/example/without_web; does not support any requested platforms.'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - 'web-server', - '--web-port=7357', - '--browser-name=chrome', - '--driver', - 'test_driver/integration_test.dart', - '--target', - 'integration_test/foo_test.dart' - ], - supportedExampleDirectory.path), - ])); - }); - - test('are skipped when there is no integration testing', () async { - createFakePackage('a_package', packagesDir, - isFlutter: true, extraFiles: ['example/web/index.html']); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--web', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for a_package'), - contains('SKIPPING: No example is configured for driver tests.'), - ]), - ); - - expect(processRunner.recordedCalls.isEmpty, true); - }); - }); - }); -} diff --git a/script/tool/test/federation_safety_check_command_test.dart b/script/tool/test/federation_safety_check_command_test.dart deleted file mode 100644 index 6b6b1a514531..000000000000 --- a/script/tool/test/federation_safety_check_command_test.dart +++ /dev/null @@ -1,411 +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:io' as io; - -import 'package:args/command_runner.dart'; -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/federation_safety_check_command.dart'; -import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; - -import 'common/package_command_test.mocks.dart'; -import 'mocks.dart'; -import 'util.dart'; - -void main() { - FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - - final MockGitDir gitDir = MockGitDir(); - when(gitDir.path).thenReturn(packagesDir.parent.path); - when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) - .thenAnswer((Invocation invocation) { - final List arguments = - invocation.positionalArguments[0]! as List; - // Route git calls through the process runner, to make mock output - // consistent with other processes. Attach the first argument to the - // command to make targeting the mock results easier. - final String gitCommand = arguments.removeAt(0); - return processRunner.run('git-$gitCommand', arguments); - }); - - processRunner = RecordingProcessRunner(); - final FederationSafetyCheckCommand command = FederationSafetyCheckCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - gitDir: gitDir); - - runner = CommandRunner('federation_safety_check_command', - 'Test for $FederationSafetyCheckCommand'); - runner.addCommand(command); - }); - - test('skips non-plugin packages', () async { - final RepositoryPackage package = createFakePackage('foo', packagesDir); - - final String changedFileOutput = [ - package.libDirectory.childFile('foo.dart'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - final List output = - await runCapturingPrint(runner, ['federation-safety-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for foo...'), - contains('Not a plugin'), - contains('Skipped 1 package(s)'), - ]), - ); - }); - - test('skips unfederated plugins', () async { - final RepositoryPackage package = createFakePlugin('foo', packagesDir); - - final String changedFileOutput = [ - package.libDirectory.childFile('foo.dart'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - final List output = - await runCapturingPrint(runner, ['federation-safety-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for foo...'), - contains('Not a federated plugin'), - contains('Skipped 1 package(s)'), - ]), - ); - }); - - test('skips interface packages', () async { - final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final RepositoryPackage platformInterface = - createFakePlugin('foo_platform_interface', pluginGroupDir); - - final String changedFileOutput = [ - platformInterface.libDirectory.childFile('foo.dart'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - final List output = - await runCapturingPrint(runner, ['federation-safety-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for foo_platform_interface...'), - contains('Platform interface changes are not validated.'), - contains('Skipped 1 package(s)'), - ]), - ); - }); - - test('allows changes to just an interface package', () async { - final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final RepositoryPackage platformInterface = - createFakePlugin('foo_platform_interface', pluginGroupDir); - createFakePlugin('foo', pluginGroupDir); - createFakePlugin('foo_ios', pluginGroupDir); - createFakePlugin('foo_android', pluginGroupDir); - - final String changedFileOutput = [ - platformInterface.libDirectory.childFile('foo.dart'), - platformInterface.pubspecFile, - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - final List output = - await runCapturingPrint(runner, ['federation-safety-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for foo/foo...'), - contains('No Dart changes.'), - contains('Running for foo_android...'), - contains('No Dart changes.'), - contains('Running for foo_ios...'), - contains('No Dart changes.'), - contains('Running for foo_platform_interface...'), - contains('Ran for 3 package(s)'), - contains('Skipped 1 package(s)'), - ]), - ); - expect( - output, - isNot(contains([ - contains('No published changes for foo_platform_interface'), - ])), - ); - }); - - test('allows changes to multiple non-interface packages', () async { - final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); - final RepositoryPackage implementation = - createFakePlugin('foo_bar', pluginGroupDir); - createFakePlugin('foo_platform_interface', pluginGroupDir); - - final String changedFileOutput = [ - appFacing.libDirectory.childFile('foo.dart'), - implementation.libDirectory.childFile('foo.dart'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - final List output = - await runCapturingPrint(runner, ['federation-safety-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for foo/foo...'), - contains('No published changes for foo_platform_interface.'), - contains('Running for foo_bar...'), - contains('No published changes for foo_platform_interface.'), - ]), - ); - }); - - test( - 'fails on changes to interface and non-interface packages in the same plugin', - () async { - final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); - final RepositoryPackage implementation = - createFakePlugin('foo_bar', pluginGroupDir); - final RepositoryPackage platformInterface = - createFakePlugin('foo_platform_interface', pluginGroupDir); - - final String changedFileOutput = [ - appFacing.libDirectory.childFile('foo.dart'), - implementation.libDirectory.childFile('foo.dart'), - platformInterface.pubspecFile, - platformInterface.libDirectory.childFile('foo.dart'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['federation-safety-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running for foo/foo...'), - contains('Dart changes are not allowed to other packages in foo in the ' - 'same PR as changes to public Dart code in foo_platform_interface, ' - 'as this can cause accidental breaking changes to be missed by ' - 'automated checks. Please split the changes to these two packages ' - 'into separate PRs.'), - contains('Running for foo_bar...'), - contains('Dart changes are not allowed to other packages in foo'), - contains('The following packages had errors:'), - contains('foo/foo:\n' - ' foo_platform_interface changed.'), - contains('foo_bar:\n' - ' foo_platform_interface changed.'), - ]), - ); - }); - - test('ignores test-only changes to interface packages', () async { - final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); - final RepositoryPackage implementation = - createFakePlugin('foo_bar', pluginGroupDir); - final RepositoryPackage platformInterface = - createFakePlugin('foo_platform_interface', pluginGroupDir); - - final String changedFileOutput = [ - appFacing.libDirectory.childFile('foo.dart'), - implementation.libDirectory.childFile('foo.dart'), - platformInterface.pubspecFile, - platformInterface.testDirectory.childFile('foo.dart'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - final List output = - await runCapturingPrint(runner, ['federation-safety-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for foo/foo...'), - contains('No public code changes for foo_platform_interface.'), - contains('Running for foo_bar...'), - contains('No public code changes for foo_platform_interface.'), - ]), - ); - }); - - test('ignores unpublished changes to interface packages', () async { - final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); - final RepositoryPackage implementation = - createFakePlugin('foo_bar', pluginGroupDir); - final RepositoryPackage platformInterface = - createFakePlugin('foo_platform_interface', pluginGroupDir); - - final String changedFileOutput = [ - appFacing.libDirectory.childFile('foo.dart'), - implementation.libDirectory.childFile('foo.dart'), - platformInterface.pubspecFile, - platformInterface.libDirectory.childFile('foo.dart'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - // Simulate no change to the version in the interface's pubspec.yaml. - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: platformInterface.pubspecFile.readAsStringSync()), - ]; - - final List output = - await runCapturingPrint(runner, ['federation-safety-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for foo/foo...'), - contains('No published changes for foo_platform_interface.'), - contains('Running for foo_bar...'), - contains('No published changes for foo_platform_interface.'), - ]), - ); - }); - - test('allows things that look like mass changes, with warning', () async { - final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); - final RepositoryPackage implementation = - createFakePlugin('foo_bar', pluginGroupDir); - final RepositoryPackage platformInterface = - createFakePlugin('foo_platform_interface', pluginGroupDir); - - final RepositoryPackage otherPlugin1 = createFakePlugin('bar', packagesDir); - final RepositoryPackage otherPlugin2 = createFakePlugin('baz', packagesDir); - - final String changedFileOutput = [ - appFacing.libDirectory.childFile('foo.dart'), - implementation.libDirectory.childFile('foo.dart'), - platformInterface.pubspecFile, - platformInterface.libDirectory.childFile('foo.dart'), - otherPlugin1.libDirectory.childFile('bar.dart'), - otherPlugin2.libDirectory.childFile('baz.dart'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - final List output = - await runCapturingPrint(runner, ['federation-safety-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for foo/foo...'), - contains( - 'Ignoring potentially dangerous change, as this appears to be a mass change.'), - contains('Running for foo_bar...'), - contains( - 'Ignoring potentially dangerous change, as this appears to be a mass change.'), - contains('Ran for 2 package(s) (2 with warnings)'), - ]), - ); - }); - - test('handles top-level files that match federated package heuristics', - () async { - final RepositoryPackage plugin = createFakePlugin('foo', packagesDir); - - final String changedFileOutput = [ - // This should be picked up as a change to 'foo', and not crash. - plugin.directory.childFile('foo_bar.baz'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - final List output = - await runCapturingPrint(runner, ['federation-safety-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for foo...'), - ]), - ); - }); - - test('handles deletion of an entire plugin', () async { - // Simulate deletion, in the form of diffs for packages that don't exist in - // the filesystem. - final String changedFileOutput = [ - packagesDir.childDirectory('foo').childFile('pubspec.yaml'), - packagesDir - .childDirectory('foo') - .childDirectory('lib') - .childFile('foo.dart'), - packagesDir - .childDirectory('foo_platform_interface') - .childFile('pubspec.yaml'), - packagesDir - .childDirectory('foo_platform_interface') - .childDirectory('lib') - .childFile('foo.dart'), - packagesDir.childDirectory('foo_web').childFile('pubspec.yaml'), - packagesDir - .childDirectory('foo_web') - .childDirectory('lib') - .childFile('foo.dart'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - final List output = - await runCapturingPrint(runner, ['federation-safety-check']); - - expect( - output, - containsAllInOrder([ - contains('Ran for 0 package(s)'), - ]), - ); - }); -} diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart deleted file mode 100644 index 68ea62b2334f..000000000000 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ /dev/null @@ -1,795 +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:io'; - -import 'package:args/command_runner.dart'; -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/file_utils.dart'; -import 'package:flutter_plugin_tools/src/firebase_test_lab_command.dart'; -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('FirebaseTestLabCommand', () { - FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final FirebaseTestLabCommand command = FirebaseTestLabCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'firebase_test_lab_command', 'Test for $FirebaseTestLabCommand'); - runner.addCommand(command); - }); - - void writeJavaTestFile(RepositoryPackage plugin, String relativeFilePath, - {String runnerClass = 'FlutterTestRunner'}) { - childFileWithSubcomponents( - plugin.directory, p.posix.split(relativeFilePath)) - .writeAsStringSync(''' -@DartIntegrationTest -@RunWith($runnerClass.class) -public class MainActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); -} -'''); - } - - test('fails if gcloud auth fails', () async { - processRunner.mockProcessesForExecutable['gcloud'] = [ - MockProcess(exitCode: 1) - ]; - - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/integration_test/foo_test.dart', - 'example/android/gradlew', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['firebase-test-lab'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to activate gcloud account.'), - ])); - }); - - test('retries gcloud set', () async { - processRunner.mockProcessesForExecutable['gcloud'] = [ - MockProcess(), // auth - MockProcess(exitCode: 1), // config - ]; - - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/integration_test/foo_test.dart', - 'example/android/gradlew', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - final List output = - await runCapturingPrint(runner, ['firebase-test-lab']); - - expect( - output, - containsAllInOrder([ - contains( - 'Warning: gcloud config set returned a non-zero exit code. Continuing anyway.'), - ])); - }); - - test('only runs gcloud configuration once', () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir, extraFiles: [ - 'test/plugin_test.dart', - 'example/integration_test/foo_test.dart', - 'example/android/gradlew', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin1, javaTestFileRelativePath); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir, extraFiles: [ - 'test/plugin_test.dart', - 'example/integration_test/bar_test.dart', - 'example/android/gradlew', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin2, javaTestFileRelativePath); - - final List output = await runCapturingPrint(runner, [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - '--device', - 'model=seoul,version=26', - '--test-run-id', - 'testRunId', - '--build-id', - 'buildId', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin1'), - contains('Firebase project configured.'), - contains('Testing example/integration_test/foo_test.dart...'), - contains('Running for plugin2'), - contains('Testing example/integration_test/bar_test.dart...'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'gcloud', - 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' - .split(' '), - null), - ProcessCall( - 'gcloud', 'config set project flutter-cirrus'.split(' '), null), - ProcessCall( - '/packages/plugin1/example/android/gradlew', - 'app:assembleAndroidTest -Pverbose=true'.split(' '), - '/packages/plugin1/example/android'), - ProcessCall( - '/packages/plugin1/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin1/example/integration_test/foo_test.dart' - .split(' '), - '/packages/plugin1/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin1/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' - .split(' '), - '/packages/plugin1/example'), - ProcessCall( - '/packages/plugin2/example/android/gradlew', - 'app:assembleAndroidTest -Pverbose=true'.split(' '), - '/packages/plugin2/example/android'), - ProcessCall( - '/packages/plugin2/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin2/example/integration_test/bar_test.dart' - .split(' '), - '/packages/plugin2/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin2/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' - .split(' '), - '/packages/plugin2/example'), - ]), - ); - }); - - test('runs integration tests', () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'test/plugin_test.dart', - 'example/integration_test/bar_test.dart', - 'example/integration_test/foo_test.dart', - 'example/integration_test/should_not_run.dart', - 'example/android/gradlew', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - final List output = await runCapturingPrint(runner, [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - '--device', - 'model=seoul,version=26', - '--test-run-id', - 'testRunId', - '--build-id', - 'buildId', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Firebase project configured.'), - contains('Testing example/integration_test/bar_test.dart...'), - contains('Testing example/integration_test/foo_test.dart...'), - ]), - ); - expect(output, isNot(contains('test/plugin_test.dart'))); - expect(output, - isNot(contains('example/integration_test/should_not_run.dart'))); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'gcloud', - 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' - .split(' '), - null), - ProcessCall( - 'gcloud', 'config set project flutter-cirrus'.split(' '), null), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleAndroidTest -Pverbose=true'.split(' '), - '/packages/plugin/example/android'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/bar_test.dart' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/1/ --device model=redfin,version=30 --device model=seoul,version=26' - .split(' '), - '/packages/plugin/example'), - ]), - ); - }); - - test('runs for all examples', () async { - const List examples = ['example1', 'example2']; - const String javaTestFileExampleRelativePath = - 'android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - examples: examples, - extraFiles: [ - for (final String example in examples) ...[ - 'example/$example/integration_test/a_test.dart', - 'example/$example/android/gradlew', - 'example/$example/$javaTestFileExampleRelativePath', - ], - ]); - for (final String example in examples) { - writeJavaTestFile( - plugin, 'example/$example/$javaTestFileExampleRelativePath'); - } - - final List output = await runCapturingPrint(runner, [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - '--device', - 'model=seoul,version=26', - '--test-run-id', - 'testRunId', - '--build-id', - 'buildId', - ]); - - expect( - output, - containsAllInOrder([ - contains('Testing example/example1/integration_test/a_test.dart...'), - contains('Testing example/example2/integration_test/a_test.dart...'), - ]), - ); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall( - '/packages/plugin/example/example1/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/example1/integration_test/a_test.dart' - .split(' '), - '/packages/plugin/example/example1/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example1/0/ --device model=redfin,version=30 --device model=seoul,version=26' - .split(' '), - '/packages/plugin/example/example1'), - ProcessCall( - '/packages/plugin/example/example2/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/example2/integration_test/a_test.dart' - .split(' '), - '/packages/plugin/example/example2/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example2/0/ --device model=redfin,version=30 --device model=seoul,version=26' - .split(' '), - '/packages/plugin/example/example2'), - ]), - ); - }); - - test('fails if a test fails twice', () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/integration_test/bar_test.dart', - 'example/integration_test/foo_test.dart', - 'example/android/gradlew', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - processRunner.mockProcessesForExecutable['gcloud'] = [ - MockProcess(), // auth - MockProcess(), // config - MockProcess(exitCode: 1), // integration test #1 - MockProcess(exitCode: 1), // integration test #1 retry - MockProcess(), // integration test #2 - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, - [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - ], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Testing example/integration_test/bar_test.dart...'), - contains('Testing example/integration_test/foo_test.dart...'), - contains('plugin:\n' - ' example/integration_test/bar_test.dart failed tests'), - ]), - ); - }); - - test('passes with warning if a test fails once, then passes on retry', - () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/integration_test/bar_test.dart', - 'example/integration_test/foo_test.dart', - 'example/android/gradlew', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - processRunner.mockProcessesForExecutable['gcloud'] = [ - MockProcess(), // auth - MockProcess(), // config - MockProcess(exitCode: 1), // integration test #1 - MockProcess(), // integration test #1 retry - MockProcess(), // integration test #2 - ]; - - final List output = await runCapturingPrint(runner, [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - ]); - - expect( - output, - containsAllInOrder([ - contains('Testing example/integration_test/bar_test.dart...'), - contains('bar_test.dart failed on attempt 1. Retrying...'), - contains('Testing example/integration_test/foo_test.dart...'), - contains('Ran for 1 package(s) (1 with warnings)'), - ]), - ); - }); - - test('fails for packages with no androidTest directory', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/integration_test/foo_test.dart', - 'example/android/gradlew', - ]); - - Error? commandError; - final List output = await runCapturingPrint( - runner, - [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - ], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No androidTest directory found.'), - contains('The following packages had errors:'), - contains('plugin:\n' - ' No tests ran (use --exclude if this is intentional).'), - ]), - ); - }); - - test('fails for packages with no integration test files', () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/android/gradlew', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - Error? commandError; - final List output = await runCapturingPrint( - runner, - [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - ], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No integration tests were run'), - contains('The following packages had errors:'), - contains('plugin:\n' - ' No tests ran (use --exclude if this is intentional).'), - ]), - ); - }); - - test('fails for packages with no integration_test runner', () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'test/plugin_test.dart', - 'example/integration_test/bar_test.dart', - 'example/integration_test/foo_test.dart', - 'example/integration_test/should_not_run.dart', - 'example/android/gradlew', - javaTestFileRelativePath, - ]); - // Use the wrong @RunWith annotation. - writeJavaTestFile(plugin, javaTestFileRelativePath, - runnerClass: 'AndroidJUnit4.class'); - - Error? commandError; - final List output = await runCapturingPrint( - runner, - [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - ], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No integration_test runner found. ' - 'See the integration_test package README for setup instructions.'), - contains('plugin:\n' - ' No integration_test runner.'), - ]), - ); - }); - - test('skips packages with no android directory', () async { - createFakePackage('package', packagesDir, extraFiles: [ - 'example/integration_test/foo_test.dart', - ]); - - final List output = await runCapturingPrint(runner, [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for package'), - contains('No examples support Android'), - ]), - ); - expect(output, - isNot(contains('Testing example/integration_test/foo_test.dart...'))); - - expect( - processRunner.recordedCalls, - orderedEquals([]), - ); - }); - - test('builds if gradlew is missing', () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/integration_test/foo_test.dart', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - final List output = await runCapturingPrint(runner, [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - '--test-run-id', - 'testRunId', - '--build-id', - 'buildId', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Running flutter build apk...'), - contains('Firebase project configured.'), - contains('Testing example/integration_test/foo_test.dart...'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'flutter', - 'build apk'.split(' '), - '/packages/plugin/example/android', - ), - ProcessCall( - 'gcloud', - 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' - .split(' '), - null), - ProcessCall( - 'gcloud', 'config set project flutter-cirrus'.split(' '), null), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleAndroidTest -Pverbose=true'.split(' '), - '/packages/plugin/example/android'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30' - .split(' '), - '/packages/plugin/example'), - ]), - ); - }); - - test('fails if building to generate gradlew fails', () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/integration_test/foo_test.dart', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(exitCode: 1) // flutter build - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, - [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - ], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to build example apk'), - ])); - }); - - test('fails if assembleAndroidTest fails', () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/integration_test/foo_test.dart', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - final String gradlewPath = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android) - .childFile('gradlew') - .path; - processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess(exitCode: 1) - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, - [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - ], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to assemble androidTest'), - ])); - }); - - test('fails if assembleDebug fails', () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/integration_test/foo_test.dart', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - final String gradlewPath = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android) - .childFile('gradlew') - .path; - processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess(), // assembleAndroidTest - MockProcess(exitCode: 1), // assembleDebug - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, - [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - ], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Could not build example/integration_test/foo_test.dart'), - contains('The following packages had errors:'), - contains(' plugin:\n' - ' example/integration_test/foo_test.dart failed to build'), - ])); - }); - - test('experimental flag', () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/integration_test/foo_test.dart', - 'example/android/gradlew', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - await runCapturingPrint(runner, [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - '--test-run-id', - 'testRunId', - '--build-id', - 'buildId', - '--enable-experiment=exp1', - ]); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'gcloud', - 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' - .split(' '), - null), - ProcessCall( - 'gcloud', 'config set project flutter-cirrus'.split(' '), null), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleAndroidTest -Pverbose=true -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30' - .split(' '), - '/packages/plugin/example'), - ]), - ); - }); - }); -} diff --git a/script/tool/test/fix_command_test.dart b/script/tool/test/fix_command_test.dart deleted file mode 100644 index 16061d2206cd..000000000000 --- a/script/tool/test/fix_command_test.dart +++ /dev/null @@ -1,78 +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:io' as io; - -import 'package:args/command_runner.dart'; -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/fix_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late RecordingProcessRunner processRunner; - late CommandRunner runner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final FixCommand command = FixCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner('fix_command', 'Test for fix_command'); - runner.addCommand(command); - }); - - test('runs fix in top-level packages and subpackages', () async { - final RepositoryPackage package = createFakePackage('a', packagesDir); - final RepositoryPackage plugin = createFakePlugin('b', packagesDir); - - await runCapturingPrint(runner, ['fix']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('dart', const ['fix', '--apply'], package.path), - ProcessCall('dart', const ['fix', '--apply'], - package.getExamples().first.path), - ProcessCall('dart', const ['fix', '--apply'], plugin.path), - ProcessCall('dart', const ['fix', '--apply'], - plugin.getExamples().first.path), - ])); - }); - - test('fails if "dart fix" fails', () async { - createFakePlugin('foo', packagesDir); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint(runner, ['fix'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to automatically fix package.'), - ]), - ); - }); -} diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart deleted file mode 100644 index 9a865053a2b6..000000000000 --- a/script/tool/test/format_command_test.dart +++ /dev/null @@ -1,624 +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:io' as io; - -import 'package:args/command_runner.dart'; -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/file_utils.dart'; -import 'package:flutter_plugin_tools/src/format_command.dart'; -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late RecordingProcessRunner processRunner; - late FormatCommand analyzeCommand; - late CommandRunner runner; - late String javaFormatPath; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - analyzeCommand = FormatCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - // Create the java formatter file that the command checks for, to avoid - // a download. - final p.Context path = analyzeCommand.path; - javaFormatPath = path.join(path.dirname(path.fromUri(mockPlatform.script)), - 'google-java-format-1.3-all-deps.jar'); - fileSystem.file(javaFormatPath).createSync(recursive: true); - - runner = CommandRunner('format_command', 'Test for format_command'); - runner.addCommand(analyzeCommand); - }); - - /// Returns a modified version of a list of [relativePaths] that are relative - /// to [package] to instead be relative to [packagesDir]. - List getPackagesDirRelativePaths( - RepositoryPackage package, List relativePaths) { - final p.Context path = analyzeCommand.path; - final String relativeBase = - path.relative(package.path, from: packagesDir.path); - return relativePaths - .map((String relativePath) => path.join(relativeBase, relativePath)) - .toList(); - } - - /// Returns a list of [count] relative paths to pass to [createFakePlugin] - /// or [createFakePackage] with name [packageName] such that each path will - /// be 99 characters long relative to [packagesDir]. - /// - /// This is for each of testing batching, since it means each file will - /// consume 100 characters of the batch length. - List get99CharacterPathExtraFiles(String packageName, int count) { - final int padding = 99 - - packageName.length - - 1 - // the path separator after the package name - 1 - // the path separator after the padding - 10; // the file name - const int filenameBase = 10000; - - final p.Context path = analyzeCommand.path; - return [ - for (int i = filenameBase; i < filenameBase + count; ++i) - path.join('a' * padding, '$i.dart'), - ]; - } - - test('formats .dart files', () async { - const List files = [ - 'lib/a.dart', - 'lib/src/b.dart', - 'lib/src/c.dart', - ]; - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - extraFiles: files, - ); - - await runCapturingPrint(runner, ['format']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - ['format', ...getPackagesDirRelativePaths(plugin, files)], - packagesDir.path), - ])); - }); - - test('does not format .dart files with pragma', () async { - const List formattedFiles = [ - 'lib/a.dart', - 'lib/src/b.dart', - 'lib/src/c.dart', - ]; - const String unformattedFile = 'lib/src/d.dart'; - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - extraFiles: [ - ...formattedFiles, - unformattedFile, - ], - ); - - final p.Context posixContext = p.posix; - childFileWithSubcomponents( - plugin.directory, posixContext.split(unformattedFile)) - .writeAsStringSync( - '// copyright bla bla\n// This file is hand-formatted.\ncode...'); - - await runCapturingPrint(runner, ['format']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - [ - 'format', - ...getPackagesDirRelativePaths(plugin, formattedFiles) - ], - packagesDir.path), - ])); - }); - - test('fails if flutter format fails', () async { - const List files = [ - 'lib/a.dart', - 'lib/src/b.dart', - 'lib/src/c.dart', - ]; - createFakePlugin('a_plugin', packagesDir, extraFiles: files); - - processRunner.mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = - [MockProcess(exitCode: 1)]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['format'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Failed to format Dart files: exit code 1.'), - ])); - }); - - test('formats .java files', () async { - const List files = [ - 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', - 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', - ]; - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - extraFiles: files, - ); - - await runCapturingPrint(runner, ['format']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - const ProcessCall('java', ['-version'], null), - ProcessCall( - 'java', - [ - '-jar', - javaFormatPath, - '--replace', - ...getPackagesDirRelativePaths(plugin, files) - ], - packagesDir.path), - ])); - }); - - test('fails with a clear message if Java is not in the path', () async { - const List files = [ - 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', - 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', - ]; - createFakePlugin('a_plugin', packagesDir, extraFiles: files); - - processRunner.mockProcessesForExecutable['java'] = [ - MockProcess(exitCode: 1) - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['format'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Unable to run "java". Make sure that it is in your path, or ' - 'provide a full path with --java.'), - ])); - }); - - test('fails if Java formatter fails', () async { - const List files = [ - 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', - 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', - ]; - createFakePlugin('a_plugin', packagesDir, extraFiles: files); - - processRunner.mockProcessesForExecutable['java'] = [ - MockProcess(), // check for working java - MockProcess(exitCode: 1), // format - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['format'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Failed to format Java files: exit code 1.'), - ])); - }); - - test('honors --java flag', () async { - const List files = [ - 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', - 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', - ]; - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - extraFiles: files, - ); - - await runCapturingPrint(runner, ['format', '--java=/path/to/java']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - const ProcessCall('/path/to/java', ['--version'], null), - ProcessCall( - '/path/to/java', - [ - '-jar', - javaFormatPath, - '--replace', - ...getPackagesDirRelativePaths(plugin, files) - ], - packagesDir.path), - ])); - }); - - test('formats c-ish files', () async { - const List files = [ - 'ios/Classes/Foo.h', - 'ios/Classes/Foo.m', - 'linux/foo_plugin.cc', - 'macos/Classes/Foo.h', - 'macos/Classes/Foo.mm', - 'windows/foo_plugin.cpp', - ]; - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - extraFiles: files, - ); - - await runCapturingPrint(runner, ['format']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - const ProcessCall('clang-format', ['--version'], null), - ProcessCall( - 'clang-format', - [ - '-i', - '--style=file', - ...getPackagesDirRelativePaths(plugin, files) - ], - packagesDir.path), - ])); - }); - - test('fails with a clear message if clang-format is not in the path', - () async { - const List files = [ - 'linux/foo_plugin.cc', - 'macos/Classes/Foo.h', - ]; - createFakePlugin('a_plugin', packagesDir, extraFiles: files); - - processRunner.mockProcessesForExecutable['clang-format'] = [ - MockProcess(exitCode: 1) - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['format'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to run "clang-format". Make sure that it is in your ' - 'path, or provide a full path with --clang-format.'), - ])); - }); - - test('honors --clang-format flag', () async { - const List files = [ - 'windows/foo_plugin.cpp', - ]; - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - extraFiles: files, - ); - - await runCapturingPrint( - runner, ['format', '--clang-format=/path/to/clang-format']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - const ProcessCall( - '/path/to/clang-format', ['--version'], null), - ProcessCall( - '/path/to/clang-format', - [ - '-i', - '--style=file', - ...getPackagesDirRelativePaths(plugin, files) - ], - packagesDir.path), - ])); - }); - - test('fails if clang-format fails', () async { - const List files = [ - 'linux/foo_plugin.cc', - 'macos/Classes/Foo.h', - ]; - createFakePlugin('a_plugin', packagesDir, extraFiles: files); - - processRunner.mockProcessesForExecutable['clang-format'] = [ - MockProcess(), // check for working clang-format - MockProcess(exitCode: 1), // format - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['format'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Failed to format C, C++, and Objective-C files: exit code 1.'), - ])); - }); - - test('skips known non-repo files', () async { - const List skipFiles = [ - '/example/build/SomeFramework.framework/Headers/SomeFramework.h', - '/example/Pods/APod.framework/Headers/APod.h', - '.dart_tool/internals/foo.cc', - '.dart_tool/internals/Bar.java', - '.dart_tool/internals/baz.dart', - ]; - const List clangFiles = ['ios/Classes/Foo.h']; - const List dartFiles = ['lib/a.dart']; - const List javaFiles = [ - 'android/src/main/java/io/flutter/plugins/a_plugin/a.java' - ]; - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - extraFiles: [ - ...skipFiles, - // Include some files that should be formatted to validate that it's - // correctly filtering even when running the commands. - ...clangFiles, - ...dartFiles, - ...javaFiles, - ], - ); - - await runCapturingPrint(runner, ['format']); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall( - 'clang-format', - [ - '-i', - '--style=file', - ...getPackagesDirRelativePaths(plugin, clangFiles) - ], - packagesDir.path), - ProcessCall( - getFlutterCommand(mockPlatform), - [ - 'format', - ...getPackagesDirRelativePaths(plugin, dartFiles) - ], - packagesDir.path), - ProcessCall( - 'java', - [ - '-jar', - javaFormatPath, - '--replace', - ...getPackagesDirRelativePaths(plugin, javaFiles) - ], - packagesDir.path), - ])); - }); - - test('fails if files are changed with --fail-on-change', () async { - const List files = [ - 'linux/foo_plugin.cc', - 'macos/Classes/Foo.h', - ]; - createFakePlugin('a_plugin', packagesDir, extraFiles: files); - - const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; - processRunner.mockProcessesForExecutable['git'] = [ - MockProcess(stdout: changedFilePath), - ]; - - Error? commandError; - final List output = - await runCapturingPrint(runner, ['format', '--fail-on-change'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('These files are not formatted correctly'), - contains(changedFilePath), - contains('patch -p1 < files = [ - 'linux/foo_plugin.cc', - 'macos/Classes/Foo.h', - ]; - createFakePlugin('a_plugin', packagesDir, extraFiles: files); - - processRunner.mockProcessesForExecutable['git'] = [ - MockProcess(exitCode: 1) - ]; - Error? commandError; - final List output = - await runCapturingPrint(runner, ['format', '--fail-on-change'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to determine changed files.'), - ])); - }); - - test('reports git diff failures', () async { - const List files = [ - 'linux/foo_plugin.cc', - 'macos/Classes/Foo.h', - ]; - createFakePlugin('a_plugin', packagesDir, extraFiles: files); - - const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; - processRunner.mockProcessesForExecutable['git'] = [ - MockProcess(stdout: changedFilePath), // ls-files - MockProcess(exitCode: 1), // diff - ]; - - Error? commandError; - final List output = - await runCapturingPrint(runner, ['format', '--fail-on-change'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('These files are not formatted correctly'), - contains(changedFilePath), - contains('Unable to determine diff.'), - ])); - }); - - test('Batches moderately long file lists on Windows', () async { - mockPlatform.isWindows = true; - - const String pluginName = 'a_plugin'; - // -1 since the command itself takes some length. - const int batchSize = (windowsCommandLineMax ~/ 100) - 1; - - // Make the file list one file longer than would fit in the batch. - final List batch1 = - get99CharacterPathExtraFiles(pluginName, batchSize + 1); - final String extraFile = batch1.removeLast(); - - createFakePlugin( - pluginName, - packagesDir, - extraFiles: [...batch1, extraFile], - ); - - await runCapturingPrint(runner, ['format']); - - // Ensure that it was batched... - expect(processRunner.recordedCalls.length, 2); - // ... and that the spillover into the second batch was only one file. - expect( - processRunner.recordedCalls, - contains( - ProcessCall( - getFlutterCommand(mockPlatform), - [ - 'format', - '$pluginName\\$extraFile', - ], - packagesDir.path), - )); - }); - - // Validates that the Windows limit--which is much lower than the limit on - // other platforms--isn't being used on all platforms, as that would make - // formatting slower on Linux and macOS. - test('Does not batch moderately long file lists on non-Windows', () async { - const String pluginName = 'a_plugin'; - // -1 since the command itself takes some length. - const int batchSize = (windowsCommandLineMax ~/ 100) - 1; - - // Make the file list one file longer than would fit in a Windows batch. - final List batch = - get99CharacterPathExtraFiles(pluginName, batchSize + 1); - - createFakePlugin( - pluginName, - packagesDir, - extraFiles: batch, - ); - - await runCapturingPrint(runner, ['format']); - - expect(processRunner.recordedCalls.length, 1); - }); - - test('Batches extremely long file lists on non-Windows', () async { - const String pluginName = 'a_plugin'; - // -1 since the command itself takes some length. - const int batchSize = (nonWindowsCommandLineMax ~/ 100) - 1; - - // Make the file list one file longer than would fit in the batch. - final List batch1 = - get99CharacterPathExtraFiles(pluginName, batchSize + 1); - final String extraFile = batch1.removeLast(); - - createFakePlugin( - pluginName, - packagesDir, - extraFiles: [...batch1, extraFile], - ); - - await runCapturingPrint(runner, ['format']); - - // Ensure that it was batched... - expect(processRunner.recordedCalls.length, 2); - // ... and that the spillover into the second batch was only one file. - expect( - processRunner.recordedCalls, - contains( - ProcessCall( - getFlutterCommand(mockPlatform), - [ - 'format', - '$pluginName/$extraFile', - ], - packagesDir.path), - )); - }); -} diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart deleted file mode 100644 index 09841df74e70..000000000000 --- a/script/tool/test/license_check_command_test.dart +++ /dev/null @@ -1,613 +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 'package:args/command_runner.dart'; -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/license_check_command.dart'; -import 'package:mockito/mockito.dart'; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'common/package_command_test.mocks.dart'; -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('LicenseCheckCommand', () { - late CommandRunner runner; - late FileSystem fileSystem; - late Platform platform; - late Directory root; - - setUp(() { - fileSystem = MemoryFileSystem(); - platform = MockPlatformWithSeparator(); - final Directory packagesDir = - fileSystem.currentDirectory.childDirectory('packages'); - root = packagesDir.parent; - - final MockGitDir gitDir = MockGitDir(); - when(gitDir.path).thenReturn(packagesDir.parent.path); - - final LicenseCheckCommand command = LicenseCheckCommand( - packagesDir, - platform: platform, - gitDir: gitDir, - ); - runner = - CommandRunner('license_test', 'Test for $LicenseCheckCommand'); - runner.addCommand(command); - }); - - /// Writes a copyright+license block to [file], defaulting to a standard - /// block for this repository. - /// - /// [commentString] is added to the start of each line. - /// [prefix] is added to the start of the entire block. - /// [suffix] is added to the end of the entire block. - void writeLicense( - File file, { - String comment = '// ', - String prefix = '', - String suffix = '', - String copyright = - 'Copyright 2013 The Flutter Authors. All rights reserved.', - List license = const [ - 'Use of this source code is governed by a BSD-style license that can be', - 'found in the LICENSE file.', - ], - bool useCrlf = false, - }) { - final List lines = ['$prefix$comment$copyright']; - for (final String line in license) { - lines.add('$comment$line'); - } - final String newline = useCrlf ? '\r\n' : '\n'; - file.writeAsStringSync(lines.join(newline) + suffix + newline); - } - - test('looks at only expected extensions', () async { - final Map extensions = { - 'c': true, - 'cc': true, - 'cpp': true, - 'dart': true, - 'h': true, - 'html': true, - 'java': true, - 'json': false, - 'kt': true, - 'm': true, - 'md': false, - 'mm': true, - 'png': false, - 'swift': true, - 'sh': true, - 'yaml': false, - }; - - const String filenameBase = 'a_file'; - for (final String fileExtension in extensions.keys) { - root.childFile('$filenameBase.$fileExtension').createSync(); - } - - final List output = await runCapturingPrint( - runner, ['license-check'], errorHandler: (Error e) { - // Ignore failure; the files are empty so the check is expected to fail, - // but this test isn't for that behavior. - }); - - extensions.forEach((String fileExtension, bool shouldCheck) { - final Matcher logLineMatcher = - contains('Checking $filenameBase.$fileExtension'); - expect(output, shouldCheck ? logLineMatcher : isNot(logLineMatcher)); - }); - }); - - test('ignore list overrides extension matches', () async { - final List ignoredFiles = [ - // Ignored base names. - 'flutter_export_environment.sh', - 'GeneratedPluginRegistrant.java', - 'GeneratedPluginRegistrant.m', - 'generated_plugin_registrant.cc', - 'generated_plugin_registrant.cpp', - // Ignored path suffixes. - 'foo.g.dart', - 'foo.mocks.dart', - // Ignored files. - 'resource.h', - ]; - - for (final String name in ignoredFiles) { - root.childFile(name).createSync(); - } - - final List output = - await runCapturingPrint(runner, ['license-check']); - - for (final String name in ignoredFiles) { - expect(output, isNot(contains('Checking $name'))); - } - }); - - test('ignores submodules', () async { - const String submoduleName = 'a_submodule'; - - final File submoduleSpec = root.childFile('.gitmodules'); - submoduleSpec.writeAsStringSync(''' -[submodule "$submoduleName"] - path = $submoduleName - url = https://github.com/foo/$submoduleName -'''); - - const List submoduleFiles = [ - '$submoduleName/foo.dart', - '$submoduleName/a/b/bar.dart', - '$submoduleName/LICENSE', - ]; - for (final String filePath in submoduleFiles) { - root.childFile(filePath).createSync(recursive: true); - } - - final List output = - await runCapturingPrint(runner, ['license-check']); - - for (final String filePath in submoduleFiles) { - expect(output, isNot(contains('Checking $filePath'))); - } - }); - - test('passes if all checked files have license blocks', () async { - final File checked = root.childFile('checked.cc'); - checked.createSync(); - writeLicense(checked); - final File notChecked = root.childFile('not_checked.md'); - notChecked.createSync(); - - final List output = - await runCapturingPrint(runner, ['license-check']); - - // Sanity check that the test did actually check a file. - expect( - output, - containsAllInOrder([ - contains('Checking checked.cc'), - contains('All files passed validation!'), - ])); - }); - - test('passes correct license blocks on Windows', () async { - final File checked = root.childFile('checked.cc'); - checked.createSync(); - writeLicense(checked, useCrlf: true); - - final List output = - await runCapturingPrint(runner, ['license-check']); - - // Sanity check that the test did actually check a file. - expect( - output, - containsAllInOrder([ - contains('Checking checked.cc'), - contains('All files passed validation!'), - ])); - }); - - test('handles the comment styles for all supported languages', () async { - final File fileA = root.childFile('file_a.cc'); - fileA.createSync(); - writeLicense(fileA); - final File fileB = root.childFile('file_b.sh'); - fileB.createSync(); - writeLicense(fileB, comment: '# '); - final File fileC = root.childFile('file_c.html'); - fileC.createSync(); - writeLicense(fileC, comment: '', prefix: ''); - - final List output = - await runCapturingPrint(runner, ['license-check']); - - // Sanity check that the test did actually check the files. - expect( - output, - containsAllInOrder([ - contains('Checking file_a.cc'), - contains('Checking file_b.sh'), - contains('Checking file_c.html'), - contains('All files passed validation!'), - ])); - }); - - test('fails if any checked files are missing license blocks', () async { - final File goodA = root.childFile('good.cc'); - goodA.createSync(); - writeLicense(goodA); - final File goodB = root.childFile('good.h'); - goodB.createSync(); - writeLicense(goodB); - root.childFile('bad.cc').createSync(); - root.childFile('bad.h').createSync(); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['license-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - // Failure should give information about the problematic files. - expect( - output, - containsAllInOrder([ - contains( - 'The license block for these files is missing or incorrect:'), - contains(' bad.cc'), - contains(' bad.h'), - ])); - // Failure shouldn't print the success message. - expect(output, isNot(contains(contains('All files passed validation!')))); - }); - - test('fails if any checked files are missing just the copyright', () async { - final File good = root.childFile('good.cc'); - good.createSync(); - writeLicense(good); - final File bad = root.childFile('bad.cc'); - bad.createSync(); - writeLicense(bad, copyright: ''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['license-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - // Failure should give information about the problematic files. - expect( - output, - containsAllInOrder([ - contains( - 'The license block for these files is missing or incorrect:'), - contains(' bad.cc'), - ])); - // Failure shouldn't print the success message. - expect(output, isNot(contains(contains('All files passed validation!')))); - }); - - test('fails if any checked files are missing just the license', () async { - final File good = root.childFile('good.cc'); - good.createSync(); - writeLicense(good); - final File bad = root.childFile('bad.cc'); - bad.createSync(); - writeLicense(bad, license: []); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['license-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - // Failure should give information about the problematic files. - expect( - output, - containsAllInOrder([ - contains( - 'The license block for these files is missing or incorrect:'), - contains(' bad.cc'), - ])); - // Failure shouldn't print the success message. - expect(output, isNot(contains(contains('All files passed validation!')))); - }); - - test('fails if any third-party code is not in a third_party directory', - () async { - final File thirdPartyFile = root.childFile('third_party.cc'); - thirdPartyFile.createSync(); - writeLicense(thirdPartyFile, copyright: 'Copyright 2017 Someone Else'); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['license-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - // Failure should give information about the problematic files. - expect( - output, - containsAllInOrder([ - contains( - 'The license block for these files is missing or incorrect:'), - contains(' third_party.cc'), - ])); - // Failure shouldn't print the success message. - expect(output, isNot(contains(contains('All files passed validation!')))); - }); - - test('succeeds for third-party code in a third_party directory', () async { - final File thirdPartyFile = root - .childDirectory('a_plugin') - .childDirectory('lib') - .childDirectory('src') - .childDirectory('third_party') - .childFile('file.cc'); - thirdPartyFile.createSync(recursive: true); - writeLicense(thirdPartyFile, - copyright: 'Copyright 2017 Workiva Inc.', - license: [ - 'Licensed under the Apache License, Version 2.0 (the "License");', - 'you may not use this file except in compliance with the License.' - ]); - - final List output = - await runCapturingPrint(runner, ['license-check']); - - // Sanity check that the test did actually check the file. - expect( - output, - containsAllInOrder([ - contains('Checking a_plugin/lib/src/third_party/file.cc'), - contains('All files passed validation!'), - ])); - }); - - test('allows first-party code in a third_party directory', () async { - final File firstPartyFileInThirdParty = root - .childDirectory('a_plugin') - .childDirectory('lib') - .childDirectory('src') - .childDirectory('third_party') - .childFile('first_party.cc'); - firstPartyFileInThirdParty.createSync(recursive: true); - writeLicense(firstPartyFileInThirdParty); - - final List output = - await runCapturingPrint(runner, ['license-check']); - - // Sanity check that the test did actually check the file. - expect( - output, - containsAllInOrder([ - contains('Checking a_plugin/lib/src/third_party/first_party.cc'), - contains('All files passed validation!'), - ])); - }); - - test('fails for licenses that the tool does not expect', () async { - final File good = root.childFile('good.cc'); - good.createSync(); - writeLicense(good); - final File bad = root.childDirectory('third_party').childFile('bad.cc'); - bad.createSync(recursive: true); - writeLicense(bad, license: [ - 'This program is free software: you can redistribute it and/or modify', - 'it under the terms of the GNU General Public License', - ]); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['license-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - // Failure should give information about the problematic files. - expect( - output, - containsAllInOrder([ - contains( - 'No recognized license was found for the following third-party files:'), - contains(' third_party/bad.cc'), - ])); - // Failure shouldn't print the success message. - expect(output, isNot(contains(contains('All files passed validation!')))); - }); - - test('Apache is not recognized for new authors without validation changes', - () async { - final File good = root.childFile('good.cc'); - good.createSync(); - writeLicense(good); - final File bad = root.childDirectory('third_party').childFile('bad.cc'); - bad.createSync(recursive: true); - writeLicense( - bad, - copyright: 'Copyright 2017 Some New Authors.', - license: [ - 'Licensed under the Apache License, Version 2.0 (the "License");', - 'you may not use this file except in compliance with the License.' - ], - ); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['license-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - // Failure should give information about the problematic files. - expect( - output, - containsAllInOrder([ - contains( - 'No recognized license was found for the following third-party files:'), - contains(' third_party/bad.cc'), - ])); - // Failure shouldn't print the success message. - expect(output, isNot(contains(contains('All files passed validation!')))); - }); - - test('passes if all first-party LICENSE files are correctly formatted', - () async { - final File license = root.childFile('LICENSE'); - license.createSync(); - license.writeAsStringSync(_correctLicenseFileText); - - final List output = - await runCapturingPrint(runner, ['license-check']); - - // Sanity check that the test did actually check the file. - expect( - output, - containsAllInOrder([ - contains('Checking LICENSE'), - contains('All files passed validation!'), - ])); - }); - - test('passes correct LICENSE files on Windows', () async { - final File license = root.childFile('LICENSE'); - license.createSync(); - license - .writeAsStringSync(_correctLicenseFileText.replaceAll('\n', '\r\n')); - - final List output = - await runCapturingPrint(runner, ['license-check']); - - // Sanity check that the test did actually check the file. - expect( - output, - containsAllInOrder([ - contains('Checking LICENSE'), - contains('All files passed validation!'), - ])); - }); - - test('fails if any first-party LICENSE files are incorrectly formatted', - () async { - final File license = root.childFile('LICENSE'); - license.createSync(); - license.writeAsStringSync(_incorrectLicenseFileText); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['license-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect(output, isNot(contains(contains('All files passed validation!')))); - }); - - test('ignores third-party LICENSE format', () async { - final File license = - root.childDirectory('third_party').childFile('LICENSE'); - license.createSync(recursive: true); - license.writeAsStringSync(_incorrectLicenseFileText); - - final List output = - await runCapturingPrint(runner, ['license-check']); - - // The file shouldn't be checked. - expect(output, isNot(contains(contains('Checking third_party/LICENSE')))); - }); - - test('outputs all errors at the end', () async { - root.childFile('bad.cc').createSync(); - root - .childDirectory('third_party') - .childFile('bad.cc') - .createSync(recursive: true); - final File license = root.childFile('LICENSE'); - license.createSync(); - license.writeAsStringSync(_incorrectLicenseFileText); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['license-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Checking LICENSE'), - contains('Checking bad.cc'), - contains('Checking third_party/bad.cc'), - contains( - 'The following LICENSE files do not follow the expected format:'), - contains(' LICENSE'), - contains( - 'The license block for these files is missing or incorrect:'), - contains(' bad.cc'), - contains( - 'No recognized license was found for the following third-party files:'), - contains(' third_party/bad.cc'), - ])); - }); - }); -} - -class MockPlatformWithSeparator extends MockPlatform { - @override - String get pathSeparator => isWindows ? r'\' : '/'; -} - -const String _correctLicenseFileText = ''' -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. -'''; - -// A common incorrect version created by copying text intended for a code file, -// with comment markers. -const String _incorrectLicenseFileText = ''' -// 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/script/tool/test/lint_android_command_test.dart b/script/tool/test/lint_android_command_test.dart deleted file mode 100644 index e4a6c5c859e4..000000000000 --- a/script/tool/test/lint_android_command_test.dart +++ /dev/null @@ -1,205 +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:io' as io; - -import 'package:args/command_runner.dart'; -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/lint_android_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('LintAndroidCommand', () { - FileSystem fileSystem; - late Directory packagesDir; - late CommandRunner runner; - late MockPlatform mockPlatform; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - mockPlatform = MockPlatform(); - processRunner = RecordingProcessRunner(); - final LintAndroidCommand command = LintAndroidCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'lint_android_test', 'Test for $LintAndroidCommand'); - runner.addCommand(command); - }); - - test('runs gradle lint', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin1', packagesDir, extraFiles: [ - 'example/android/gradlew', - ], platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }); - - final Directory androidDir = - plugin.getExamples().first.platformDirectory(FlutterPlatform.android); - - final List output = - await runCapturingPrint(runner, ['lint-android']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - androidDir.childFile('gradlew').path, - const ['plugin1:lintDebug'], - androidDir.path, - ), - ]), - ); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin1'), - contains('No issues found!'), - ])); - }); - - test('runs on all examples', () async { - final List examples = ['example1', 'example2']; - final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, - examples: examples, - extraFiles: [ - 'example/example1/android/gradlew', - 'example/example2/android/gradlew', - ], - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }); - - final Iterable exampleAndroidDirs = plugin.getExamples().map( - (RepositoryPackage example) => - example.platformDirectory(FlutterPlatform.android)); - - final List output = - await runCapturingPrint(runner, ['lint-android']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - for (final Directory directory in exampleAndroidDirs) - ProcessCall( - directory.childFile('gradlew').path, - const ['plugin1:lintDebug'], - directory.path, - ), - ]), - ); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin1'), - contains('No issues found!'), - ])); - }); - - test('fails if gradlew is missing', () async { - createFakePlugin('plugin1', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['lint-android'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder( - [ - contains('Build examples before linting'), - ], - )); - }); - - test('fails if linting finds issues', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin1', packagesDir, extraFiles: [ - 'example/android/gradlew', - ], platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }); - - final String gradlewPath = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android) - .childFile('gradlew') - .path; - processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['lint-android'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder( - [ - contains('The following packages had errors:'), - ], - )); - }); - - test('skips non-Android plugins', () async { - createFakePlugin('plugin1', packagesDir); - - final List output = - await runCapturingPrint(runner, ['lint-android']); - - expect( - output, - containsAllInOrder( - [ - contains( - 'SKIPPING: Plugin does not have an Android implementation.') - ], - )); - }); - - test('skips non-inline plugins', () async { - createFakePlugin('plugin1', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.federated) - }); - - final List output = - await runCapturingPrint(runner, ['lint-android']); - - expect( - output, - containsAllInOrder( - [ - contains( - 'SKIPPING: Plugin does not have an Android implementation.') - ], - )); - }); - }); -} diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart deleted file mode 100644 index 097bcff338a5..000000000000 --- a/script/tool/test/lint_podspecs_command_test.dart +++ /dev/null @@ -1,222 +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:io' as io; - -import 'package:args/command_runner.dart'; -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/lint_podspecs_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('$LintPodspecsCommand', () { - FileSystem fileSystem; - late Directory packagesDir; - late CommandRunner runner; - late MockPlatform mockPlatform; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - - mockPlatform = MockPlatform(isMacOS: true); - processRunner = RecordingProcessRunner(); - final LintPodspecsCommand command = LintPodspecsCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = - CommandRunner('podspec_test', 'Test for $LintPodspecsCommand'); - runner.addCommand(command); - }); - - test('only runs on macOS', () async { - createFakePlugin('plugin1', packagesDir, - extraFiles: ['plugin1.podspec']); - mockPlatform.isMacOS = false; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['podspecs'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - processRunner.recordedCalls, - equals([]), - ); - - expect( - output, - containsAllInOrder( - [contains('only supported on macOS')], - )); - }); - - test('runs pod lib lint on a podspec', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin1', - packagesDir, - extraFiles: [ - 'ios/plugin1.podspec', - 'bogus.dart', // Ignore non-podspecs. - ], - ); - - processRunner.mockProcessesForExecutable['pod'] = [ - MockProcess(stdout: 'Foo', stderr: 'Bar'), - MockProcess(), - ]; - - final List output = - await runCapturingPrint(runner, ['podspecs']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('which', const ['pod'], packagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - plugin - .platformDirectory(FlutterPlatform.ios) - .childFile('plugin1.podspec') - .path, - '--configuration=Debug', - '--skip-tests', - '--use-modular-headers', - '--use-libraries' - ], - packagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - plugin - .platformDirectory(FlutterPlatform.ios) - .childFile('plugin1.podspec') - .path, - '--configuration=Debug', - '--skip-tests', - '--use-modular-headers', - ], - packagesDir.path), - ]), - ); - - expect(output, contains('Linting plugin1.podspec')); - expect(output, contains('Foo')); - expect(output, contains('Bar')); - }); - - test('fails if pod is missing', () async { - createFakePlugin('plugin1', packagesDir, - extraFiles: ['plugin1.podspec']); - - // Simulate failure from `which pod`. - processRunner.mockProcessesForExecutable['which'] = [ - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['podspecs'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder( - [ - contains('Unable to find "pod". Make sure it is in your path.'), - ], - )); - }); - - test('fails if linting as a framework fails', () async { - createFakePlugin('plugin1', packagesDir, - extraFiles: ['plugin1.podspec']); - - // Simulate failure from `pod`. - processRunner.mockProcessesForExecutable['pod'] = [ - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['podspecs'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder( - [ - contains('The following packages had errors:'), - contains('plugin1:\n' - ' plugin1.podspec') - ], - )); - }); - - test('fails if linting as a static library fails', () async { - createFakePlugin('plugin1', packagesDir, - extraFiles: ['plugin1.podspec']); - - // Simulate failure from the second call to `pod`. - processRunner.mockProcessesForExecutable['pod'] = [ - MockProcess(), - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['podspecs'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder( - [ - contains('The following packages had errors:'), - contains('plugin1:\n' - ' plugin1.podspec') - ], - )); - }); - - test('skips when there are no podspecs', () async { - createFakePlugin('plugin1', packagesDir); - - final List output = - await runCapturingPrint(runner, ['podspecs']); - - expect( - output, - containsAllInOrder( - [contains('SKIPPING: No podspecs.')], - )); - }); - }); -} diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart deleted file mode 100644 index f19215c89b9e..000000000000 --- a/script/tool/test/list_command_test.dart +++ /dev/null @@ -1,197 +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 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/list_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('ListCommand', () { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - final ListCommand command = - ListCommand(packagesDir, platform: mockPlatform); - - runner = CommandRunner('list_test', 'Test for $ListCommand'); - runner.addCommand(command); - }); - - test('lists top-level packages', () async { - createFakePackage('package1', packagesDir); - createFakePlugin('plugin2', packagesDir); - - final List plugins = - await runCapturingPrint(runner, ['list', '--type=package']); - - expect( - plugins, - orderedEquals([ - '/packages/package1', - '/packages/plugin2', - ]), - ); - }); - - test('lists examples', () async { - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir, - examples: ['example1', 'example2']); - createFakePlugin('plugin3', packagesDir, examples: []); - - final List examples = - await runCapturingPrint(runner, ['list', '--type=example']); - - expect( - examples, - orderedEquals([ - '/packages/plugin1/example', - '/packages/plugin2/example/example1', - '/packages/plugin2/example/example2', - ]), - ); - }); - - test('lists packages and subpackages', () async { - createFakePackage('package1', packagesDir); - createFakePlugin('plugin2', packagesDir, - examples: ['example1', 'example2']); - createFakePlugin('plugin3', packagesDir, examples: []); - - final List packages = await runCapturingPrint( - runner, ['list', '--type=package-or-subpackage']); - - expect( - packages, - unorderedEquals([ - '/packages/package1', - '/packages/package1/example', - '/packages/plugin2', - '/packages/plugin2/example/example1', - '/packages/plugin2/example/example2', - '/packages/plugin3', - ]), - ); - }); - - test('lists files', () async { - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir, - examples: ['example1', 'example2']); - createFakePlugin('plugin3', packagesDir, examples: []); - - final List examples = - await runCapturingPrint(runner, ['list', '--type=file']); - - expect( - examples, - unorderedEquals([ - '/packages/plugin1/pubspec.yaml', - '/packages/plugin1/AUTHORS', - '/packages/plugin1/CHANGELOG.md', - '/packages/plugin1/README.md', - '/packages/plugin1/example/pubspec.yaml', - '/packages/plugin2/pubspec.yaml', - '/packages/plugin2/AUTHORS', - '/packages/plugin2/CHANGELOG.md', - '/packages/plugin2/README.md', - '/packages/plugin2/example/example1/pubspec.yaml', - '/packages/plugin2/example/example2/pubspec.yaml', - '/packages/plugin3/pubspec.yaml', - '/packages/plugin3/AUTHORS', - '/packages/plugin3/CHANGELOG.md', - '/packages/plugin3/README.md', - ]), - ); - }); - - test('lists plugins using federated plugin layout', () async { - createFakePlugin('plugin1', packagesDir); - - // Create a federated plugin by creating a directory under the packages - // directory with several packages underneath. - final Directory federatedPluginDir = - packagesDir.childDirectory('my_plugin')..createSync(); - createFakePlugin('my_plugin', federatedPluginDir); - createFakePlugin('my_plugin_web', federatedPluginDir); - createFakePlugin('my_plugin_macos', federatedPluginDir); - - // Test without specifying `--type`. - final List plugins = - await runCapturingPrint(runner, ['list']); - - expect( - plugins, - unorderedEquals([ - '/packages/plugin1', - '/packages/my_plugin/my_plugin', - '/packages/my_plugin/my_plugin_web', - '/packages/my_plugin/my_plugin_macos', - ]), - ); - }); - - test('can filter plugins with the --packages argument', () async { - createFakePlugin('plugin1', packagesDir); - - // Create a federated plugin by creating a directory under the packages - // directory with several packages underneath. - final Directory federatedPluginDir = - packagesDir.childDirectory('my_plugin')..createSync(); - createFakePlugin('my_plugin', federatedPluginDir); - createFakePlugin('my_plugin_web', federatedPluginDir); - createFakePlugin('my_plugin_macos', federatedPluginDir); - - List plugins = await runCapturingPrint( - runner, ['list', '--packages=plugin1']); - expect( - plugins, - unorderedEquals([ - '/packages/plugin1', - ]), - ); - - plugins = await runCapturingPrint( - runner, ['list', '--packages=my_plugin']); - expect( - plugins, - unorderedEquals([ - '/packages/my_plugin/my_plugin', - '/packages/my_plugin/my_plugin_web', - '/packages/my_plugin/my_plugin_macos', - ]), - ); - - plugins = await runCapturingPrint( - runner, ['list', '--packages=my_plugin/my_plugin_web']); - expect( - plugins, - unorderedEquals([ - '/packages/my_plugin/my_plugin_web', - ]), - ); - - plugins = await runCapturingPrint(runner, - ['list', '--packages=my_plugin/my_plugin_web,plugin1']); - expect( - plugins, - unorderedEquals([ - '/packages/plugin1', - '/packages/my_plugin/my_plugin_web', - ]), - ); - }); - }); -} diff --git a/script/tool/test/make_deps_path_based_command_test.dart b/script/tool/test/make_deps_path_based_command_test.dart deleted file mode 100644 index e846a63fc68e..000000000000 --- a/script/tool/test/make_deps_path_based_command_test.dart +++ /dev/null @@ -1,483 +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:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/make_deps_path_based_command.dart'; -import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; - -import 'common/package_command_test.mocks.dart'; -import 'mocks.dart'; -import 'util.dart'; - -void main() { - FileSystem fileSystem; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - - final MockGitDir gitDir = MockGitDir(); - when(gitDir.path).thenReturn(packagesDir.parent.path); - when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) - .thenAnswer((Invocation invocation) { - final List arguments = - invocation.positionalArguments[0]! as List; - // Route git calls through the process runner, to make mock output - // consistent with other processes. Attach the first argument to the - // command to make targeting the mock results easier. - final String gitCommand = arguments.removeAt(0); - return processRunner.run('git-$gitCommand', arguments); - }); - - processRunner = RecordingProcessRunner(); - final MakeDepsPathBasedCommand command = - MakeDepsPathBasedCommand(packagesDir, gitDir: gitDir); - - runner = CommandRunner( - 'make-deps-path-based_command', 'Test for $MakeDepsPathBasedCommand'); - runner.addCommand(command); - }); - - /// Adds dummy 'dependencies:' entries for each package in [dependencies] - /// to [package]. - void addDependencies( - RepositoryPackage package, Iterable dependencies) { - final List lines = package.pubspecFile.readAsLinesSync(); - final int dependenciesStartIndex = lines.indexOf('dependencies:'); - assert(dependenciesStartIndex != -1); - lines.insertAll(dependenciesStartIndex + 1, [ - for (final String dependency in dependencies) ' $dependency: ^1.0.0', - ]); - package.pubspecFile.writeAsStringSync(lines.join('\n')); - } - - /// Adds a 'dev_dependencies:' section with entries for each package in - /// [dependencies] to [package]. - void addDevDependenciesSection( - RepositoryPackage package, Iterable devDependencies) { - final String originalContent = package.pubspecFile.readAsStringSync(); - package.pubspecFile.writeAsStringSync(''' -$originalContent - -dev_dependencies: -${devDependencies.map((String dep) => ' $dep: ^1.0.0').join('\n')} -'''); - } - - test('no-ops for no plugins', () async { - createFakePackage('foo', packagesDir, isFlutter: true); - final RepositoryPackage packageBar = - createFakePackage('bar', packagesDir, isFlutter: true); - addDependencies(packageBar, ['foo']); - final String originalPubspecContents = - packageBar.pubspecFile.readAsStringSync(); - - final List output = - await runCapturingPrint(runner, ['make-deps-path-based']); - - expect( - output, - containsAllInOrder([ - contains('No target dependencies'), - ]), - ); - // The 'foo' reference should not have been modified. - expect(packageBar.pubspecFile.readAsStringSync(), originalPubspecContents); - }); - - test('rewrites "dependencies" references', () async { - final RepositoryPackage simplePackage = - createFakePackage('foo', packagesDir, isFlutter: true); - final Directory pluginGroup = packagesDir.childDirectory('bar'); - - createFakePackage('bar_platform_interface', pluginGroup, isFlutter: true); - final RepositoryPackage pluginImplementation = - createFakePlugin('bar_android', pluginGroup); - final RepositoryPackage pluginAppFacing = - createFakePlugin('bar', pluginGroup); - - addDependencies(simplePackage, [ - 'bar', - 'bar_android', - 'bar_platform_interface', - ]); - addDependencies(pluginAppFacing, [ - 'bar_platform_interface', - 'bar_android', - ]); - addDependencies(pluginImplementation, [ - 'bar_platform_interface', - ]); - - final List output = await runCapturingPrint(runner, [ - 'make-deps-path-based', - '--target-dependencies=bar,bar_platform_interface' - ]); - - expect( - output, - containsAll([ - 'Rewriting references to: bar, bar_platform_interface...', - ' Modified packages/bar/bar/pubspec.yaml', - ' Modified packages/bar/bar_android/pubspec.yaml', - ' Modified packages/foo/pubspec.yaml', - ])); - expect( - output, - isNot(contains( - ' Modified packages/bar/bar_platform_interface/pubspec.yaml'))); - - expect( - simplePackage.pubspecFile.readAsLinesSync(), - containsAllInOrder([ - '# FOR TESTING ONLY. DO NOT MERGE.', - 'dependency_overrides:', - ' bar:', - ' path: ../bar/bar', - ' bar_platform_interface:', - ' path: ../bar/bar_platform_interface', - ])); - expect( - pluginAppFacing.pubspecFile.readAsLinesSync(), - containsAllInOrder([ - 'dependency_overrides:', - ' bar_platform_interface:', - ' path: ../../bar/bar_platform_interface', - ])); - }); - - test('rewrites "dev_dependencies" references', () async { - createFakePackage('foo', packagesDir); - final RepositoryPackage builderPackage = - createFakePackage('foo_builder', packagesDir); - - addDevDependenciesSection(builderPackage, [ - 'foo', - ]); - - final List output = await runCapturingPrint( - runner, ['make-deps-path-based', '--target-dependencies=foo']); - - expect( - output, - containsAll([ - 'Rewriting references to: foo...', - ' Modified packages/foo_builder/pubspec.yaml', - ])); - - expect( - builderPackage.pubspecFile.readAsLinesSync(), - containsAllInOrder([ - '# FOR TESTING ONLY. DO NOT MERGE.', - 'dependency_overrides:', - ' foo:', - ' path: ../foo', - ])); - }); - - test( - 'alphabetizes overrides from different sectinos to avoid lint warnings in analysis', - () async { - createFakePackage('a', packagesDir); - createFakePackage('b', packagesDir); - createFakePackage('c', packagesDir); - final RepositoryPackage targetPackage = - createFakePackage('target', packagesDir); - - addDependencies(targetPackage, ['a', 'c']); - addDevDependenciesSection(targetPackage, ['b']); - - final List output = await runCapturingPrint(runner, - ['make-deps-path-based', '--target-dependencies=c,a,b']); - - expect( - output, - containsAllInOrder([ - 'Rewriting references to: c, a, b...', - ' Modified packages/target/pubspec.yaml', - ])); - - expect( - targetPackage.pubspecFile.readAsLinesSync(), - containsAllInOrder([ - '# FOR TESTING ONLY. DO NOT MERGE.', - 'dependency_overrides:', - ' a:', - ' path: ../a', - ' b:', - ' path: ../b', - ' c:', - ' path: ../c', - ])); - }); - - // This test case ensures that running CI using this command on an interim - // PR that itself used this command won't fail on the rewrite step. - test('running a second time no-ops without failing', () async { - final RepositoryPackage simplePackage = - createFakePackage('foo', packagesDir, isFlutter: true); - final Directory pluginGroup = packagesDir.childDirectory('bar'); - - createFakePackage('bar_platform_interface', pluginGroup, isFlutter: true); - final RepositoryPackage pluginImplementation = - createFakePlugin('bar_android', pluginGroup); - final RepositoryPackage pluginAppFacing = - createFakePlugin('bar', pluginGroup); - - addDependencies(simplePackage, [ - 'bar', - 'bar_android', - 'bar_platform_interface', - ]); - addDependencies(pluginAppFacing, [ - 'bar_platform_interface', - 'bar_android', - ]); - addDependencies(pluginImplementation, [ - 'bar_platform_interface', - ]); - - await runCapturingPrint(runner, [ - 'make-deps-path-based', - '--target-dependencies=bar,bar_platform_interface' - ]); - final List output = await runCapturingPrint(runner, [ - 'make-deps-path-based', - '--target-dependencies=bar,bar_platform_interface' - ]); - - expect( - output, - containsAll([ - 'Rewriting references to: bar, bar_platform_interface...', - ' Skipped packages/bar/bar/pubspec.yaml - Already rewritten', - ' Skipped packages/bar/bar_android/pubspec.yaml - Already rewritten', - ' Skipped packages/foo/pubspec.yaml - Already rewritten', - ])); - }); - - group('target-dependencies-with-non-breaking-updates', () { - test('no-ops for no published changes', () async { - final RepositoryPackage package = createFakePackage('foo', packagesDir); - - final String changedFileOutput = [ - package.pubspecFile, - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - // Simulate no change to the version in the interface's pubspec.yaml. - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: package.pubspecFile.readAsStringSync()), - ]; - - final List output = await runCapturingPrint(runner, [ - 'make-deps-path-based', - '--target-dependencies-with-non-breaking-updates' - ]); - - expect( - output, - containsAllInOrder([ - contains('No target dependencies'), - ]), - ); - }); - - test('no-ops for no deleted packages', () async { - final String changedFileOutput = [ - // A change for a file that's not on disk simulates a deletion. - packagesDir.childDirectory('foo').childFile('pubspec.yaml'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - final List output = await runCapturingPrint(runner, [ - 'make-deps-path-based', - '--target-dependencies-with-non-breaking-updates' - ]); - - expect( - output, - containsAllInOrder([ - contains('Skipping foo; deleted.'), - contains('No target dependencies'), - ]), - ); - }); - - test('includes bugfix version changes as targets', () async { - const String newVersion = '1.0.1'; - final RepositoryPackage package = - createFakePackage('foo', packagesDir, version: newVersion); - - final File pubspecFile = package.pubspecFile; - final String changedFileOutput = [ - pubspecFile, - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - final String gitPubspecContents = - pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0'); - // Simulate no change to the version in the interface's pubspec.yaml. - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: gitPubspecContents), - ]; - - final List output = await runCapturingPrint(runner, [ - 'make-deps-path-based', - '--target-dependencies-with-non-breaking-updates' - ]); - - expect( - output, - containsAllInOrder([ - contains('Rewriting references to: foo...'), - ]), - ); - }); - - test('includes minor version changes to 1.0+ as targets', () async { - const String newVersion = '1.1.0'; - final RepositoryPackage package = - createFakePackage('foo', packagesDir, version: newVersion); - - final File pubspecFile = package.pubspecFile; - final String changedFileOutput = [ - pubspecFile, - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - final String gitPubspecContents = - pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0'); - // Simulate no change to the version in the interface's pubspec.yaml. - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: gitPubspecContents), - ]; - - final List output = await runCapturingPrint(runner, [ - 'make-deps-path-based', - '--target-dependencies-with-non-breaking-updates' - ]); - - expect( - output, - containsAllInOrder([ - contains('Rewriting references to: foo...'), - ]), - ); - }); - - test('does not include major version changes as targets', () async { - const String newVersion = '2.0.0'; - final RepositoryPackage package = - createFakePackage('foo', packagesDir, version: newVersion); - - final File pubspecFile = package.pubspecFile; - final String changedFileOutput = [ - pubspecFile, - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - final String gitPubspecContents = - pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0'); - // Simulate no change to the version in the interface's pubspec.yaml. - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: gitPubspecContents), - ]; - - final List output = await runCapturingPrint(runner, [ - 'make-deps-path-based', - '--target-dependencies-with-non-breaking-updates' - ]); - - expect( - output, - containsAllInOrder([ - contains('No target dependencies'), - ]), - ); - }); - - test('does not include minor version changes to 0.x as targets', () async { - const String newVersion = '0.8.0'; - final RepositoryPackage package = - createFakePackage('foo', packagesDir, version: newVersion); - - final File pubspecFile = package.pubspecFile; - final String changedFileOutput = [ - pubspecFile, - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - final String gitPubspecContents = - pubspecFile.readAsStringSync().replaceAll(newVersion, '0.7.0'); - // Simulate no change to the version in the interface's pubspec.yaml. - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: gitPubspecContents), - ]; - - final List output = await runCapturingPrint(runner, [ - 'make-deps-path-based', - '--target-dependencies-with-non-breaking-updates' - ]); - - expect( - output, - containsAllInOrder([ - contains('No target dependencies'), - ]), - ); - }); - - test('skips anything outside of the packages directory', () async { - final Directory toolDir = packagesDir.parent.childDirectory('tool'); - const String newVersion = '1.1.0'; - final RepositoryPackage package = createFakePackage( - 'flutter_plugin_tools', toolDir, - version: newVersion); - - // Simulate a minor version change so it would be a target. - final File pubspecFile = package.pubspecFile; - final String changedFileOutput = [ - pubspecFile, - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - final String gitPubspecContents = - pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: gitPubspecContents), - ]; - - final List output = await runCapturingPrint(runner, [ - 'make-deps-path-based', - '--target-dependencies-with-non-breaking-updates' - ]); - - expect( - output, - containsAllInOrder([ - contains( - 'Skipping /tool/flutter_plugin_tools/pubspec.yaml; not in packages directory.'), - contains('No target dependencies'), - ]), - ); - }); - }); -} diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart deleted file mode 100644 index f6333ebd367d..000000000000 --- a/script/tool/test/mocks.dart +++ /dev/null @@ -1,89 +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:async'; -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:mockito/mockito.dart'; -import 'package:platform/platform.dart'; - -class MockPlatform extends Mock implements Platform { - MockPlatform({ - this.isLinux = false, - this.isMacOS = false, - this.isWindows = false, - }); - - @override - bool isLinux; - - @override - bool isMacOS; - - @override - bool isWindows; - - @override - Uri get script => isWindows - ? Uri.file(r'C:\foo\bar', windows: true) - : Uri.file('/foo/bar', windows: false); - - @override - Map environment = {}; -} - -class MockProcess extends Mock implements io.Process { - /// Creates a mock process with the given results. - /// - /// The default encodings match the ProcessRunner defaults; mocks for - /// processes run with a different encoding will need to be created with - /// the matching encoding. - MockProcess({ - int exitCode = 0, - String? stdout, - String? stderr, - Encoding stdoutEncoding = io.systemEncoding, - Encoding stderrEncoding = io.systemEncoding, - }) : _exitCode = exitCode { - if (stdout != null) { - _stdoutController.add(stdoutEncoding.encoder.convert(stdout)); - } - if (stderr != null) { - _stderrController.add(stderrEncoding.encoder.convert(stderr)); - } - _stdoutController.close(); - _stderrController.close(); - } - - final int _exitCode; - final StreamController> _stdoutController = - StreamController>(); - final StreamController> _stderrController = - StreamController>(); - final MockIOSink stdinMock = MockIOSink(); - - @override - int get pid => 99; - - @override - Future get exitCode async => _exitCode; - - @override - Stream> get stdout => _stdoutController.stream; - - @override - Stream> get stderr => _stderrController.stream; - - @override - IOSink get stdin => stdinMock; -} - -class MockIOSink extends Mock implements IOSink { - List lines = []; - - @override - void writeln([Object? obj = '']) => lines.add(obj.toString()); -} diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart deleted file mode 100644 index f24d014bbfea..000000000000 --- a/script/tool/test/native_test_command_test.dart +++ /dev/null @@ -1,1796 +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:convert'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/cmake.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/file_utils.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; -import 'package:flutter_plugin_tools/src/native_test_command.dart'; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -const String _androidIntegrationTestFilter = - '-Pandroid.testInstrumentationRunnerArguments.' - 'notAnnotation=io.flutter.plugins.DartIntegrationTest'; - -final Map _kDeviceListMap = { - 'runtimes': >[ - { - 'bundlePath': - '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime', - 'buildversion': '17L255', - 'runtimeRoot': - '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot', - 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-4', - 'version': '13.4', - 'isAvailable': true, - 'name': 'iOS 13.4' - }, - ], - 'devices': { - 'com.apple.CoreSimulator.SimRuntime.iOS-13-4': >[ - { - 'dataPath': - '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', - 'logPath': - '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'udid': '1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'isAvailable': true, - 'deviceTypeIdentifier': - 'com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus', - 'state': 'Shutdown', - 'name': 'iPhone 8 Plus' - } - ] - } -}; - -const String _fakeCmakeCommand = 'path/to/cmake'; - -void _createFakeCMakeCache(RepositoryPackage plugin, Platform platform) { - final CMakeProject project = CMakeProject(getExampleDir(plugin), - platform: platform, buildMode: 'Release'); - final File cache = project.buildDirectory.childFile('CMakeCache.txt'); - cache.createSync(recursive: true); - cache.writeAsStringSync('CMAKE_COMMAND:INTERNAL=$_fakeCmakeCommand'); -} - -// TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of -// doing all the process mocking and validation. -void main() { - const String kDestination = '--ios-destination'; - - group('test native_test_command on Posix', () { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - // iOS and macOS tests expect macOS, Linux tests expect Linux; nothing - // needs to distinguish between Linux and macOS, so set both to true to - // allow them to share a setup group. - mockPlatform = MockPlatform(isMacOS: true, isLinux: true); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final NativeTestCommand command = NativeTestCommand(packagesDir, - processRunner: processRunner, platform: mockPlatform); - - runner = CommandRunner( - 'native_test_command', 'Test for native_test_command'); - runner.addCommand(command); - }); - - // Returns a MockProcess to provide for "xcrun xcodebuild -list" for a - // project that contains [targets]. - MockProcess getMockXcodebuildListProcess(List targets) { - final Map projects = { - 'project': { - 'targets': targets, - } - }; - return MockProcess(stdout: jsonEncode(projects)); - } - - // Returns the ProcessCall to expect for checking the targets present in - // the [package]'s [platform]/Runner.xcodeproj. - ProcessCall getTargetCheckCall(Directory package, String platform) { - return ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - package - .childDirectory(platform) - .childDirectory('Runner.xcodeproj') - .path, - ], - null); - } - - // Returns the ProcessCall to expect for running the tests in the - // workspace [platform]/Runner.xcworkspace, with the given extra flags. - ProcessCall getRunTestCall( - Directory package, - String platform, { - String? destination, - List extraFlags = const [], - }) { - return ProcessCall( - 'xcrun', - [ - 'xcodebuild', - 'test', - '-workspace', - '$platform/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - if (destination != null) ...['-destination', destination], - ...extraFlags, - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - package.path); - } - - // Returns the ProcessCall to expect for build the Linux unit tests for the - // given plugin. - ProcessCall getLinuxBuildCall(RepositoryPackage plugin) { - return ProcessCall( - 'cmake', - [ - '--build', - getExampleDir(plugin) - .childDirectory('build') - .childDirectory('linux') - .childDirectory('x64') - .childDirectory('release') - .path, - '--target', - 'unit_tests' - ], - null); - } - - test('fails if no platforms are provided', () async { - Error? commandError; - final List output = await runCapturingPrint( - runner, ['native-test'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('At least one platform flag must be provided.'), - ]), - ); - }); - - test('fails if all test types are disabled', () async { - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--macos', - '--no-unit', - '--no-integration', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('At least one test type must be enabled.'), - ]), - ); - }); - - test('reports skips with no tests', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess(['RunnerTests', 'RunnerUITests']), - // Exit code 66 from testing indicates no tests. - MockProcess(exitCode: 66), - ]; - final List output = await runCapturingPrint( - runner, ['native-test', '--macos', '--no-unit']); - - expect( - output, - containsAllInOrder([ - contains('No tests found.'), - contains('Skipped 1 package(s)'), - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getTargetCheckCall(pluginExampleDirectory, 'macos'), - getRunTestCall(pluginExampleDirectory, 'macos', - extraFlags: ['-only-testing:RunnerUITests']), - ])); - }); - - group('iOS', () { - test('skip if iOS is not supported', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final List output = await runCapturingPrint(runner, - ['native-test', '--ios', kDestination, 'foo_destination']); - expect( - output, - containsAllInOrder([ - contains('No implementation for iOS.'), - contains('SKIPPING: Nothing to test for target platform(s).'), - ])); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('skip if iOS is implemented in a federated package', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.federated) - }); - - final List output = await runCapturingPrint(runner, - ['native-test', '--ios', kDestination, 'foo_destination']); - expect( - output, - containsAllInOrder([ - contains('No implementation for iOS.'), - contains('SKIPPING: Nothing to test for target platform(s).'), - ])); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('running with correct destination', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess( - ['RunnerTests', 'RunnerUITests']), - ]; - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--ios', - kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Successfully ran iOS xctest for plugin/example') - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getTargetCheckCall(pluginExampleDirectory, 'ios'), - getRunTestCall(pluginExampleDirectory, 'ios', - destination: 'foo_destination'), - ])); - }); - - test('Not specifying --ios-destination assigns an available simulator', - () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); - final Directory pluginExampleDirectory = getExampleDir(plugin); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(stdout: jsonEncode(_kDeviceListMap)), // simctl - getMockXcodebuildListProcess( - ['RunnerTests', 'RunnerUITests']), - ]; - - await runCapturingPrint(runner, ['native-test', '--ios']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - const ProcessCall( - 'xcrun', - [ - 'simctl', - 'list', - 'devices', - 'runtimes', - 'available', - '--json', - ], - null), - getTargetCheckCall(pluginExampleDirectory, 'ios'), - getRunTestCall(pluginExampleDirectory, 'ios', - destination: 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A'), - ])); - }); - }); - - group('macOS', () { - test('skip if macOS is not supported', () async { - createFakePlugin('plugin', packagesDir); - - final List output = - await runCapturingPrint(runner, ['native-test', '--macos']); - - expect( - output, - containsAllInOrder([ - contains('No implementation for macOS.'), - contains('SKIPPING: Nothing to test for target platform(s).'), - ])); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('skip if macOS is implemented in a federated package', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.federated), - }); - - final List output = - await runCapturingPrint(runner, ['native-test', '--macos']); - - expect( - output, - containsAllInOrder([ - contains('No implementation for macOS.'), - contains('SKIPPING: Nothing to test for target platform(s).'), - ])); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('runs for macOS plugin', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess( - ['RunnerTests', 'RunnerUITests']), - ]; - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--macos', - ]); - - expect( - output, - contains( - contains('Successfully ran macOS xctest for plugin/example'))); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getTargetCheckCall(pluginExampleDirectory, 'macos'), - getRunTestCall(pluginExampleDirectory, 'macos'), - ])); - }); - }); - - group('Android', () { - test('runs Java unit tests in Android implementation folder', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'example/android/gradlew', - 'android/src/test/example_test.java', - ], - ); - - await runCapturingPrint(runner, ['native-test', '--android']); - - final Directory androidFolder = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest'], - androidFolder.path, - ), - ]), - ); - }); - - test('runs Java unit tests in example folder', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/test/example_test.java', - ], - ); - - await runCapturingPrint(runner, ['native-test', '--android']); - - final Directory androidFolder = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest'], - androidFolder.path, - ), - ]), - ); - }); - - test('runs Java integration tests', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/androidTest/IntegrationTest.java', - ], - ); - - await runCapturingPrint( - runner, ['native-test', '--android', '--no-unit']); - - final Directory androidFolder = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - androidFolder.childFile('gradlew').path, - const [ - 'app:connectedAndroidTest', - _androidIntegrationTestFilter, - ], - androidFolder.path, - ), - ]), - ); - }); - - test( - 'ignores Java integration test files associated with integration_test', - () async { - createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java', - 'example/android/app/src/androidTest/java/io/flutter/plugins/plugin/FlutterActivityTest.java', - 'example/android/app/src/androidTest/java/io/flutter/plugins/plugin/MainActivityTest.java', - ], - ); - - await runCapturingPrint( - runner, ['native-test', '--android', '--no-unit']); - - // Nothing should run since those files are all - // integration_test-specific. - expect( - processRunner.recordedCalls, - orderedEquals([]), - ); - }); - - test('runs all tests when present', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'android/src/test/example_test.java', - 'example/android/gradlew', - 'example/android/app/src/androidTest/IntegrationTest.java', - ], - ); - - await runCapturingPrint(runner, ['native-test', '--android']); - - final Directory androidFolder = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest'], - androidFolder.path, - ), - ProcessCall( - androidFolder.childFile('gradlew').path, - const [ - 'app:connectedAndroidTest', - _androidIntegrationTestFilter, - ], - androidFolder.path, - ), - ]), - ); - }); - - test('honors --no-unit', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'android/src/test/example_test.java', - 'example/android/gradlew', - 'example/android/app/src/androidTest/IntegrationTest.java', - ], - ); - - await runCapturingPrint( - runner, ['native-test', '--android', '--no-unit']); - - final Directory androidFolder = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - androidFolder.childFile('gradlew').path, - const [ - 'app:connectedAndroidTest', - _androidIntegrationTestFilter, - ], - androidFolder.path, - ), - ]), - ); - }); - - test('honors --no-integration', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'android/src/test/example_test.java', - 'example/android/gradlew', - 'example/android/app/src/androidTest/IntegrationTest.java', - ], - ); - - await runCapturingPrint( - runner, ['native-test', '--android', '--no-integration']); - - final Directory androidFolder = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest'], - androidFolder.path, - ), - ]), - ); - }); - - test('fails when the app needs to be built', () async { - createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'example/android/app/src/test/example_test.java', - ], - ); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['native-test', '--android'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder([ - contains('ERROR: Run "flutter build apk" on plugin/example'), - contains('plugin:\n' - ' Examples must be built before testing.') - ]), - ); - }); - - test('logs missing test types', () async { - // No unit tests. - createFakePlugin( - 'plugin1', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/androidTest/IntegrationTest.java', - ], - ); - // No integration tests. - createFakePlugin( - 'plugin2', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'android/src/test/example_test.java', - 'example/android/gradlew', - ], - ); - - final List output = await runCapturingPrint( - runner, ['native-test', '--android'], - errorHandler: (Error e) { - // Having no unit tests is fatal, but that's not the point of this - // test so just ignore the failure. - }); - - expect( - output, - containsAllInOrder([ - contains('No Android unit tests found for plugin1/example'), - contains('Running integration tests...'), - contains( - 'No Android integration tests found for plugin2/example'), - contains('Running unit tests...'), - ])); - }); - - test('fails when a unit test fails', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/test/example_test.java', - ], - ); - - final String gradlewPath = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android) - .childFile('gradlew') - .path; - processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess(exitCode: 1) - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['native-test', '--android'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder([ - contains('plugin/example unit tests failed.'), - contains('The following packages had errors:'), - contains('plugin') - ]), - ); - }); - - test('fails when an integration test fails', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/test/example_test.java', - 'example/android/app/src/androidTest/IntegrationTest.java', - ], - ); - - final String gradlewPath = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android) - .childFile('gradlew') - .path; - processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess(), // unit passes - MockProcess(exitCode: 1), // integration fails - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['native-test', '--android'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder([ - contains('plugin/example integration tests failed.'), - contains('The following packages had errors:'), - contains('plugin') - ]), - ); - }); - - test('fails if there are no unit tests', () async { - createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/androidTest/IntegrationTest.java', - ], - ); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['native-test', '--android'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder([ - contains('No Android unit tests found for plugin/example'), - contains( - 'No unit tests ran. Plugins are required to have unit tests.'), - contains('The following packages had errors:'), - contains('plugin:\n' - ' No unit tests ran (use --exclude if this is intentional).') - ]), - ); - }); - - test('skips if Android is not supported', () async { - createFakePlugin( - 'plugin', - packagesDir, - ); - - final List output = await runCapturingPrint( - runner, ['native-test', '--android']); - - expect( - output, - containsAllInOrder([ - contains('No implementation for Android.'), - contains('SKIPPING: Nothing to test for target platform(s).'), - ]), - ); - }); - - test('skips when running no tests in integration-only mode', () async { - createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - ); - - final List output = await runCapturingPrint( - runner, ['native-test', '--android', '--no-unit']); - - expect( - output, - containsAllInOrder([ - contains('No Android integration tests found for plugin/example'), - contains('SKIPPING: No tests found.'), - ]), - ); - }); - }); - - group('Linux', () { - test('builds and runs unit tests', () async { - const String testBinaryRelativePath = - 'build/linux/x64/release/bar/plugin_test'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/$testBinaryRelativePath' - ], platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline), - }); - _createFakeCMakeCache(plugin, mockPlatform); - - final File testBinary = childFileWithSubcomponents(plugin.directory, - ['example', ...testBinaryRelativePath.split('/')]); - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--linux', - '--no-integration', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running plugin_test...'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getLinuxBuildCall(plugin), - ProcessCall(testBinary.path, const [], null), - ])); - }); - - test('only runs release unit tests', () async { - const String debugTestBinaryRelativePath = - 'build/linux/x64/debug/bar/plugin_test'; - const String releaseTestBinaryRelativePath = - 'build/linux/x64/release/bar/plugin_test'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/$debugTestBinaryRelativePath', - 'example/$releaseTestBinaryRelativePath' - ], platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline), - }); - _createFakeCMakeCache(plugin, mockPlatform); - - final File releaseTestBinary = childFileWithSubcomponents( - plugin.directory, - ['example', ...releaseTestBinaryRelativePath.split('/')]); - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--linux', - '--no-integration', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running plugin_test...'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getLinuxBuildCall(plugin), - ProcessCall(releaseTestBinary.path, const [], null), - ])); - }); - - test('fails if CMake has not been configured', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline), - }); - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--linux', - '--no-integration', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('plugin:\n' - ' Examples must be built before testing.') - ]), - ); - - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('fails if there are no unit tests', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline), - }); - _createFakeCMakeCache(plugin, mockPlatform); - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--linux', - '--no-integration', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No test binaries found.'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getLinuxBuildCall(plugin), - ])); - }); - - test('fails if a unit test fails', () async { - const String testBinaryRelativePath = - 'build/linux/x64/release/bar/plugin_test'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/$testBinaryRelativePath' - ], platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline), - }); - _createFakeCMakeCache(plugin, mockPlatform); - - final File testBinary = childFileWithSubcomponents(plugin.directory, - ['example', ...testBinaryRelativePath.split('/')]); - - processRunner.mockProcessesForExecutable[testBinary.path] = - [MockProcess(exitCode: 1)]; - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--linux', - '--no-integration', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running plugin_test...'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getLinuxBuildCall(plugin), - ProcessCall(testBinary.path, const [], null), - ])); - }); - }); - - // Tests behaviors of implementation that is shared between iOS and macOS. - group('iOS/macOS', () { - test('fails if xcrun fails', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(exitCode: 1) - ]; - - Error? commandError; - final List output = - await runCapturingPrint(runner, ['native-test', '--macos'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains(' plugin'), - ]), - ); - }); - - test('honors unit-only', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess( - ['RunnerTests', 'RunnerUITests']), - ]; - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--macos', - '--no-integration', - ]); - - expect( - output, - contains( - contains('Successfully ran macOS xctest for plugin/example'))); - - // --no-integration should translate to '-only-testing:RunnerTests'. - expect( - processRunner.recordedCalls, - orderedEquals([ - getTargetCheckCall(pluginExampleDirectory, 'macos'), - getRunTestCall(pluginExampleDirectory, 'macos', - extraFlags: ['-only-testing:RunnerTests']), - ])); - }); - - test('honors integration-only', () async { - final RepositoryPackage plugin1 = createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin1); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess( - ['RunnerTests', 'RunnerUITests']), - ]; - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--macos', - '--no-unit', - ]); - - expect( - output, - contains( - contains('Successfully ran macOS xctest for plugin/example'))); - - // --no-unit should translate to '-only-testing:RunnerUITests'. - expect( - processRunner.recordedCalls, - orderedEquals([ - getTargetCheckCall(pluginExampleDirectory, 'macos'), - getRunTestCall(pluginExampleDirectory, 'macos', - extraFlags: ['-only-testing:RunnerUITests']), - ])); - }); - - test('skips when the requested target is not present', () async { - final RepositoryPackage plugin1 = createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin1); - - // Simulate a project with unit tests but no integration tests... - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess(['RunnerTests']), - ]; - - // ... then try to run only integration tests. - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--macos', - '--no-unit', - ]); - - expect( - output, - containsAllInOrder([ - contains( - 'No "RunnerUITests" target in plugin/example; skipping.'), - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getTargetCheckCall(pluginExampleDirectory, 'macos'), - ])); - }); - - test('fails if there are no unit tests', () async { - final RepositoryPackage plugin1 = createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin1); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess(['RunnerUITests']), - ]; - - Error? commandError; - final List output = - await runCapturingPrint(runner, ['native-test', '--macos'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No "RunnerTests" target in plugin/example; skipping.'), - contains( - 'No unit tests ran. Plugins are required to have unit tests.'), - contains('The following packages had errors:'), - contains('plugin:\n' - ' No unit tests ran (use --exclude if this is intentional).'), - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getTargetCheckCall(pluginExampleDirectory, 'macos'), - ])); - }); - - test('fails if unable to check for requested target', () async { - final RepositoryPackage plugin1 = createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin1); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(exitCode: 1), // xcodebuild -list - ]; - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--macos', - '--no-integration', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to check targets for plugin/example.'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getTargetCheckCall(pluginExampleDirectory, 'macos'), - ])); - }); - }); - - group('multiplatform', () { - test('runs all platfroms when supported', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/android/gradlew', - 'android/src/test/example_test.java', - ], - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - final Directory androidFolder = - pluginExampleDirectory.childDirectory('android'); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess( - ['RunnerTests', 'RunnerUITests']), // iOS list - MockProcess(), // iOS run - getMockXcodebuildListProcess( - ['RunnerTests', 'RunnerUITests']), // macOS list - MockProcess(), // macOS run - ]; - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--android', - '--ios', - '--macos', - kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAll([ - contains('Running Android tests for plugin/example'), - contains('Successfully ran iOS xctest for plugin/example'), - contains('Successfully ran macOS xctest for plugin/example'), - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest'], androidFolder.path), - getTargetCheckCall(pluginExampleDirectory, 'ios'), - getRunTestCall(pluginExampleDirectory, 'ios', - destination: 'foo_destination'), - getTargetCheckCall(pluginExampleDirectory, 'macos'), - getRunTestCall(pluginExampleDirectory, 'macos'), - ])); - }); - - test('runs only macOS for a macOS plugin', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess( - ['RunnerTests', 'RunnerUITests']), - ]; - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--ios', - '--macos', - kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAllInOrder([ - contains('No implementation for iOS.'), - contains('Successfully ran macOS xctest for plugin/example'), - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getTargetCheckCall(pluginExampleDirectory, 'macos'), - getRunTestCall(pluginExampleDirectory, 'macos'), - ])); - }); - - test('runs only iOS for a iOS plugin', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess( - ['RunnerTests', 'RunnerUITests']), - ]; - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--ios', - '--macos', - kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAllInOrder([ - contains('No implementation for macOS.'), - contains('Successfully ran iOS xctest for plugin/example') - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getTargetCheckCall(pluginExampleDirectory, 'ios'), - getRunTestCall(pluginExampleDirectory, 'ios', - destination: 'foo_destination'), - ])); - }); - - test('skips when nothing is supported', () async { - createFakePlugin('plugin', packagesDir); - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--android', - '--ios', - '--macos', - '--windows', - kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAllInOrder([ - contains('No implementation for Android.'), - contains('No implementation for iOS.'), - contains('No implementation for macOS.'), - contains('SKIPPING: Nothing to test for target platform(s).'), - ])); - - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('skips Dart-only plugins', () async { - createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline, - hasDartCode: true, hasNativeCode: false), - platformWindows: const PlatformDetails(PlatformSupport.inline, - hasDartCode: true, hasNativeCode: false), - }, - ); - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--macos', - '--windows', - kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAllInOrder([ - contains('No native code for macOS.'), - contains('No native code for Windows.'), - contains('SKIPPING: Nothing to test for target platform(s).'), - ])); - - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('failing one platform does not stop the tests', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/test/example_test.java', - ], - ); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess( - ['RunnerTests', 'RunnerUITests']), - ]; - - // Simulate failing Android, but not iOS. - final String gradlewPath = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android) - .childFile('gradlew') - .path; - processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess(exitCode: 1) - ]; - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--android', - '--ios', - '--ios-destination', - 'foo_destination', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder([ - contains('Running tests for Android...'), - contains('plugin/example unit tests failed.'), - contains('Running tests for iOS...'), - contains('Successfully ran iOS xctest for plugin/example'), - contains('The following packages had errors:'), - contains('plugin:\n' - ' Android') - ]), - ); - }); - - test('failing multiple platforms reports multiple failures', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/test/example_test.java', - ], - ); - - // Simulate failing Android. - final String gradlewPath = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android) - .childFile('gradlew') - .path; - processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess(exitCode: 1) - ]; - // Simulate failing Android. - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(exitCode: 1) - ]; - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--android', - '--ios', - '--ios-destination', - 'foo_destination', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder([ - contains('Running tests for Android...'), - contains('Running tests for iOS...'), - contains('The following packages had errors:'), - contains('plugin:\n' - ' Android\n' - ' iOS') - ]), - ); - }); - }); - }); - - group('test native_test_command on Windows', () { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(style: FileSystemStyle.windows); - mockPlatform = MockPlatform(isWindows: true); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final NativeTestCommand command = NativeTestCommand(packagesDir, - processRunner: processRunner, platform: mockPlatform); - - runner = CommandRunner( - 'native_test_command', 'Test for native_test_command'); - runner.addCommand(command); - }); - - // Returns the ProcessCall to expect for build the Windows unit tests for - // the given plugin. - ProcessCall getWindowsBuildCall(RepositoryPackage plugin) { - return ProcessCall( - _fakeCmakeCommand, - [ - '--build', - getExampleDir(plugin) - .childDirectory('build') - .childDirectory('windows') - .path, - '--target', - 'unit_tests', - '--config', - 'Debug' - ], - null); - } - - group('Windows', () { - test('runs unit tests', () async { - const String testBinaryRelativePath = - 'build/windows/Debug/bar/plugin_test.exe'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/$testBinaryRelativePath' - ], platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.inline), - }); - _createFakeCMakeCache(plugin, mockPlatform); - - final File testBinary = childFileWithSubcomponents(plugin.directory, - ['example', ...testBinaryRelativePath.split('/')]); - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--windows', - '--no-integration', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running plugin_test.exe...'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getWindowsBuildCall(plugin), - ProcessCall(testBinary.path, const [], null), - ])); - }); - - test('only runs debug unit tests', () async { - const String debugTestBinaryRelativePath = - 'build/windows/Debug/bar/plugin_test.exe'; - const String releaseTestBinaryRelativePath = - 'build/windows/Release/bar/plugin_test.exe'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/$debugTestBinaryRelativePath', - 'example/$releaseTestBinaryRelativePath' - ], platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.inline), - }); - _createFakeCMakeCache(plugin, mockPlatform); - - final File debugTestBinary = childFileWithSubcomponents( - plugin.directory, - ['example', ...debugTestBinaryRelativePath.split('/')]); - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--windows', - '--no-integration', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running plugin_test.exe...'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getWindowsBuildCall(plugin), - ProcessCall(debugTestBinary.path, const [], null), - ])); - }); - - test('fails if CMake has not been configured', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.inline), - }); - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--windows', - '--no-integration', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('plugin:\n' - ' Examples must be built before testing.') - ]), - ); - - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('fails if there are no unit tests', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.inline), - }); - _createFakeCMakeCache(plugin, mockPlatform); - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--windows', - '--no-integration', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No test binaries found.'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getWindowsBuildCall(plugin), - ])); - }); - - test('fails if a unit test fails', () async { - const String testBinaryRelativePath = - 'build/windows/Debug/bar/plugin_test.exe'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/$testBinaryRelativePath' - ], platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.inline), - }); - _createFakeCMakeCache(plugin, mockPlatform); - - final File testBinary = childFileWithSubcomponents(plugin.directory, - ['example', ...testBinaryRelativePath.split('/')]); - - processRunner.mockProcessesForExecutable[testBinary.path] = - [MockProcess(exitCode: 1)]; - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--windows', - '--no-integration', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running plugin_test.exe...'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getWindowsBuildCall(plugin), - ProcessCall(testBinary.path, const [], null), - ])); - }); - }); - }); -} diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart deleted file mode 100644 index 575f8509fd25..000000000000 --- a/script/tool/test/publish_check_command_test.dart +++ /dev/null @@ -1,464 +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:convert'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -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/publish_check_command.dart'; -import 'package:http/http.dart' as http; -import 'package:http/testing.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('$PublishCheckCommand tests', () { - FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late RecordingProcessRunner processRunner; - late CommandRunner runner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final PublishCheckCommand publishCheckCommand = PublishCheckCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - runner.addCommand(publishCheckCommand); - }); - - test('publish check all packages', () async { - final RepositoryPackage plugin1 = createFakePlugin( - 'plugin_tools_test_package_a', - packagesDir, - examples: [], - ); - final RepositoryPackage plugin2 = createFakePlugin( - 'plugin_tools_test_package_b', - packagesDir, - examples: [], - ); - - await runCapturingPrint(runner, ['publish-check']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'flutter', - const ['pub', 'publish', '--', '--dry-run'], - plugin1.path), - ProcessCall( - 'flutter', - const ['pub', 'publish', '--', '--dry-run'], - plugin2.path), - ])); - }); - - test('publish prepares dependencies of examples (when present)', () async { - final RepositoryPackage plugin1 = createFakePlugin( - 'plugin_tools_test_package_a', - packagesDir, - examples: ['example1', 'example2'], - ); - final RepositoryPackage plugin2 = createFakePlugin( - 'plugin_tools_test_package_b', - packagesDir, - examples: [], - ); - - await runCapturingPrint(runner, ['publish-check']); - - // For plugin1, these are the expected pub get calls that will happen - final Iterable pubGetCalls = - plugin1.getExamples().map((RepositoryPackage example) { - return ProcessCall( - 'dart', - const ['pub', 'get'], - example.path, - ); - }); - - expect(pubGetCalls, hasLength(2)); - expect( - processRunner.recordedCalls, - orderedEquals([ - // plugin1 has 2 examples, so there's some 'dart pub get' calls. - ...pubGetCalls, - ProcessCall( - 'flutter', - const ['pub', 'publish', '--', '--dry-run'], - plugin1.path), - // plugin2 has no examples, so there's no extra 'dart pub get' calls. - ProcessCall( - 'flutter', - const ['pub', 'publish', '--', '--dry-run'], - plugin2.path), - ]), - ); - }); - - test('fail on negative test', () async { - createFakePlugin('plugin_tools_test_package_a', packagesDir); - - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(exitCode: 1, stdout: 'Some error from pub') - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['publish-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Some error from pub'), - contains('Unable to publish plugin_tools_test_package_a'), - ]), - ); - }); - - test('fail on bad pubspec', () async { - final RepositoryPackage package = createFakePlugin('c', packagesDir); - await package.pubspecFile.writeAsString('bad-yaml'); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['publish-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No valid pubspec found.'), - ]), - ); - }); - - test('fails if AUTHORS is missing', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - package.authorsFile.delete(); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['publish-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'No AUTHORS file found. Packages must include an AUTHORS file.'), - ]), - ); - }); - - test('does not require AUTHORS for third-party', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', - packagesDir.parent - .childDirectory('third_party') - .childDirectory('packages')); - package.authorsFile.delete(); - - final List output = - await runCapturingPrint(runner, ['publish-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for a_package'), - ]), - ); - }); - - test('pass on prerelease if --allow-pre-release flag is on', () async { - createFakePlugin('d', packagesDir); - - final MockProcess process = MockProcess( - exitCode: 1, - stdout: 'Package has 1 warning.\n' - 'Packages with an SDK constraint on a pre-release of the Dart ' - 'SDK should themselves be published as a pre-release version.'); - processRunner.mockProcessesForExecutable['flutter'] = [ - process, - ]; - - expect( - runCapturingPrint( - runner, ['publish-check', '--allow-pre-release']), - completes); - }); - - test('fail on prerelease if --allow-pre-release flag is off', () async { - createFakePlugin('d', packagesDir); - - final MockProcess process = MockProcess( - exitCode: 1, - stdout: 'Package has 1 warning.\n' - 'Packages with an SDK constraint on a pre-release of the Dart ' - 'SDK should themselves be published as a pre-release version.'); - processRunner.mockProcessesForExecutable['flutter'] = [ - process, - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['publish-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Packages with an SDK constraint on a pre-release of the Dart SDK'), - contains('Unable to publish d'), - ]), - ); - }); - - test('Success message on stderr is not printed as an error', () async { - createFakePlugin('d', packagesDir); - - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(stdout: 'Package has 0 warnings.'), - ]; - - final List output = - await runCapturingPrint(runner, ['publish-check']); - - expect(output, isNot(contains(contains('ERROR:')))); - }); - - test( - '--machine: Log JSON with status:no-publish and correct human message, if there are no packages need to be published. ', - () async { - const Map httpResponseA = { - 'name': 'a', - 'versions': [ - '0.0.1', - '0.1.0', - ], - }; - - const Map httpResponseB = { - 'name': 'b', - 'versions': [ - '0.0.1', - '0.1.0', - '0.2.0', - ], - }; - - final MockClient mockClient = MockClient((http.Request request) async { - if (request.url.pathSegments.last == 'no_publish_a.json') { - return http.Response(json.encode(httpResponseA), 200); - } else if (request.url.pathSegments.last == 'no_publish_b.json') { - return http.Response(json.encode(httpResponseB), 200); - } - return http.Response('', 500); - }); - final PublishCheckCommand command = PublishCheckCommand(packagesDir, - processRunner: processRunner, httpClient: mockClient); - - runner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - runner.addCommand(command); - - createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); - createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); - - final List output = await runCapturingPrint( - runner, ['publish-check', '--machine']); - - expect(output.first, r''' -{ - "status": "no-publish", - "humanMessage": [ - "\n============================================================\n|| Running for no_publish_a\n============================================================\n", - "Package no_publish_a version: 0.1.0 has already be published on pub.", - "\n============================================================\n|| Running for no_publish_b\n============================================================\n", - "Package no_publish_b version: 0.2.0 has already be published on pub.", - "\n", - "------------------------------------------------------------", - "Run overview:", - " no_publish_a - ran", - " no_publish_b - ran", - "", - "Ran for 2 package(s)", - "\n", - "No issues found!" - ] -}'''); - }); - - test( - '--machine: Log JSON with status:needs-publish and correct human message, if there is at least 1 plugin needs to be published.', - () async { - const Map httpResponseA = { - 'name': 'a', - 'versions': [ - '0.0.1', - '0.1.0', - ], - }; - - const Map httpResponseB = { - 'name': 'b', - 'versions': [ - '0.0.1', - '0.1.0', - ], - }; - - final MockClient mockClient = MockClient((http.Request request) async { - if (request.url.pathSegments.last == 'no_publish_a.json') { - return http.Response(json.encode(httpResponseA), 200); - } else if (request.url.pathSegments.last == 'no_publish_b.json') { - return http.Response(json.encode(httpResponseB), 200); - } - return http.Response('', 500); - }); - final PublishCheckCommand command = PublishCheckCommand(packagesDir, - processRunner: processRunner, httpClient: mockClient); - - runner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - runner.addCommand(command); - - createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); - createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); - - final List output = await runCapturingPrint( - runner, ['publish-check', '--machine']); - - expect(output.first, r''' -{ - "status": "needs-publish", - "humanMessage": [ - "\n============================================================\n|| Running for no_publish_a\n============================================================\n", - "Package no_publish_a version: 0.1.0 has already be published on pub.", - "\n============================================================\n|| Running for no_publish_b\n============================================================\n", - "Running pub publish --dry-run:", - "Package no_publish_b is able to be published.", - "\n", - "------------------------------------------------------------", - "Run overview:", - " no_publish_a - ran", - " no_publish_b - ran", - "", - "Ran for 2 package(s)", - "\n", - "No issues found!" - ] -}'''); - }); - - test( - '--machine: Log correct JSON, if there is at least 1 plugin contains error.', - () async { - const Map httpResponseA = { - 'name': 'a', - 'versions': [ - '0.0.1', - '0.1.0', - ], - }; - - const Map httpResponseB = { - 'name': 'b', - 'versions': [ - '0.0.1', - '0.1.0', - ], - }; - - final MockClient mockClient = MockClient((http.Request request) async { - print('url ${request.url}'); - print(request.url.pathSegments.last); - if (request.url.pathSegments.last == 'no_publish_a.json') { - return http.Response(json.encode(httpResponseA), 200); - } else if (request.url.pathSegments.last == 'no_publish_b.json') { - return http.Response(json.encode(httpResponseB), 200); - } - return http.Response('', 500); - }); - final PublishCheckCommand command = PublishCheckCommand(packagesDir, - processRunner: processRunner, httpClient: mockClient); - - runner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - runner.addCommand(command); - - final RepositoryPackage plugin = - createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); - createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); - - await plugin.pubspecFile.writeAsString('bad-yaml'); - - bool hasError = false; - final List output = await runCapturingPrint( - runner, ['publish-check', '--machine'], - errorHandler: (Error error) { - expect(error, isA()); - hasError = true; - }); - expect(hasError, isTrue); - - expect(output.first, contains(r''' -{ - "status": "error", - "humanMessage": [ - "\n============================================================\n|| Running for no_publish_a\n============================================================\n", - "Failed to parse `pubspec.yaml` at /packages/no_publish_a/pubspec.yaml: ParsedYamlException:''')); - // This is split into two checks since the details of the YamlException - // aren't controlled by this package, so asserting its exact format would - // make the test fragile to irrelevant changes in those details. - expect(output.first, contains(r''' - "No valid pubspec found.", - "\n============================================================\n|| Running for no_publish_b\n============================================================\n", - "url https://pub.dev/packages/no_publish_b.json", - "no_publish_b.json", - "Running pub publish --dry-run:", - "Package no_publish_b is able to be published.", - "\n", - "The following packages had errors:", - " no_publish_a", - "See above for full details." - ] -}''')); - }); - }); -} diff --git a/script/tool/test/publish_command_test.dart b/script/tool/test/publish_command_test.dart deleted file mode 100644 index da5f9c871f05..000000000000 --- a/script/tool/test/publish_command_test.dart +++ /dev/null @@ -1,922 +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:async'; -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -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/publish_command.dart'; -import 'package:http/http.dart' as http; -import 'package:http/testing.dart'; -import 'package:mockito/mockito.dart'; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'common/package_command_test.mocks.dart'; -import 'mocks.dart'; -import 'util.dart'; - -void main() { - final String flutterCommand = getFlutterCommand(const LocalPlatform()); - - late Directory packagesDir; - late MockGitDir gitDir; - late TestProcessRunner processRunner; - late CommandRunner commandRunner; - late MockStdin mockStdin; - late FileSystem fileSystem; - // Map of package name to mock response. - late Map> mockHttpResponses; - - void createMockCredentialFile() { - final String credentialPath = PublishCommand.getCredentialPath(); - fileSystem.file(credentialPath) - ..createSync(recursive: true) - ..writeAsStringSync('some credential'); - } - - setUp(() async { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = TestProcessRunner(); - - mockHttpResponses = >{}; - final MockClient mockClient = MockClient((http.Request request) async { - final String packageName = - request.url.pathSegments.last.replaceAll('.json', ''); - final Map? response = mockHttpResponses[packageName]; - if (response != null) { - return http.Response(json.encode(response), 200); - } - // Default to simulating the plugin never having been published. - return http.Response('', 404); - }); - - gitDir = MockGitDir(); - when(gitDir.path).thenReturn(packagesDir.parent.path); - when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) - .thenAnswer((Invocation invocation) { - final List arguments = - invocation.positionalArguments[0]! as List; - // Route git calls through the process runner, to make mock output - // consistent with outer processes. Attach the first argument to the - // command to make targeting the mock results easier. - final String gitCommand = arguments.removeAt(0); - return processRunner.run('git-$gitCommand', arguments); - }); - - mockStdin = MockStdin(); - commandRunner = CommandRunner('tester', '') - ..addCommand(PublishCommand( - packagesDir, - processRunner: processRunner, - stdinput: mockStdin, - gitDir: gitDir, - httpClient: mockClient, - )); - }); - - group('Initial validation', () { - test('refuses to proceed with dirty files', () async { - final RepositoryPackage plugin = - createFakePlugin('foo', packagesDir, examples: []); - - processRunner.mockProcessesForExecutable['git-status'] = [ - MockProcess(stdout: '?? ${plugin.directory.childFile('tmp').path}\n') - ]; - - Error? commandError; - final List output = - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=foo', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains("There are files in the package directory that haven't " - 'been saved in git. Refusing to publish these files:\n\n' - '?? /packages/foo/tmp\n\n' - 'If the directory should be clean, you can run `git clean -xdf && ' - 'git reset --hard HEAD` to wipe all local changes.'), - contains('foo:\n' - ' uncommitted changes'), - ])); - }); - - test("fails immediately if the remote doesn't exist", () async { - createFakePlugin('foo', packagesDir, examples: []); - - processRunner.mockProcessesForExecutable['git-remote'] = [ - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - commandRunner, ['publish', '--packages=foo'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Unable to find URL for remote upstream; cannot push tags'), - ])); - }); - }); - - group('Publishes package', () { - test('while showing all output from pub publish to the user', () async { - createFakePlugin('plugin1', packagesDir, examples: []); - createFakePlugin('plugin2', packagesDir, examples: []); - - processRunner.mockProcessesForExecutable[flutterCommand] = [ - MockProcess( - stdout: 'Foo', - stderr: 'Bar', - stdoutEncoding: utf8, - stderrEncoding: utf8), // pub publish for plugin1 - MockProcess( - stdout: 'Baz', - stdoutEncoding: utf8, - stderrEncoding: utf8), // pub publish for plugin1 - ]; - - final List output = await runCapturingPrint( - commandRunner, ['publish', '--packages=plugin1,plugin2']); - - expect( - output, - containsAllInOrder([ - contains('Running `pub publish ` in /packages/plugin1...'), - contains('Foo'), - contains('Bar'), - contains('Package published!'), - contains('Running `pub publish ` in /packages/plugin2...'), - contains('Baz'), - contains('Package published!'), - ])); - }); - - test('forwards input from the user to `pub publish`', () async { - createFakePlugin('foo', packagesDir, examples: []); - - mockStdin.mockUserInputs.add(utf8.encode('user input')); - - await runCapturingPrint( - commandRunner, ['publish', '--packages=foo']); - - expect(processRunner.mockPublishProcess.stdinMock.lines, - contains('user input')); - }); - - test('forwards --pub-publish-flags to pub publish', () async { - final RepositoryPackage plugin = - createFakePlugin('foo', packagesDir, examples: []); - - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=foo', - '--pub-publish-flags', - '--dry-run,--server=bar' - ]); - - expect( - processRunner.recordedCalls, - contains(ProcessCall( - flutterCommand, - const ['pub', 'publish', '--dry-run', '--server=bar'], - plugin.path))); - }); - - test( - '--skip-confirmation flag automatically adds --force to --pub-publish-flags', - () async { - createMockCredentialFile(); - final RepositoryPackage plugin = - createFakePlugin('foo', packagesDir, examples: []); - - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=foo', - '--skip-confirmation', - '--pub-publish-flags', - '--server=bar' - ]); - - expect( - processRunner.recordedCalls, - contains(ProcessCall( - flutterCommand, - const ['pub', 'publish', '--server=bar', '--force'], - plugin.path))); - }); - - test('--force is only added once, regardless of plugin count', () async { - createMockCredentialFile(); - final RepositoryPackage plugin1 = - createFakePlugin('plugin_a', packagesDir, examples: []); - final RepositoryPackage plugin2 = - createFakePlugin('plugin_b', packagesDir, examples: []); - - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=plugin_a,plugin_b', - '--skip-confirmation', - '--pub-publish-flags', - '--server=bar' - ]); - - expect( - processRunner.recordedCalls, - containsAllInOrder([ - ProcessCall( - flutterCommand, - const ['pub', 'publish', '--server=bar', '--force'], - plugin1.path), - ProcessCall( - flutterCommand, - const ['pub', 'publish', '--server=bar', '--force'], - plugin2.path), - ])); - }); - - test('throws if pub publish fails', () async { - createFakePlugin('foo', packagesDir, examples: []); - - processRunner.mockProcessesForExecutable[flutterCommand] = [ - MockProcess(exitCode: 128) // pub publish - ]; - - Error? commandError; - final List output = - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=foo', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Publishing foo failed.'), - ])); - }); - - test('publish, dry run', () async { - final RepositoryPackage plugin = - createFakePlugin('foo', packagesDir, examples: []); - - final List output = - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=foo', - '--dry-run', - ]); - - expect( - processRunner.recordedCalls - .map((ProcessCall call) => call.executable), - isNot(contains('git-push'))); - expect( - output, - containsAllInOrder([ - contains('=============== DRY RUN ==============='), - contains('Running for foo'), - contains('Running `pub publish ` in ${plugin.path}...'), - contains('Tagging release foo-v0.0.1...'), - contains('Pushing tag to upstream...'), - contains('Published foo successfully!'), - ])); - }); - - test('can publish non-flutter package', () async { - const String packageName = 'a_package'; - createFakePackage(packageName, packagesDir); - - final List output = - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=$packageName', - ]); - - expect( - output, - containsAllInOrder( - [ - contains('Running `pub publish ` in /packages/a_package...'), - contains('Package published!'), - ], - ), - ); - }); - }); - - group('Tags release', () { - test('with the version and name from the pubspec.yaml', () async { - createFakePlugin('foo', packagesDir, examples: []); - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=foo', - ]); - - expect(processRunner.recordedCalls, - contains(const ProcessCall('git-tag', ['foo-v0.0.1'], null))); - }); - - test('only if publishing succeeded', () async { - createFakePlugin('foo', packagesDir, examples: []); - - processRunner.mockProcessesForExecutable[flutterCommand] = [ - MockProcess(exitCode: 128) // pub publish - ]; - - Error? commandError; - final List output = - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=foo', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Publishing foo failed.'), - ])); - expect( - processRunner.recordedCalls, - isNot(contains( - const ProcessCall('git-tag', ['foo-v0.0.1'], null)))); - }); - }); - - group('Pushes tags', () { - test('to upstream by default', () async { - createFakePlugin('foo', packagesDir, examples: []); - - mockStdin.readLineOutput = 'y'; - - final List output = - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=foo', - ]); - - expect( - processRunner.recordedCalls, - contains(const ProcessCall( - 'git-push', ['upstream', 'foo-v0.0.1'], null))); - expect( - output, - containsAllInOrder([ - contains('Pushing tag to upstream...'), - contains('Published foo successfully!'), - ])); - }); - - test('does not ask for user input if the --skip-confirmation flag is on', - () async { - createMockCredentialFile(); - createFakePlugin('foo', packagesDir, examples: []); - - final List output = - await runCapturingPrint(commandRunner, [ - 'publish', - '--skip-confirmation', - '--packages=foo', - ]); - - expect( - processRunner.recordedCalls, - contains(const ProcessCall( - 'git-push', ['upstream', 'foo-v0.0.1'], null))); - expect( - output, - containsAllInOrder([ - contains('Published foo successfully!'), - ])); - }); - - test('to upstream by default, dry run', () async { - final RepositoryPackage plugin = - createFakePlugin('foo', packagesDir, examples: []); - - mockStdin.readLineOutput = 'y'; - - final List output = await runCapturingPrint( - commandRunner, ['publish', '--packages=foo', '--dry-run']); - - expect( - processRunner.recordedCalls - .map((ProcessCall call) => call.executable), - isNot(contains('git-push'))); - expect( - output, - containsAllInOrder([ - contains('=============== DRY RUN ==============='), - contains('Running `pub publish ` in ${plugin.path}...'), - contains('Tagging release foo-v0.0.1...'), - contains('Pushing tag to upstream...'), - contains('Published foo successfully!'), - ])); - }); - - test('to different remotes based on a flag', () async { - createFakePlugin('foo', packagesDir, examples: []); - - mockStdin.readLineOutput = 'y'; - - final List output = - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=foo', - '--remote', - 'origin', - ]); - - expect( - processRunner.recordedCalls, - contains(const ProcessCall( - 'git-push', ['origin', 'foo-v0.0.1'], null))); - expect( - output, - containsAllInOrder([ - contains('Published foo successfully!'), - ])); - }); - }); - - group('Auto release (all-changed flag)', () { - test('can release newly created plugins', () async { - mockHttpResponses['plugin1'] = { - 'name': 'plugin1', - 'versions': [], - }; - - mockHttpResponses['plugin2'] = { - 'name': 'plugin2', - 'versions': [], - }; - - // Non-federated - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - // federated - final RepositoryPackage plugin2 = createFakePlugin( - 'plugin2', - packagesDir.childDirectory('plugin2'), - ); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess( - stdout: '${plugin1.pubspecFile.path}\n' - '${plugin2.pubspecFile.path}\n') - ]; - mockStdin.readLineOutput = 'y'; - - final List output = await runCapturingPrint(commandRunner, - ['publish', '--all-changed', '--base-sha=HEAD~']); - - expect( - output, - containsAllInOrder([ - contains( - 'Publishing all packages that have changed relative to "HEAD~"'), - contains('Running `pub publish ` in ${plugin1.path}...'), - contains('Running `pub publish ` in ${plugin2.path}...'), - contains('plugin1 - \x1B[32mpublished\x1B[0m'), - contains('plugin2/plugin2 - \x1B[32mpublished\x1B[0m'), - ])); - expect( - processRunner.recordedCalls, - contains(const ProcessCall( - 'git-push', ['upstream', 'plugin1-v0.0.1'], null))); - expect( - processRunner.recordedCalls, - contains(const ProcessCall( - 'git-push', ['upstream', 'plugin2-v0.0.1'], null))); - }); - - test('can release newly created plugins, while there are existing plugins', - () async { - mockHttpResponses['plugin0'] = { - 'name': 'plugin0', - 'versions': ['0.0.1'], - }; - - mockHttpResponses['plugin1'] = { - 'name': 'plugin1', - 'versions': [], - }; - - mockHttpResponses['plugin2'] = { - 'name': 'plugin2', - 'versions': [], - }; - - // The existing plugin. - createFakePlugin('plugin0', packagesDir); - // Non-federated - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - // federated - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - - // Git results for plugin0 having been released already, and plugin1 and - // plugin2 being new. - processRunner.mockProcessesForExecutable['git-tag'] = [ - MockProcess(stdout: 'plugin0-v0.0.1\n') - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess( - stdout: '${plugin1.pubspecFile.path}\n' - '${plugin2.pubspecFile.path}\n') - ]; - - mockStdin.readLineOutput = 'y'; - - final List output = await runCapturingPrint(commandRunner, - ['publish', '--all-changed', '--base-sha=HEAD~']); - - expect( - output, - containsAllInOrder([ - 'Running `pub publish ` in ${plugin1.path}...\n', - 'Running `pub publish ` in ${plugin2.path}...\n', - ])); - expect( - processRunner.recordedCalls, - contains(const ProcessCall( - 'git-push', ['upstream', 'plugin1-v0.0.1'], null))); - expect( - processRunner.recordedCalls, - contains(const ProcessCall( - 'git-push', ['upstream', 'plugin2-v0.0.1'], null))); - }); - - test('can release newly created plugins, dry run', () async { - mockHttpResponses['plugin1'] = { - 'name': 'plugin1', - 'versions': [], - }; - - mockHttpResponses['plugin2'] = { - 'name': 'plugin2', - 'versions': [], - }; - - // Non-federated - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - // federated - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess( - stdout: '${plugin1.pubspecFile.path}\n' - '${plugin2.pubspecFile.path}\n') - ]; - mockStdin.readLineOutput = 'y'; - - final List output = await runCapturingPrint( - commandRunner, [ - 'publish', - '--all-changed', - '--base-sha=HEAD~', - '--dry-run' - ]); - - expect( - output, - containsAllInOrder([ - contains('=============== DRY RUN ==============='), - contains('Running `pub publish ` in ${plugin1.path}...'), - contains('Tagging release plugin1-v0.0.1...'), - contains('Pushing tag to upstream...'), - contains('Published plugin1 successfully!'), - contains('Running `pub publish ` in ${plugin2.path}...'), - contains('Tagging release plugin2-v0.0.1...'), - contains('Pushing tag to upstream...'), - contains('Published plugin2 successfully!'), - ])); - expect( - processRunner.recordedCalls - .map((ProcessCall call) => call.executable), - isNot(contains('git-push'))); - }); - - test('version change triggers releases.', () async { - mockHttpResponses['plugin1'] = { - 'name': 'plugin1', - 'versions': ['0.0.1'], - }; - - mockHttpResponses['plugin2'] = { - 'name': 'plugin2', - 'versions': ['0.0.1'], - }; - - // Non-federated - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir, version: '0.0.2'); - // federated - final RepositoryPackage plugin2 = createFakePlugin( - 'plugin2', packagesDir.childDirectory('plugin2'), - version: '0.0.2'); - - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess( - stdout: '${plugin1.pubspecFile.path}\n' - '${plugin2.pubspecFile.path}\n') - ]; - - mockStdin.readLineOutput = 'y'; - - final List output2 = await runCapturingPrint(commandRunner, - ['publish', '--all-changed', '--base-sha=HEAD~']); - expect( - output2, - containsAllInOrder([ - contains('Running `pub publish ` in ${plugin1.path}...'), - contains('Published plugin1 successfully!'), - contains('Running `pub publish ` in ${plugin2.path}...'), - contains('Published plugin2 successfully!'), - ])); - expect( - processRunner.recordedCalls, - contains(const ProcessCall( - 'git-push', ['upstream', 'plugin1-v0.0.2'], null))); - expect( - processRunner.recordedCalls, - contains(const ProcessCall( - 'git-push', ['upstream', 'plugin2-v0.0.2'], null))); - }); - - test( - 'delete package will not trigger publish but exit the command successfully!', - () async { - mockHttpResponses['plugin1'] = { - 'name': 'plugin1', - 'versions': ['0.0.1'], - }; - - mockHttpResponses['plugin2'] = { - 'name': 'plugin2', - 'versions': ['0.0.1'], - }; - - // Non-federated - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir, version: '0.0.2'); - // federated - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - plugin2.directory.deleteSync(recursive: true); - - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess( - stdout: '${plugin1.pubspecFile.path}\n' - '${plugin2.pubspecFile.path}\n') - ]; - - mockStdin.readLineOutput = 'y'; - - final List output2 = await runCapturingPrint(commandRunner, - ['publish', '--all-changed', '--base-sha=HEAD~']); - expect( - output2, - containsAllInOrder([ - contains('Running `pub publish ` in ${plugin1.path}...'), - contains('Published plugin1 successfully!'), - contains( - 'The pubspec file for plugin2/plugin2 does not exist, so no publishing will happen.\nSafe to ignore if the package is deleted in this commit.\n'), - contains('SKIPPING: package deleted'), - contains('skipped (with warning)'), - ])); - expect( - processRunner.recordedCalls, - contains(const ProcessCall( - 'git-push', ['upstream', 'plugin1-v0.0.2'], null))); - }); - - test('Existing versions do not trigger release, also prints out message.', - () async { - mockHttpResponses['plugin1'] = { - 'name': 'plugin1', - 'versions': ['0.0.2'], - }; - - mockHttpResponses['plugin2'] = { - 'name': 'plugin2', - 'versions': ['0.0.2'], - }; - - // Non-federated - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir, version: '0.0.2'); - // federated - final RepositoryPackage plugin2 = createFakePlugin( - 'plugin2', packagesDir.childDirectory('plugin2'), - version: '0.0.2'); - - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess( - stdout: '${plugin1.pubspecFile.path}\n' - '${plugin2.pubspecFile.path}\n') - ]; - processRunner.mockProcessesForExecutable['git-tag'] = [ - MockProcess( - stdout: 'plugin1-v0.0.2\n' - 'plugin2-v0.0.2\n') - ]; - - final List output = await runCapturingPrint(commandRunner, - ['publish', '--all-changed', '--base-sha=HEAD~']); - - expect( - output, - containsAllInOrder([ - contains('plugin1 0.0.2 has already been published'), - contains('SKIPPING: already published'), - contains('plugin2 0.0.2 has already been published'), - contains('SKIPPING: already published'), - ])); - - expect( - processRunner.recordedCalls - .map((ProcessCall call) => call.executable), - isNot(contains('git-push'))); - }); - - test( - 'Existing versions do not trigger release, but fail if the tags do not exist.', - () async { - mockHttpResponses['plugin1'] = { - 'name': 'plugin1', - 'versions': ['0.0.2'], - }; - - mockHttpResponses['plugin2'] = { - 'name': 'plugin2', - 'versions': ['0.0.2'], - }; - - // Non-federated - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir, version: '0.0.2'); - // federated - final RepositoryPackage plugin2 = createFakePlugin( - 'plugin2', packagesDir.childDirectory('plugin2'), - version: '0.0.2'); - - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess( - stdout: '${plugin1.pubspecFile.path}\n' - '${plugin2.pubspecFile.path}\n') - ]; - - Error? commandError; - final List output = await runCapturingPrint(commandRunner, - ['publish', '--all-changed', '--base-sha=HEAD~'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('plugin1 0.0.2 has already been published, ' - 'however the git release tag (plugin1-v0.0.2) was not found.'), - contains('plugin2 0.0.2 has already been published, ' - 'however the git release tag (plugin2-v0.0.2) was not found.'), - ])); - expect( - processRunner.recordedCalls - .map((ProcessCall call) => call.executable), - isNot(contains('git-push'))); - }); - - test('No version change does not release any plugins', () async { - // Non-federated - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - // federated - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess( - stdout: '${plugin1.libDirectory.childFile('plugin1.dart').path}\n' - '${plugin2.libDirectory.childFile('plugin2.dart').path}\n') - ]; - - final List output = await runCapturingPrint(commandRunner, - ['publish', '--all-changed', '--base-sha=HEAD~']); - - expect(output, containsAllInOrder(['Ran for 0 package(s)'])); - expect( - processRunner.recordedCalls - .map((ProcessCall call) => call.executable), - isNot(contains('git-push'))); - }); - - test('Do not release flutter_plugin_tools', () async { - mockHttpResponses['plugin1'] = { - 'name': 'flutter_plugin_tools', - 'versions': [], - }; - - final RepositoryPackage flutterPluginTools = - createFakePlugin('flutter_plugin_tools', packagesDir); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: flutterPluginTools.pubspecFile.path) - ]; - - final List output = await runCapturingPrint(commandRunner, - ['publish', '--all-changed', '--base-sha=HEAD~']); - - expect( - output, - containsAllInOrder([ - contains( - 'SKIPPING: publishing flutter_plugin_tools via the tool is not supported') - ])); - expect( - output.contains( - 'Running `pub publish ` in ${flutterPluginTools.path}...', - ), - isFalse); - expect( - processRunner.recordedCalls - .map((ProcessCall call) => call.executable), - isNot(contains('git-push'))); - }); - }); -} - -/// An extension of [RecordingProcessRunner] that stores 'flutter pub publish' -/// calls so that their input streams can be checked in tests. -class TestProcessRunner extends RecordingProcessRunner { - // Most recent returned publish process. - late MockProcess mockPublishProcess; - - @override - Future start(String executable, List args, - {Directory? workingDirectory}) async { - final io.Process process = - await super.start(executable, args, workingDirectory: workingDirectory); - if (executable == getFlutterCommand(const LocalPlatform()) && - args.isNotEmpty && - args[0] == 'pub' && - args[1] == 'publish') { - mockPublishProcess = process as MockProcess; - } - return process; - } -} - -class MockStdin extends Mock implements io.Stdin { - List> mockUserInputs = >[]; - final StreamController> _controller = StreamController>(); - String? readLineOutput; - - @override - Stream transform(StreamTransformer, S> streamTransformer) { - mockUserInputs.forEach(_addUserInputsToSteam); - return _controller.stream.transform(streamTransformer); - } - - @override - StreamSubscription> listen(void Function(List event)? onData, - {Function? onError, void Function()? onDone, bool? cancelOnError}) { - return _controller.stream.listen(onData, - onError: onError, onDone: onDone, cancelOnError: cancelOnError); - } - - @override - String? readLineSync( - {Encoding encoding = io.systemEncoding, - bool retainNewlines = false}) => - readLineOutput; - - void _addUserInputsToSteam(List input) => _controller.add(input); -} diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart deleted file mode 100644 index 2c254ca94984..000000000000 --- a/script/tool/test/pubspec_check_command_test.dart +++ /dev/null @@ -1,982 +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 'package:args/command_runner.dart'; -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/pubspec_check_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -/// Returns the top section of a pubspec.yaml for a package named [name], -/// for either a flutter/packages or flutter/plugins package depending on -/// the values of [isPlugin]. -/// -/// By default it will create a header that includes all of the expected -/// values, elements can be changed via arguments to create incorrect -/// entries. -/// -/// If [includeRepository] is true, by default the path in the link will -/// be "packages/[name]"; a different "packages"-relative path can be -/// provided with [repositoryPackagesDirRelativePath]. -String _headerSection( - String name, { - bool isPlugin = false, - bool includeRepository = true, - String repositoryBranch = 'main', - String? repositoryPackagesDirRelativePath, - bool includeHomepage = false, - bool includeIssueTracker = true, - bool publishable = true, - String? description, -}) { - final String repositoryPath = repositoryPackagesDirRelativePath ?? name; - final List repoLinkPathComponents = [ - 'flutter', - if (isPlugin) 'plugins' else 'packages', - 'tree', - repositoryBranch, - 'packages', - repositoryPath, - ]; - final String repoLink = - 'https://github.com/${repoLinkPathComponents.join('/')}'; - final String issueTrackerLink = 'https://github.com/flutter/flutter/issues?' - 'q=is%3Aissue+is%3Aopen+label%3A%22p%3A+$name%22'; - description ??= 'A test package for validating that the pubspec.yaml ' - 'follows repo best practices.'; - return ''' -name: $name -description: $description -${includeRepository ? 'repository: $repoLink' : ''} -${includeHomepage ? 'homepage: $repoLink' : ''} -${includeIssueTracker ? 'issue_tracker: $issueTrackerLink' : ''} -version: 1.0.0 -${publishable ? '' : "publish_to: 'none'"} -'''; -} - -String _environmentSection() { - return ''' -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" -'''; -} - -String _flutterSection({ - bool isPlugin = false, - String? implementedPackage, - Map> pluginPlatformDetails = - const >{}, -}) { - String pluginEntry = ''' - plugin: -${implementedPackage == null ? '' : ' implements: $implementedPackage'} - platforms: -'''; - - for (final MapEntry> platform - in pluginPlatformDetails.entries) { - pluginEntry += ''' - ${platform.key}: -'''; - for (final MapEntry detail in platform.value.entries) { - pluginEntry += ''' - ${detail.key}: ${detail.value} -'''; - } - } - - return ''' -flutter: -${isPlugin ? pluginEntry : ''} -'''; -} - -String _dependenciesSection() { - return ''' -dependencies: - flutter: - sdk: flutter -'''; -} - -String _devDependenciesSection() { - return ''' -dev_dependencies: - flutter_test: - sdk: flutter -'''; -} - -String _falseSecretsSection() { - return ''' -false_secrets: - - /lib/main.dart -'''; -} - -void main() { - group('test pubspec_check_command', () { - late CommandRunner runner; - late RecordingProcessRunner processRunner; - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = fileSystem.currentDirectory.childDirectory('packages'); - createPackagesDirectory(parentDir: packagesDir.parent); - processRunner = RecordingProcessRunner(); - final PubspecCheckCommand command = PubspecCheckCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'pubspec_check_command', 'Test for pubspec_check_command'); - runner.addCommand(command); - }); - - test('passes for a plugin following conventions', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true)} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -${_falseSecretsSection()} -'''); - - plugin.getExamples().first.pubspecFile.writeAsStringSync(''' -${_headerSection( - 'plugin_example', - publishable: false, - includeRepository: false, - includeIssueTracker: false, - )} -${_environmentSection()} -${_dependenciesSection()} -${_flutterSection()} -'''); - - final List output = await runCapturingPrint(runner, [ - 'pubspec-check', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin...'), - contains('Running for plugin/example...'), - contains('No issues found!'), - ]), - ); - }); - - test('passes for a Flutter package following conventions', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - package.pubspecFile.writeAsStringSync(''' -${_headerSection('a_package')} -${_environmentSection()} -${_dependenciesSection()} -${_devDependenciesSection()} -${_flutterSection()} -${_falseSecretsSection()} -'''); - - package.getExamples().first.pubspecFile.writeAsStringSync(''' -${_headerSection( - 'a_package', - publishable: false, - includeRepository: false, - includeIssueTracker: false, - )} -${_environmentSection()} -${_dependenciesSection()} -${_flutterSection()} -'''); - - final List output = await runCapturingPrint(runner, [ - 'pubspec-check', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for a_package...'), - contains('Running for a_package/example...'), - contains('No issues found!'), - ]), - ); - }); - - test('passes for a minimal package following conventions', () async { - final RepositoryPackage package = - createFakePackage('package', packagesDir, examples: []); - - package.pubspecFile.writeAsStringSync(''' -${_headerSection('package')} -${_environmentSection()} -${_dependenciesSection()} -'''); - - final List output = await runCapturingPrint(runner, [ - 'pubspec-check', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for package...'), - contains('No issues found!'), - ]), - ); - }); - - test('fails when homepage is included', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true, includeHomepage: true)} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Found a "homepage" entry; only "repository" should be used.'), - ]), - ); - }); - - test('fails when repository is missing', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true, includeRepository: false)} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Missing "repository"'), - ]), - ); - }); - - test('fails when homepage is given instead of repository', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true, includeHomepage: true, includeRepository: false)} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Found a "homepage" entry; only "repository" should be used.'), - ]), - ); - }); - - test('fails when repository package name is incorrect', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true, repositoryPackagesDirRelativePath: 'different_plugin')} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The "repository" link should end with the package path.'), - ]), - ); - }); - - test('fails when repository uses master instead of main', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true, repositoryBranch: 'master')} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The "repository" link should use "main", not "master".'), - ]), - ); - }); - - test('fails when issue tracker is missing', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true, includeIssueTracker: false)} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('A package should have an "issue_tracker" link'), - ]), - ); - }); - - test('fails when description is too short', () async { - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', packagesDir.childDirectory('a_plugin'), - examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true, description: 'Too short')} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('"description" is too short. pub.dev recommends package ' - 'descriptions of 60-180 characters.'), - ]), - ); - }); - - test( - 'allows short descriptions for non-app-facing parts of federated plugins', - () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true, description: 'Too short')} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('"description" is too short. pub.dev recommends package ' - 'descriptions of 60-180 characters.'), - ]), - ); - }); - - test('fails when description is too long', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - const String description = 'This description is too long. It just goes ' - 'on and on and on and on and on. pub.dev will down-score it because ' - 'there is just too much here. Someone shoul really cut this down to just ' - 'the core description so that search results are more useful and the ' - 'package does not lose pub points.'; - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true, description: description)} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('"description" is too long. pub.dev recommends package ' - 'descriptions of 60-180 characters.'), - ]), - ); - }); - - test('fails when environment section is out of order', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true)} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -${_environmentSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Major sections should follow standard repository ordering:'), - ]), - ); - }); - - test('fails when flutter section is out of order', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true)} -${_flutterSection(isPlugin: true)} -${_environmentSection()} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Major sections should follow standard repository ordering:'), - ]), - ); - }); - - test('fails when dependencies section is out of order', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true)} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_devDependenciesSection()} -${_dependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Major sections should follow standard repository ordering:'), - ]), - ); - }); - - test('fails when dev_dependencies section is out of order', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true)} -${_environmentSection()} -${_devDependenciesSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Major sections should follow standard repository ordering:'), - ]), - ); - }); - - test('fails when false_secrets section is out of order', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true)} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_falseSecretsSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Major sections should follow standard repository ordering:'), - ]), - ); - }); - - test('fails when an implemenation package is missing "implements"', - () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), - examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin_a_foo', isPlugin: true)} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Missing "implements: plugin_a" in "plugin" section.'), - ]), - ); - }); - - test('fails when an implemenation package has the wrong "implements"', - () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), - examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin_a_foo', isPlugin: true)} -${_environmentSection()} -${_flutterSection(isPlugin: true, implementedPackage: 'plugin_a_foo')} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Expecetd "implements: plugin_a"; ' - 'found "implements: plugin_a_foo".'), - ]), - ); - }); - - test('passes for a correct implemenation package', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), - examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection( - 'plugin_a_foo', - isPlugin: true, - repositoryPackagesDirRelativePath: 'plugin_a/plugin_a_foo', - )} -${_environmentSection()} -${_flutterSection(isPlugin: true, implementedPackage: 'plugin_a')} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - final List output = - await runCapturingPrint(runner, ['pubspec-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin_a_foo...'), - contains('No issues found!'), - ]), - ); - }); - - test('fails when a "default_package" looks incorrect', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin_a', packagesDir.childDirectory('plugin_a'), - examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection( - 'plugin_a', - isPlugin: true, - repositoryPackagesDirRelativePath: 'plugin_a/plugin_a', - )} -${_environmentSection()} -${_flutterSection( - isPlugin: true, - pluginPlatformDetails: >{ - 'android': {'default_package': 'plugin_b_android'} - }, - )} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - '"plugin_b_android" is not an expected implementation name for "plugin_a"'), - ]), - ); - }); - - test( - 'fails when a "default_package" does not have a corresponding dependency', - () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin_a', packagesDir.childDirectory('plugin_a'), - examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection( - 'plugin_a', - isPlugin: true, - repositoryPackagesDirRelativePath: 'plugin_a/plugin_a', - )} -${_environmentSection()} -${_flutterSection( - isPlugin: true, - pluginPlatformDetails: >{ - 'android': {'default_package': 'plugin_a_android'} - }, - )} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following default_packages are missing corresponding ' - 'dependencies:\n plugin_a_android'), - ]), - ); - }); - - test('passes for an app-facing package without "implements"', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin_a', packagesDir.childDirectory('plugin_a'), - examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection( - 'plugin_a', - isPlugin: true, - repositoryPackagesDirRelativePath: 'plugin_a/plugin_a', - )} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - final List output = - await runCapturingPrint(runner, ['pubspec-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin_a/plugin_a...'), - contains('No issues found!'), - ]), - ); - }); - - test('passes for a platform interface package without "implements"', - () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin_a_platform_interface', packagesDir.childDirectory('plugin_a'), - examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection( - 'plugin_a_platform_interface', - isPlugin: true, - repositoryPackagesDirRelativePath: - 'plugin_a/plugin_a_platform_interface', - )} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - final List output = - await runCapturingPrint(runner, ['pubspec-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin_a_platform_interface...'), - contains('No issues found!'), - ]), - ); - }); - - test('validates some properties even for unpublished packages', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), - examples: []); - - // Environment section is in the wrong location. - // Missing 'implements'. - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin_a_foo', isPlugin: true, publishable: false)} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -${_environmentSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Major sections should follow standard repository ordering:'), - contains('Missing "implements: plugin_a" in "plugin" section.'), - ]), - ); - }); - - test('ignores some checks for unpublished packages', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - // Missing metadata that is only useful for published packages, such as - // repository and issue tracker. - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection( - 'plugin', - isPlugin: true, - publishable: false, - includeRepository: false, - includeIssueTracker: false, - )} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - final List output = - await runCapturingPrint(runner, ['pubspec-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin...'), - contains('No issues found!'), - ]), - ); - }); - }); - - group('test pubspec_check_command on Windows', () { - late CommandRunner runner; - late RecordingProcessRunner processRunner; - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(style: FileSystemStyle.windows); - mockPlatform = MockPlatform(isWindows: true); - packagesDir = fileSystem.currentDirectory.childDirectory('packages'); - createPackagesDirectory(parentDir: packagesDir.parent); - processRunner = RecordingProcessRunner(); - final PubspecCheckCommand command = PubspecCheckCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'pubspec_check_command', 'Test for pubspec_check_command'); - runner.addCommand(command); - }); - - test('repository check works', () async { - final RepositoryPackage package = - createFakePackage('package', packagesDir, examples: []); - - package.pubspecFile.writeAsStringSync(''' -${_headerSection('package')} -${_environmentSection()} -${_dependenciesSection()} -'''); - - final List output = - await runCapturingPrint(runner, ['pubspec-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for package...'), - contains('No issues found!'), - ]), - ); - }); - }); -} diff --git a/script/tool/test/readme_check_command_test.dart b/script/tool/test/readme_check_command_test.dart deleted file mode 100644 index eb2b6c8e7512..000000000000 --- a/script/tool/test/readme_check_command_test.dart +++ /dev/null @@ -1,741 +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 'package:args/command_runner.dart'; -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/readme_check_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - late CommandRunner runner; - late RecordingProcessRunner processRunner; - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = fileSystem.currentDirectory.childDirectory('packages'); - createPackagesDirectory(parentDir: packagesDir.parent); - processRunner = RecordingProcessRunner(); - final ReadmeCheckCommand command = ReadmeCheckCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'readme_check_command', 'Test for readme_check_command'); - runner.addCommand(command); - }); - - test('prints paths of checked READMEs', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', packagesDir, - examples: ['example1', 'example2']); - for (final RepositoryPackage example in package.getExamples()) { - example.readmeFile.writeAsStringSync('A readme'); - } - getExampleDir(package).childFile('README.md').writeAsStringSync('A readme'); - - final List output = - await runCapturingPrint(runner, ['readme-check']); - - expect( - output, - containsAll([ - contains(' Checking README.md...'), - contains(' Checking example/README.md...'), - contains(' Checking example/example1/README.md...'), - contains(' Checking example/example2/README.md...'), - ]), - ); - }); - - test('fails when package README is missing', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - package.readmeFile.deleteSync(); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Missing README.md'), - ]), - ); - }); - - test('passes when example README is missing', () async { - createFakePackage('a_package', packagesDir); - - final List output = - await runCapturingPrint(runner, ['readme-check']); - - expect( - output, - containsAllInOrder([ - contains('No README for example'), - ]), - ); - }); - - test('does not inculde non-example subpackages', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - const String subpackageName = 'special_test'; - final RepositoryPackage miscSubpackage = - createFakePackage(subpackageName, package.directory); - miscSubpackage.readmeFile.delete(); - - final List output = - await runCapturingPrint(runner, ['readme-check']); - - expect(output, isNot(contains(subpackageName))); - }); - - test('fails when README still has plugin template boilerplate', () async { - final RepositoryPackage package = createFakePlugin('a_plugin', packagesDir); - package.readmeFile.writeAsStringSync(''' -## Getting Started - -This project is a starting point for a Flutter -[plug-in package](https://flutter.dev/developing-packages/), -a specialized package that includes platform-specific implementation code for -Android and/or iOS. - -For help getting started with Flutter development, view the -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The boilerplate section about getting started with Flutter ' - 'should not be left in.'), - contains('Contains template boilerplate'), - ]), - ); - }); - - test('fails when example README still has application template boilerplate', - () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - package.getExamples().first.readmeFile.writeAsStringSync(''' -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The boilerplate section about getting started with Flutter ' - 'should not be left in.'), - contains('Contains template boilerplate'), - ]), - ); - }); - - test( - 'fails when a plugin implementation package example README has the ' - 'template boilerplate', () async { - final RepositoryPackage package = createFakePlugin( - 'a_plugin_ios', packagesDir.childDirectory('a_plugin')); - package.getExamples().first.readmeFile.writeAsStringSync(''' -# a_plugin_ios_example - -Demonstrates how to use the a_plugin_ios plugin. -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The boilerplate should not be left in for a federated plugin ' - "implementation package's example."), - contains('Contains template boilerplate'), - ]), - ); - }); - - test( - 'allows the template boilerplate in the example README for packages ' - 'other than plugin implementation packages', () async { - final RepositoryPackage package = createFakePlugin( - 'a_plugin', - packagesDir.childDirectory('a_plugin'), - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - }, - ); - // Write a README with an OS support table so that the main README check - // passes. - package.readmeFile.writeAsStringSync(''' -# a_plugin - -| | Android | -|----------------|---------| -| **Support** | SDK 19+ | - -A great plugin. -'''); - package.getExamples().first.readmeFile.writeAsStringSync(''' -# a_plugin_example - -Demonstrates how to use the a_plugin plugin. -'''); - - final List output = - await runCapturingPrint(runner, ['readme-check']); - - expect( - output, - containsAll([ - contains(' Checking README.md...'), - contains(' Checking example/README.md...'), - ]), - ); - }); - - test( - 'fails when a plugin implementation package example README does not have ' - 'the repo-standard message', () async { - final RepositoryPackage package = createFakePlugin( - 'a_plugin_ios', packagesDir.childDirectory('a_plugin')); - package.getExamples().first.readmeFile.writeAsStringSync(''' -# a_plugin_ios_example - -Some random description. -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The example README for a platform implementation package ' - 'should warn readers about its intended use. Please copy the ' - 'example README from another implementation package in this ' - 'repository.'), - contains('Missing implementation package example warning'), - ]), - ); - }); - - test('passes for a plugin implementation package with the expected content', - () async { - final RepositoryPackage package = createFakePlugin( - 'a_plugin', - packagesDir.childDirectory('a_plugin'), - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - }, - ); - // Write a README with an OS support table so that the main README check - // passes. - package.readmeFile.writeAsStringSync(''' -# a_plugin - -| | Android | -|----------------|---------| -| **Support** | SDK 19+ | - -A great plugin. -'''); - package.getExamples().first.readmeFile.writeAsStringSync(''' -# 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. -'''); - - final List output = - await runCapturingPrint(runner, ['readme-check']); - - expect( - output, - containsAll([ - contains(' Checking README.md...'), - contains(' Checking example/README.md...'), - ]), - ); - }); - - test( - 'fails when multi-example top-level example directory README still has ' - 'application template boilerplate', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', packagesDir, - examples: ['example1', 'example2']); - package.directory - .childDirectory('example') - .childFile('README.md') - .writeAsStringSync(''' -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The boilerplate section about getting started with Flutter ' - 'should not be left in.'), - contains('Contains template boilerplate'), - ]), - ); - }); - - group('plugin OS support', () { - test( - 'does not check support table for anything other than app-facing plugin packages', - () async { - const String federatedPluginName = 'a_federated_plugin'; - final Directory federatedDir = - packagesDir.childDirectory(federatedPluginName); - // A non-plugin package. - createFakePackage('a_package', packagesDir); - // Non-app-facing parts of a federated plugin. - createFakePlugin( - '${federatedPluginName}_platform_interface', federatedDir); - createFakePlugin('${federatedPluginName}_android', federatedDir); - - final List output = await runCapturingPrint(runner, [ - 'readme-check', - ]); - - expect( - output, - containsAll([ - contains('Running for a_package...'), - contains('Running for a_federated_plugin_platform_interface...'), - contains('Running for a_federated_plugin_android...'), - contains('No issues found!'), - ]), - ); - }); - - test('fails when non-federated plugin is missing an OS support table', - () async { - createFakePlugin('a_plugin', packagesDir); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No OS support table found'), - ]), - ); - }); - - test( - 'fails when app-facing part of a federated plugin is missing an OS support table', - () async { - createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No OS support table found'), - ]), - ); - }); - - test('fails the OS support table is missing the header', () async { - final RepositoryPackage plugin = - createFakePlugin('a_plugin', packagesDir); - - plugin.readmeFile.writeAsStringSync(''' -A very useful plugin. - -| **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] | -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('OS support table does not have the expected header format'), - ]), - ); - }); - - test('fails if the OS support table is missing a supported OS', () async { - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - platformWeb: const PlatformDetails(PlatformSupport.inline), - }, - ); - - plugin.readmeFile.writeAsStringSync(''' -A very useful plugin. - -| | Android | iOS | -|----------------|---------|----------| -| **Support** | SDK 21+ | iOS 10+* | -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains(' OS support table does not match supported platforms:\n' - ' Actual: android, ios, web\n' - ' Documented: android, ios'), - contains('Incorrect OS support table'), - ]), - ); - }); - - test('fails if the OS support table lists an extra OS', () async { - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - plugin.readmeFile.writeAsStringSync(''' -A very useful plugin. - -| | Android | iOS | Web | -|----------------|---------|----------|------------------------| -| **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] | -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains(' OS support table does not match supported platforms:\n' - ' Actual: android, ios\n' - ' Documented: android, ios, web'), - contains('Incorrect OS support table'), - ]), - ); - }); - - test('fails if the OS support table has unexpected OS formatting', - () async { - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - platformMacOS: const PlatformDetails(PlatformSupport.inline), - platformWeb: const PlatformDetails(PlatformSupport.inline), - }, - ); - - plugin.readmeFile.writeAsStringSync(''' -A very useful plugin. - -| | android | ios | MacOS | web | -|----------------|---------|----------|-------|------------------------| -| **Support** | SDK 21+ | iOS 10+* | 10.11 | [See `camera_web `][1] | -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains(' Incorrect OS capitalization: android, ios, MacOS, web\n' - ' Please use standard capitalizations: Android, iOS, macOS, Web\n'), - contains('Incorrect OS support formatting'), - ]), - ); - }); - }); - - group('code blocks', () { - test('fails on missing info string', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - package.readmeFile.writeAsStringSync(''' -Example: - -``` -void main() { - // ... -} -``` -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Code block at line 3 is missing a language identifier.'), - contains('Missing language identifier for code block'), - ]), - ); - }); - - test('allows unknown info strings', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - package.readmeFile.writeAsStringSync(''' -Example: - -```someunknowninfotag -A B C -``` -'''); - - final List output = await runCapturingPrint(runner, [ - 'readme-check', - ]); - - expect( - output, - containsAll([ - contains('Running for a_package...'), - contains('No issues found!'), - ]), - ); - }); - - test('allows space around info strings', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - package.readmeFile.writeAsStringSync(''' -Example: - -``` dart -A B C -``` -'''); - - final List output = await runCapturingPrint(runner, [ - 'readme-check', - ]); - - expect( - output, - containsAll([ - contains('Running for a_package...'), - contains('No issues found!'), - ]), - ); - }); - - test('passes when excerpt requirement is met', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', - packagesDir, - extraFiles: [kReadmeExcerptConfigPath], - ); - - package.readmeFile.writeAsStringSync(''' -Example: - - -```dart -A B C -``` -'''); - - final List output = await runCapturingPrint( - runner, ['readme-check', '--require-excerpts']); - - expect( - output, - containsAll([ - contains('Running for a_package...'), - contains('No issues found!'), - ]), - ); - }); - - test('fails when excerpts are used but the package is not configured', - () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - package.readmeFile.writeAsStringSync(''' -Example: - - -```dart -A B C -``` -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check', '--require-excerpts'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('code-excerpt tag found, but the package is not configured ' - 'for excerpting. Follow the instructions at\n' - 'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages\n' - 'for setting up a build.excerpt.yaml file.'), - contains('Missing code-excerpt configuration'), - ]), - ); - }); - - test('fails on missing excerpt tag when requested', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - package.readmeFile.writeAsStringSync(''' -Example: - -```dart -A B C -``` -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check', '--require-excerpts'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Dart code block at line 3 is not managed by code-excerpt.'), - // Ensure that the failure message links to instructions. - contains( - 'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages'), - contains('Missing code-excerpt management for code block'), - ]), - ); - }); - }); -} diff --git a/script/tool/test/remove_dev_dependencies_test.dart b/script/tool/test/remove_dev_dependencies_test.dart deleted file mode 100644 index 776cbf197838..000000000000 --- a/script/tool/test/remove_dev_dependencies_test.dart +++ /dev/null @@ -1,102 +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 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/remove_dev_dependencies.dart'; -import 'package:test/test.dart'; - -import 'util.dart'; - -void main() { - late FileSystem fileSystem; - late Directory packagesDir; - late CommandRunner runner; - - setUp(() { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - - final RemoveDevDependenciesCommand command = RemoveDevDependenciesCommand( - packagesDir, - ); - runner = CommandRunner('trim_dev_dependencies_command', - 'Test for trim_dev_dependencies_command'); - runner.addCommand(command); - }); - - void addToPubspec(RepositoryPackage package, String addition) { - final String originalContent = package.pubspecFile.readAsStringSync(); - package.pubspecFile.writeAsStringSync(''' -$originalContent -$addition -'''); - } - - test('skips if nothing is removed', () async { - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - final List output = - await runCapturingPrint(runner, ['remove-dev-dependencies']); - - expect( - output, - containsAllInOrder([ - contains('SKIPPING: Nothing to remove.'), - ]), - ); - }); - - test('removes dev_dependencies', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - addToPubspec(package, ''' -dev_dependencies: - some_dependency: ^2.1.8 - another_dependency: ^1.0.0 -'''); - - final List output = - await runCapturingPrint(runner, ['remove-dev-dependencies']); - - expect( - output, - containsAllInOrder([ - contains('Removed dev_dependencies'), - ]), - ); - expect(package.pubspecFile.readAsStringSync(), - isNot(contains('some_dependency:'))); - expect(package.pubspecFile.readAsStringSync(), - isNot(contains('another_dependency:'))); - }); - - test('removes from examples', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - final RepositoryPackage example = package.getExamples().first; - addToPubspec(example, ''' -dev_dependencies: - some_dependency: ^2.1.8 - another_dependency: ^1.0.0 -'''); - - final List output = - await runCapturingPrint(runner, ['remove-dev-dependencies']); - - expect( - output, - containsAllInOrder([ - contains('Removed dev_dependencies'), - ]), - ); - expect(package.pubspecFile.readAsStringSync(), - isNot(contains('some_dependency:'))); - expect(package.pubspecFile.readAsStringSync(), - isNot(contains('another_dependency:'))); - }); -} diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart deleted file mode 100644 index 14a1e4a67c1f..000000000000 --- a/script/tool/test/test_command_test.dart +++ /dev/null @@ -1,268 +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:io' as io; - -import 'package:args/command_runner.dart'; -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:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('$TestCommand', () { - late FileSystem fileSystem; - late Platform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final TestCommand command = TestCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner('test_test', 'Test for $TestCommand'); - runner.addCommand(command); - }); - - 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']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['test', '--color'], plugin1.path), - ProcessCall(getFlutterCommand(mockPlatform), - const ['test', '--color'], plugin2.path), - ]), - ); - }); - - test('runs flutter test on Flutter package example tests', () async { - final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, - extraFiles: [ - 'test/empty_test.dart', - 'example/test/an_example_test.dart' - ]); - - await runCapturingPrint(runner, ['test']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['test', '--color'], plugin.path), - ProcessCall(getFlutterCommand(mockPlatform), - const ['test', '--color'], getExampleDir(plugin).path), - ]), - ); - }); - - test('fails when Flutter tests fail', () async { - createFakePlugin('plugin1', packagesDir, - extraFiles: ['test/empty_test.dart']); - createFakePlugin('plugin2', packagesDir, - extraFiles: ['test/empty_test.dart']); - - processRunner - .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = - [ - MockProcess(exitCode: 1), // plugin 1 test - MockProcess(), // plugin 2 test - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['test'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains(' plugin1'), - ])); - }); - - test('skips testing plugins without test directory', () async { - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir, - extraFiles: ['test/empty_test.dart']); - - await runCapturingPrint(runner, ['test']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['test', '--color'], plugin2.path), - ]), - ); - }); - - test('runs dart run test on non-Flutter packages', () async { - final RepositoryPackage plugin = createFakePlugin('a', packagesDir, - extraFiles: ['test/empty_test.dart']); - final RepositoryPackage package = createFakePackage('b', packagesDir, - extraFiles: ['test/empty_test.dart']); - - await runCapturingPrint( - runner, ['test', '--enable-experiment=exp1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const ['test', '--color', '--enable-experiment=exp1'], - plugin.path), - ProcessCall('dart', const ['pub', 'get'], package.path), - ProcessCall( - 'dart', - const ['run', '--enable-experiment=exp1', 'test'], - package.path), - ]), - ); - }); - - test('runs dart run test on non-Flutter package examples', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', packagesDir, extraFiles: [ - 'test/empty_test.dart', - 'example/test/an_example_test.dart' - ]); - - await runCapturingPrint(runner, ['test']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('dart', const ['pub', 'get'], package.path), - ProcessCall('dart', const ['run', 'test'], package.path), - ProcessCall('dart', const ['pub', 'get'], - getExampleDir(package).path), - ProcessCall('dart', const ['run', 'test'], - getExampleDir(package).path), - ]), - ); - }); - - test('fails when getting non-Flutter package dependencies fails', () async { - createFakePackage('a_package', packagesDir, - extraFiles: ['test/empty_test.dart']); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(exitCode: 1), // dart pub get - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['test'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to fetch dependencies'), - contains('The following packages had errors:'), - contains(' a_package'), - ])); - }); - - test('fails when non-Flutter tests fail', () async { - createFakePackage('a_package', packagesDir, - extraFiles: ['test/empty_test.dart']); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(), // dart pub get - MockProcess(exitCode: 1), // dart pub run test - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['test'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains(' a_package'), - ])); - }); - - test('runs on Chrome for web plugins', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: ['test/empty_test.dart'], - platformSupport: { - platformWeb: const PlatformDetails(PlatformSupport.inline), - }, - ); - - await runCapturingPrint(runner, ['test']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const ['test', '--color', '--platform=chrome'], - plugin.path), - ]), - ); - }); - - test('enable-experiment flag', () async { - final RepositoryPackage plugin = createFakePlugin('a', packagesDir, - extraFiles: ['test/empty_test.dart']); - final RepositoryPackage package = createFakePackage('b', packagesDir, - extraFiles: ['test/empty_test.dart']); - - await runCapturingPrint( - runner, ['test', '--enable-experiment=exp1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const ['test', '--color', '--enable-experiment=exp1'], - plugin.path), - ProcessCall('dart', const ['pub', 'get'], package.path), - ProcessCall( - 'dart', - const ['run', '--enable-experiment=exp1', 'test'], - package.path), - ]), - ); - }); - }); -} diff --git a/script/tool/test/update_excerpts_command_test.dart b/script/tool/test/update_excerpts_command_test.dart deleted file mode 100644 index 79f53d8779bb..000000000000 --- a/script/tool/test/update_excerpts_command_test.dart +++ /dev/null @@ -1,280 +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:io' as io; - -import 'package:args/command_runner.dart'; -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/update_excerpts_command.dart'; -import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; - -import 'common/package_command_test.mocks.dart'; -import 'mocks.dart'; -import 'util.dart'; - -void main() { - late FileSystem fileSystem; - late Directory packagesDir; - late RecordingProcessRunner processRunner; - late CommandRunner runner; - - setUp(() { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - final MockGitDir gitDir = MockGitDir(); - when(gitDir.path).thenReturn(packagesDir.parent.path); - processRunner = RecordingProcessRunner(); - final UpdateExcerptsCommand command = UpdateExcerptsCommand( - packagesDir, - processRunner: processRunner, - platform: MockPlatform(), - gitDir: gitDir, - ); - - runner = CommandRunner( - 'update_excerpts_command', 'Test for update_excerpts_command'); - runner.addCommand(command); - }); - - test('runs pub get before running scripts', () async { - final RepositoryPackage package = createFakePlugin('a_package', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); - final Directory example = getExampleDir(package); - - await runCapturingPrint(runner, ['update-excerpts']); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall('dart', const ['pub', 'get'], example.path), - ProcessCall( - 'dart', - const [ - 'run', - 'build_runner', - 'build', - '--config', - 'excerpt', - '--output', - 'excerpts', - '--delete-conflicting-outputs', - ], - example.path), - ])); - }); - - test('runs when config is present', () async { - final RepositoryPackage package = createFakePlugin('a_package', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); - 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', - ], - example.path), - ])); - - expect( - output, - containsAllInOrder([ - contains('Ran for 1 package(s)'), - ])); - }); - - test('skips when no config is present', () async { - createFakePlugin('a_package', packagesDir); - - final List output = - await runCapturingPrint(runner, ['update-excerpts']); - - expect(processRunner.recordedCalls, isEmpty); - - expect( - output, - containsAllInOrder([ - contains('Skipped 1 package(s)'), - ])); - }); - - test('restores pubspec even if running the script fails', () async { - final RepositoryPackage package = createFakePlugin('a_package', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(exitCode: 1), // dart pub get - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['update-excerpts'], errorHandler: (Error e) { - commandError = e; - }); - - // Check that it's definitely a failure in a step between making the changes - // and restoring the original. - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains('a_package:\n' - ' Unable to get script dependencies') - ])); - - final String examplePubspecContent = - package.getExamples().first.pubspecFile.readAsStringSync(); - expect(examplePubspecContent, isNot(contains('code_excerpter'))); - expect(examplePubspecContent, isNot(contains('code_excerpt_updater'))); - }); - - test('fails if pub get fails', () async { - createFakePlugin('a_package', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(exitCode: 1), // dart pub get - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['update-excerpts'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains('a_package:\n' - ' Unable to get script dependencies') - ])); - }); - - test('fails if extraction fails', () async { - createFakePlugin('a_package', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(), // dart pub get - MockProcess(exitCode: 1), // dart run build_runner ... - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['update-excerpts'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains('a_package:\n' - ' Unable to extract excerpts') - ])); - }); - - test('fails if injection fails', () async { - createFakePlugin('a_package', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(), // dart pub get - MockProcess(), // dart run build_runner ... - MockProcess(exitCode: 1), // dart run code_excerpt_updater ... - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['update-excerpts'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains('a_package:\n' - ' Unable to inject excerpts') - ])); - }); - - test('fails if files are changed with --fail-on-change', () async { - createFakePlugin('a_plugin', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); - - const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; - processRunner.mockProcessesForExecutable['git'] = [ - MockProcess(stdout: changedFilePath), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['update-excerpts', '--fail-on-change'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('README.md is out of sync with its source excerpts'), - ])); - }); - - test('fails if git ls-files fails', () async { - createFakePlugin('a_plugin', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); - - processRunner.mockProcessesForExecutable['git'] = [ - MockProcess(exitCode: 1) - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['update-excerpts', '--fail-on-change'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to determine local file state'), - ])); - }); -} diff --git a/script/tool/test/update_release_info_command_test.dart b/script/tool/test/update_release_info_command_test.dart deleted file mode 100644 index 8cd2e9591e70..000000000000 --- a/script/tool/test/update_release_info_command_test.dart +++ /dev/null @@ -1,645 +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:io' as io; - -import 'package:args/command_runner.dart'; -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/update_release_info_command.dart'; -import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; - -import 'common/package_command_test.mocks.dart'; -import 'mocks.dart'; -import 'util.dart'; - -void main() { - late FileSystem fileSystem; - late Directory packagesDir; - late MockGitDir gitDir; - late RecordingProcessRunner processRunner; - late CommandRunner runner; - - setUp(() { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - - gitDir = MockGitDir(); - when(gitDir.path).thenReturn(packagesDir.parent.path); - when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) - .thenAnswer((Invocation invocation) { - final List arguments = - invocation.positionalArguments[0]! as List; - // Route git calls through a process runner, to make mock output - // consistent with other processes. Attach the first argument to the - // command to make targeting the mock results easier. - final String gitCommand = arguments.removeAt(0); - return processRunner.run('git-$gitCommand', arguments); - }); - - final UpdateReleaseInfoCommand command = UpdateReleaseInfoCommand( - packagesDir, - gitDir: gitDir, - ); - runner = CommandRunner( - 'update_release_info_command', 'Test for update_release_info_command'); - runner.addCommand(command); - }); - - group('flags', () { - test('fails if --changelog is missing', () async { - Exception? commandError; - await runCapturingPrint(runner, [ - 'update-release-info', - '--version=next', - ], exceptionHandler: (Exception e) { - commandError = e; - }); - - expect(commandError, isA()); - }); - - test('fails if --changelog is blank', () async { - Exception? commandError; - await runCapturingPrint(runner, [ - 'update-release-info', - '--version=next', - '--changelog', - '', - ], exceptionHandler: (Exception e) { - commandError = e; - }); - - expect(commandError, isA()); - }); - - test('fails if --version is missing', () async { - Exception? commandError; - await runCapturingPrint( - runner, ['update-release-info', '--changelog', ''], - exceptionHandler: (Exception e) { - commandError = e; - }); - - expect(commandError, isA()); - }); - - test('fails if --version is an unknown value', () async { - Exception? commandError; - await runCapturingPrint(runner, [ - 'update-release-info', - '--version=foo', - '--changelog', - '', - ], exceptionHandler: (Exception e) { - commandError = e; - }); - - expect(commandError, isA()); - }); - }); - - group('changelog', () { - test('adds new NEXT section', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - const String originalChangelog = ''' -## 1.0.0 - -* Previous changes. -'''; - package.changelogFile.writeAsStringSync(originalChangelog); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=next', - '--changelog', - 'A change.' - ]); - - final String newChangelog = package.changelogFile.readAsStringSync(); - const String expectedChangeLog = ''' -## NEXT - -* A change. - -$originalChangelog'''; - - expect( - output, - containsAllInOrder([ - contains(' Added a NEXT section.'), - ]), - ); - expect(newChangelog, expectedChangeLog); - }); - - test('adds to existing NEXT section', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - const String originalChangelog = ''' -## NEXT - -* Already-pending changes. - -## 1.0.0 - -* Old changes. -'''; - package.changelogFile.writeAsStringSync(originalChangelog); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=next', - '--changelog', - 'A change.' - ]); - - final String newChangelog = package.changelogFile.readAsStringSync(); - const String expectedChangeLog = ''' -## NEXT - -* A change. -* Already-pending changes. - -## 1.0.0 - -* Old changes. -'''; - - expect(output, - containsAllInOrder([contains(' Updated NEXT section.')])); - expect(newChangelog, expectedChangeLog); - }); - - test('adds new version section', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - const String originalChangelog = ''' -## 1.0.0 - -* Previous changes. -'''; - package.changelogFile.writeAsStringSync(originalChangelog); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=bugfix', - '--changelog', - 'A change.' - ]); - - final String newChangelog = package.changelogFile.readAsStringSync(); - const String expectedChangeLog = ''' -## 1.0.1 - -* A change. - -$originalChangelog'''; - - expect( - output, - containsAllInOrder([ - contains(' Added a 1.0.1 section.'), - ]), - ); - expect(newChangelog, expectedChangeLog); - }); - - test('converts existing NEXT section to version section', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - const String originalChangelog = ''' -## NEXT - -* Already-pending changes. - -## 1.0.0 - -* Old changes. -'''; - package.changelogFile.writeAsStringSync(originalChangelog); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=bugfix', - '--changelog', - 'A change.' - ]); - - final String newChangelog = package.changelogFile.readAsStringSync(); - const String expectedChangeLog = ''' -## 1.0.1 - -* A change. -* Already-pending changes. - -## 1.0.0 - -* Old changes. -'''; - - expect(output, - containsAllInOrder([contains(' Updated NEXT section.')])); - expect(newChangelog, expectedChangeLog); - }); - - test('treats multiple lines as multiple list items', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - const String originalChangelog = ''' -## 1.0.0 - -* Previous changes. -'''; - package.changelogFile.writeAsStringSync(originalChangelog); - - await runCapturingPrint(runner, [ - 'update-release-info', - '--version=bugfix', - '--changelog', - 'First change.\nSecond change.' - ]); - - final String newChangelog = package.changelogFile.readAsStringSync(); - const String expectedChangeLog = ''' -## 1.0.1 - -* First change. -* Second change. - -$originalChangelog'''; - - expect(newChangelog, expectedChangeLog); - }); - - test('adds a period to any lines missing it, and removes whitespace', - () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - const String originalChangelog = ''' -## 1.0.0 - -* Previous changes. -'''; - package.changelogFile.writeAsStringSync(originalChangelog); - - await runCapturingPrint(runner, [ - 'update-release-info', - '--version=bugfix', - '--changelog', - 'First change \nSecond change' - ]); - - final String newChangelog = package.changelogFile.readAsStringSync(); - const String expectedChangeLog = ''' -## 1.0.1 - -* First change. -* Second change. - -$originalChangelog'''; - - expect(newChangelog, expectedChangeLog); - }); - - test('handles non-standard changelog format', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - const String originalChangelog = ''' -# 1.0.0 - -* A version with the wrong heading format. -'''; - package.changelogFile.writeAsStringSync(originalChangelog); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=next', - '--changelog', - 'A change.' - ]); - - final String newChangelog = package.changelogFile.readAsStringSync(); - const String expectedChangeLog = ''' -## NEXT - -* A change. - -$originalChangelog'''; - - expect(output, - containsAllInOrder([contains(' Added a NEXT section.')])); - expect(newChangelog, expectedChangeLog); - }); - - test('adds to existing NEXT section using - list style', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - const String originalChangelog = ''' -## NEXT - - - Already-pending changes. - -## 1.0.0 - - - Previous changes. -'''; - package.changelogFile.writeAsStringSync(originalChangelog); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=next', - '--changelog', - 'A change.' - ]); - - final String newChangelog = package.changelogFile.readAsStringSync(); - const String expectedChangeLog = ''' -## NEXT - - - A change. - - Already-pending changes. - -## 1.0.0 - - - Previous changes. -'''; - - expect(output, - containsAllInOrder([contains(' Updated NEXT section.')])); - expect(newChangelog, expectedChangeLog); - }); - - test('skips for "minimal" when there are no changes at all', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.1'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/different_package/test/plugin_test.dart -'''), - ]; - final String originalChangelog = package.changelogFile.readAsStringSync(); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=minimal', - '--changelog', - 'A change.', - ]); - - final String version = package.parsePubspec().version?.toString() ?? ''; - expect(version, '1.0.1'); - expect(package.changelogFile.readAsStringSync(), originalChangelog); - expect( - output, - containsAllInOrder([ - contains('No changes to package'), - contains('Skipped 1 package') - ])); - }); - - test('fails if CHANGELOG.md is missing', () async { - createFakePackage('a_package', packagesDir, includeCommonFiles: false); - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=minor', - '--changelog', - 'A change.', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect(output, - containsAllInOrder([contains(' Missing CHANGELOG.md.')])); - }); - - test('fails if CHANGELOG.md has unexpected NEXT block format', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - const String originalChangelog = ''' -## NEXT - -Some free-form text that isn't a list. - -## 1.0.0 - -- Previous changes. -'''; - package.changelogFile.writeAsStringSync(originalChangelog); - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=minor', - '--changelog', - 'A change.', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains(' Existing NEXT section has unrecognized format.') - ])); - }); - }); - - group('pubspec', () { - test('does not change for --next', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - await runCapturingPrint(runner, [ - 'update-release-info', - '--version=next', - '--changelog', - 'A change.' - ]); - - final String version = package.parsePubspec().version?.toString() ?? ''; - expect(version, '1.0.0'); - }); - - test('updates bugfix version for pre-1.0 without existing build number', - () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '0.1.0'); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=bugfix', - '--changelog', - 'A change.', - ]); - - final String version = package.parsePubspec().version?.toString() ?? ''; - expect(version, '0.1.0+1'); - expect( - output, - containsAllInOrder( - [contains(' Incremented version to 0.1.0+1')])); - }); - - test('updates bugfix version for pre-1.0 with existing build number', - () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '0.1.0+2'); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=bugfix', - '--changelog', - 'A change.', - ]); - - final String version = package.parsePubspec().version?.toString() ?? ''; - expect(version, '0.1.0+3'); - expect( - output, - containsAllInOrder( - [contains(' Incremented version to 0.1.0+3')])); - }); - - test('updates bugfix version for post-1.0', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.1'); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=bugfix', - '--changelog', - 'A change.', - ]); - - final String version = package.parsePubspec().version?.toString() ?? ''; - expect(version, '1.0.2'); - expect( - output, - containsAllInOrder( - [contains(' Incremented version to 1.0.2')])); - }); - - test('updates minor version for pre-1.0', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '0.1.0+2'); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=minor', - '--changelog', - 'A change.', - ]); - - final String version = package.parsePubspec().version?.toString() ?? ''; - expect(version, '0.1.1'); - expect( - output, - containsAllInOrder( - [contains(' Incremented version to 0.1.1')])); - }); - - test('updates minor version for post-1.0', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.1'); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=minor', - '--changelog', - 'A change.', - ]); - - final String version = package.parsePubspec().version?.toString() ?? ''; - expect(version, '1.1.0'); - expect( - output, - containsAllInOrder( - [contains(' Incremented version to 1.1.0')])); - }); - - test('updates bugfix version for "minimal" with publish-worthy changes', - () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.1'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/a_package/lib/plugin.dart -'''), - ]; - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=minimal', - '--changelog', - 'A change.', - ]); - - final String version = package.parsePubspec().version?.toString() ?? ''; - expect(version, '1.0.2'); - expect( - output, - containsAllInOrder( - [contains(' Incremented version to 1.0.2')])); - }); - - test('no version change for "minimal" with non-publish-worthy changes', - () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.1'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/a_package/test/plugin_test.dart -'''), - ]; - - await runCapturingPrint(runner, [ - 'update-release-info', - '--version=minimal', - '--changelog', - 'A change.', - ]); - - final String version = package.parsePubspec().version?.toString() ?? ''; - expect(version, '1.0.1'); - }); - - test('fails if there is no version in pubspec', () async { - createFakePackage('a_package', packagesDir, version: null); - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=minor', - '--changelog', - 'A change.', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder( - [contains('Could not determine current version.')])); - }); - }); -} diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart deleted file mode 100644 index a8cb527d9238..000000000000 --- a/script/tool/test/util.dart +++ /dev/null @@ -1,471 +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:async'; -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -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/file_utils.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; -import 'package:flutter_plugin_tools/src/common/process_runner.dart'; -import 'package:flutter_plugin_tools/src/common/repository_package.dart'; -import 'package:meta/meta.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:quiver/collection.dart'; - -import 'mocks.dart'; - -export 'package:flutter_plugin_tools/src/common/repository_package.dart'; - -/// The relative path from a package to the file that is used to enable -/// README excerpting for a package. -// This is a shared constant to ensure that both readme-check and -// update-excerpt are looking for the same file, so that readme-check can't -// get out of sync with what actually drives excerpting. -const String kReadmeExcerptConfigPath = 'example/build.excerpt.yaml'; - -const String _defaultDartConstraint = '>=2.14.0 <3.0.0'; -const String _defaultFlutterConstraint = '>=2.5.0'; - -/// Returns the exe name that command will use when running Flutter on -/// [platform]. -String getFlutterCommand(Platform platform) => - platform.isWindows ? 'flutter.bat' : 'flutter'; - -/// Creates a packages directory in the given location. -/// -/// If [parentDir] is set the packages directory will be created there, -/// otherwise [fileSystem] must be provided and it will be created an arbitrary -/// location in that filesystem. -Directory createPackagesDirectory( - {Directory? parentDir, FileSystem? fileSystem}) { - assert(parentDir != null || fileSystem != null, - 'One of parentDir or fileSystem must be provided'); - assert(fileSystem == null || fileSystem is MemoryFileSystem, - 'If using a real filesystem, parentDir must be provided'); - final Directory packagesDir = - (parentDir ?? fileSystem!.currentDirectory).childDirectory('packages'); - packagesDir.createSync(); - return packagesDir; -} - -/// Details for platform support in a plugin. -@immutable -class PlatformDetails { - const PlatformDetails( - this.type, { - this.hasNativeCode = true, - this.hasDartCode = false, - }); - - /// The type of support for the platform. - final PlatformSupport type; - - /// Whether or not the plugin includes native code. - /// - /// Ignored for web, which does not have native code. - final bool hasNativeCode; - - /// Whether or not the plugin includes Dart code. - /// - /// Ignored for web, which always has native code. - final bool hasDartCode; -} - -/// Returns the 'example' directory for [package]. -/// -/// This is deliberately not a method on [RepositoryPackage] since actual tool -/// code should essentially never need this, and instead be using -/// [RepositoryPackage.getExamples] to avoid assuming there's a single example -/// directory. However, needing to construct paths with the example directory -/// is very common in test code. -/// -/// This returns a Directory rather than a RepositoryPackage because there is no -/// guarantee that the returned directory is a package. -Directory getExampleDir(RepositoryPackage package) { - return package.directory.childDirectory('example'); -} - -/// Creates a plugin package with the given [name] in [packagesDirectory]. -/// -/// [platformSupport] is a map of platform string to the support details for -/// that platform. -/// -/// [extraFiles] is an optional list of plugin-relative paths, using Posix -/// separators, of extra files to create in the plugin. -RepositoryPackage createFakePlugin( - String name, - Directory parentDirectory, { - List examples = const ['example'], - List extraFiles = const [], - Map platformSupport = - const {}, - String? version = '0.0.1', - String flutterConstraint = _defaultFlutterConstraint, - String dartConstraint = _defaultDartConstraint, -}) { - final RepositoryPackage package = createFakePackage( - name, - parentDirectory, - isFlutter: true, - examples: examples, - extraFiles: extraFiles, - version: version, - flutterConstraint: flutterConstraint, - dartConstraint: dartConstraint, - ); - - createFakePubspec( - package, - name: name, - isPlugin: true, - platformSupport: platformSupport, - version: version, - flutterConstraint: flutterConstraint, - dartConstraint: dartConstraint, - ); - - return package; -} - -/// Creates a plugin package with the given [name] in [packagesDirectory]. -/// -/// [extraFiles] is an optional list of package-relative paths, using unix-style -/// separators, of extra files to create in the package. -/// -/// If [includeCommonFiles] is true, common but non-critical files like -/// CHANGELOG.md, README.md, and AUTHORS will be included. -/// -/// If non-null, [directoryName] will be used for the directory instead of -/// [name]. -RepositoryPackage createFakePackage( - String name, - Directory parentDirectory, { - List examples = const ['example'], - List extraFiles = const [], - bool isFlutter = false, - String? version = '0.0.1', - String flutterConstraint = _defaultFlutterConstraint, - String dartConstraint = _defaultDartConstraint, - bool includeCommonFiles = true, - String? directoryName, - String? publishTo, -}) { - final RepositoryPackage package = - RepositoryPackage(parentDirectory.childDirectory(directoryName ?? name)); - package.directory.createSync(recursive: true); - - package.libDirectory.createSync(); - createFakePubspec(package, - name: name, - isFlutter: isFlutter, - version: version, - flutterConstraint: flutterConstraint, - dartConstraint: dartConstraint); - if (includeCommonFiles) { - package.changelogFile.writeAsStringSync(''' -## $version - * Some changes. - '''); - package.readmeFile.writeAsStringSync('A very useful package'); - package.authorsFile.writeAsStringSync('Google Inc.'); - } - - if (examples.length == 1) { - createFakePackage('${name}_example', package.directory, - directoryName: examples.first, - examples: [], - includeCommonFiles: false, - isFlutter: isFlutter, - publishTo: 'none', - flutterConstraint: flutterConstraint, - dartConstraint: dartConstraint); - } else if (examples.isNotEmpty) { - final Directory examplesDirectory = getExampleDir(package)..createSync(); - for (final String exampleName in examples) { - createFakePackage(exampleName, examplesDirectory, - examples: [], - includeCommonFiles: false, - isFlutter: isFlutter, - publishTo: 'none', - flutterConstraint: flutterConstraint, - dartConstraint: dartConstraint); - } - } - - final p.Context posixContext = p.posix; - for (final String file in extraFiles) { - childFileWithSubcomponents(package.directory, posixContext.split(file)) - .createSync(recursive: true); - } - - return package; -} - -/// Creates a `pubspec.yaml` file for [package]. -/// -/// [platformSupport] is a map of platform string to the support details for -/// that platform. If empty, no `plugin` entry will be created unless `isPlugin` -/// is set to `true`. -void createFakePubspec( - RepositoryPackage package, { - String name = 'fake_package', - bool isFlutter = true, - bool isPlugin = false, - Map platformSupport = - const {}, - String? publishTo, - String? version, - String dartConstraint = _defaultDartConstraint, - String flutterConstraint = _defaultFlutterConstraint, -}) { - isPlugin |= platformSupport.isNotEmpty; - - String environmentSection = ''' -environment: - sdk: "$dartConstraint" -'''; - String dependenciesSection = ''' -dependencies: -'''; - String pluginSection = ''; - - // Add Flutter-specific entries if requested. - if (isFlutter) { - environmentSection += ''' - flutter: "$flutterConstraint" -'''; - dependenciesSection += ''' - flutter: - sdk: flutter -'''; - - if (isPlugin) { - pluginSection += ''' -flutter: - plugin: - platforms: -'''; - for (final MapEntry platform - in platformSupport.entries) { - pluginSection += - _pluginPlatformSection(platform.key, platform.value, name); - } - } - } - - // Default to a fake server to avoid ever accidentally publishing something - // from a test. Does not use 'none' since that changes the behavior of some - // commands. - final String publishToSection = - 'publish_to: ${publishTo ?? 'http://no_pub_server.com'}'; - - final String yaml = ''' -name: $name -${(version != null) ? 'version: $version' : ''} -$publishToSection - -$environmentSection - -$dependenciesSection - -$pluginSection -'''; - - package.pubspecFile.createSync(); - package.pubspecFile.writeAsStringSync(yaml); -} - -String _pluginPlatformSection( - String platform, PlatformDetails support, String packageName) { - String entry = ''; - // Build the main plugin entry. - if (support.type == PlatformSupport.federated) { - entry = ''' - $platform: - default_package: ${packageName}_$platform -'''; - } else { - final List lines = [ - ' $platform:', - ]; - switch (platform) { - case platformAndroid: - lines.add(' package: io.flutter.plugins.fake'); - continue nativeByDefault; - nativeByDefault: - case platformIOS: - case platformLinux: - case platformMacOS: - case platformWindows: - if (support.hasNativeCode) { - final String className = - platform == platformIOS ? 'FLTFakePlugin' : 'FakePlugin'; - lines.add(' pluginClass: $className'); - } - if (support.hasDartCode) { - lines.add(' dartPluginClass: FakeDartPlugin'); - } - break; - case platformWeb: - lines.addAll([ - ' pluginClass: FakePlugin', - ' fileName: ${packageName}_web.dart', - ]); - break; - default: - assert(false, 'Unrecognized platform: $platform'); - break; - } - entry = '${lines.join('\n')}\n'; - } - - return entry; -} - -/// Run the command [runner] with the given [args] and return -/// what was printed. -/// A custom [errorHandler] can be used to handle the runner error as desired without throwing. -Future> runCapturingPrint( - CommandRunner runner, - List args, { - Function(Error error)? errorHandler, - Function(Exception error)? exceptionHandler, -}) async { - final List prints = []; - final ZoneSpecification spec = ZoneSpecification( - print: (_, __, ___, String message) { - prints.add(message); - }, - ); - try { - await Zone.current - .fork(specification: spec) - .run>(() => runner.run(args)); - } on Error catch (e) { - if (errorHandler == null) { - rethrow; - } - errorHandler(e); - } on Exception catch (e) { - if (exceptionHandler == null) { - rethrow; - } - exceptionHandler(e); - } - - return prints; -} - -/// A mock [ProcessRunner] which records process calls. -class RecordingProcessRunner extends ProcessRunner { - final List recordedCalls = []; - - /// Maps an executable to a list of processes that should be used for each - /// successive call to it via [run], [runAndStream], or [start]. - final Map> mockProcessesForExecutable = - >{}; - - @override - Future runAndStream( - String executable, - List args, { - Directory? workingDir, - bool exitOnError = false, - }) async { - recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); - final io.Process? processToReturn = _getProcessToReturn(executable); - final int exitCode = - processToReturn == null ? 0 : await processToReturn.exitCode; - if (exitOnError && (exitCode != 0)) { - throw io.ProcessException(executable, args); - } - return Future.value(exitCode); - } - - /// Returns [io.ProcessResult] created from [mockProcessesForExecutable]. - @override - Future run( - String executable, - List args, { - Directory? workingDir, - bool exitOnError = false, - bool logOnError = false, - Encoding stdoutEncoding = io.systemEncoding, - Encoding stderrEncoding = io.systemEncoding, - }) async { - recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); - - final io.Process? process = _getProcessToReturn(executable); - final List? processStdout = - await process?.stdout.transform(stdoutEncoding.decoder).toList(); - final String stdout = processStdout?.join() ?? ''; - final List? processStderr = - await process?.stderr.transform(stderrEncoding.decoder).toList(); - final String stderr = processStderr?.join() ?? ''; - - final io.ProcessResult result = process == null - ? io.ProcessResult(1, 0, '', '') - : io.ProcessResult(process.pid, await process.exitCode, stdout, stderr); - - if (exitOnError && (result.exitCode != 0)) { - throw io.ProcessException(executable, args); - } - - return Future.value(result); - } - - @override - Future start(String executable, List args, - {Directory? workingDirectory}) async { - recordedCalls.add(ProcessCall(executable, args, workingDirectory?.path)); - return Future.value( - _getProcessToReturn(executable) ?? MockProcess()); - } - - io.Process? _getProcessToReturn(String executable) { - final List? processes = mockProcessesForExecutable[executable]; - if (processes != null && processes.isNotEmpty) { - return processes.removeAt(0); - } - return null; - } -} - -/// A recorded process call. -@immutable -class ProcessCall { - const ProcessCall(this.executable, this.args, this.workingDir); - - /// The executable that was called. - final String executable; - - /// The arguments passed to [executable] in the call. - final List args; - - /// The working directory this process was called from. - final String? workingDir; - - @override - bool operator ==(dynamic other) { - return other is ProcessCall && - executable == other.executable && - listsEqual(args, other.args) && - workingDir == other.workingDir; - } - - @override - int get hashCode => Object.hash(executable, args, workingDir); - - @override - String toString() { - final List command = [executable, ...args]; - return '"${command.join(' ')}" in $workingDir'; - } -} diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart deleted file mode 100644 index d485d81ceaf2..000000000000 --- a/script/tool/test/version_check_command_test.dart +++ /dev/null @@ -1,1468 +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:convert'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -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/version_check_command.dart'; -import 'package:http/http.dart' as http; -import 'package:http/testing.dart'; -import 'package:mockito/mockito.dart'; -import 'package:pub_semver/pub_semver.dart'; -import 'package:test/test.dart'; - -import 'common/package_command_test.mocks.dart'; -import 'mocks.dart'; -import 'util.dart'; - -void testAllowedVersion( - String mainVersion, - String headVersion, { - bool allowed = true, - NextVersionType? nextVersionType, -}) { - final Version main = Version.parse(mainVersion); - final Version head = Version.parse(headVersion); - final Map allowedVersions = - getAllowedNextVersions(main, newVersion: head); - if (allowed) { - expect(allowedVersions, contains(head)); - if (nextVersionType != null) { - expect(allowedVersions[head], equals(nextVersionType)); - } - } else { - expect(allowedVersions, isNot(contains(head))); - } -} - -class MockProcessResult extends Mock implements io.ProcessResult {} - -void main() { - const String indentation = ' '; - group('VersionCheckCommand', () { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - late MockGitDir gitDir; - // Ignored if mockHttpResponse is set. - int mockHttpStatus; - Map? mockHttpResponse; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - - gitDir = MockGitDir(); - when(gitDir.path).thenReturn(packagesDir.parent.path); - when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) - .thenAnswer((Invocation invocation) { - final List arguments = - invocation.positionalArguments[0]! as List; - // Route git calls through the process runner, to make mock output - // consistent with other processes. Attach the first argument to the - // command to make targeting the mock results easier. - final String gitCommand = arguments.removeAt(0); - return processRunner.run('git-$gitCommand', arguments); - }); - - // Default to simulating the plugin never having been published. - mockHttpStatus = 404; - mockHttpResponse = null; - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response(json.encode(mockHttpResponse), - mockHttpResponse == null ? mockHttpStatus : 200); - }); - - processRunner = RecordingProcessRunner(); - final VersionCheckCommand command = VersionCheckCommand(packagesDir, - processRunner: processRunner, - platform: mockPlatform, - gitDir: gitDir, - httpClient: mockClient); - - runner = CommandRunner( - 'version_check_command', 'Test for $VersionCheckCommand'); - runner.addCommand(command); - }); - - test('allows valid version', () async { - createFakePlugin('plugin', packagesDir, version: '2.0.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('1.0.0 -> 2.0.0'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall( - 'git-show', ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - - test('denies invalid version', () async { - createFakePlugin('plugin', packagesDir, version: '0.2.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 0.0.1'), - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Incorrectly updated version.'), - ])); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall( - 'git-show', ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - - test('uses merge-base without explicit base-sha', () async { - createFakePlugin('plugin', packagesDir, version: '2.0.0'); - processRunner.mockProcessesForExecutable['git-merge-base'] = [ - MockProcess(stdout: 'abc123'), - MockProcess(stdout: 'abc123'), - ]; - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - final List output = - await runCapturingPrint(runner, ['version-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('1.0.0 -> 2.0.0'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall('git-merge-base', - ['--fork-point', 'FETCH_HEAD', 'HEAD'], null), - ProcessCall('git-show', - ['abc123:packages/plugin/pubspec.yaml'], null), - ])); - }); - - test('allows valid version for new package.', () async { - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - final List output = - await runCapturingPrint(runner, ['version-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Unable to find previous version at git base.'), - ]), - ); - }); - - test('allows likely reverts.', () async { - createFakePlugin('plugin', packagesDir, version: '0.6.1'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 0.6.2'), - ]; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - - expect( - output, - containsAllInOrder([ - contains('New version is lower than previous version. ' - 'This is assumed to be a revert.'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall( - 'git-show', ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - - test('denies lower version that could not be a simple revert', () async { - createFakePlugin('plugin', packagesDir, version: '0.5.1'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 0.6.2'), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Incorrectly updated version.'), - ])); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall( - 'git-show', ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - - test('allows minor changes to platform interfaces', () async { - createFakePlugin('plugin_platform_interface', packagesDir, - version: '1.1.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('1.0.0 -> 1.1.0'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall( - 'git-show', - [ - 'main:packages/plugin_platform_interface/pubspec.yaml' - ], - null) - ])); - }); - - test('disallows breaking changes to platform interfaces by default', - () async { - createFakePlugin('plugin_platform_interface', packagesDir, - version: '2.0.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - ' Breaking changes to platform interfaces are not allowed ' - 'without explicit justification.\n' - ' See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages ' - 'for more information.'), - ])); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall( - 'git-show', - [ - 'main:packages/plugin_platform_interface/pubspec.yaml' - ], - null) - ])); - }); - - test('allows breaking changes to platform interfaces with override label', - () async { - createFakePlugin('plugin_platform_interface', packagesDir, - version: '2.0.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=main', - '--pr-labels=some label,override: allow breaking change,another-label' - ]); - - expect( - output, - containsAllInOrder([ - contains('Allowing breaking change to plugin_platform_interface ' - 'due to the "override: allow breaking change" label.'), - contains('Ran for 1 package(s) (1 with warnings)'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall( - 'git-show', - [ - 'main:packages/plugin_platform_interface/pubspec.yaml' - ], - null) - ])); - }); - - test('allows breaking changes to platform interfaces with bypass flag', - () async { - createFakePlugin('plugin_platform_interface', packagesDir, - version: '2.0.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=main', - '--ignore-platform-interface-breaks' - ]); - - expect( - output, - containsAllInOrder([ - contains('Allowing breaking change to plugin_platform_interface due ' - 'to --ignore-platform-interface-breaks'), - contains('Ran for 1 package(s) (1 with warnings)'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall( - 'git-show', - [ - 'main:packages/plugin_platform_interface/pubspec.yaml' - ], - null) - ])); - }); - - test('Allow empty lines in front of the first version in CHANGELOG', - () async { - const String version = '1.0.1'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: version); - const String changelog = ''' - -## $version -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - ]), - ); - }); - - test('Throws if versions in changelog and pubspec do not match', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.1'); - const String changelog = ''' -## 1.0.2 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - Error? commandError; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main', '--against-pub'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Versions in CHANGELOG.md and pubspec.yaml do not match.'), - ]), - ); - }); - - test('Success if CHANGELOG and pubspec versions match', () async { - const String version = '1.0.1'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: version); - - const String changelog = ''' -## $version -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - ]), - ); - }); - - test( - 'Fail if pubspec version only matches an older version listed in CHANGELOG', - () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.1 -* Some changes. -## 1.0.0 -* Some other changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - bool hasError = false; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main', '--against-pub'], - errorHandler: (Error e) { - expect(e, isA()); - hasError = true; - }); - expect(hasError, isTrue); - - expect( - output, - containsAllInOrder([ - contains('Versions in CHANGELOG.md and pubspec.yaml do not match.'), - ]), - ); - }); - - test('Allow NEXT as a placeholder for gathering CHANGELOG entries', - () async { - const String version = '1.0.0'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: version); - - const String changelog = ''' -## NEXT -* Some changes that won't be published until the next time there's a release. -## $version -* Some other changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Found NEXT; validating next version in the CHANGELOG.'), - ]), - ); - }); - - test('Fail if NEXT appears after a version', () async { - const String version = '1.0.1'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: version); - - const String changelog = ''' -## $version -* Some changes. -## NEXT -* Some changes that should have been folded in 1.0.1. -## 1.0.0 -* Some other changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - bool hasError = false; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main', '--against-pub'], - errorHandler: (Error e) { - expect(e, isA()); - hasError = true; - }); - expect(hasError, isTrue); - - expect( - output, - containsAllInOrder([ - contains('When bumping the version for release, the NEXT section ' - "should be incorporated into the new version's release notes.") - ]), - ); - }); - - test('Fail if NEXT is left in the CHANGELOG when adding a version bump', - () async { - const String version = '1.0.1'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: version); - - const String changelog = ''' -## NEXT -* Some changes that should have been folded in 1.0.1. -## $version -* Some changes. -## 1.0.0 -* Some other changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - - bool hasError = false; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main', '--against-pub'], - errorHandler: (Error e) { - expect(e, isA()); - hasError = true; - }); - expect(hasError, isTrue); - - expect( - output, - containsAllInOrder([ - contains('When bumping the version for release, the NEXT section ' - "should be incorporated into the new version's release notes."), - contains('plugin:\n' - ' CHANGELOG.md failed validation.'), - ]), - ); - }); - - test('fails if the version increases without replacing NEXT', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.1'); - - const String changelog = ''' -## NEXT -* Some changes that should be listed as part of 1.0.1. -## 1.0.0 -* Some other changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - - bool hasError = false; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main', '--against-pub'], - errorHandler: (Error e) { - expect(e, isA()); - hasError = true; - }); - expect(hasError, isTrue); - - expect( - output, - containsAllInOrder([ - contains('When bumping the version for release, the NEXT section ' - "should be incorporated into the new version's release notes.") - ]), - ); - }); - - test('allows NEXT for a revert', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## NEXT -* Some changes that should be listed as part of 1.0.1. -## 1.0.0 -* Some other changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.1'), - ]; - - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - expect( - output, - containsAllInOrder([ - contains('New version is lower than previous version. ' - 'This is assumed to be a revert.'), - ]), - ); - }); - - test( - 'fails gracefully if the version headers are not found due to using the wrong style', - () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## NEXT -* Some changes for a later release. -# 1.0.0 -* Some other changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=main', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to find a version in CHANGELOG.md'), - contains('The current version should be on a line starting with ' - '"## ", either on the first non-empty line or after a "## NEXT" ' - 'section.'), - ]), - ); - }); - - test('fails gracefully if the version is unparseable', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## Alpha -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=main', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('"Alpha" could not be parsed as a version.'), - ]), - ); - }); - - group('missing change detection', () { - Future> runWithMissingChangeDetection(List extraArgs, - {void Function(Error error)? errorHandler}) async { - return runCapturingPrint( - runner, - [ - 'version-check', - '--base-sha=main', - '--check-for-missing-changes', - ...extraArgs, - ], - errorHandler: errorHandler); - } - - test('passes for unchanged packages', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''), - ]; - - final List output = - await runWithMissingChangeDetection([]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - ]), - ); - }); - - test( - 'fails if a version change is missing from a change that does not ' - 'pass the exemption check', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/lib/plugin.dart -'''), - ]; - - Error? commandError; - final List output = await runWithMissingChangeDetection( - [], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No version change found'), - contains('plugin:\n' - ' Missing version change'), - ]), - ); - }); - - test('passes version change requirement when version changes', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.1'); - - const String changelog = ''' -## 1.0.1 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/lib/plugin.dart -packages/plugin/CHANGELOG.md -packages/plugin/pubspec.yaml -'''), - ]; - - final List output = - await runWithMissingChangeDetection([]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - ]), - ); - }); - - test('version change check ignores files outside the package', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin_a/lib/plugin.dart -tool/plugin/lib/plugin.dart -'''), - ]; - - final List output = - await runWithMissingChangeDetection([]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - ]), - ); - }); - - test('allows missing version change for exempt changes', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/example/android/lint-baseline.xml -packages/plugin/example/android/src/androidTest/foo/bar/FooTest.java -packages/plugin/example/ios/RunnerTests/Foo.m -packages/plugin/example/ios/RunnerUITests/info.plist -packages/plugin/CHANGELOG.md -'''), - ]; - - final List output = - await runWithMissingChangeDetection([]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - ]), - ); - }); - - test('allows missing version change with override label', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/lib/plugin.dart -packages/plugin/CHANGELOG.md -packages/plugin/pubspec.yaml -'''), - ]; - - final List output = - await runWithMissingChangeDetection([ - '--pr-labels=some label,override: no versioning needed,another-label' - ]); - - expect( - output, - containsAllInOrder([ - contains('Ignoring lack of version change due to the ' - '"override: no versioning needed" label.'), - ]), - ); - }); - - test('fails if a CHANGELOG change is missing', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/example/lib/foo.dart -'''), - ]; - - Error? commandError; - final List output = await runWithMissingChangeDetection( - [], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No CHANGELOG change found'), - contains('plugin:\n' - ' Missing CHANGELOG change'), - ]), - ); - }); - - test('passes CHANGELOG check when the CHANGELOG is changed', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/example/lib/foo.dart -packages/plugin/CHANGELOG.md -'''), - ]; - - final List output = - await runWithMissingChangeDetection([]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - ]), - ); - }); - - test('fails CHANGELOG check if only another package CHANGELOG chages', - () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/example/lib/foo.dart -packages/another_plugin/CHANGELOG.md -'''), - ]; - - Error? commandError; - final List output = await runWithMissingChangeDetection( - [], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No CHANGELOG change found'), - ]), - ); - }); - - test('allows missing CHANGELOG change with justification', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/example/lib/foo.dart -'''), - ]; - - final List output = - await runWithMissingChangeDetection([ - '--pr-labels=some label,override: no changelog needed,another-label' - ]); - - expect( - output, - containsAllInOrder([ - contains('Ignoring lack of CHANGELOG update due to the ' - '"override: no changelog needed" label.'), - ]), - ); - }); - - // This test ensures that Dependabot Gradle changes to test-only files - // aren't flagged by the version check. - test( - 'allows missing CHANGELOG and version change for test-only Gradle changes', - () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - // File list. - MockProcess(stdout: ''' -packages/plugin/android/build.gradle -'''), - // build.gradle diff - MockProcess(stdout: ''' -- androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' -- testImplementation 'junit:junit:4.10.0' -+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' -+ testImplementation 'junit:junit:4.13.2' -'''), - ]; - - final List output = - await runWithMissingChangeDetection([]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - ]), - ); - }); - - test('allows missing CHANGELOG and version change for dev-only changes', - () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - // File list. - MockProcess(stdout: ''' -packages/plugin/tool/run_tests.dart -packages/plugin/run_tests.sh -'''), - ]; - - final List output = - await runWithMissingChangeDetection([]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - ]), - ); - }); - }); - - test('allows valid against pub', () async { - mockHttpResponse = { - 'name': 'some_package', - 'versions': [ - '0.0.1', - '0.0.2', - '1.0.0', - ], - }; - - createFakePlugin('plugin', packagesDir, version: '2.0.0'); - final List output = await runCapturingPrint(runner, - ['version-check', '--base-sha=main', '--against-pub']); - - expect( - output, - containsAllInOrder([ - contains('plugin: Current largest version on pub: 1.0.0'), - ]), - ); - }); - - test('denies invalid against pub', () async { - mockHttpResponse = { - 'name': 'some_package', - 'versions': [ - '0.0.1', - '0.0.2', - ], - }; - - createFakePlugin('plugin', packagesDir, version: '2.0.0'); - - bool hasError = false; - final List result = await runCapturingPrint( - runner, ['version-check', '--base-sha=main', '--against-pub'], - errorHandler: (Error e) { - expect(e, isA()); - hasError = true; - }); - expect(hasError, isTrue); - - expect( - result, - containsAllInOrder([ - contains(''' -${indentation}Incorrectly updated version. -${indentation}HEAD: 2.0.0, pub: 0.0.2. -${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: NextVersionType.MINOR, 0.0.3: NextVersionType.PATCH}''') - ]), - ); - }); - - test( - 'throw and print error message if http request failed when checking against pub', - () async { - mockHttpStatus = 400; - - createFakePlugin('plugin', packagesDir, version: '2.0.0'); - bool hasError = false; - final List result = await runCapturingPrint( - runner, ['version-check', '--base-sha=main', '--against-pub'], - errorHandler: (Error e) { - expect(e, isA()); - hasError = true; - }); - expect(hasError, isTrue); - - expect( - result, - containsAllInOrder([ - contains(''' -${indentation}Error fetching version on pub for plugin. -${indentation}HTTP Status 400 -${indentation}HTTP response: null -''') - ]), - ); - }); - - test('when checking against pub, allow any version if http status is 404.', - () async { - mockHttpStatus = 404; - - createFakePlugin('plugin', packagesDir, version: '2.0.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - final List result = await runCapturingPrint(runner, - ['version-check', '--base-sha=main', '--against-pub']); - - expect( - result, - containsAllInOrder([ - contains('Unable to find previous version on pub server.'), - ]), - ); - }); - - group('prelease versions', () { - test( - 'allow an otherwise-valid transition that also adds a pre-release component', - () async { - createFakePlugin('plugin', packagesDir, version: '2.0.0-dev'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('1.0.0 -> 2.0.0-dev'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall('git-show', - ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - - test('allow releasing a pre-release', () async { - createFakePlugin('plugin', packagesDir, version: '1.2.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.2.0-dev'), - ]; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('1.2.0-dev -> 1.2.0'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall('git-show', - ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - - // Allow abandoning a pre-release version in favor of a different version - // change type. - test( - 'allow an otherwise-valid transition that also removes a pre-release component', - () async { - createFakePlugin('plugin', packagesDir, version: '2.0.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.2.0-dev'), - ]; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('1.2.0-dev -> 2.0.0'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall('git-show', - ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - - test('allow changing only the pre-release version', () async { - createFakePlugin('plugin', packagesDir, version: '1.2.0-dev.2'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.2.0-dev.1'), - ]; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('1.2.0-dev.1 -> 1.2.0-dev.2'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall('git-show', - ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - - test('denies invalid version change that also adds a pre-release', - () async { - createFakePlugin('plugin', packagesDir, version: '0.2.0-dev'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 0.0.1'), - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Incorrectly updated version.'), - ])); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall('git-show', - ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - - test('denies invalid version change that also removes a pre-release', - () async { - createFakePlugin('plugin', packagesDir, version: '0.2.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 0.0.1-dev'), - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Incorrectly updated version.'), - ])); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall('git-show', - ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - - test('denies invalid version change between pre-releases', () async { - createFakePlugin('plugin', packagesDir, version: '0.2.0-dev'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 0.0.1-dev'), - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Incorrectly updated version.'), - ])); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall('git-show', - ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - }); - }); - - group('Pre 1.0', () { - test('nextVersion allows patch version', () { - testAllowedVersion('0.12.0', '0.12.0+1', - nextVersionType: NextVersionType.PATCH); - testAllowedVersion('0.12.0+4', '0.12.0+5', - nextVersionType: NextVersionType.PATCH); - }); - - test('nextVersion does not allow jumping patch', () { - testAllowedVersion('0.12.0', '0.12.0+2', allowed: false); - testAllowedVersion('0.12.0+2', '0.12.0+4', allowed: false); - }); - - test('nextVersion does not allow going back', () { - testAllowedVersion('0.12.0', '0.11.0', allowed: false); - testAllowedVersion('0.12.0+2', '0.12.0+1', allowed: false); - testAllowedVersion('0.12.0+1', '0.12.0', allowed: false); - }); - - test('nextVersion allows minor version', () { - testAllowedVersion('0.12.0', '0.12.1', - nextVersionType: NextVersionType.MINOR); - testAllowedVersion('0.12.0+4', '0.12.1', - nextVersionType: NextVersionType.MINOR); - }); - - test('nextVersion does not allow jumping minor', () { - testAllowedVersion('0.12.0', '0.12.2', allowed: false); - testAllowedVersion('0.12.0+2', '0.12.3', allowed: false); - }); - }); - - group('Releasing 1.0', () { - test('nextVersion allows releasing 1.0', () { - testAllowedVersion('0.12.0', '1.0.0', - nextVersionType: NextVersionType.BREAKING_MAJOR); - testAllowedVersion('0.12.0+4', '1.0.0', - nextVersionType: NextVersionType.BREAKING_MAJOR); - }); - - test('nextVersion does not allow jumping major', () { - testAllowedVersion('0.12.0', '2.0.0', allowed: false); - testAllowedVersion('0.12.0+4', '2.0.0', allowed: false); - }); - - test('nextVersion does not allow un-releasing', () { - testAllowedVersion('1.0.0', '0.12.0+4', allowed: false); - testAllowedVersion('1.0.0', '0.12.0', allowed: false); - }); - }); - - group('Post 1.0', () { - test('nextVersion allows patch jumps', () { - testAllowedVersion('1.0.1', '1.0.2', - nextVersionType: NextVersionType.PATCH); - testAllowedVersion('1.0.0', '1.0.1', - nextVersionType: NextVersionType.PATCH); - }); - - test('nextVersion does not allow build jumps', () { - testAllowedVersion('1.0.1', '1.0.1+1', allowed: false); - testAllowedVersion('1.0.0+5', '1.0.0+6', allowed: false); - }); - - test('nextVersion does not allow skipping patches', () { - testAllowedVersion('1.0.1', '1.0.3', allowed: false); - testAllowedVersion('1.0.0', '1.0.6', allowed: false); - }); - - test('nextVersion allows minor version jumps', () { - testAllowedVersion('1.0.1', '1.1.0', - nextVersionType: NextVersionType.MINOR); - testAllowedVersion('1.0.0', '1.1.0', - nextVersionType: NextVersionType.MINOR); - }); - - test('nextVersion does not allow skipping minor versions', () { - testAllowedVersion('1.0.1', '1.2.0', allowed: false); - testAllowedVersion('1.1.0', '1.3.0', allowed: false); - }); - - test('nextVersion allows breaking changes', () { - testAllowedVersion('1.0.1', '2.0.0', - nextVersionType: NextVersionType.BREAKING_MAJOR); - testAllowedVersion('1.0.0', '2.0.0', - nextVersionType: NextVersionType.BREAKING_MAJOR); - }); - - test('nextVersion does not allow skipping major versions', () { - testAllowedVersion('1.0.1', '3.0.0', allowed: false); - testAllowedVersion('1.1.0', '2.3.0', allowed: false); - }); - }); -} diff --git a/script/tool/test/xcode_analyze_command_test.dart b/script/tool/test/xcode_analyze_command_test.dart deleted file mode 100644 index 418c695f295c..000000000000 --- a/script/tool/test/xcode_analyze_command_test.dart +++ /dev/null @@ -1,484 +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:io' as io; - -import 'package:args/command_runner.dart'; -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/xcode_analyze_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -// TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of -// doing all the process mocking and validation. -void main() { - group('test xcode_analyze_command', () { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(isMacOS: true); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final XcodeAnalyzeCommand command = XcodeAnalyzeCommand(packagesDir, - processRunner: processRunner, platform: mockPlatform); - - runner = CommandRunner( - 'xcode_analyze_command', 'Test for xcode_analyze_command'); - runner.addCommand(command); - }); - - test('Fails if no platforms are provided', () async { - Error? commandError; - final List output = await runCapturingPrint( - runner, ['xcode-analyze'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('At least one platform flag must be provided'), - ]), - ); - }); - - group('iOS', () { - test('skip if iOS is not supported', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final List output = - await runCapturingPrint(runner, ['xcode-analyze', '--ios']); - expect(output, - contains(contains('Not implemented for target platform(s).'))); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('skip if iOS is implemented in a federated package', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.federated) - }); - - final List output = - await runCapturingPrint(runner, ['xcode-analyze', '--ios']); - expect(output, - contains(contains('Not implemented for target platform(s).'))); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('runs for iOS plugin', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, [ - 'xcode-analyze', - '--ios', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('plugin/example (iOS) passed analysis.') - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'analyze', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'generic/platform=iOS Simulator', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('passes min iOS deployment version when requested', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, - ['xcode-analyze', '--ios', '--ios-min-version=14.0']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('plugin/example (iOS) passed analysis.') - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'analyze', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'generic/platform=iOS Simulator', - 'IPHONEOS_DEPLOYMENT_TARGET=14.0', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('fails if xcrun fails', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(exitCode: 1) - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, - [ - 'xcode-analyze', - '--ios', - ], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains(' plugin'), - ])); - }); - }); - - group('macOS', () { - test('skip if macOS is not supported', () async { - createFakePlugin( - 'plugin', - packagesDir, - ); - - final List output = await runCapturingPrint( - runner, ['xcode-analyze', '--macos']); - expect(output, - contains(contains('Not implemented for target platform(s).'))); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('skip if macOS is implemented in a federated package', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.federated), - }); - - final List output = await runCapturingPrint( - runner, ['xcode-analyze', '--macos']); - expect(output, - contains(contains('Not implemented for target platform(s).'))); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('runs for macOS plugin', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, [ - 'xcode-analyze', - '--macos', - ]); - - expect(output, - contains(contains('plugin/example (macOS) passed analysis.'))); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'analyze', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('passes min macOS deployment version when requested', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, - ['xcode-analyze', '--macos', '--macos-min-version=12.0']); - - expect(output, - contains(contains('plugin/example (macOS) passed analysis.'))); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'analyze', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'MACOSX_DEPLOYMENT_TARGET=12.0', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('fails if xcrun fails', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(exitCode: 1) - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['xcode-analyze', '--macos'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains(' plugin'), - ]), - ); - }); - }); - - group('combined', () { - test('runs both iOS and macOS when supported', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline), - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, [ - 'xcode-analyze', - '--ios', - '--macos', - ]); - - expect( - output, - containsAll([ - contains('plugin/example (iOS) passed analysis.'), - contains('plugin/example (macOS) passed analysis.'), - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'analyze', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'generic/platform=iOS Simulator', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'analyze', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('runs only macOS for a macOS plugin', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, [ - 'xcode-analyze', - '--ios', - '--macos', - ]); - - expect( - output, - containsAllInOrder([ - contains('plugin/example (macOS) passed analysis.'), - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'analyze', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('runs only iOS for a iOS plugin', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, [ - 'xcode-analyze', - '--ios', - '--macos', - ]); - - expect( - output, - containsAllInOrder( - [contains('plugin/example (iOS) passed analysis.')])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'analyze', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'generic/platform=iOS Simulator', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('skips when neither are supported', () async { - createFakePlugin('plugin', packagesDir); - - final List output = await runCapturingPrint(runner, [ - 'xcode-analyze', - '--ios', - '--macos', - ]); - - expect( - output, - containsAllInOrder([ - contains('SKIPPING: Not implemented for target platform(s).'), - ])); - - expect(processRunner.recordedCalls, orderedEquals([])); - }); - }); - }); -} diff --git a/script/tool_runner.sh b/script/tool_runner.sh index 221071550cc1..ba7bec6579d1 100755 --- a/script/tool_runner.sh +++ b/script/tool_runner.sh @@ -6,18 +6,20 @@ set -e # WARNING! Do not remove this script, or change its behavior, unless you have -# verified that it will not break the flutter/flutter analysis run of this -# repository: https://github.com/flutter/flutter/blob/master/dev/bots/test.dart +# verified that it will not break the dart-lang analysis run of this +# repository: https://github.com/dart-lang/sdk/blob/main/tools/bots/flutter/analyze_flutter_plugins.sh readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" readonly REPO_DIR="$(dirname "$SCRIPT_DIR")" -readonly TOOL_PATH="$REPO_DIR/script/tool/bin/flutter_plugin_tools.dart" -# Ensure that the tool dependencies have been fetched. -(pushd "$REPO_DIR/script/tool" && dart pub get && popd) >/dev/null # The tool expects to be run from the repo root. -cd "$REPO_DIR" -# Run from the in-tree source. # PACKAGE_SHARDING is (optionally) set from Cirrus. See .cirrus.yml -dart run "$TOOL_PATH" "$@" --packages-for-branch --log-timing $PACKAGE_SHARDING +cd "$REPO_DIR" +# Ensure that the tooling has been activated. +.ci/scripts/prepare_tool.sh + +dart pub global run flutter_plugin_tools "$@" \ + --packages-for-branch \ + --log-timing \ + $PACKAGE_SHARDING