From eb330b09828f4fed9d6f96e1454eb2b3864368ea Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 16 Feb 2022 07:00:22 -0800 Subject: [PATCH 01/12] [image_picker] Update app-facing and web analysis options (#4838) --- .../image_picker/image_picker/CHANGELOG.md | 4 + .../image_picker/analysis_options.yaml | 1 - .../image_picker/example/lib/main.dart | 48 ++++----- .../image_picker/example/pubspec.yaml | 3 +- .../image_picker/image_picker/pubspec.yaml | 3 +- .../test/image_picker_deprecated_test.dart | 4 +- .../image_picker/test/image_picker_test.dart | 6 +- .../image_picker_for_web/CHANGELOG.md | 4 + .../analysis_options.yaml | 1 - .../image_picker_for_web_test.dart | 59 ++++++----- .../integration_test/image_resizer_test.dart | 99 ++++++++++--------- .../example/lib/main.dart | 2 +- .../image_picker_for_web/example/pubspec.yaml | 10 +- .../lib/image_picker_for_web.dart | 68 +++++++------ .../lib/src/image_resizer.dart | 37 ++++--- .../lib/src/image_resizer_utils.dart | 11 ++- .../image_picker_for_web/pubspec.yaml | 3 +- .../test/image_resizer_utils_test.dart | 54 +++++----- script/configs/custom_analysis.yaml | 2 - 19 files changed, 223 insertions(+), 196 deletions(-) delete mode 100644 packages/image_picker/image_picker/analysis_options.yaml delete mode 100644 packages/image_picker/image_picker_for_web/analysis_options.yaml diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 992c8b299345..382798f53f90 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.4+9 + +* Internal code cleanup for stricter analysis options. + ## 0.8.4+8 * Configures the `UIImagePicker` to default to gallery instead of camera when diff --git a/packages/image_picker/image_picker/analysis_options.yaml b/packages/image_picker/image_picker/analysis_options.yaml deleted file mode 100644 index 5aeb4e7c5e21..000000000000 --- a/packages/image_picker/image_picker/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../../analysis_options_legacy.yaml diff --git a/packages/image_picker/image_picker/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index 0f5ba76db6df..f3ad2375b8f2 100755 --- a/packages/image_picker/image_picker/example/lib/main.dart +++ b/packages/image_picker/image_picker/example/lib/main.dart @@ -19,7 +19,7 @@ void main() { class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( + return const MaterialApp( title: 'Image Picker Demo', home: MyHomePage(title: 'Image Picker Example'), ); @@ -27,7 +27,7 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - MyHomePage({Key? key, this.title}) : super(key: key); + const MyHomePage({Key? key, this.title}) : super(key: key); final String? title; @@ -39,7 +39,7 @@ class _MyHomePageState extends State { List? _imageFileList; set _imageFile(XFile? value) { - _imageFileList = value == null ? null : [value]; + _imageFileList = value == null ? null : [value]; } dynamic _pickImageError; @@ -69,7 +69,7 @@ class _MyHomePageState extends State { // Mute the video so it auto-plays in web! // This is not needed if the call to .play is the result of user // interaction (clicking on a "play" button, for example). - final double volume = kIsWeb ? 0.0 : 1.0; + const double volume = kIsWeb ? 0.0 : 1.0; await controller.setVolume(volume); await controller.initialize(); await controller.setLooping(true); @@ -78,7 +78,7 @@ class _MyHomePageState extends State { } } - void _onImageButtonPressed(ImageSource source, + Future _onImageButtonPressed(ImageSource source, {BuildContext? context, bool isMultiImage = false}) async { if (_controller != null) { await _controller!.setVolume(0.0); @@ -91,7 +91,7 @@ class _MyHomePageState extends State { await _displayPickImageDialog(context!, (double? maxWidth, double? maxHeight, int? quality) async { try { - final pickedFileList = await _picker.pickMultiImage( + final List? pickedFileList = await _picker.pickMultiImage( maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: quality, @@ -109,7 +109,7 @@ class _MyHomePageState extends State { await _displayPickImageDialog(context!, (double? maxWidth, double? maxHeight, int? quality) async { try { - final pickedFile = await _picker.pickImage( + final XFile? pickedFile = await _picker.pickImage( source: source, maxWidth: maxWidth, maxHeight: maxHeight, @@ -179,7 +179,7 @@ class _MyHomePageState extends State { return Semantics( child: ListView.builder( key: UniqueKey(), - itemBuilder: (context, index) { + itemBuilder: (BuildContext context, int index) { // Why network for web? // See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform return Semantics( @@ -358,28 +358,30 @@ class _MyHomePageState extends State { BuildContext context, OnPickImageCallback onPick) async { return showDialog( context: context, - builder: (context) { + builder: (BuildContext context) { return AlertDialog( - title: Text('Add optional parameters'), + title: const Text('Add optional parameters'), content: Column( children: [ TextField( controller: maxWidthController, - keyboardType: TextInputType.numberWithOptions(decimal: true), - decoration: - InputDecoration(hintText: "Enter maxWidth if desired"), + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxWidth if desired'), ), TextField( controller: maxHeightController, - keyboardType: TextInputType.numberWithOptions(decimal: true), - decoration: - InputDecoration(hintText: "Enter maxHeight if desired"), + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxHeight if desired'), ), TextField( controller: qualityController, keyboardType: TextInputType.number, - decoration: - InputDecoration(hintText: "Enter quality if desired"), + decoration: const InputDecoration( + hintText: 'Enter quality if desired'), ), ], ), @@ -393,13 +395,13 @@ class _MyHomePageState extends State { TextButton( child: const Text('PICK'), onPressed: () { - double? width = maxWidthController.text.isNotEmpty + final double? width = maxWidthController.text.isNotEmpty ? double.parse(maxWidthController.text) : null; - double? height = maxHeightController.text.isNotEmpty + final double? height = maxHeightController.text.isNotEmpty ? double.parse(maxHeightController.text) : null; - int? quality = qualityController.text.isNotEmpty + final int? quality = qualityController.text.isNotEmpty ? int.parse(qualityController.text) : null; onPick(width, height, quality); @@ -411,11 +413,11 @@ class _MyHomePageState extends State { } } -typedef void OnPickImageCallback( +typedef OnPickImageCallback = void Function( double? maxWidth, double? maxHeight, int? quality); class AspectRatioVideo extends StatefulWidget { - AspectRatioVideo(this.controller); + const AspectRatioVideo(this.controller); final VideoPlayerController? controller; diff --git a/packages/image_picker/image_picker/example/pubspec.yaml b/packages/image_picker/image_picker/example/pubspec.yaml index e11da82d5da8..28b37197d8ff 100755 --- a/packages/image_picker/image_picker/example/pubspec.yaml +++ b/packages/image_picker/image_picker/example/pubspec.yaml @@ -7,7 +7,6 @@ environment: flutter: ">=2.5.0" dependencies: - video_player: ^2.1.4 flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 @@ -18,6 +17,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: ^2.1.4 dev_dependencies: espresso: ^0.1.0+2 @@ -25,7 +25,6 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index 43143f1dbd57..1d280f16560b 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. repository: https://github.com/flutter/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.4+8 +version: 0.8.4+9 environment: sdk: ">=2.14.0 <3.0.0" @@ -31,5 +31,4 @@ dev_dependencies: flutter_test: sdk: flutter mockito: ^5.0.0 - pedantic: ^1.10.0 plugin_platform_interface: ^2.0.0 diff --git a/packages/image_picker/image_picker/test/image_picker_deprecated_test.dart b/packages/image_picker/image_picker/test/image_picker_deprecated_test.dart index f295e3d02f66..00049e14f808 100644 --- a/packages/image_picker/image_picker/test/image_picker_deprecated_test.dart +++ b/packages/image_picker/image_picker/test/image_picker_deprecated_test.dart @@ -23,7 +23,7 @@ void main() { final List log = []; - final picker = ImagePicker(); + final ImagePicker picker = ImagePicker(); test('ImagePicker platform instance overrides the actual platform used', () { @@ -359,7 +359,7 @@ void main() { setUp(() { channel.setMockMethodCallHandler((MethodCall methodCall) async { log.add(methodCall); - return []; + return []; }); log.clear(); }); diff --git a/packages/image_picker/image_picker/test/image_picker_test.dart b/packages/image_picker/image_picker/test/image_picker_test.dart index 10bc64082aca..b41fbe3381df 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.dart @@ -18,7 +18,7 @@ void main() { final List log = []; - final picker = ImagePicker(); + final ImagePicker picker = ImagePicker(); test('ImagePicker platform instance overrides the actual platform used', () { @@ -321,7 +321,7 @@ void main() { return { 'type': 'image', 'path': '/example/path1', - 'pathList': ['/example/path0', '/example/path1'], + 'pathList': ['/example/path0', '/example/path1'], }; }); @@ -372,7 +372,7 @@ void main() { setUp(() { channel.setMockMethodCallHandler((MethodCall methodCall) async { log.add(methodCall); - return []; + return []; }); log.clear(); }); diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md index 78629a4d252d..dcf353fe19b1 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 @@ +## 2.1.6 + +* Internal code cleanup for stricter analysis options. + ## 2.1.5 * Removes dependency on `meta`. diff --git a/packages/image_picker/image_picker_for_web/analysis_options.yaml b/packages/image_picker/image_picker_for_web/analysis_options.yaml deleted file mode 100644 index 5aeb4e7c5e21..000000000000 --- a/packages/image_picker/image_picker_for_web/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../../analysis_options_legacy.yaml diff --git a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart index c1025a9f07d3..9fe40da2557c 100644 --- a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart +++ b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart @@ -11,16 +11,17 @@ import 'package:image_picker_for_web/image_picker_for_web.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:integration_test/integration_test.dart'; -final String expectedStringContents = 'Hello, world!'; -final String otherStringContents = 'Hello again, world!'; +const String expectedStringContents = 'Hello, world!'; +const String otherStringContents = 'Hello again, world!'; final Uint8List bytes = utf8.encode(expectedStringContents) as Uint8List; final Uint8List otherBytes = utf8.encode(otherStringContents) as Uint8List; -final Map options = { +final Map options = { 'type': 'text/plain', 'lastModified': DateTime.utc(2017, 12, 13).millisecondsSinceEpoch, }; -final html.File textFile = html.File([bytes], 'hello.txt', options); -final html.File secondTextFile = html.File([otherBytes], 'secondFile.txt'); +final html.File textFile = html.File([bytes], 'hello.txt', options); +final html.File secondTextFile = + html.File([otherBytes], 'secondFile.txt'); void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -33,16 +34,17 @@ void main() { }); testWidgets('Can select a file (Deprecated)', (WidgetTester tester) async { - final mockInput = html.FileUploadInputElement(); + final html.FileUploadInputElement mockInput = html.FileUploadInputElement(); - final overrides = ImagePickerPluginTestOverrides() - ..createInputElement = ((_, __) => mockInput) - ..getMultipleFilesFromInput = ((_) => [textFile]); + final ImagePickerPluginTestOverrides overrides = + ImagePickerPluginTestOverrides() + ..createInputElement = ((_, __) => mockInput) + ..getMultipleFilesFromInput = ((_) => [textFile]); - final plugin = ImagePickerPlugin(overrides: overrides); + final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides); // Init the pick file dialog... - final file = plugin.pickFile(); + final Future file = plugin.pickFile(); // Mock the browser behavior of selecting a file... mockInput.dispatchEvent(html.Event('change')); @@ -54,16 +56,17 @@ void main() { }); testWidgets('Can select a file', (WidgetTester tester) async { - final mockInput = html.FileUploadInputElement(); + final html.FileUploadInputElement mockInput = html.FileUploadInputElement(); - final overrides = ImagePickerPluginTestOverrides() - ..createInputElement = ((_, __) => mockInput) - ..getMultipleFilesFromInput = ((_) => [textFile]); + final ImagePickerPluginTestOverrides overrides = + ImagePickerPluginTestOverrides() + ..createInputElement = ((_, __) => mockInput) + ..getMultipleFilesFromInput = ((_) => [textFile]); - final plugin = ImagePickerPlugin(overrides: overrides); + final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides); // Init the pick file dialog... - final image = plugin.getImage(source: ImageSource.camera); + final Future image = plugin.getImage(source: ImageSource.camera); // Mock the browser behavior of selecting a file... mockInput.dispatchEvent(html.Event('change')); @@ -85,16 +88,18 @@ void main() { }); testWidgets('Can select multiple files', (WidgetTester tester) async { - final mockInput = html.FileUploadInputElement(); + final html.FileUploadInputElement mockInput = html.FileUploadInputElement(); - final overrides = ImagePickerPluginTestOverrides() - ..createInputElement = ((_, __) => mockInput) - ..getMultipleFilesFromInput = ((_) => [textFile, secondTextFile]); + final ImagePickerPluginTestOverrides overrides = + ImagePickerPluginTestOverrides() + ..createInputElement = ((_, __) => mockInput) + ..getMultipleFilesFromInput = + ((_) => [textFile, secondTextFile]); - final plugin = ImagePickerPlugin(overrides: overrides); + final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides); // Init the pick file dialog... - final files = plugin.getMultiImage(); + final Future> files = plugin.getMultiImage(); // Mock the browser behavior of selecting a file... mockInput.dispatchEvent(html.Event('change')); @@ -135,7 +140,7 @@ void main() { group('createInputElement', () { testWidgets('accept: any, capture: null', (WidgetTester tester) async { - html.Element input = plugin.createInputElement('any', null); + final html.Element input = plugin.createInputElement('any', null); expect(input.attributes, containsPair('accept', 'any')); expect(input.attributes, isNot(contains('capture'))); @@ -143,7 +148,7 @@ void main() { }); testWidgets('accept: any, capture: something', (WidgetTester tester) async { - html.Element input = plugin.createInputElement('any', 'something'); + final html.Element input = plugin.createInputElement('any', 'something'); expect(input.attributes, containsPair('accept', 'any')); expect(input.attributes, containsPair('capture', 'something')); @@ -152,7 +157,7 @@ void main() { testWidgets('accept: any, capture: null, multi: true', (WidgetTester tester) async { - html.Element input = + final html.Element input = plugin.createInputElement('any', null, multiple: true); expect(input.attributes, containsPair('accept', 'any')); @@ -162,7 +167,7 @@ void main() { testWidgets('accept: any, capture: something, multi: true', (WidgetTester tester) async { - html.Element input = + final html.Element input = plugin.createInputElement('any', 'something', multiple: true); expect(input.attributes, containsPair('accept', 'any')); diff --git a/packages/image_picker/image_picker_for_web/example/integration_test/image_resizer_test.dart b/packages/image_picker/image_picker_for_web/example/integration_test/image_resizer_test.dart index 067c7750eb11..91794a7d5e78 100644 --- a/packages/image_picker/image_picker_for_web/example/integration_test/image_resizer_test.dart +++ b/packages/image_picker/image_picker_for_web/example/integration_test/image_resizer_test.dart @@ -13,8 +13,8 @@ import 'package:image_picker_platform_interface/image_picker_platform_interface. import 'package:integration_test/integration_test.dart'; //This is a sample 10x10 png image -final String pngFileBase64Contents = - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKAQMAAAC3/F3+AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABlBMVEXqQzX+/v6lfubTAAAAAWJLR0QB/wIt3gAAAAlwSFlzAAAHEwAABxMBziAPCAAAAAd0SU1FB+UJHgsdDM0ErZoAAAALSURBVAjXY2DABwAAHgABboVHMgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMS0wOS0zMFQxMToyOToxMi0wNDowMHCDC24AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjEtMDktMzBUMTE6Mjk6MTItMDQ6MDAB3rPSAAAAAElFTkSuQmCC"; +const String pngFileBase64Contents = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKAQMAAAC3/F3+AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABlBMVEXqQzX+/v6lfubTAAAAAWJLR0QB/wIt3gAAAAlwSFlzAAAHEwAABxMBziAPCAAAAAd0SU1FB+UJHgsdDM0ErZoAAAALSURBVAjXY2DABwAAHgABboVHMgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMS0wOS0zMFQxMToyOToxMi0wNDowMHCDC24AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjEtMDktMzBUMTE6Mjk6MTItMDQ6MDAB3rPSAAAAAElFTkSuQmCC'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -24,105 +24,114 @@ void main() { late XFile pngFile; setUp(() { imageResizer = ImageResizer(); - final pngHtmlFile = _base64ToFile(pngFileBase64Contents, "pngImage.png"); + final html.File pngHtmlFile = + _base64ToFile(pngFileBase64Contents, 'pngImage.png'); pngFile = XFile(html.Url.createObjectUrl(pngHtmlFile), name: pngHtmlFile.name, mimeType: pngHtmlFile.type); }); - testWidgets("image is loaded correctly ", (WidgetTester tester) async { - final imageElement = await imageResizer.loadImage(pngFile.path); + testWidgets('image is loaded correctly ', (WidgetTester tester) async { + final html.ImageElement imageElement = + await imageResizer.loadImage(pngFile.path); expect(imageElement.width!, 10); expect(imageElement.height!, 10); }); testWidgets( "canvas is loaded with image's width and height when max width and max height are null", - (widgetTester) async { - final imageElement = await imageResizer.loadImage(pngFile.path); - final canvas = imageResizer.resizeImageElement(imageElement, null, null); + (WidgetTester widgetTester) async { + final html.ImageElement imageElement = + await imageResizer.loadImage(pngFile.path); + final html.CanvasElement canvas = + imageResizer.resizeImageElement(imageElement, null, null); expect(canvas.width, imageElement.width); expect(canvas.height, imageElement.height); }); testWidgets( - "canvas size is scaled when max width and max height are not null", - (widgetTester) async { - final imageElement = await imageResizer.loadImage(pngFile.path); - final canvas = imageResizer.resizeImageElement(imageElement, 8, 8); + 'canvas size is scaled when max width and max height are not null', + (WidgetTester widgetTester) async { + final html.ImageElement imageElement = + await imageResizer.loadImage(pngFile.path); + final html.CanvasElement canvas = + imageResizer.resizeImageElement(imageElement, 8, 8); expect(canvas.width, 8); expect(canvas.height, 8); }); - testWidgets("resized image is returned after converting canvas to file", - (widgetTester) async { - final imageElement = await imageResizer.loadImage(pngFile.path); - final canvas = imageResizer.resizeImageElement(imageElement, null, null); - final resizedImage = + testWidgets('resized image is returned after converting canvas to file', + (WidgetTester widgetTester) async { + final html.ImageElement imageElement = + await imageResizer.loadImage(pngFile.path); + final html.CanvasElement canvas = + imageResizer.resizeImageElement(imageElement, null, null); + final XFile resizedImage = await imageResizer.writeCanvasToFile(pngFile, canvas, null); - expect(resizedImage.name, "scaled_${pngFile.name}"); + expect(resizedImage.name, 'scaled_${pngFile.name}'); }); - testWidgets("image is scaled when maxWidth is set", + testWidgets('image is scaled when maxWidth is set', (WidgetTester tester) async { - final scaledImage = + final XFile scaledImage = await imageResizer.resizeImageIfNeeded(pngFile, 5, null, null); - expect(scaledImage.name, "scaled_${pngFile.name}"); - final scaledImageSize = await _getImageSize(scaledImage); - expect(scaledImageSize, Size(5, 5)); + expect(scaledImage.name, 'scaled_${pngFile.name}'); + final Size scaledImageSize = await _getImageSize(scaledImage); + expect(scaledImageSize, const Size(5, 5)); }); - testWidgets("image is scaled when maxHeight is set", + testWidgets('image is scaled when maxHeight is set', (WidgetTester tester) async { - final scaledImage = + final XFile scaledImage = await imageResizer.resizeImageIfNeeded(pngFile, null, 6, null); - expect(scaledImage.name, "scaled_${pngFile.name}"); - final scaledImageSize = await _getImageSize(scaledImage); - expect(scaledImageSize, Size(6, 6)); + expect(scaledImage.name, 'scaled_${pngFile.name}'); + final Size scaledImageSize = await _getImageSize(scaledImage); + expect(scaledImageSize, const Size(6, 6)); }); - testWidgets("image is scaled when imageQuality is set", + testWidgets('image is scaled when imageQuality is set', (WidgetTester tester) async { - final scaledImage = + final XFile scaledImage = await imageResizer.resizeImageIfNeeded(pngFile, null, null, 89); - expect(scaledImage.name, "scaled_${pngFile.name}"); + expect(scaledImage.name, 'scaled_${pngFile.name}'); }); - testWidgets("image is scaled when maxWidth,maxHeight,imageQuality are set", + testWidgets('image is scaled when maxWidth,maxHeight,imageQuality are set', (WidgetTester tester) async { - final scaledImage = + final XFile scaledImage = await imageResizer.resizeImageIfNeeded(pngFile, 3, 4, 89); - expect(scaledImage.name, "scaled_${pngFile.name}"); + expect(scaledImage.name, 'scaled_${pngFile.name}'); }); - testWidgets("image is not scaled when maxWidth,maxHeight, is set", + testWidgets('image is not scaled when maxWidth,maxHeight, is set', (WidgetTester tester) async { - final scaledImage = + final XFile scaledImage = await imageResizer.resizeImageIfNeeded(pngFile, null, null, null); expect(scaledImage.name, pngFile.name); }); } Future _getImageSize(XFile file) async { - final completer = Completer(); - final image = html.ImageElement(src: file.path); - image.onLoad.listen((event) { + final Completer completer = Completer(); + final html.ImageElement image = html.ImageElement(src: file.path); + image.onLoad.listen((html.Event event) { completer.complete(Size(image.width!.toDouble(), image.height!.toDouble())); }); - image.onError.listen((event) { - completer.complete(Size(0, 0)); + image.onError.listen((html.Event event) { + completer.complete(const Size(0, 0)); }); return completer.future; } html.File _base64ToFile(String data, String fileName) { - var arr = data.split(','); - var bstr = html.window.atob(arr[1]); - var n = bstr.length, u8arr = Uint8List(n); + final List arr = data.split(','); + final String bstr = html.window.atob(arr[1]); + int n = bstr.length; + final Uint8List u8arr = Uint8List(n); while (n >= 1) { u8arr[n - 1] = bstr.codeUnitAt(n - 1); n--; } - return html.File([u8arr], fileName); + return html.File([u8arr], fileName); } diff --git a/packages/image_picker/image_picker_for_web/example/lib/main.dart b/packages/image_picker/image_picker_for_web/example/lib/main.dart index e1a38dcdcd46..341913a18490 100644 --- a/packages/image_picker/image_picker_for_web/example/lib/main.dart +++ b/packages/image_picker/image_picker_for_web/example/lib/main.dart @@ -17,7 +17,7 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { @override Widget build(BuildContext context) { - return Directionality( + return const Directionality( textDirection: TextDirection.ltr, child: Text('Testing... Look at the console output for results!'), ); 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 306a857731e8..a9d6c7b9b5bd 100644 --- a/packages/image_picker/image_picker_for_web/example/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/example/pubspec.yaml @@ -6,16 +6,16 @@ environment: flutter: ">=2.2.0" dependencies: - image_picker_for_web: - path: ../ flutter: sdk: flutter + image_picker_for_web: + path: ../ dev_dependencies: - js: ^0.6.3 - flutter_test: - sdk: flutter flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: sdk: flutter + js: ^0.6.3 diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart index 132087576c1d..88d439c5487f 100644 --- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -10,22 +10,14 @@ import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:image_picker_for_web/src/image_resizer.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; -final String _kImagePickerInputsDomId = '__image_picker_web-file-input'; -final String _kAcceptImageMimeType = 'image/*'; -final String _kAcceptVideoMimeType = 'video/3gpp,video/x-m4v,video/mp4,video/*'; +const String _kImagePickerInputsDomId = '__image_picker_web-file-input'; +const String _kAcceptImageMimeType = 'image/*'; +const String _kAcceptVideoMimeType = 'video/3gpp,video/x-m4v,video/mp4,video/*'; /// The web implementation of [ImagePickerPlatform]. /// /// This class implements the `package:image_picker` functionality for the web. class ImagePickerPlugin extends ImagePickerPlatform { - final ImagePickerPluginTestOverrides? _overrides; - - bool get _hasOverrides => _overrides != null; - - late html.Element _target; - - late ImageResizer _imageResizer; - /// A constructor that allows tests to override the function that creates file inputs. ImagePickerPlugin({ @visibleForTesting ImagePickerPluginTestOverrides? overrides, @@ -35,6 +27,14 @@ class ImagePickerPlugin extends ImagePickerPlatform { _target = _ensureInitialized(_kImagePickerInputsDomId); } + final ImagePickerPluginTestOverrides? _overrides; + + bool get _hasOverrides => _overrides != null; + + late html.Element _target; + + late ImageResizer _imageResizer; + /// Registers this class as the default instance of [ImagePickerPlatform]. static void registerWith(Registrar registrar) { ImagePickerPlatform.instance = ImagePickerPlugin(); @@ -60,7 +60,8 @@ class ImagePickerPlugin extends ImagePickerPlatform { int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { - String? capture = computeCaptureAttribute(source, preferredCameraDevice); + final String? capture = + computeCaptureAttribute(source, preferredCameraDevice); return pickFile(accept: _kAcceptImageMimeType, capture: capture); } @@ -82,7 +83,8 @@ class ImagePickerPlugin extends ImagePickerPlatform { CameraDevice preferredCameraDevice = CameraDevice.rear, Duration? maxDuration, }) { - String? capture = computeCaptureAttribute(source, preferredCameraDevice); + final String? capture = + computeCaptureAttribute(source, preferredCameraDevice); return pickFile(accept: _kAcceptVideoMimeType, capture: capture); } @@ -96,7 +98,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { String? accept, String? capture, }) { - html.FileUploadInputElement input = + final html.FileUploadInputElement input = createInputElement(accept, capture) as html.FileUploadInputElement; _injectAndActivate(input); return _getSelectedFile(input); @@ -122,8 +124,9 @@ class ImagePickerPlugin extends ImagePickerPlatform { int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { - String? capture = computeCaptureAttribute(source, preferredCameraDevice); - List files = await getFiles( + final String? capture = + computeCaptureAttribute(source, preferredCameraDevice); + final List files = await getFiles( accept: _kAcceptImageMimeType, capture: capture, ); @@ -153,8 +156,9 @@ class ImagePickerPlugin extends ImagePickerPlatform { CameraDevice preferredCameraDevice = CameraDevice.rear, Duration? maxDuration, }) async { - String? capture = computeCaptureAttribute(source, preferredCameraDevice); - List files = await getFiles( + final String? capture = + computeCaptureAttribute(source, preferredCameraDevice); + final List files = await getFiles( accept: _kAcceptVideoMimeType, capture: capture, ); @@ -173,7 +177,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { multiple: true, ); final Iterable> resized = images.map( - (image) => _imageResizer.resizeImageIfNeeded( + (XFile image) => _imageResizer.resizeImageIfNeeded( image, maxWidth, maxHeight, @@ -199,7 +203,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { String? capture, bool multiple = false, }) { - html.FileUploadInputElement input = createInputElement( + final html.FileUploadInputElement input = createInputElement( accept, capture, multiple: multiple, @@ -232,24 +236,24 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// Handles the OnChange event from a FileUploadInputElement object /// Returns a list of selected files. List? _handleOnChangeEvent(html.Event event) { - final html.FileUploadInputElement input = - event.target as html.FileUploadInputElement; - return _getFilesFromInput(input); + final html.FileUploadInputElement? input = + event.target as html.FileUploadInputElement?; + return input == null ? null : _getFilesFromInput(input); } /// Monitors an and returns the selected file. Future _getSelectedFile(html.FileUploadInputElement input) { final Completer _completer = Completer(); // Observe the input until we can return something - input.onChange.first.then((event) { - final files = _handleOnChangeEvent(event); + input.onChange.first.then((html.Event event) { + final List? files = _handleOnChangeEvent(event); if (!_completer.isCompleted && files != null) { _completer.complete(PickedFile( html.Url.createObjectUrl(files.first), )); } }); - input.onError.first.then((event) { + input.onError.first.then((html.Event event) { if (!_completer.isCompleted) { _completer.completeError(event); } @@ -264,10 +268,10 @@ class ImagePickerPlugin extends ImagePickerPlatform { Future> _getSelectedXFiles(html.FileUploadInputElement input) { final Completer> _completer = Completer>(); // Observe the input until we can return something - input.onChange.first.then((event) { - final files = _handleOnChangeEvent(event); + input.onChange.first.then((html.Event event) { + final List? files = _handleOnChangeEvent(event); if (!_completer.isCompleted && files != null) { - _completer.complete(files.map((file) { + _completer.complete(files.map((html.File file) { return XFile( html.Url.createObjectUrl(file), name: file.name, @@ -280,7 +284,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { }).toList()); } }); - input.onError.first.then((event) { + input.onError.first.then((html.Event event) { if (!_completer.isCompleted) { _completer.completeError(event); } @@ -293,7 +297,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// Initializes a DOM container where we can host input elements. html.Element _ensureInitialized(String id) { - var target = html.querySelector('#${id}'); + html.Element? target = html.querySelector('#$id'); if (target == null) { final html.Element targetElement = html.Element.tag('flt-image-picker-inputs')..id = id; @@ -316,7 +320,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { return _overrides!.createInputElement(accept, capture); } - html.Element element = html.FileUploadInputElement() + final html.Element element = html.FileUploadInputElement() ..accept = accept ..multiple = multiple; diff --git a/packages/image_picker/image_picker_for_web/lib/src/image_resizer.dart b/packages/image_picker/image_picker_for_web/lib/src/image_resizer.dart index 6ee7c5f015e2..e063099e3319 100644 --- a/packages/image_picker/image_picker_for_web/lib/src/image_resizer.dart +++ b/packages/image_picker/image_picker_for_web/lib/src/image_resizer.dart @@ -3,11 +3,12 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:html' as html; import 'dart:math'; import 'dart:ui'; + import 'package:image_picker_for_web/src/image_resizer_utils.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; -import 'dart:html' as html; /// Helper class that resizes images. class ImageResizer { @@ -16,14 +17,16 @@ class ImageResizer { Future resizeImageIfNeeded(XFile file, double? maxWidth, double? maxHeight, int? imageQuality) async { if (!imageResizeNeeded(maxWidth, maxHeight, imageQuality) || - file.mimeType == "image/gif") { + file.mimeType == 'image/gif') { // Implement maxWidth and maxHeight for image/gif return file; } try { - final imageElement = await loadImage(file.path); - final canvas = resizeImageElement(imageElement, maxWidth, maxHeight); - final resizedImage = await writeCanvasToFile(file, canvas, imageQuality); + final html.ImageElement imageElement = await loadImage(file.path); + final html.CanvasElement canvas = + resizeImageElement(imageElement, maxWidth, maxHeight); + final XFile resizedImage = + await writeCanvasToFile(file, canvas, imageQuality); html.Url.revokeObjectUrl(file.path); return resizedImage; } catch (e) { @@ -33,15 +36,16 @@ class ImageResizer { /// function that loads the blobUrl into an imageElement Future loadImage(String blobUrl) { - final imageLoadCompleter = Completer(); - final imageElement = html.ImageElement(); + final Completer imageLoadCompleter = + Completer(); + final html.ImageElement imageElement = html.ImageElement(); imageElement.src = blobUrl; - imageElement.onLoad.listen((event) { + imageElement.onLoad.listen((html.Event event) { imageLoadCompleter.complete(imageElement); }); - imageElement.onError.listen((event) { - final exception = ("Error while loading image."); + imageElement.onError.listen((html.Event event) { + const String exception = 'Error while loading image.'; imageElement.remove(); imageLoadCompleter.completeError(exception); }); @@ -51,14 +55,14 @@ class ImageResizer { /// Draws image to a canvas while resizing the image to fit the [maxWidth],[maxHeight] constraints html.CanvasElement resizeImageElement( html.ImageElement source, double? maxWidth, double? maxHeight) { - final newImageSize = calculateSizeOfDownScaledImage( + final Size newImageSize = calculateSizeOfDownScaledImage( Size(source.width!.toDouble(), source.height!.toDouble()), maxWidth, maxHeight); - final canvas = html.CanvasElement(); + final html.CanvasElement canvas = html.CanvasElement(); canvas.width = newImageSize.width.toInt(); canvas.height = newImageSize.height.toInt(); - final context = canvas.context2D; + final html.CanvasRenderingContext2D context = canvas.context2D; if (maxHeight == null && maxWidth == null) { context.drawImage(source, 0, 0); } else { @@ -71,12 +75,13 @@ class ImageResizer { /// [imageQuality] is only supported for jpeg and webp images. Future writeCanvasToFile( XFile originalFile, html.CanvasElement canvas, int? imageQuality) async { - final calculatedImageQuality = ((min(imageQuality ?? 100, 100)) / 100.0); - final blob = + final double calculatedImageQuality = + (min(imageQuality ?? 100, 100)) / 100.0; + final html.Blob blob = await canvas.toBlob(originalFile.mimeType, calculatedImageQuality); return XFile(html.Url.createObjectUrlFromBlob(blob), mimeType: originalFile.mimeType, - name: "scaled_" + originalFile.name, + name: 'scaled_' + originalFile.name, lastModified: DateTime.now(), length: blob.size); } diff --git a/packages/image_picker/image_picker_for_web/lib/src/image_resizer_utils.dart b/packages/image_picker/image_picker_for_web/lib/src/image_resizer_utils.dart index 6ef789254b3f..e906a88f00fe 100644 --- a/packages/image_picker/image_picker_for_web/lib/src/image_resizer_utils.dart +++ b/packages/image_picker/image_picker_for_web/lib/src/image_resizer_utils.dart @@ -16,7 +16,7 @@ bool imageResizeNeeded(double? maxWidth, double? maxHeight, int? imageQuality) { /// a function that checks if image quality is between 0 to 100 bool isImageQualityValid(int imageQuality) { - return (imageQuality >= 0 && imageQuality <= 100); + return imageQuality >= 0 && imageQuality <= 100; } /// a function that calculates the size of the downScaled image. @@ -26,8 +26,9 @@ bool isImageQualityValid(int imageQuality) { /// maxHeight is the maximum height of the scaled image Size calculateSizeOfDownScaledImage( Size imageSize, double? maxWidth, double? maxHeight) { - double widthFactor = maxWidth != null ? imageSize.width / maxWidth : 1; - double heightFactor = maxHeight != null ? imageSize.height / maxHeight : 1; - double resizeFactor = max(widthFactor, heightFactor); - return (resizeFactor > 1 ? imageSize ~/ resizeFactor : imageSize); + final double widthFactor = maxWidth != null ? imageSize.width / maxWidth : 1; + final double heightFactor = + maxHeight != null ? imageSize.height / maxHeight : 1; + final double resizeFactor = max(widthFactor, heightFactor); + return resizeFactor > 1 ? imageSize ~/ resizeFactor : imageSize; } diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index 285656ac9e4f..deccd2b50a1f 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_for_web description: Web platform implementation of image_picker repository: https://github.com/flutter/plugins/tree/main/packages/image_picker/image_picker_for_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 2.1.5 +version: 2.1.6 environment: sdk: ">=2.12.0 <3.0.0" @@ -22,7 +22,6 @@ dependencies: flutter_web_plugins: sdk: flutter image_picker_platform_interface: ^2.2.0 - pedantic: ^1.10.0 dev_dependencies: flutter_test: diff --git a/packages/image_picker/image_picker_for_web/test/image_resizer_utils_test.dart b/packages/image_picker/image_picker_for_web/test/image_resizer_utils_test.dart index 352d2bea48a5..0bfa81729bf0 100644 --- a/packages/image_picker/image_picker_for_web/test/image_resizer_utils_test.dart +++ b/packages/image_picker/image_picker_for_web/test/image_resizer_utils_test.dart @@ -8,34 +8,34 @@ import 'package:image_picker_for_web/src/image_resizer_utils.dart'; void main() { group('Image Resizer Utils', () { - group("calculateSizeOfScaledImage", () { + group('calculateSizeOfScaledImage', () { test( "scaled image height and width are same if max width and max height are same as image's width and height", () { - expect(calculateSizeOfDownScaledImage(Size(500, 300), 500, 300), - Size(500, 300)); + expect(calculateSizeOfDownScaledImage(const Size(500, 300), 500, 300), + const Size(500, 300)); }); test( - "scaled image height and width are same if max width and max height are null", + 'scaled image height and width are same if max width and max height are null', () { - expect(calculateSizeOfDownScaledImage(Size(500, 300), null, null), - Size(500, 300)); + expect(calculateSizeOfDownScaledImage(const Size(500, 300), null, null), + const Size(500, 300)); }); - test("image size is scaled when maxWidth is set", () { - final imageSize = Size(500, 300); - final maxWidth = 400; - final scaledSize = calculateSizeOfDownScaledImage( + test('image size is scaled when maxWidth is set', () { + const Size imageSize = Size(500, 300); + const int maxWidth = 400; + final Size scaledSize = calculateSizeOfDownScaledImage( Size(imageSize.width, imageSize.height), maxWidth.toDouble(), null); expect(scaledSize.height <= imageSize.height, true); expect(scaledSize.width <= maxWidth, true); }); - test("image size is scaled when maxHeight is set", () { - final imageSize = Size(500, 300); - final maxHeight = 400; - final scaledSize = calculateSizeOfDownScaledImage( + test('image size is scaled when maxHeight is set', () { + const Size imageSize = Size(500, 300); + const int maxHeight = 400; + final Size scaledSize = calculateSizeOfDownScaledImage( Size(imageSize.width, imageSize.height), null, maxHeight.toDouble()); @@ -43,11 +43,11 @@ void main() { expect(scaledSize.width <= imageSize.width, true); }); - test("image size is scaled when both maxWidth and maxHeight is set", () { - final imageSize = Size(1120, 2000); - final maxHeight = 1200; - final maxWidth = 99; - final scaledSize = calculateSizeOfDownScaledImage( + test('image size is scaled when both maxWidth and maxHeight is set', () { + const Size imageSize = Size(1120, 2000); + const int maxHeight = 1200; + const int maxWidth = 99; + final Size scaledSize = calculateSizeOfDownScaledImage( Size(imageSize.width, imageSize.height), maxWidth.toDouble(), maxHeight.toDouble()); @@ -55,34 +55,34 @@ void main() { expect(scaledSize.width <= maxWidth, true); }); }); - group("imageResizeNeeded", () { - test("image needs to be resized when maxWidth is set", () { + group('imageResizeNeeded', () { + test('image needs to be resized when maxWidth is set', () { expect(imageResizeNeeded(50, null, null), true); }); - test("image needs to be resized when maxHeight is set", () { + test('image needs to be resized when maxHeight is set', () { expect(imageResizeNeeded(null, 50, null), true); }); - test("image needs to be resized when imageQuality is set", () { + test('image needs to be resized when imageQuality is set', () { expect(imageResizeNeeded(null, null, 100), true); }); - test("image will not be resized when imageQuality is not valid", () { + test('image will not be resized when imageQuality is not valid', () { expect(imageResizeNeeded(null, null, 101), false); expect(imageResizeNeeded(null, null, -1), false); }); }); - group("isImageQualityValid", () { - test("image quality is valid in 0 to 100", () { + group('isImageQualityValid', () { + test('image quality is valid in 0 to 100', () { expect(isImageQualityValid(50), true); expect(isImageQualityValid(0), true); expect(isImageQualityValid(100), true); }); test( - "image quality is not valid when imageQuality is less than 0 or greater than 100", + 'image quality is not valid when imageQuality is less than 0 or greater than 100', () { expect(isImageQualityValid(-1), false); expect(isImageQualityValid(101), false); diff --git a/script/configs/custom_analysis.yaml b/script/configs/custom_analysis.yaml index d434b5d56662..13a6819acd79 100644 --- a/script/configs/custom_analysis.yaml +++ b/script/configs/custom_analysis.yaml @@ -17,8 +17,6 @@ - google_sign_in/google_sign_in - google_sign_in/google_sign_in_platform_interface - google_sign_in/google_sign_in_web -- image_picker/image_picker -- image_picker/image_picker_for_web - image_picker/image_picker_platform_interface - in_app_purchase/in_app_purchase - in_app_purchase/in_app_purchase_android From 9318efc766bb26d7b71e2704ea18be9a8584c53f Mon Sep 17 00:00:00 2001 From: Alex Furaiev <33546696+furaiev@users.noreply.github.com> Date: Wed, 16 Feb 2022 17:02:43 +0200 Subject: [PATCH 02/12] [local_auth] support localizedFallbackTitle in IOSAuthMessages (#3806) Add support localizedFallbackTitle in IOSAuthMessages. --- packages/local_auth/local_auth/CHANGELOG.md | 4 + .../ios/RunnerTests/FLTLocalAuthPluginTests.m | 83 ++++++++ .../ios/Classes/FLTLocalAuthPlugin.m | 9 +- .../local_auth/lib/auth_strings.dart | 4 + packages/local_auth/local_auth/pubspec.yaml | 2 +- .../local_auth/test/local_auth_test.dart | 177 +++++++++++++----- 6 files changed, 228 insertions(+), 51 deletions(-) diff --git a/packages/local_auth/local_auth/CHANGELOG.md b/packages/local_auth/local_auth/CHANGELOG.md index d17eb4a97a21..9387540d6795 100644 --- a/packages/local_auth/local_auth/CHANGELOG.md +++ b/packages/local_auth/local_auth/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.1.11 + +* Adds support `localizedFallbackTitle` in authenticateWithBiometrics on iOS. + ## 1.1.10 * Removes dependency on `meta`. diff --git a/packages/local_auth/local_auth/example/ios/RunnerTests/FLTLocalAuthPluginTests.m b/packages/local_auth/local_auth/example/ios/RunnerTests/FLTLocalAuthPluginTests.m index dc409da9f57c..3572524d8991 100644 --- a/packages/local_auth/local_auth/example/ios/RunnerTests/FLTLocalAuthPluginTests.m +++ b/packages/local_auth/local_auth/example/ios/RunnerTests/FLTLocalAuthPluginTests.m @@ -186,4 +186,87 @@ - (void)testFailedAuthWithoutBiometrics { [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } +- (void)testLocalizedFallbackTitle { + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; + id mockAuthContext = OCMClassMock([LAContext class]); + plugin.authContextOverrides = @[ mockAuthContext ]; + + const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; + NSString *reason = @"a reason"; + NSString *localizedFallbackTitle = @"a title"; + 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:99 userInfo:nil]); + }); + }; + OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]]) + .andDo(backgroundThreadReplyCaller); + + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"authenticate" + arguments:@{ + @"biometricOnly" : @(NO), + @"localizedReason" : reason, + @"localizedFallbackTitle" : localizedFallbackTitle, + }]; + + 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]; +} + +- (void)testSkippedLocalizedFallbackTitle { + 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:99 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:[NSNumber class]]); + OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]); + XCTAssertFalse([result boolValue]); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kTimeout handler:nil]; +} + @end diff --git a/packages/local_auth/local_auth/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/local_auth/ios/Classes/FLTLocalAuthPlugin.m index c2dc9db25fc8..70113efa00a0 100644 --- a/packages/local_auth/local_auth/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth/ios/Classes/FLTLocalAuthPlugin.m @@ -122,7 +122,9 @@ - (void)authenticateWithBiometrics:(NSDictionary *)arguments NSError *authError = nil; self.lastCallArgs = nil; self.lastResult = nil; - context.localizedFallbackTitle = @""; + context.localizedFallbackTitle = arguments[@"localizedFallbackTitle"] == [NSNull null] + ? nil + : arguments[@"localizedFallbackTitle"]; if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authError]) { @@ -146,7 +148,9 @@ - (void)authenticate:(NSDictionary *)arguments withFlutterResult:(FlutterResult) NSError *authError = nil; _lastCallArgs = nil; _lastResult = nil; - context.localizedFallbackTitle = @""; + context.localizedFallbackTitle = arguments[@"localizedFallbackTitle"] == [NSNull null] + ? nil + : arguments[@"localizedFallbackTitle"]; if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError]) { [context evaluatePolicy:kLAPolicyDeviceOwnerAuthentication @@ -176,6 +180,7 @@ - (void)handleAuthReplyWithSuccess:(BOOL)success case LAErrorPasscodeNotSet: case LAErrorTouchIDNotAvailable: case LAErrorTouchIDNotEnrolled: + case LAErrorUserFallback: case LAErrorTouchIDLockout: [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; return; diff --git a/packages/local_auth/local_auth/lib/auth_strings.dart b/packages/local_auth/local_auth/lib/auth_strings.dart index 537340b79d4e..3e34659b8dad 100644 --- a/packages/local_auth/local_auth/lib/auth_strings.dart +++ b/packages/local_auth/local_auth/lib/auth_strings.dart @@ -68,12 +68,14 @@ class IOSAuthMessages { this.goToSettingsButton, this.goToSettingsDescription, this.cancelButton, + this.localizedFallbackTitle, }); final String? lockOut; final String? goToSettingsButton; final String? goToSettingsDescription; final String? cancelButton; + final String? localizedFallbackTitle; Map get args { return { @@ -82,6 +84,8 @@ class IOSAuthMessages { 'goToSettingDescriptionIOS': goToSettingsDescription ?? iOSGoToSettingsDescription, 'okButton': cancelButton ?? iOSOkButton, + if (localizedFallbackTitle != null) + 'localizedFallbackTitle': localizedFallbackTitle!, }; } } diff --git a/packages/local_auth/local_auth/pubspec.yaml b/packages/local_auth/local_auth/pubspec.yaml index cd9d0d9760f4..78c79f4abce4 100644 --- a/packages/local_auth/local_auth/pubspec.yaml +++ b/packages/local_auth/local_auth/pubspec.yaml @@ -3,7 +3,7 @@ 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: 1.1.10 +version: 1.1.11 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/local_auth/local_auth/test/local_auth_test.dart b/packages/local_auth/local_auth/test/local_auth_test.dart index 758b9cec0e97..3de9758f9d0c 100644 --- a/packages/local_auth/local_auth/test/local_auth_test.dart +++ b/packages/local_auth/local_auth/test/local_auth_test.dart @@ -40,14 +40,28 @@ void main() { expect( log, [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': true, - }..addAll(const AndroidAuthMessages().args)), + isMethodCall( + 'authenticate', + arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': true, + 'biometricHint': androidBiometricHint, + 'biometricNotRecognized': androidBiometricNotRecognized, + 'biometricSuccess': androidBiometricSuccess, + 'biometricRequired': androidBiometricRequiredTitle, + 'cancelButton': androidCancelButton, + 'deviceCredentialsRequired': + androidDeviceCredentialsRequiredTitle, + 'deviceCredentialsSetupDescription': + androidDeviceCredentialsSetupDescription, + 'goToSetting': goToSettings, + 'goToSettingDescription': androidGoToSettingsDescription, + 'signInTitle': androidSignInTitle, + }, + ), ], ); }); @@ -61,14 +75,45 @@ void main() { expect( log, [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': true, - }..addAll(const IOSAuthMessages().args)), + isMethodCall('authenticate', arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': true, + 'lockOut': iOSLockOut, + 'goToSetting': goToSettings, + 'goToSettingDescriptionIOS': iOSGoToSettingsDescription, + 'okButton': iOSOkButton, + }), + ], + ); + }); + + test('authenticate with `localizedFallbackTitle` on iOS.', () async { + const IOSAuthMessages iosAuthMessages = + IOSAuthMessages(localizedFallbackTitle: 'Enter PIN'); + setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios')); + await localAuthentication.authenticate( + localizedReason: 'Needs secure', + biometricOnly: true, + iOSAuthStrings: iosAuthMessages, + ); + expect( + log, + [ + isMethodCall('authenticate', arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': true, + 'lockOut': iOSLockOut, + 'goToSetting': goToSettings, + 'goToSettingDescriptionIOS': iOSGoToSettingsDescription, + 'okButton': iOSOkButton, + 'localizedFallbackTitle': 'Enter PIN', + }), ], ); }); @@ -95,14 +140,25 @@ void main() { expect( log, [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'Insecure', - 'useErrorDialogs': false, - 'stickyAuth': false, - 'sensitiveTransaction': false, - 'biometricOnly': true, - }..addAll(const AndroidAuthMessages().args)), + isMethodCall('authenticate', arguments: { + 'localizedReason': 'Insecure', + 'useErrorDialogs': false, + 'stickyAuth': false, + 'sensitiveTransaction': false, + 'biometricOnly': true, + 'biometricHint': androidBiometricHint, + 'biometricNotRecognized': androidBiometricNotRecognized, + 'biometricSuccess': androidBiometricSuccess, + 'biometricRequired': androidBiometricRequiredTitle, + 'cancelButton': androidCancelButton, + 'deviceCredentialsRequired': + androidDeviceCredentialsRequiredTitle, + 'deviceCredentialsSetupDescription': + androidDeviceCredentialsSetupDescription, + 'goToSetting': goToSettings, + 'goToSettingDescription': androidGoToSettingsDescription, + 'signInTitle': androidSignInTitle, + }), ], ); }); @@ -117,14 +173,25 @@ void main() { expect( log, [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': false, - }..addAll(const AndroidAuthMessages().args)), + isMethodCall('authenticate', arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': false, + 'biometricHint': androidBiometricHint, + 'biometricNotRecognized': androidBiometricNotRecognized, + 'biometricSuccess': androidBiometricSuccess, + 'biometricRequired': androidBiometricRequiredTitle, + 'cancelButton': androidCancelButton, + 'deviceCredentialsRequired': + androidDeviceCredentialsRequiredTitle, + 'deviceCredentialsSetupDescription': + androidDeviceCredentialsSetupDescription, + 'goToSetting': goToSettings, + 'goToSettingDescription': androidGoToSettingsDescription, + 'signInTitle': androidSignInTitle, + }), ], ); }); @@ -137,14 +204,17 @@ void main() { expect( log, [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': false, - }..addAll(const IOSAuthMessages().args)), + isMethodCall('authenticate', arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': false, + 'lockOut': iOSLockOut, + 'goToSetting': goToSettings, + 'goToSettingDescriptionIOS': iOSGoToSettingsDescription, + 'okButton': iOSOkButton, + }), ], ); }); @@ -159,14 +229,25 @@ void main() { expect( log, [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'Insecure', - 'useErrorDialogs': false, - 'stickyAuth': false, - 'sensitiveTransaction': false, - 'biometricOnly': false, - }..addAll(const AndroidAuthMessages().args)), + isMethodCall('authenticate', arguments: { + 'localizedReason': 'Insecure', + 'useErrorDialogs': false, + 'stickyAuth': false, + 'sensitiveTransaction': false, + 'biometricOnly': false, + 'biometricHint': androidBiometricHint, + 'biometricNotRecognized': androidBiometricNotRecognized, + 'biometricSuccess': androidBiometricSuccess, + 'biometricRequired': androidBiometricRequiredTitle, + 'cancelButton': androidCancelButton, + 'deviceCredentialsRequired': + androidDeviceCredentialsRequiredTitle, + 'deviceCredentialsSetupDescription': + androidDeviceCredentialsSetupDescription, + 'goToSetting': goToSettings, + 'goToSettingDescription': androidGoToSettingsDescription, + 'signInTitle': androidSignInTitle, + }), ], ); }); From 2c21e7dbcba5be5764a97dbc2d2ad58cccea5e68 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Wed, 16 Feb 2022 10:40:22 -0500 Subject: [PATCH 03/12] Roll Flutter from 286c9751d372 to 14a2b1323bf9 (1 revision) (#4865) --- .ci/flutter_master.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index bc5d568fa433..126098df94b1 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -286c9751d372e5f8e0857052378fa5c817321357 +14a2b1323bf96e6bd7333d5d8df3fac624e52448 From 304b83d75177c0d2b615527e6220f91b4d45086c Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 16 Feb 2022 09:15:23 -0800 Subject: [PATCH 04/12] [ci] Re-enable stable webview Android tests (#4867) --- .cirrus.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index c4951a6a7750..f2372a91f2f5 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -230,15 +230,7 @@ task: - export CIRRUS_COMMIT_MESSAGE="" - if [[ -n "$GCLOUD_FIREBASE_TESTLAB_KEY" ]]; then - echo $GCLOUD_FIREBASE_TESTLAB_KEY > ${HOME}/gcloud-service-key.json - # TODO(stuartmorgan): Remove this condition once - # https://github.com/flutter/flutter/issues/96661 is fixed and - # cherry picked. Currently webview_flutter tests crash on stable on - # Android so must be skipped. - - if [[ "$CHANNEL" == "stable" ]]; then - - ./script/tool_runner.sh firebase-test-lab --device model=redfin,version=30 --device model=starqlteue,version=26 --exclude=script/configs/exclude_integration_android.yaml,webview_flutter - - else - - ./script/tool_runner.sh firebase-test-lab --device model=redfin,version=30 --device model=starqlteue,version=26 --exclude=script/configs/exclude_integration_android.yaml - - fi + - ./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 From 0a579fe86cb1061655d694db7f1948424941e3ad Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 16 Feb 2022 09:50:25 -0800 Subject: [PATCH 05/12] [image_picker] Update platform interface analysis options (#4837) --- .../CHANGELOG.md | 4 ++ .../analysis_options.yaml | 1 - .../lib/image_picker_platform_interface.dart | 2 +- .../method_channel_image_picker.dart | 41 +++++++++++-------- .../image_picker_platform.dart | 2 +- .../lib/src/types/picked_file/base.dart | 3 +- .../lib/src/types/picked_file/html.dart | 15 +++---- .../lib/src/types/picked_file/io.dart | 6 +-- .../lib/src/types/types.dart | 4 +- .../pubspec.yaml | 5 +-- .../new_method_channel_image_picker_test.dart | 20 ++++----- .../test/picked_file_html_test.dart | 6 +-- .../test/picked_file_io_test.dart | 6 +-- script/configs/custom_analysis.yaml | 1 - 14 files changed, 63 insertions(+), 53 deletions(-) delete mode 100644 packages/image_picker/image_picker_platform_interface/analysis_options.yaml diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index 8f80610f307a..9f6d1749c671 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 @@ +## 2.4.4 + +* Internal code cleanup for stricter analysis options. + ## 2.4.3 * Removes dependency on `meta`. diff --git a/packages/image_picker/image_picker_platform_interface/analysis_options.yaml b/packages/image_picker/image_picker_platform_interface/analysis_options.yaml deleted file mode 100644 index 5aeb4e7c5e21..000000000000 --- a/packages/image_picker/image_picker_platform_interface/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../../analysis_options_legacy.yaml diff --git a/packages/image_picker/image_picker_platform_interface/lib/image_picker_platform_interface.dart b/packages/image_picker/image_picker_platform_interface/lib/image_picker_platform_interface.dart index 133c05ecfebf..bdc168617567 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/image_picker_platform_interface.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/image_picker_platform_interface.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. +export 'package:cross_file/cross_file.dart'; export 'package:image_picker_platform_interface/src/platform_interface/image_picker_platform.dart'; export 'package:image_picker_platform_interface/src/types/types.dart'; -export 'package:cross_file/cross_file.dart'; diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart index 3731c5626ea2..e1e6082c8047 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart @@ -9,7 +9,7 @@ import 'package:flutter/services.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; -final MethodChannel _channel = MethodChannel('plugins.flutter.io/image_picker'); +const MethodChannel _channel = MethodChannel('plugins.flutter.io/image_picker'); /// An implementation of [ImagePickerPlatform] that uses method channels. class MethodChannelImagePicker extends ImagePickerPlatform { @@ -25,7 +25,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { - String? path = await _getImagePath( + final String? path = await _getImagePath( source: source, maxWidth: maxWidth, maxHeight: maxHeight, @@ -46,9 +46,11 @@ class MethodChannelImagePicker extends ImagePickerPlatform { maxHeight: maxHeight, imageQuality: imageQuality, ); - if (paths == null) return null; + if (paths == null) { + return null; + } - return paths.map((path) => PickedFile(path)).toList(); + return paths.map((dynamic path) => PickedFile(path as String)).toList(); } Future?> _getMultiImagePath({ @@ -151,7 +153,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { assert(result.containsKey('path') != result.containsKey('errorCode')); - final String? type = result['type']; + final String? type = result['type'] as String?; assert(type == kTypeImage || type == kTypeVideo); RetrieveType? retrieveType; @@ -164,10 +166,11 @@ class MethodChannelImagePicker extends ImagePickerPlatform { PlatformException? exception; if (result.containsKey('errorCode')) { exception = PlatformException( - code: result['errorCode'], message: result['errorMessage']); + code: result['errorCode']! as String, + message: result['errorMessage'] as String?); } - final String? path = result['path']; + final String? path = result['path'] as String?; return LostData( file: path != null ? PickedFile(path) : null, @@ -184,7 +187,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { - String? path = await _getImagePath( + final String? path = await _getImagePath( source: source, maxWidth: maxWidth, maxHeight: maxHeight, @@ -205,9 +208,11 @@ class MethodChannelImagePicker extends ImagePickerPlatform { maxHeight: maxHeight, imageQuality: imageQuality, ); - if (paths == null) return null; + if (paths == null) { + return null; + } - return paths.map((path) => XFile(path)).toList(); + return paths.map((dynamic path) => XFile(path as String)).toList(); } @override @@ -228,7 +233,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { Future getLostData() async { List? pickedFileList; - Map? result = + final Map? result = await _channel.invokeMapMethod('retrieve'); if (result == null) { @@ -237,7 +242,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { assert(result.containsKey('path') != result.containsKey('errorCode')); - final String? type = result['type']; + final String? type = result['type'] as String?; assert(type == kTypeImage || type == kTypeVideo); RetrieveType? retrieveType; @@ -250,15 +255,17 @@ class MethodChannelImagePicker extends ImagePickerPlatform { PlatformException? exception; if (result.containsKey('errorCode')) { exception = PlatformException( - code: result['errorCode'], message: result['errorMessage']); + code: result['errorCode']! as String, + message: result['errorMessage'] as String?); } - final String? path = result['path']; + final String? path = result['path'] as String?; - final pathList = result['pathList']; + final List? pathList = + (result['pathList'] as List?)?.cast(); if (pathList != null) { - pickedFileList = []; - for (String path in pathList) { + pickedFileList = []; + for (final String path in pathList) { pickedFileList.add(XFile(path)); } } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart index dbc8d89f8ac0..8f02e1683267 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart @@ -5,9 +5,9 @@ import 'dart:async'; import 'package:cross_file/cross_file.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:image_picker_platform_interface/src/method_channel/method_channel_image_picker.dart'; import 'package:image_picker_platform_interface/src/types/types.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; /// The interface that implementations of image_picker must implement. /// diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart index d63bc6aad993..77bf87ca045d 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart @@ -18,7 +18,8 @@ import 'package:flutter/foundation.dart' show immutable; @immutable abstract class PickedFileBase { /// Construct a PickedFile - PickedFileBase(String path); + // ignore: avoid_unused_constructor_parameters + const PickedFileBase(String path); /// Get the path of the picked file. /// diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart index 24e1931008b6..7d9761a57602 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart @@ -13,20 +13,21 @@ import './base.dart'; /// /// It wraps the bytes of a selected file. class PickedFile extends PickedFileBase { - final String path; - final Uint8List? _initBytes; - /// Construct a PickedFile object from its ObjectUrl. /// /// Optionally, this can be initialized with `bytes` /// so no http requests are performed to retrieve files later. - PickedFile(this.path, {Uint8List? bytes}) + const PickedFile(this.path, {Uint8List? bytes}) : _initBytes = bytes, super(path); + @override + final String path; + final Uint8List? _initBytes; + Future get _bytes async { if (_initBytes != null) { - return Future.value(UnmodifiableUint8ListView(_initBytes!)); + return Future.value(UnmodifiableUint8ListView(_initBytes!)); } return http.readBytes(Uri.parse(path)); } @@ -38,12 +39,12 @@ class PickedFile extends PickedFileBase { @override Future readAsBytes() async { - return Future.value(await _bytes); + return Future.value(await _bytes); } @override Stream openRead([int? start, int? end]) async* { - final bytes = await _bytes; + final Uint8List bytes = await _bytes; yield bytes.sublist(start ?? 0, end ?? bytes.length); } } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart index 7037b6b7121a..500cc65a0870 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart @@ -10,13 +10,13 @@ import './base.dart'; /// A PickedFile backed by a dart:io File. class PickedFile extends PickedFileBase { - final File _file; - /// Construct a PickedFile object backed by a dart:io File. PickedFile(String path) : _file = File(path), super(path); + final File _file; + @override String get path { return _file.path; @@ -36,6 +36,6 @@ class PickedFile extends PickedFileBase { Stream openRead([int? start, int? end]) { return _file .openRead(start ?? 0, end) - .map((chunk) => Uint8List.fromList(chunk)); + .map((List chunk) => Uint8List.fromList(chunk)); } } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart index ad7cd3fbcaab..7f2844230287 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart @@ -4,9 +4,9 @@ export 'camera_device.dart'; export 'image_source.dart'; -export 'retrieve_type.dart'; -export 'picked_file/picked_file.dart'; export 'lost_data_response.dart'; +export 'picked_file/picked_file.dart'; +export 'retrieve_type.dart'; /// Denotes that an image is being picked. const String kTypeImage = 'image'; diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index 0dfe2c009d59..54fd17e47260 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -4,20 +4,19 @@ repository: https://github.com/flutter/plugins/tree/main/packages/image_picker/i issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.4.3 +version: 2.4.4 environment: sdk: ">=2.12.0 <3.0.0" flutter: ">=2.0.0" dependencies: + cross_file: ^0.3.1+1 flutter: sdk: flutter http: ^0.13.0 plugin_platform_interface: ^2.1.0 - cross_file: ^0.3.1+1 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.10.0 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 17caa8456621..79d971f217f0 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 @@ -12,7 +12,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('$MethodChannelImagePicker', () { - MethodChannelImagePicker picker = MethodChannelImagePicker(); + final MethodChannelImagePicker picker = MethodChannelImagePicker(); final List log = []; dynamic returnValue = ''; @@ -223,7 +223,7 @@ void main() { group('#pickMultiImage', () { test('calls the method correctly', () async { - returnValue = ['0', '1']; + returnValue = ['0', '1']; await picker.pickMultiImage(); expect( @@ -239,7 +239,7 @@ void main() { }); test('passes the width and height arguments correctly', () async { - returnValue = ['0', '1']; + returnValue = ['0', '1']; await picker.pickMultiImage(); await picker.pickMultiImage( maxWidth: 10.0, @@ -308,7 +308,7 @@ void main() { }); test('does not accept a negative width or height argument', () { - returnValue = ['0', '1']; + returnValue = ['0', '1']; expect( () => picker.pickMultiImage(maxWidth: -1.0), throwsArgumentError, @@ -321,7 +321,7 @@ void main() { }); test('does not accept a invalid imageQuality argument', () { - returnValue = ['0', '1']; + returnValue = ['0', '1']; expect( () => picker.pickMultiImage(imageQuality: -1), throwsArgumentError, @@ -691,7 +691,7 @@ void main() { group('#getMultiImage', () { test('calls the method correctly', () async { - returnValue = ['0', '1']; + returnValue = ['0', '1']; await picker.getMultiImage(); expect( @@ -707,7 +707,7 @@ void main() { }); test('passes the width and height arguments correctly', () async { - returnValue = ['0', '1']; + returnValue = ['0', '1']; await picker.getMultiImage(); await picker.getMultiImage( maxWidth: 10.0, @@ -776,7 +776,7 @@ void main() { }); test('does not accept a negative width or height argument', () { - returnValue = ['0', '1']; + returnValue = ['0', '1']; expect( () => picker.getMultiImage(maxWidth: -1.0), throwsArgumentError, @@ -789,7 +789,7 @@ void main() { }); test('does not accept a invalid imageQuality argument', () { - returnValue = ['0', '1']; + returnValue = ['0', '1']; expect( () => picker.getMultiImage(imageQuality: -1), throwsArgumentError, @@ -934,7 +934,7 @@ void main() { return { 'type': 'image', 'path': '/example/path1', - 'pathList': ['/example/path0', '/example/path1'], + 'pathList': ['/example/path0', '/example/path1'], }; }); final LostDataResponse response = await picker.getLostData(); diff --git a/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart b/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart index 7721f66148e0..17233376114a 100644 --- a/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart +++ b/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart @@ -10,14 +10,14 @@ import 'dart:html' as html; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; -final String expectedStringContents = 'Hello, world!'; +const String expectedStringContents = 'Hello, world!'; final List bytes = utf8.encode(expectedStringContents); -final html.File textFile = html.File([bytes], 'hello.txt'); +final html.File textFile = html.File(>[bytes], 'hello.txt'); final String textFileUrl = html.Url.createObjectUrl(textFile); void main() { group('Create with an objectUrl', () { - final pickedFile = PickedFile(textFileUrl); + final PickedFile pickedFile = PickedFile(textFileUrl); test('Can be read as a string', () async { expect(await pickedFile.readAsString(), equals(expectedStringContents)); diff --git a/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart b/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart index d366204c36bf..3201d3adea41 100644 --- a/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart +++ b/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart @@ -11,10 +11,10 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; -final pathPrefix = +final String pathPrefix = Directory.current.path.endsWith('test') ? './assets/' : './test/assets/'; -final path = pathPrefix + 'hello.txt'; -final String expectedStringContents = 'Hello, world!'; +final String path = pathPrefix + 'hello.txt'; +const String expectedStringContents = 'Hello, world!'; final Uint8List bytes = Uint8List.fromList(utf8.encode(expectedStringContents)); final File textFile = File(path); final String textFilePath = textFile.path; diff --git a/script/configs/custom_analysis.yaml b/script/configs/custom_analysis.yaml index 13a6819acd79..b802db11011c 100644 --- a/script/configs/custom_analysis.yaml +++ b/script/configs/custom_analysis.yaml @@ -17,7 +17,6 @@ - google_sign_in/google_sign_in - google_sign_in/google_sign_in_platform_interface - google_sign_in/google_sign_in_web -- image_picker/image_picker_platform_interface - in_app_purchase/in_app_purchase - in_app_purchase/in_app_purchase_android - in_app_purchase/in_app_purchase_storekit From 956cdcdcb313f4e9a630d7dc9452b29c42bdd3ad Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Wed, 16 Feb 2022 13:55:15 -0500 Subject: [PATCH 06/12] Roll Flutter from 14a2b1323bf9 to b62327903d31 (5 revisions) (#4870) --- .ci/flutter_master.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index 126098df94b1..8169840cef6f 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -14a2b1323bf96e6bd7333d5d8df3fac624e52448 +b62327903d3190a50abd4b6c3693583f560285f3 From a4081546aebbc1ee3feb8f65eb1ed20c1d373f1a Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Wed, 16 Feb 2022 16:50:23 -0500 Subject: [PATCH 07/12] Roll Flutter from b62327903d31 to 919d20511cf6 (2 revisions) (#4871) --- .ci/flutter_master.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index 8169840cef6f..e152d93498ce 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -b62327903d3190a50abd4b6c3693583f560285f3 +919d20511cf667d1dc0b8b4bac06b202b60af641 From 6f1f8d8017eba88158cabb5c53317603c5f24332 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 16 Feb 2022 14:30:23 -0800 Subject: [PATCH 08/12] [google_sign_in] Update platform interface analysis options (#4872) --- .../CHANGELOG.md | 4 ++++ .../analysis_options.yaml | 1 - .../src/method_channel_google_sign_in.dart | 2 +- .../lib/src/types.dart | 24 +++++++++++++++---- .../lib/src/utils.dart | 18 +++++++------- .../pubspec.yaml | 3 +-- ...oogle_sign_in_platform_interface_test.dart | 2 +- .../method_channel_google_sign_in_test.dart | 18 +++++++------- script/configs/custom_analysis.yaml | 1 - 9 files changed, 46 insertions(+), 27 deletions(-) delete mode 100644 packages/google_sign_in/google_sign_in_platform_interface/analysis_options.yaml 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 928186029a48..66fdb3e72a56 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,3 +1,7 @@ +## 2.1.2 + +* Internal code cleanup for stricter analysis options. + ## 2.1.1 * Removes dependency on `meta`. diff --git a/packages/google_sign_in/google_sign_in_platform_interface/analysis_options.yaml b/packages/google_sign_in/google_sign_in_platform_interface/analysis_options.yaml deleted file mode 100644 index 5aeb4e7c5e21..000000000000 --- a/packages/google_sign_in/google_sign_in_platform_interface/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../../analysis_options_legacy.yaml diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart index c569133a1b34..1abda09fa99f 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart @@ -55,7 +55,7 @@ class MethodChannelGoogleSignIn extends GoogleSignInPlatform { .invokeMapMethod('getTokens', { 'email': email, 'shouldRecoverAuth': shouldRecoverAuth, - }).then((result) => getTokenDataFromMap(result!)); + }).then((Map? result) => getTokenDataFromMap(result!)); } @override diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart index e30966f87598..bc50a1d2516d 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart @@ -71,13 +71,21 @@ class GoogleSignInUserData { String? serverAuthCode; @override + // TODO(stuartmorgan): Make this class immutable in the next breaking change. + // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => hashObjects( [displayName, email, id, photoUrl, idToken, serverAuthCode]); @override + // TODO(stuartmorgan): Make this class immutable in the next breaking change. + // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(dynamic other) { - if (identical(this, other)) return true; - if (other is! GoogleSignInUserData) return false; + if (identical(this, other)) { + return true; + } + if (other is! GoogleSignInUserData) { + return false; + } final GoogleSignInUserData otherUserData = other; return otherUserData.displayName == displayName && otherUserData.email == email && @@ -107,12 +115,20 @@ class GoogleSignInTokenData { String? serverAuthCode; @override + // TODO(stuartmorgan): Make this class immutable in the next breaking change. + // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => hash3(idToken, accessToken, serverAuthCode); @override + // TODO(stuartmorgan): Make this class immutable in the next breaking change. + // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(dynamic other) { - if (identical(this, other)) return true; - if (other is! GoogleSignInTokenData) return false; + if (identical(this, other)) { + return true; + } + if (other is! GoogleSignInTokenData) { + return false; + } final GoogleSignInTokenData otherTokenData = other; return otherTokenData.idToken == idToken && otherTokenData.accessToken == accessToken && diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart index 0d89835fb498..6f03a6c357fe 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart @@ -10,19 +10,19 @@ GoogleSignInUserData? getUserDataFromMap(Map? data) { return null; } return GoogleSignInUserData( - email: data['email']!, - id: data['id']!, - displayName: data['displayName'], - photoUrl: data['photoUrl'], - idToken: data['idToken'], - serverAuthCode: data['serverAuthCode']); + email: data['email']! as String, + id: data['id']! as String, + displayName: data['displayName'] as String?, + photoUrl: data['photoUrl'] as String?, + idToken: data['idToken'] as String?, + serverAuthCode: data['serverAuthCode'] as String?); } /// Converts token data coming from native code into the proper platform interface type. GoogleSignInTokenData getTokenDataFromMap(Map data) { return GoogleSignInTokenData( - idToken: data['idToken'], - accessToken: data['accessToken'], - serverAuthCode: data['serverAuthCode'], + idToken: data['idToken'] as String?, + accessToken: data['accessToken'] as String?, + serverAuthCode: data['serverAuthCode'] as String?, ); } 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 f5a7eb866ef9..a5bbaedd51e7 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 @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/main/packages/google_sign_in issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.1.1 +version: 2.1.2 environment: sdk: ">=2.12.0 <3.0.0" @@ -19,4 +19,3 @@ dev_dependencies: flutter_test: sdk: flutter mockito: ^5.0.0 - pedantic: ^1.10.0 diff --git a/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart b/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart index a3450b6f3fb9..78e57a3eb2ac 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart @@ -2,8 +2,8 @@ // 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'; import 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'package:mockito/mockito.dart'; void main() { 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 fcb443f84293..a1d83c3f05e6 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 @@ -9,10 +9,10 @@ import 'package:google_sign_in_platform_interface/src/types.dart'; import 'package:google_sign_in_platform_interface/src/utils.dart'; const Map kUserData = { - "email": "john.doe@gmail.com", - "id": "8162538176523816253123", - "photoUrl": "https://lh5.googleusercontent.com/photo.jpg", - "displayName": "John Doe", + 'email': 'john.doe@gmail.com', + 'id': '8162538176523816253123', + 'photoUrl': 'https://lh5.googleusercontent.com/photo.jpg', + 'displayName': 'John Doe', 'idToken': '123', 'serverAuthCode': '789', }; @@ -35,7 +35,7 @@ const Map kDefaultResponses = { }; final GoogleSignInUserData? kUser = getUserDataFromMap(kUserData); -final GoogleSignInTokenData? kToken = +final GoogleSignInTokenData kToken = getTokenDataFromMap(kTokenData as Map); void main() { @@ -122,16 +122,18 @@ void main() { 'token': 'abc', }), () { - googleSignIn.requestScopes(['newScope', 'anotherScope']); + googleSignIn.requestScopes(['newScope', 'anotherScope']); }: isMethodCall('requestScopes', arguments: { - 'scopes': ['newScope', 'anotherScope'], + 'scopes': ['newScope', 'anotherScope'], }), googleSignIn.signOut: isMethodCall('signOut', arguments: null), googleSignIn.disconnect: isMethodCall('disconnect', arguments: null), googleSignIn.isSignedIn: isMethodCall('isSignedIn', arguments: null), }; - tests.keys.forEach((Function f) => f()); + for (final Function f in tests.keys) { + f(); + } expect(log, tests.values); }); diff --git a/script/configs/custom_analysis.yaml b/script/configs/custom_analysis.yaml index b802db11011c..0022beee149a 100644 --- a/script/configs/custom_analysis.yaml +++ b/script/configs/custom_analysis.yaml @@ -15,7 +15,6 @@ - google_maps_flutter/google_maps_flutter_platform_interface - google_maps_flutter/google_maps_flutter_web - google_sign_in/google_sign_in -- google_sign_in/google_sign_in_platform_interface - google_sign_in/google_sign_in_web - in_app_purchase/in_app_purchase - in_app_purchase/in_app_purchase_android From 6407c3e16d298eaf34e203c77d9674ab3bc21dc0 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Wed, 16 Feb 2022 19:00:21 -0500 Subject: [PATCH 09/12] Roll Flutter from 919d20511cf6 to adafd66d9cc7 (5 revisions) (#4876) --- .ci/flutter_master.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index e152d93498ce..c2fc9dc220d4 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -919d20511cf667d1dc0b8b4bac06b202b60af641 +adafd66d9cc7796214404c8117395e207bf23d01 From 0b969a4282ddc185f972075873764f70bda5a69c Mon Sep 17 00:00:00 2001 From: hellohuanlin <41930132+hellohuanlin@users.noreply.github.com> Date: Wed, 16 Feb 2022 17:10:18 -0800 Subject: [PATCH 10/12] [camera]remove "selfRef" for SavePhotoDelegate and ensure thread safety (#4780) --- packages/camera/camera/CHANGELOG.md | 4 + .../ios/Runner.xcodeproj/project.pbxproj | 14 ++- .../ios/RunnerTests/FLTCamPhotoCaptureTests.m | 114 ++++++++++++++++++ ...QueueTests.m => FLTCamSampleBufferTests.m} | 4 +- .../RunnerTests/FLTSavePhotoDelegateTests.m | 93 +++++++------- .../camera/camera/ios/Classes/CameraPlugin.m | 4 + packages/camera/camera/ios/Classes/FLTCam.m | 36 ++++-- .../camera/camera/ios/Classes/FLTCam_Test.h | 13 ++ .../camera/ios/Classes/FLTSavePhotoDelegate.h | 27 +++-- .../camera/ios/Classes/FLTSavePhotoDelegate.m | 24 ++-- .../ios/Classes/FLTSavePhotoDelegate_Test.h | 5 + .../camera/camera/ios/Classes/QueueHelper.h | 10 ++ .../camera/camera/ios/Classes/QueueHelper.m | 4 + packages/camera/camera/pubspec.yaml | 2 +- 14 files changed, 272 insertions(+), 82 deletions(-) create mode 100644 packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m rename packages/camera/camera/example/ios/RunnerTests/{SampleBufferQueueTests.m => FLTCamSampleBufferTests.m} (93%) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index d2b2f907a6fb..0089130b5e90 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.4+13 + +* Updates iOS camera's photo capture delegate reference on a background queue to prevent potential race conditions, and some related internal code cleanup. + ## 0.9.4+12 * Skips unnecessary AppDelegate setup for unit tests on iOS. diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index ac39de2e3f84..5f788fc9b9f9 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 46; objects = { /* Begin PBXBuildFile section */ @@ -23,11 +23,12 @@ E01EE4A82799F3A5008C1950 /* QueueHelperTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */; }; E032F250279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */; }; E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */; }; + E071CF7227B3061B006EF3BA /* FLTCamPhotoCaptureTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */; }; + E071CF7427B31DE4006EF3BA /* FLTCamSampleBufferTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */; }; E0C6E2002770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */; }; E0C6E2012770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */; }; E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */; }; E0F95E3D27A32AB900699390 /* CameraPropertiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0F95E3C27A32AB900699390 /* CameraPropertiesTests.m */; }; - E0F95E4427A36B9200699390 /* SampleBufferQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0F95E4327A36B9200699390 /* SampleBufferQueueTests.m */; }; E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */; }; F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */ = {isa = PBXBuildFile; fileRef = F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */; }; /* End PBXBuildFile section */ @@ -85,11 +86,12 @@ E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QueueHelperTests.m; sourceTree = ""; }; E032F24F279F5E94009E9028 /* CameraCaptureSessionQueueRaceConditionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CameraCaptureSessionQueueRaceConditionTests.m; sourceTree = ""; }; E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTSavePhotoDelegateTests.m; sourceTree = ""; }; + E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTCamPhotoCaptureTests.m; sourceTree = ""; }; + E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTCamSampleBufferTests.m; sourceTree = ""; }; E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeMethodChannelTests.m; sourceTree = ""; }; E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeTextureRegistryTests.m; sourceTree = ""; }; E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeEventChannelTests.m; sourceTree = ""; }; E0F95E3C27A32AB900699390 /* CameraPropertiesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPropertiesTests.m; sourceTree = ""; }; - E0F95E4327A36B9200699390 /* SampleBufferQueueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SampleBufferQueueTests.m; sourceTree = ""; }; E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPreviewPauseTests.m; sourceTree = ""; }; F63F9EED27143B19002479BF /* MockFLTThreadSafeFlutterResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockFLTThreadSafeFlutterResult.h; sourceTree = ""; }; F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockFLTThreadSafeFlutterResult.m; sourceTree = ""; }; @@ -126,8 +128,9 @@ E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */, E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */, E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */, - E0F95E4327A36B9200699390 /* SampleBufferQueueTests.m */, E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */, + E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */, + E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */, E01EE4A72799F3A5008C1950 /* QueueHelperTests.m */, E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */, F6EE622E2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m */, @@ -396,12 +399,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - E0F95E4427A36B9200699390 /* SampleBufferQueueTests.m in Sources */, 03F6F8B226CBB4670024B8D3 /* ThreadSafeFlutterResultTests.m in Sources */, 033B94BE269C40A200B4DF97 /* CameraMethodChannelTests.m in Sources */, + E071CF7227B3061B006EF3BA /* FLTCamPhotoCaptureTests.m in Sources */, E0F95E3D27A32AB900699390 /* CameraPropertiesTests.m in Sources */, 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */, E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */, + E071CF7427B31DE4006EF3BA /* FLTCamSampleBufferTests.m in Sources */, E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */, F6EE622F2710A6FC00905E4A /* MockFLTThreadSafeFlutterResult.m in Sources */, 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */, diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m new file mode 100644 index 000000000000..fdb2abd4933e --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/FLTCamPhotoCaptureTests.m @@ -0,0 +1,114 @@ +// Copyright 2013 The Flutter 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 camera; +@import camera.Test; +@import AVFoundation; +@import XCTest; +#import + +@interface FLTCamPhotoCaptureTests : XCTestCase + +@end + +@implementation FLTCamPhotoCaptureTests + +- (void)testCaptureToFile_mustReportErrorToResultIfSavePhotoDelegateCompletionsWithError { + XCTestExpectation *errorExpectation = + [self expectationWithDescription: + @"Must send error to result if save photo delegate completes with error."]; + + dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL); + dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific, + (void *)FLTCaptureSessionQueueSpecific, NULL); + FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue]; + AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; + id mockSettings = OCMClassMock([AVCapturePhotoSettings class]); + OCMStub([mockSettings photoSettings]).andReturn(settings); + + NSError *error = [NSError errorWithDomain:@"test" code:0 userInfo:nil]; + id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); + OCMStub([mockResult sendError:error]).andDo(^(NSInvocation *invocation) { + [errorExpectation fulfill]; + }); + + id mockOutput = OCMClassMock([AVCapturePhotoOutput class]); + OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)]; + // Completion runs on IO queue. + dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL); + dispatch_async(ioQueue, ^{ + delegate.completionHandler(nil, error); + }); + }); + cam.capturePhotoOutput = mockOutput; + + // `FLTCam::captureToFile` runs on capture session queue. + dispatch_async(captureSessionQueue, ^{ + [cam captureToFile:mockResult]; + }); + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testCaptureToFile_mustReportPathToResultIfSavePhotoDelegateCompletionsWithPath { + XCTestExpectation *pathExpectation = + [self expectationWithDescription: + @"Must send file path to result if save photo delegate completes with file path."]; + + dispatch_queue_t captureSessionQueue = dispatch_queue_create("capture_session_queue", NULL); + dispatch_queue_set_specific(captureSessionQueue, FLTCaptureSessionQueueSpecific, + (void *)FLTCaptureSessionQueueSpecific, NULL); + FLTCam *cam = [self createFLTCamWithCaptureSessionQueue:captureSessionQueue]; + + AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; + id mockSettings = OCMClassMock([AVCapturePhotoSettings class]); + OCMStub([mockSettings photoSettings]).andReturn(settings); + + NSString *filePath = @"test"; + id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); + OCMStub([mockResult sendSuccessWithData:filePath]).andDo(^(NSInvocation *invocation) { + [pathExpectation fulfill]; + }); + + id mockOutput = OCMClassMock([AVCapturePhotoOutput class]); + OCMStub([mockOutput capturePhotoWithSettings:OCMOCK_ANY delegate:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + FLTSavePhotoDelegate *delegate = cam.inProgressSavePhotoDelegates[@(settings.uniqueID)]; + // Completion runs on IO queue. + dispatch_queue_t ioQueue = dispatch_queue_create("io_queue", NULL); + dispatch_async(ioQueue, ^{ + delegate.completionHandler(filePath, nil); + }); + }); + cam.capturePhotoOutput = mockOutput; + + // `FLTCam::captureToFile` runs on capture session queue. + dispatch_async(captureSessionQueue, ^{ + [cam captureToFile:mockResult]; + }); + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +/// Creates an `FLTCam` that runs its operations on a given capture session queue. +- (FLTCam *)createFLTCamWithCaptureSessionQueue:(dispatch_queue_t)captureSessionQueue { + id inputMock = OCMClassMock([AVCaptureDeviceInput class]); + OCMStub([inputMock deviceInputWithDevice:[OCMArg any] error:[OCMArg setTo:nil]]) + .andReturn(inputMock); + + id sessionMock = OCMClassMock([AVCaptureSession class]); + OCMStub([sessionMock alloc]).andReturn(sessionMock); + OCMStub([sessionMock addInputWithNoConnections:[OCMArg any]]); // no-op + OCMStub([sessionMock canSetSessionPreset:[OCMArg any]]).andReturn(YES); + + return [[FLTCam alloc] initWithCameraName:@"camera" + resolutionPreset:@"medium" + enableAudio:true + orientation:UIDeviceOrientationPortrait + captureSessionQueue:captureSessionQueue + error:nil]; +} + +@end diff --git a/packages/camera/camera/example/ios/RunnerTests/SampleBufferQueueTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTCamSampleBufferTests.m similarity index 93% rename from packages/camera/camera/example/ios/RunnerTests/SampleBufferQueueTests.m rename to packages/camera/camera/example/ios/RunnerTests/FLTCamSampleBufferTests.m index 19cead905f61..ccc8de5b23bd 100644 --- a/packages/camera/camera/example/ios/RunnerTests/SampleBufferQueueTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/FLTCamSampleBufferTests.m @@ -8,11 +8,11 @@ @import XCTest; #import -@interface SampleBufferQueueTests : XCTestCase +@interface FLTCamSampleBufferTests : XCTestCase @end -@implementation SampleBufferQueueTests +@implementation FLTCamSampleBufferTests - (void)testSampleBufferCallbackQueueMustBeCaptureSessionQueue { id inputMock = OCMClassMock([AVCaptureDeviceInput class]); diff --git a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m index b6ea84da449c..9e8e2441f0b9 100644 --- a/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/FLTSavePhotoDelegateTests.m @@ -14,40 +14,44 @@ @interface FLTSavePhotoDelegateTests : XCTestCase @implementation FLTSavePhotoDelegateTests -- (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToCapture { - NSError *error = [NSError errorWithDomain:@"test" code:0 userInfo:nil]; - dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); - id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); - FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test" - result:mockResult - ioQueue:ioQueue]; +- (void)testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToCapture { + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"Must complete with error if failed to capture photo."]; - [delegate handlePhotoCaptureResultWithError:error + NSError *captureError = [NSError errorWithDomain:@"test" code:0 userInfo:nil]; + dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); + FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] + initWithPath:@"test" + ioQueue:ioQueue + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { + XCTAssertEqualObjects(captureError, error); + XCTAssertNil(path); + [completionExpectation fulfill]; + }]; + + [delegate handlePhotoCaptureResultWithError:captureError photoDataProvider:^NSData * { return nil; }]; - OCMVerify([mockResult sendError:error]); + [self waitForExpectationsWithTimeout:1 handler:nil]; } -- (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToWrite { - XCTestExpectation *resultExpectation = - [self expectationWithDescription:@"Must send IOError to the result if failed to write file."]; +- (void)testHandlePhotoCaptureResult_mustCompleteWithErrorIfFailedToWrite { + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"Must complete with error if failed to write file."]; dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); - id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); NSError *ioError = [NSError errorWithDomain:@"IOError" code:0 userInfo:@{NSLocalizedDescriptionKey : @"Localized IO Error"}]; - - OCMStub([mockResult sendErrorWithCode:@"IOError" - message:@"Unable to write file" - details:ioError.localizedDescription]) - .andDo(^(NSInvocation *invocation) { - [resultExpectation fulfill]; - }); - FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test" - result:mockResult - ioQueue:ioQueue]; + FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] + initWithPath:@"test" + ioQueue:ioQueue + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { + XCTAssertEqualObjects(ioError, error); + XCTAssertNil(path); + [completionExpectation fulfill]; + }]; // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. @@ -63,23 +67,25 @@ - (void)testHandlePhotoCaptureResult_mustSendErrorIfFailedToWrite { [self waitForExpectationsWithTimeout:1 handler:nil]; } -- (void)testHandlePhotoCaptureResult_mustSendSuccessIfSuccessToWrite { - XCTestExpectation *resultExpectation = [self - expectationWithDescription:@"Must send file path to the result if success to write file."]; +- (void)testHandlePhotoCaptureResult_mustCompleteWithFilePathIfSuccessToWrite { + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"Must complete with file path if success to write file."]; dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); - id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); - FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test" - result:mockResult - ioQueue:ioQueue]; - OCMStub([mockResult sendSuccessWithData:delegate.path]).andDo(^(NSInvocation *invocation) { - [resultExpectation fulfill]; - }); + NSString *filePath = @"test"; + FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] + initWithPath:filePath + ioQueue:ioQueue + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqualObjects(filePath, path); + [completionExpectation fulfill]; + }]; // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. id mockData = OCMPartialMock([NSData data]); - OCMStub([mockData writeToFile:OCMOCK_ANY options:NSDataWritingAtomic error:[OCMArg setTo:nil]]) + OCMStub([mockData writeToFile:filePath options:NSDataWritingAtomic error:[OCMArg setTo:nil]]) .andReturn(YES); [delegate handlePhotoCaptureResultWithError:nil @@ -94,16 +100,12 @@ - (void)testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue [self expectationWithDescription:@"Data provider must run on io queue."]; XCTestExpectation *writeFileQueueExpectation = [self expectationWithDescription:@"File writing must run on io queue"]; - XCTestExpectation *resultExpectation = [self - expectationWithDescription:@"Must send file path to the result if success to write file."]; + XCTestExpectation *completionExpectation = + [self expectationWithDescription:@"Must complete with file path if success to write file."]; dispatch_queue_t ioQueue = dispatch_queue_create("test", NULL); const char *ioQueueSpecific = "io_queue_specific"; dispatch_queue_set_specific(ioQueue, ioQueueSpecific, (void *)ioQueueSpecific, NULL); - id mockResult = OCMClassMock([FLTThreadSafeFlutterResult class]); - OCMStub([mockResult sendSuccessWithData:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { - [resultExpectation fulfill]; - }); // We can't use OCMClassMock for NSData because some XCTest APIs uses NSData (e.g. // `XCTRunnerIDESession::logDebugMessage:`) on a private queue. @@ -116,9 +118,14 @@ - (void)testHandlePhotoCaptureResult_bothProvideDataAndSaveFileMustRunOnIOQueue }) .andReturn(YES); - FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] initWithPath:@"test" - result:mockResult - ioQueue:ioQueue]; + NSString *filePath = @"test"; + FLTSavePhotoDelegate *delegate = [[FLTSavePhotoDelegate alloc] + initWithPath:filePath + ioQueue:ioQueue + completionHandler:^(NSString *_Nullable path, NSError *_Nullable error) { + [completionExpectation fulfill]; + }]; + [delegate handlePhotoCaptureResultWithError:nil photoDataProvider:^NSData * { if (dispatch_get_specific(ioQueueSpecific)) { diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 97e995429e14..fcea190de705 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -13,6 +13,7 @@ #import "FLTThreadSafeFlutterResult.h" #import "FLTThreadSafeMethodChannel.h" #import "FLTThreadSafeTextureRegistry.h" +#import "QueueHelper.h" @interface CameraPlugin () @property(readonly, nonatomic) FLTThreadSafeTextureRegistry *registry; @@ -38,6 +39,9 @@ - (instancetype)initWithRegistry:(NSObject *)registry _registry = [[FLTThreadSafeTextureRegistry alloc] initWithTextureRegistry:registry]; _messenger = messenger; _captureSessionQueue = dispatch_queue_create("io.flutter.camera.captureSessionQueue", NULL); + dispatch_queue_set_specific(_captureSessionQueue, FLTCaptureSessionQueueSpecific, + (void *)FLTCaptureSessionQueueSpecific, NULL); + [self initDeviceEventMethodChannel]; [self startOrientationListener]; return self; diff --git a/packages/camera/camera/ios/Classes/FLTCam.m b/packages/camera/camera/ios/Classes/FLTCam.m index 94f985066675..31a9decd59b9 100644 --- a/packages/camera/camera/ios/Classes/FLTCam.m +++ b/packages/camera/camera/ios/Classes/FLTCam.m @@ -5,6 +5,7 @@ #import "FLTCam.h" #import "FLTCam_Test.h" #import "FLTSavePhotoDelegate.h" +#import "QueueHelper.h" @import CoreMotion; #import @@ -50,7 +51,6 @@ @interface FLTCam () *inProgressSavePhotoDelegates; + @end diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h index a773b4613931..40e4562e4483 100644 --- a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h +++ b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.h @@ -4,34 +4,35 @@ @import AVFoundation; @import Foundation; -@import Flutter; #import "FLTThreadSafeFlutterResult.h" NS_ASSUME_NONNULL_BEGIN +/// The completion handler block for save photo operations. +/// Can be called from either main queue or IO queue. +/// If success, `error` will be present and `path` will be nil. Otherewise, `error` will be nil and +/// `path` will be present. +/// @param path the path for successfully saved photo file. +/// @param error photo capture error or IO error. +typedef void (^FLTSavePhotoDelegateCompletionHandler)(NSString *_Nullable path, + NSError *_Nullable error); + /** Delegate object that handles photo capture results. */ @interface FLTSavePhotoDelegate : NSObject -/// The file path for the captured photo. -@property(readonly, nonatomic) NSString *path; -/// The thread safe flutter result wrapper to report the result. -@property(readonly, nonatomic) FLTThreadSafeFlutterResult *result; -/// The queue on which captured photos are wrote to disk. -@property(strong, nonatomic) dispatch_queue_t ioQueue; -/// Used to keep the delegate alive until didFinishProcessingPhotoSampleBuffer. -@property(strong, nonatomic, nullable) FLTSavePhotoDelegate *selfReference; /** * Initialize a photo capture delegate. * @param path the path for captured photo file. - * @param result the thread safe flutter result wrapper to report the result. - * @param ioQueue the queue on which captured photos are wrote to disk. + * @param ioQueue the queue on which captured photos are written to disk. + * @param completionHandler The completion handler block for save photo operations. Can + * be called from either main queue or IO queue. */ - (instancetype)initWithPath:(NSString *)path - result:(FLTThreadSafeFlutterResult *)result - ioQueue:(dispatch_queue_t)ioQueue; + ioQueue:(dispatch_queue_t)ioQueue + completionHandler:(FLTSavePhotoDelegateCompletionHandler)completionHandler; @end NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m index 8dadfec7fecd..ced3cb5e407f 100644 --- a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m +++ b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate.m @@ -3,37 +3,41 @@ // found in the LICENSE file. #import "FLTSavePhotoDelegate.h" +#import "FLTSavePhotoDelegate_Test.h" + +@interface FLTSavePhotoDelegate () +/// The file path for the captured photo. +@property(readonly, nonatomic) NSString *path; +/// The queue on which captured photos are written to disk. +@property(readonly, nonatomic) dispatch_queue_t ioQueue; +@end @implementation FLTSavePhotoDelegate - (instancetype)initWithPath:(NSString *)path - result:(FLTThreadSafeFlutterResult *)result - ioQueue:(dispatch_queue_t)ioQueue { + ioQueue:(dispatch_queue_t)ioQueue + completionHandler:(FLTSavePhotoDelegateCompletionHandler)completionHandler { self = [super init]; NSAssert(self, @"super init cannot be nil"); _path = path; - _selfReference = self; - _result = result; _ioQueue = ioQueue; + _completionHandler = completionHandler; return self; } - (void)handlePhotoCaptureResultWithError:(NSError *)error photoDataProvider:(NSData * (^)(void))photoDataProvider { - self.selfReference = nil; if (error) { - [self.result sendError:error]; + self.completionHandler(nil, error); return; } dispatch_async(self.ioQueue, ^{ NSData *data = photoDataProvider(); NSError *ioError; if ([data writeToFile:self.path options:NSDataWritingAtomic error:&ioError]) { - [self.result sendSuccessWithData:self.path]; + self.completionHandler(self.path, nil); } else { - [self.result sendErrorWithCode:@"IOError" - message:@"Unable to write file" - details:ioError.localizedDescription]; + self.completionHandler(nil, ioError); } }); } diff --git a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h index c0b77c7bac83..2d0d4f96be9d 100644 --- a/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h +++ b/packages/camera/camera/ios/Classes/FLTSavePhotoDelegate_Test.h @@ -9,6 +9,11 @@ */ @interface FLTSavePhotoDelegate () +/// The completion handler block for capture and save photo operations. +/// Can be called from either main queue or IO queue. +/// Exposed for unit tests to manually trigger the completion. +@property(readonly, nonatomic) FLTSavePhotoDelegateCompletionHandler completionHandler; + /// Handler to write captured photo data into a file. /// @param error the capture error. /// @param photoDataProvider a closure that provides photo data. diff --git a/packages/camera/camera/ios/Classes/QueueHelper.h b/packages/camera/camera/ios/Classes/QueueHelper.h index c2548148c499..dc7373220521 100644 --- a/packages/camera/camera/ios/Classes/QueueHelper.h +++ b/packages/camera/camera/ios/Classes/QueueHelper.h @@ -6,8 +6,18 @@ NS_ASSUME_NONNULL_BEGIN +/// Queue-specific context data to be associated with the capture session queue. +extern const char *FLTCaptureSessionQueueSpecific; + +/// A class that contains dispatch queue related helper functions. @interface QueueHelper : NSObject + +/// Ensures the given block to be run on the main queue. +/// If caller site is already on the main queue, the block will be run synchronously. Otherwise, the +/// block will be dispatched asynchronously to the main queue. +/// @param block the block to be run on the main queue. + (void)ensureToRunOnMainQueue:(void (^)(void))block; + @end NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera/ios/Classes/QueueHelper.m b/packages/camera/camera/ios/Classes/QueueHelper.m index 194dfa9cfb95..2cef7b677bfa 100644 --- a/packages/camera/camera/ios/Classes/QueueHelper.m +++ b/packages/camera/camera/ios/Classes/QueueHelper.m @@ -4,7 +4,10 @@ #import "QueueHelper.h" +const char *FLTCaptureSessionQueueSpecific = "capture_session_queue"; + @implementation QueueHelper + + (void)ensureToRunOnMainQueue:(void (^)(void))block { if (!NSThread.isMainThread) { dispatch_async(dispatch_get_main_queue(), block); @@ -12,4 +15,5 @@ + (void)ensureToRunOnMainQueue:(void (^)(void))block { block(); } } + @end diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 7421a7738ddd..80d0393087d8 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.9.4+12 +version: 0.9.4+13 environment: sdk: ">=2.14.0 <3.0.0" From 3fd2e5d0d54218a8d395ed00dd1135c3407d9070 Mon Sep 17 00:00:00 2001 From: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Date: Thu, 17 Feb 2022 07:04:14 -0800 Subject: [PATCH 11/12] [shared_preferences] upgraded ios to using pigeon (#4732) --- .../shared_preferences_ios/CHANGELOG.md | 4 + .../ios/Classes/FLTSharedPreferencesPlugin.m | 109 +++++----- .../ios/Classes/messages.g.h | 33 +++ .../ios/Classes/messages.g.m | 178 ++++++++++++++++ .../lib/messages.g.dart | 179 ++++++++++++++++ .../lib/shared_preferences_ios.dart | 67 +++--- .../pigeons/copyright_header.txt | 3 + .../pigeons/messages.dart | 22 ++ .../shared_preferences_ios/pubspec.yaml | 5 +- .../test/messages.g.dart | 145 +++++++++++++ .../test/shared_preferences_ios_test.dart | 194 ++++++++---------- 11 files changed, 745 insertions(+), 194 deletions(-) create mode 100644 packages/shared_preferences/shared_preferences_ios/ios/Classes/messages.g.h create mode 100644 packages/shared_preferences/shared_preferences_ios/ios/Classes/messages.g.m create mode 100644 packages/shared_preferences/shared_preferences_ios/lib/messages.g.dart create mode 100644 packages/shared_preferences/shared_preferences_ios/pigeons/copyright_header.txt create mode 100644 packages/shared_preferences/shared_preferences_ios/pigeons/messages.dart create mode 100644 packages/shared_preferences/shared_preferences_ios/test/messages.g.dart diff --git a/packages/shared_preferences/shared_preferences_ios/CHANGELOG.md b/packages/shared_preferences/shared_preferences_ios/CHANGELOG.md index b2a9f141ae68..a5cc1d34e034 100644 --- a/packages/shared_preferences/shared_preferences_ios/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.0 + +* Upgrades to using Pigeon. + ## 2.0.10 * Switches to an in-package method channel implementation. diff --git a/packages/shared_preferences/shared_preferences_ios/ios/Classes/FLTSharedPreferencesPlugin.m b/packages/shared_preferences/shared_preferences_ios/ios/Classes/FLTSharedPreferencesPlugin.m index 4d49e3b14619..bb11da2b5406 100644 --- a/packages/shared_preferences/shared_preferences_ios/ios/Classes/FLTSharedPreferencesPlugin.m +++ b/packages/shared_preferences/shared_preferences_ios/ios/Classes/FLTSharedPreferencesPlugin.m @@ -3,68 +3,7 @@ // found in the LICENSE file. #import "FLTSharedPreferencesPlugin.h" - -static NSString *const CHANNEL_NAME = @"plugins.flutter.io/shared_preferences_ios"; - -@implementation FLTSharedPreferencesPlugin - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:CHANNEL_NAME - binaryMessenger:registrar.messenger]; - [channel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) { - NSString *method = [call method]; - NSDictionary *arguments = [call arguments]; - - if ([method isEqualToString:@"getAll"]) { - result(getAllPrefs()); - } else if ([method isEqualToString:@"setBool"]) { - NSString *key = arguments[@"key"]; - NSNumber *value = arguments[@"value"]; - [[NSUserDefaults standardUserDefaults] setBool:value.boolValue forKey:key]; - result(@YES); - } else if ([method isEqualToString:@"setInt"]) { - NSString *key = arguments[@"key"]; - NSNumber *value = arguments[@"value"]; - // int type in Dart can come to native side in a variety of forms - // It is best to store it as is and send it back when needed. - // Platform channel will handle the conversion. - [[NSUserDefaults standardUserDefaults] setValue:value forKey:key]; - result(@YES); - } else if ([method isEqualToString:@"setDouble"]) { - NSString *key = arguments[@"key"]; - NSNumber *value = arguments[@"value"]; - [[NSUserDefaults standardUserDefaults] setDouble:value.doubleValue forKey:key]; - result(@YES); - } else if ([method isEqualToString:@"setString"]) { - NSString *key = arguments[@"key"]; - NSString *value = arguments[@"value"]; - [[NSUserDefaults standardUserDefaults] setValue:value forKey:key]; - result(@YES); - } else if ([method isEqualToString:@"setStringList"]) { - NSString *key = arguments[@"key"]; - NSArray *value = arguments[@"value"]; - [[NSUserDefaults standardUserDefaults] setValue:value forKey:key]; - result(@YES); - } else if ([method isEqualToString:@"commit"]) { - // synchronize is deprecated. - // "this method is unnecessary and shouldn't be used." - result(@YES); - } else if ([method isEqualToString:@"remove"]) { - [[NSUserDefaults standardUserDefaults] removeObjectForKey:arguments[@"key"]]; - result(@YES); - } else if ([method isEqualToString:@"clear"]) { - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - for (NSString *key in getAllPrefs()) { - [defaults removeObjectForKey:key]; - } - result(@YES); - } else { - result(FlutterMethodNotImplemented); - } - }]; -} - -#pragma mark - Private +#import "messages.g.h" static NSMutableDictionary *getAllPrefs() { NSString *appDomain = [[NSBundle mainBundle] bundleIdentifier]; @@ -80,4 +19,50 @@ + (void)registerWithRegistrar:(NSObject *)registrar { 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 new file mode 100644 index 000000000000..592402344a04 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/ios/Classes/messages.g.h @@ -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. +// 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 new file mode 100644 index 000000000000..ea8c45c7a66c --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/ios/Classes/messages.g.m @@ -0,0 +1,178 @@ +// Copyright 2013 The Flutter 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/lib/messages.g.dart b/packages/shared_preferences/shared_preferences_ios/lib/messages.g.dart new file mode 100644 index 000000000000..0e76291fb655 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/lib/messages.g.dart @@ -0,0 +1,179 @@ +// Copyright 2013 The Flutter 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 +// 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 _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(); + + 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) { + 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?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setBool(String arg_key, bool arg_value) async { + 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) { + 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?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setDouble(String arg_key, double arg_value) async { + 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) { + 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?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setValue(String arg_key, Object arg_value) async { + 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) { + 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?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future> getAll() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UserDefaultsApi.getAll', 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.', + details: null, + ); + } 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 Map?)! + .cast(); + } + } + + Future clear() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UserDefaultsApi.clear', 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.', + details: null, + ); + } 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; + } + } +} diff --git a/packages/shared_preferences/shared_preferences_ios/lib/shared_preferences_ios.dart b/packages/shared_preferences/shared_preferences_ios/lib/shared_preferences_ios.dart index 15f1e2f9d94d..10638840804e 100644 --- a/packages/shared_preferences/shared_preferences_ios/lib/shared_preferences_ios.dart +++ b/packages/shared_preferences/shared_preferences_ios/lib/shared_preferences_ios.dart @@ -2,52 +2,65 @@ // 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'; +import 'messages.g.dart'; -const MethodChannel _kChannel = - MethodChannel('plugins.flutter.io/shared_preferences_ios'); +typedef _Setter = Future Function(String key, Object value); -/// The macOS implementation of [SharedPreferencesStorePlatform]. -/// -/// This class implements the `package:shared_preferences` functionality for iOS. +/// iOS implementation of shared_preferences. class SharedPreferencesIOS extends SharedPreferencesStorePlatform { - /// Registers this class as the default instance of [SharedPreferencesStorePlatform]. + final UserDefaultsApi _api = UserDefaultsApi(); + late final Map _setters = { + 'Bool': (String key, Object value) { + return _api.setBool(key, value as bool); + }, + 'Double': (String key, Object value) { + return _api.setDouble(key, value as double); + }, + 'Int': (String key, Object value) { + return _api.setValue(key, value as int); + }, + 'String': (String key, Object value) { + return _api.setValue(key, value as String); + }, + 'StringList': (String key, Object value) { + return _api.setValue(key, value as List); + }, + }; + + /// Registers this class as the default instance of [PathProviderPlatform]. static void registerWith() { SharedPreferencesStorePlatform.instance = SharedPreferencesIOS(); } @override - Future remove(String key) async { - return (await _kChannel.invokeMethod( - 'remove', - {'key': key}, - ))!; + Future clear() async { + await _api.clear(); + return true; } @override - Future setValue(String valueType, String key, Object value) async { - return (await _kChannel.invokeMethod( - 'set$valueType', - {'key': key, 'value': value}, - ))!; + Future> getAll() async { + final Map result = await _api.getAll(); + return result.cast(); } @override - Future clear() async { - return (await _kChannel.invokeMethod('clear'))!; + Future remove(String key) async { + await _api.remove(key); + return true; } @override - Future> getAll() async { - final Map? preferences = - await _kChannel.invokeMapMethod('getAll'); - - if (preferences == null) { - return {}; + Future setValue(String valueType, String key, Object value) async { + final _Setter? setter = _setters[valueType]; + if (setter == null) { + throw PlatformException( + code: 'InvalidOperation', + message: '"$valueType" is not a supported type.'); } - return preferences; + await setter(key, value); + return true; } } diff --git a/packages/shared_preferences/shared_preferences_ios/pigeons/copyright_header.txt b/packages/shared_preferences/shared_preferences_ios/pigeons/copyright_header.txt new file mode 100644 index 000000000000..fb682b1ab965 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/pigeons/copyright_header.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. \ No newline at end of file diff --git a/packages/shared_preferences/shared_preferences_ios/pigeons/messages.dart b/packages/shared_preferences/shared_preferences_ios/pigeons/messages.dart new file mode 100644 index 000000000000..6b5648f9e2f0 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/pigeons/messages.dart @@ -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. + +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', + copyrightHeader: 'pigeons/copyright_header.txt', +)) +@HostApi(dartHostTestHandler: 'TestUserDefaultsApi') +abstract class UserDefaultsApi { + 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(); +} diff --git a/packages/shared_preferences/shared_preferences_ios/pubspec.yaml b/packages/shared_preferences/shared_preferences_ios/pubspec.yaml index 68ab03523841..33bf5baffd18 100644 --- a/packages/shared_preferences/shared_preferences_ios/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_ios/pubspec.yaml @@ -2,7 +2,7 @@ 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.0.10 +version: 2.1.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -13,8 +13,8 @@ flutter: implements: shared_preferences platforms: ios: - pluginClass: FLTSharedPreferencesPlugin dartPluginClass: SharedPreferencesIOS + pluginClass: FLTSharedPreferencesPlugin dependencies: flutter: @@ -24,3 +24,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + pigeon: ^1.0.16 diff --git a/packages/shared_preferences/shared_preferences_ios/test/messages.g.dart b/packages/shared_preferences/shared_preferences_ios/test/messages.g.dart new file mode 100644 index 000000000000..12fbc0635784 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_ios/test/messages.g.dart @@ -0,0 +1,145 @@ +// Copyright 2013 The Flutter 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 +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import +// @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 'package:shared_preferences_ios/messages.g.dart'; + +class _TestUserDefaultsApiCodec extends StandardMessageCodec { + const _TestUserDefaultsApiCodec(); +} + +abstract class TestUserDefaultsApi { + static const MessageCodec codec = _TestUserDefaultsApiCodec(); + + 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}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UserDefaultsApi.remove', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.remove was null.'); + final List args = (message as List?)!; + final String? arg_key = (args[0] as String?); + assert(arg_key != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.remove was null, expected non-null String.'); + api.remove(arg_key!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UserDefaultsApi.setBool', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.setBool was null.'); + final List args = (message as List?)!; + final String? arg_key = (args[0] as String?); + assert(arg_key != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.setBool was null, expected non-null String.'); + final bool? arg_value = (args[1] as bool?); + assert(arg_value != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.setBool was null, expected non-null bool.'); + api.setBool(arg_key!, arg_value!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UserDefaultsApi.setDouble', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.setDouble was null.'); + final List args = (message as List?)!; + final String? arg_key = (args[0] as String?); + assert(arg_key != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.setDouble was null, expected non-null String.'); + final double? arg_value = (args[1] as double?); + assert(arg_value != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.setDouble was null, expected non-null double.'); + api.setDouble(arg_key!, arg_value!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UserDefaultsApi.setValue', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.setValue was null.'); + final List args = (message as List?)!; + final String? arg_key = (args[0] as String?); + assert(arg_key != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.setValue was null, expected non-null String.'); + final Object? arg_value = (args[1] as Object?); + assert(arg_value != null, + 'Argument for dev.flutter.pigeon.UserDefaultsApi.setValue was null, expected non-null Object.'); + api.setValue(arg_key!, arg_value!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UserDefaultsApi.getAll', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + final Map output = api.getAll(); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UserDefaultsApi.clear', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + api.clear(); + return {}; + }); + } + } + } +} diff --git a/packages/shared_preferences/shared_preferences_ios/test/shared_preferences_ios_test.dart b/packages/shared_preferences/shared_preferences_ios/test/shared_preferences_ios_test.dart index 8eb23f28c424..efafb230d9de 100644 --- a/packages/shared_preferences/shared_preferences_ios/test/shared_preferences_ios_test.dart +++ b/packages/shared_preferences/shared_preferences_ios/test/shared_preferences_ios_test.dart @@ -5,113 +5,101 @@ 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_platform_interface/method_channel_shared_preferences.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; +import 'messages.g.dart'; + +class _MockSharedPreferencesApi implements TestUserDefaultsApi { + final Map items = {}; + + @override + Map getAll() { + return items; + } + + @override + void remove(String key) { + items.remove(key); + } + + @override + void setBool(String key, bool value) { + items[key] = value; + } + + @override + void setDouble(String key, double value) { + items[key] = value; + } + + @override + void setValue(String key, Object value) { + items[key] = value; + } + + @override + void clear() { + items.clear(); + } +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); + _MockSharedPreferencesApi api = _MockSharedPreferencesApi(); + SharedPreferencesIOS plugin = SharedPreferencesIOS(); + + setUp(() { + api = _MockSharedPreferencesApi(); + TestUserDefaultsApi.setup(api); + plugin = SharedPreferencesIOS(); + }); + + test('registerWith', () { + SharedPreferencesIOS.registerWith(); + expect( + SharedPreferencesStorePlatform.instance, isA()); + }); + + test('remove', () async { + api.items['flutter.hi'] = 'world'; + expect(await plugin.remove('flutter.hi'), isTrue); + expect(api.items.containsKey('flutter.hi'), isFalse); + }); + + test('clear', () async { + 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 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']); + }); + + test('setValue', () async { + 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); + expect(api.items['flutter.Double'], 1.5); + expect(await plugin.setValue('Int', 'flutter.Int', 12), isTrue); + expect(api.items['flutter.Int'], 12); + expect(await plugin.setValue('String', 'flutter.String', 'hi'), isTrue); + expect(api.items['flutter.String'], 'hi'); + expect( + await plugin + .setValue('StringList', 'flutter.StringList', ['hi']), + isTrue); + expect(api.items['flutter.StringList'], ['hi']); + }); - group(MethodChannelSharedPreferencesStore, () { - const MethodChannel channel = MethodChannel( - 'plugins.flutter.io/shared_preferences_ios', - ); - - 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('registered instance', () { - SharedPreferencesIOS.registerWith(); - expect( - SharedPreferencesStorePlatform.instance, isA()); - }); - - test('getAll', () async { - store = SharedPreferencesIOS(); - testData = InMemorySharedPreferencesStore.withData(kTestValues); - expect(await store.getAll(), kTestValues); - expect(log.single.method, 'getAll'); - }); - - test('remove', () async { - store = SharedPreferencesIOS(); - 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 = SharedPreferencesIOS(); - 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 = SharedPreferencesIOS(); - testData = InMemorySharedPreferencesStore.withData(kTestValues); - expect(await testData.getAll(), isNotEmpty); - expect(await store.clear(), true); - expect(await testData.getAll(), isEmpty); - expect(log.single.method, 'clear'); - }); + test('setValue with unsupported type', () { + expect(() async { + await plugin.setValue('Map', 'flutter.key', {}); + }, throwsA(isA())); }); } From ed29396208cd915ab7dec5113e81216422224cc1 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Thu, 17 Feb 2022 10:44:23 -0500 Subject: [PATCH 12/12] Roll Flutter from adafd66d9cc7 to 93c0c043cf2c (6 revisions) (#4880) --- .ci/flutter_master.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index c2fc9dc220d4..143dde748c88 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -adafd66d9cc7796214404c8117395e207bf23d01 +93c0c043cf2c38947e0e95c81e936aa46541015c