From 083b45e19d3bfbfbfca0b6059aed6c1124019854 Mon Sep 17 00:00:00 2001 From: Taha Tesser Date: Fri, 6 Aug 2021 20:35:04 +0300 Subject: [PATCH 01/57] [image_picker] Fix README example (#4220) --- packages/image_picker/image_picker/CHANGELOG.md | 4 ++++ packages/image_picker/image_picker/README.md | 4 +--- packages/image_picker/image_picker/pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index bd0a7a06b4fc..f9c7640183d5 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.3+1 + +* Fixed README Example. + ## 0.8.3 * Move `ImagePickerFromLimitedGalleryUITests` to `RunnerUITests` target. diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index 18fd96d890fd..7499c356f3aa 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -47,9 +47,7 @@ import 'package:image_picker/image_picker.dart'; // Capture a video final XFile? photo = await _picker.pickVideo(source: ImageSource.camera); // Pick multiple images - final List? images = await _picker.pickMultiImage(source: ImageSource.gallery); - // Pick multiple photos - final List? photos = await _picker.pickMultiImage(source: ImageSource.camera); + final List? images = await _picker.pickMultiImage(); ... ``` diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index f56250f53715..e67e79fbba14 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/master/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.3 +version: 0.8.3+1 environment: sdk: ">=2.12.0 <3.0.0" From c70465665790a885d4cdbd4b8bc4bd6544bc6d3c Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Sat, 7 Aug 2021 03:55:04 +0200 Subject: [PATCH 02/57] [camera_web] Add `onCameraResolutionChanged` implementation (#4217) --- .../example/integration_test/camera_web_test.dart | 6 +++--- packages/camera/camera_web/lib/src/camera_web.dart | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index 1b540a50e48d..083a25dd06bb 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -1068,11 +1068,11 @@ void main() { await streamQueue.cancel(); }); - testWidgets('onCameraResolutionChanged throws UnimplementedError', + testWidgets('onCameraResolutionChanged emits an empty stream', (tester) async { expect( - () => CameraPlatform.instance.onCameraResolutionChanged(cameraId), - throwsUnimplementedError, + CameraPlatform.instance.onCameraResolutionChanged(cameraId), + emits(isEmpty), ); }); diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 35241d0c9b8b..dbfbcacd3ce0 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -302,9 +302,14 @@ class CameraPlugin extends CameraPlatform { return _cameraEvents(cameraId).whereType(); } + /// Emits an empty stream as there is no event corresponding to a change + /// in the camera resolution on the web. + /// + /// In order to change the camera resolution a new camera with appropriate + /// [CameraOptions.video] constraints has to be created and initialized. @override Stream onCameraResolutionChanged(int cameraId) { - throw UnimplementedError('onCameraResolutionChanged() is not implemented.'); + return const Stream.empty(); } @override From acc3202466a8b4c0b1bcb91fc1ce520c7b021e44 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Sat, 7 Aug 2021 04:00:05 +0200 Subject: [PATCH 03/57] [camera_web] Add support for device orientation (#4219) --- .../camera_settings_test.dart | 114 ++++++ .../integration_test/camera_web_test.dart | 349 ++++++++++++++++-- .../integration_test/helpers/mocks.dart | 8 + .../camera_web/lib/src/camera_settings.dart | 34 ++ .../camera/camera_web/lib/src/camera_web.dart | 63 +++- .../lib/src/types/camera_error_code.dart | 4 + .../lib/src/types/orientation_type.dart | 26 ++ .../camera_web/lib/src/types/types.dart | 2 +- .../test/types/camera_error_code_test.dart | 7 + 9 files changed, 576 insertions(+), 31 deletions(-) create mode 100644 packages/camera/camera_web/lib/src/types/orientation_type.dart diff --git a/packages/camera/camera_web/example/integration_test/camera_settings_test.dart b/packages/camera/camera_web/example/integration_test/camera_settings_test.dart index 7e5119003129..bc228b2e35c6 100644 --- a/packages/camera/camera_web/example/integration_test/camera_settings_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_settings_test.dart @@ -607,6 +607,120 @@ void main() { ); }); }); + + group('mapDeviceOrientationToOrientationType', () { + testWidgets( + 'returns portraitPrimary ' + 'when the device orientation is portraitUp', (tester) async { + expect( + settings.mapDeviceOrientationToOrientationType( + DeviceOrientation.portraitUp, + ), + equals(OrientationType.portraitPrimary), + ); + }); + + testWidgets( + 'returns landscapePrimary ' + 'when the device orientation is landscapeLeft', (tester) async { + expect( + settings.mapDeviceOrientationToOrientationType( + DeviceOrientation.landscapeLeft, + ), + equals(OrientationType.landscapePrimary), + ); + }); + + testWidgets( + 'returns portraitSecondary ' + 'when the device orientation is portraitDown', (tester) async { + expect( + settings.mapDeviceOrientationToOrientationType( + DeviceOrientation.portraitDown, + ), + equals(OrientationType.portraitSecondary), + ); + }); + + testWidgets( + 'returns landscapeSecondary ' + 'when the device orientation is landscapeRight', (tester) async { + expect( + settings.mapDeviceOrientationToOrientationType( + DeviceOrientation.landscapeRight, + ), + equals(OrientationType.landscapeSecondary), + ); + }); + }); + + group('mapOrientationTypeToDeviceOrientation', () { + testWidgets( + 'returns portraitUp ' + 'when the orientation type is portraitPrimary', (tester) async { + expect( + settings.mapOrientationTypeToDeviceOrientation( + OrientationType.portraitPrimary, + ), + equals(DeviceOrientation.portraitUp), + ); + }); + + testWidgets( + 'returns landscapeLeft ' + 'when the orientation type is landscapePrimary', (tester) async { + expect( + settings.mapOrientationTypeToDeviceOrientation( + OrientationType.landscapePrimary, + ), + equals(DeviceOrientation.landscapeLeft), + ); + }); + + testWidgets( + 'returns portraitDown ' + 'when the orientation type is portraitSecondary', (tester) async { + expect( + settings.mapOrientationTypeToDeviceOrientation( + OrientationType.portraitSecondary, + ), + equals(DeviceOrientation.portraitDown), + ); + }); + + testWidgets( + 'returns portraitDown ' + 'when the orientation type is portraitSecondary', (tester) async { + expect( + settings.mapOrientationTypeToDeviceOrientation( + OrientationType.portraitSecondary, + ), + equals(DeviceOrientation.portraitDown), + ); + }); + + testWidgets( + 'returns landscapeRight ' + 'when the orientation type is landscapeSecondary', (tester) async { + expect( + settings.mapOrientationTypeToDeviceOrientation( + OrientationType.landscapeSecondary, + ), + equals(DeviceOrientation.landscapeRight), + ); + }); + + testWidgets( + 'returns portraitUp ' + 'for an unknown orientation type', (tester) async { + expect( + settings.mapOrientationTypeToDeviceOrientation( + 'unknown', + ), + equals(DeviceOrientation.portraitUp), + ); + }); + }); }); } diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index 083a25dd06bb..e11634d83fce 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -30,6 +30,11 @@ void main() { late Navigator navigator; late MediaDevices mediaDevices; late VideoElement videoElement; + late Screen screen; + late ScreenOrientation screenOrientation; + late Document document; + late Element documentElement; + late CameraSettings cameraSettings; setUp(() async { @@ -39,11 +44,23 @@ void main() { videoElement = getVideoElementWithBlankStream(Size(10, 10)); - cameraSettings = MockCameraSettings(); - when(() => window.navigator).thenReturn(navigator); when(() => navigator.mediaDevices).thenReturn(mediaDevices); + screen = MockScreen(); + screenOrientation = MockScreenOrientation(); + + when(() => screen.orientation).thenReturn(screenOrientation); + when(() => window.screen).thenReturn(screen); + + document = MockDocument(); + documentElement = MockElement(); + + when(() => document.documentElement).thenReturn(documentElement); + when(() => window.document).thenReturn(document); + + cameraSettings = MockCameraSettings(); + when( () => cameraSettings.getMediaStreamForOptions( any(), @@ -636,23 +653,236 @@ void main() { }); }); - testWidgets('lockCaptureOrientation throws UnimplementedError', - (tester) async { - expect( - () => CameraPlatform.instance.lockCaptureOrientation( + group('lockCaptureOrientation', () { + setUp(() { + when( + () => cameraSettings.mapDeviceOrientationToOrientationType(any()), + ).thenReturn(OrientationType.portraitPrimary); + }); + + testWidgets( + 'requests full-screen mode ' + 'on documentElement', (tester) async { + await CameraPlatform.instance.lockCaptureOrientation( cameraId, - DeviceOrientation.landscapeLeft, - ), - throwsUnimplementedError, - ); + DeviceOrientation.portraitUp, + ); + + verify(documentElement.requestFullscreen).called(1); + }); + + testWidgets( + 'locks the capture orientation ' + 'based on the given device orientation', (tester) async { + when( + () => cameraSettings.mapDeviceOrientationToOrientationType( + DeviceOrientation.landscapeRight, + ), + ).thenReturn(OrientationType.landscapeSecondary); + + await CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.landscapeRight, + ); + + verify( + () => cameraSettings.mapDeviceOrientationToOrientationType( + DeviceOrientation.landscapeRight, + ), + ).called(1); + + verify( + () => screenOrientation.lock( + OrientationType.landscapeSecondary, + ), + ).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with orientationNotSupported error ' + 'when screen is not supported', (tester) async { + when(() => window.screen).thenReturn(null); + + expect( + () => CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.portraitUp, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + }); + + testWidgets( + 'with orientationNotSupported error ' + 'when screen orientation is not supported', (tester) async { + when(() => screen.orientation).thenReturn(null); + + expect( + () => CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.portraitUp, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + }); + + testWidgets( + 'with orientationNotSupported error ' + 'when documentElement is not available', (tester) async { + when(() => document.documentElement).thenReturn(null); + + expect( + () => CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.portraitUp, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + }); + + testWidgets('when lock throws DomException', (tester) async { + final exception = FakeDomException(DomException.NOT_ALLOWED); + + when(() => screenOrientation.lock(any())).thenThrow(exception); + + expect( + () => CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.portraitDown, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); }); - testWidgets('unlockCaptureOrientation throws UnimplementedError', - (tester) async { - expect( - () => CameraPlatform.instance.unlockCaptureOrientation(cameraId), - throwsUnimplementedError, - ); + group('unlockCaptureOrientation', () { + setUp(() { + when( + () => cameraSettings.mapDeviceOrientationToOrientationType(any()), + ).thenReturn(OrientationType.portraitPrimary); + }); + + testWidgets( + 'requests full-screen mode ' + 'on documentElement', (tester) async { + await CameraPlatform.instance.unlockCaptureOrientation( + cameraId, + ); + + verify(documentElement.requestFullscreen).called(1); + }); + + testWidgets('unlocks the capture orientation', (tester) async { + await CameraPlatform.instance.unlockCaptureOrientation( + cameraId, + ); + + verify(screenOrientation.unlock).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with orientationNotSupported error ' + 'when screen is not supported', (tester) async { + when(() => window.screen).thenReturn(null); + + expect( + () => CameraPlatform.instance.unlockCaptureOrientation( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + }); + + testWidgets( + 'with orientationNotSupported error ' + 'when screen orientation is not supported', (tester) async { + when(() => screen.orientation).thenReturn(null); + + expect( + () => CameraPlatform.instance.unlockCaptureOrientation( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + }); + + testWidgets( + 'with orientationNotSupported error ' + 'when documentElement is not available', (tester) async { + when(() => document.documentElement).thenReturn(null); + + expect( + () => CameraPlatform.instance.unlockCaptureOrientation( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + }); + + testWidgets('when unlock throws DomException', (tester) async { + final exception = FakeDomException(DomException.NOT_ALLOWED); + + when(screenOrientation.unlock).thenThrow(exception); + + expect( + () => CameraPlatform.instance.unlockCaptureOrientation( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); }); group('takePicture', () { @@ -1213,12 +1443,87 @@ void main() { ); }); - testWidgets('onDeviceOrientationChanged throws UnimplementedError', - (tester) async { - expect( - () => CameraPlatform.instance.onDeviceOrientationChanged(), - throwsUnimplementedError, - ); + group('onDeviceOrientationChanged', () { + group('emits an empty stream', () { + testWidgets('when screen is not supported', (tester) async { + when(() => window.screen).thenReturn(null); + + expect( + CameraPlatform.instance.onDeviceOrientationChanged(), + emits(isEmpty), + ); + }); + + testWidgets('when screen orientation is not supported', + (tester) async { + when(() => screen.orientation).thenReturn(null); + + expect( + CameraPlatform.instance.onDeviceOrientationChanged(), + emits(isEmpty), + ); + }); + }); + + testWidgets( + 'emits a DeviceOrientationChangedEvent ' + 'when the screen orientation is changed', (tester) async { + when( + () => cameraSettings.mapOrientationTypeToDeviceOrientation( + OrientationType.landscapePrimary, + ), + ).thenReturn(DeviceOrientation.landscapeLeft); + + when( + () => cameraSettings.mapOrientationTypeToDeviceOrientation( + OrientationType.portraitSecondary, + ), + ).thenReturn(DeviceOrientation.portraitDown); + + final eventStreamController = StreamController(); + + when(() => screenOrientation.onChange) + .thenAnswer((_) => eventStreamController.stream); + + final Stream eventStream = + CameraPlatform.instance.onDeviceOrientationChanged(); + + final streamQueue = StreamQueue(eventStream); + + // Change the screen orientation to landscapePrimary and + // emit an event on the screenOrientation.onChange stream. + when(() => screenOrientation.type) + .thenReturn(OrientationType.landscapePrimary); + + eventStreamController.add(Event('orientationChanged')); + + expect( + await streamQueue.next, + equals( + DeviceOrientationChangedEvent( + DeviceOrientation.landscapeLeft, + ), + ), + ); + + // Change the screen orientation to portraitSecondary and + // emit an event on the screenOrientation.onChange stream. + when(() => screenOrientation.type) + .thenReturn(OrientationType.portraitSecondary); + + eventStreamController.add(Event('orientationChanged')); + + expect( + await streamQueue.next, + equals( + DeviceOrientationChangedEvent( + DeviceOrientation.portraitDown, + ), + ), + ); + + await streamQueue.cancel(); + }); }); }); }); diff --git a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart index 8af3a9c3cd81..5fa52dd3398d 100644 --- a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart +++ b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart @@ -14,6 +14,14 @@ import 'package:mocktail/mocktail.dart'; class MockWindow extends Mock implements Window {} +class MockScreen extends Mock implements Screen {} + +class MockScreenOrientation extends Mock implements ScreenOrientation {} + +class MockDocument extends Mock implements Document {} + +class MockElement extends Mock implements Element {} + class MockNavigator extends Mock implements Navigator {} class MockMediaDevices extends Mock implements MediaDevices {} diff --git a/packages/camera/camera_web/lib/src/camera_settings.dart b/packages/camera/camera_web/lib/src/camera_settings.dart index 1412248a2371..ce713bc52468 100644 --- a/packages/camera/camera_web/lib/src/camera_settings.dart +++ b/packages/camera/camera_web/lib/src/camera_settings.dart @@ -230,4 +230,38 @@ class CameraSettings { return Size(320, 240); } } + + /// Maps the given [deviceOrientation] to [OrientationType]. + String mapDeviceOrientationToOrientationType( + DeviceOrientation deviceOrientation, + ) { + switch (deviceOrientation) { + case DeviceOrientation.portraitUp: + return OrientationType.portraitPrimary; + case DeviceOrientation.landscapeLeft: + return OrientationType.landscapePrimary; + case DeviceOrientation.portraitDown: + return OrientationType.portraitSecondary; + case DeviceOrientation.landscapeRight: + return OrientationType.landscapeSecondary; + } + } + + /// Maps the given [orientationType] to [DeviceOrientation]. + DeviceOrientation mapOrientationTypeToDeviceOrientation( + String orientationType, + ) { + switch (orientationType) { + case OrientationType.portraitPrimary: + return DeviceOrientation.portraitUp; + case OrientationType.landscapePrimary: + return DeviceOrientation.landscapeLeft; + case OrientationType.portraitSecondary: + return DeviceOrientation.portraitDown; + case OrientationType.landscapeSecondary: + return DeviceOrientation.landscapeRight; + default: + return DeviceOrientation.portraitUp; + } + } } diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index dbfbcacd3ce0..01fc0a23aa34 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -329,22 +329,69 @@ class CameraPlugin extends CameraPlatform { @override Stream onDeviceOrientationChanged() { - throw UnimplementedError( - 'onDeviceOrientationChanged() is not implemented.', - ); + final orientation = window?.screen?.orientation; + + if (orientation != null) { + return orientation.onChange.map( + (html.Event _) { + final deviceOrientation = _cameraSettings + .mapOrientationTypeToDeviceOrientation(orientation.type!); + return DeviceOrientationChangedEvent(deviceOrientation); + }, + ); + } else { + return const Stream.empty(); + } } @override Future lockCaptureOrientation( int cameraId, - DeviceOrientation orientation, - ) { - throw UnimplementedError('lockCaptureOrientation() is not implemented.'); + DeviceOrientation deviceOrientation, + ) async { + try { + final orientation = window?.screen?.orientation; + final documentElement = window?.document.documentElement; + + if (orientation != null && documentElement != null) { + final orientationType = _cameraSettings + .mapDeviceOrientationToOrientationType(deviceOrientation); + + // Full-screen mode may be required to modify the device orientation. + // See: https://w3c.github.io/screen-orientation/#interaction-with-fullscreen-api + documentElement.requestFullscreen(); + await orientation.lock(orientationType.toString()); + } else { + throw PlatformException( + code: CameraErrorCode.orientationNotSupported.toString(), + message: 'Orientation is not supported in the current browser.', + ); + } + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } } @override - Future unlockCaptureOrientation(int cameraId) { - throw UnimplementedError('unlockCaptureOrientation() is not implemented.'); + Future unlockCaptureOrientation(int cameraId) async { + try { + final orientation = window?.screen?.orientation; + final documentElement = window?.document.documentElement; + + if (orientation != null && documentElement != null) { + // Full-screen mode may be required to modify the device orientation. + // See: https://w3c.github.io/screen-orientation/#interaction-with-fullscreen-api + documentElement.requestFullscreen(); + orientation.unlock(); + } else { + throw PlatformException( + code: CameraErrorCode.orientationNotSupported.toString(), + message: 'Orientation is not supported in the current browser.', + ); + } + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } } @override diff --git a/packages/camera/camera_web/lib/src/types/camera_error_code.dart b/packages/camera/camera_web/lib/src/types/camera_error_code.dart index 3dcace3ca2d6..9a70663c4aaf 100644 --- a/packages/camera/camera_web/lib/src/types/camera_error_code.dart +++ b/packages/camera/camera_web/lib/src/types/camera_error_code.dart @@ -48,6 +48,10 @@ class CameraErrorCode { static const CameraErrorCode missingMetadata = CameraErrorCode._('cameraMissingMetadata'); + /// The camera orientation is not supported. + static const CameraErrorCode orientationNotSupported = + CameraErrorCode._('orientationNotSupported'); + /// An unknown camera error. static const CameraErrorCode unknown = CameraErrorCode._('cameraUnknown'); diff --git a/packages/camera/camera_web/lib/src/types/orientation_type.dart b/packages/camera/camera_web/lib/src/types/orientation_type.dart new file mode 100644 index 000000000000..717f5f399541 --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/orientation_type.dart @@ -0,0 +1,26 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; + +/// A screen orientation type. +/// +/// See: https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation/type +abstract class OrientationType { + /// The primary portrait mode orientation. + /// Corresponds to [DeviceOrientation.portraitUp]. + static const String portraitPrimary = 'portrait-primary'; + + /// The secondary portrait mode orientation. + /// Corresponds to [DeviceOrientation.portraitSecondary]. + static const String portraitSecondary = 'portrait-secondary'; + + /// The primary landscape mode orientation. + /// Corresponds to [DeviceOrientation.landscapeLeft]. + static const String landscapePrimary = 'landscape-primary'; + + /// The secondary landscape mode orientation. + /// Corresponds to [DeviceOrientation.landscapeRight]. + static const String landscapeSecondary = 'landscape-secondary'; +} diff --git a/packages/camera/camera_web/lib/src/types/types.dart b/packages/camera/camera_web/lib/src/types/types.dart index 788ec79de205..4e3902fcb3ee 100644 --- a/packages/camera/camera_web/lib/src/types/types.dart +++ b/packages/camera/camera_web/lib/src/types/types.dart @@ -1,9 +1,9 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - export 'camera_error_code.dart'; export 'camera_metadata.dart'; export 'camera_options.dart'; export 'camera_web_exception.dart'; export 'media_device_kind.dart'; +export 'orientation_type.dart'; diff --git a/packages/camera/camera_web/test/types/camera_error_code_test.dart b/packages/camera/camera_web/test/types/camera_error_code_test.dart index ca896e8696d7..6f2d7dd1cd09 100644 --- a/packages/camera/camera_web/test/types/camera_error_code_test.dart +++ b/packages/camera/camera_web/test/types/camera_error_code_test.dart @@ -75,6 +75,13 @@ void main() { ); }); + test('orientationNotSupported', () { + expect( + CameraErrorCode.orientationNotSupported.toString(), + equals('orientationNotSupported'), + ); + }); + test('unknown', () { expect( CameraErrorCode.unknown.toString(), From b9512b627343f58ab543396b7bfed27a8add5a4e Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Mon, 9 Aug 2021 19:20:05 +0200 Subject: [PATCH 04/57] [camera_web] Add support for a flash mode (#4222) --- .../example/integration_test/camera_test.dart | 396 +++++++++++++++++- .../integration_test/camera_web_test.dart | 127 +++++- .../camera/camera_web/lib/src/camera.dart | 98 ++++- .../camera/camera_web/lib/src/camera_web.dart | 11 +- .../lib/src/types/camera_error_code.dart | 8 + .../test/types/camera_error_code_test.dart | 14 + 6 files changed, 619 insertions(+), 35 deletions(-) diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart index 49690ed38ab5..740e24f87819 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -5,6 +5,7 @@ import 'dart:html'; import 'dart:ui'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/camera.dart'; import 'package:camera_web/src/camera_settings.dart'; import 'package:camera_web/src/types/types.dart'; @@ -18,10 +19,23 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('Camera', () { + const textureId = 1; + + late Window window; + late Navigator navigator; + late MediaDevices mediaDevices; + late MediaStream mediaStream; late CameraSettings cameraSettings; setUp(() { + window = MockWindow(); + navigator = MockNavigator(); + mediaDevices = MockMediaDevices(); + + when(() => window.navigator).thenReturn(navigator); + when(() => navigator.mediaDevices).thenReturn(mediaDevices); + cameraSettings = MockCameraSettings(); final videoElement = getVideoElementWithBlankStream(Size(10, 10)); @@ -51,7 +65,7 @@ void main() { ); final camera = Camera( - textureId: 1, + textureId: textureId, options: options, cameraSettings: cameraSettings, ); @@ -61,7 +75,7 @@ void main() { verify( () => cameraSettings.getMediaStreamForOptions( options, - cameraId: 1, + cameraId: textureId, ), ).called(1); }); @@ -72,7 +86,7 @@ void main() { const audioConstraints = AudioConstraints(enabled: true); final camera = Camera( - textureId: 1, + textureId: textureId, options: CameraOptions( audio: audioConstraints, ), @@ -100,7 +114,7 @@ void main() { 'creates a wrapping div element ' 'with correct properties', (tester) async { final camera = Camera( - textureId: 1, + textureId: textureId, cameraSettings: cameraSettings, ); @@ -111,6 +125,17 @@ void main() { expect(camera.divElement.children, contains(camera.videoElement)); }); + testWidgets('initializes the camera stream', (tester) async { + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ); + + await camera.initialize(); + + expect(camera.stream, mediaStream); + }); + testWidgets( 'throws an exception ' 'when CameraSettings.getMediaStreamForOptions throws', @@ -121,7 +146,7 @@ void main() { cameraId: any(named: 'cameraId'))).thenThrow(exception); final camera = Camera( - textureId: 1, + textureId: textureId, cameraSettings: cameraSettings, ); @@ -137,7 +162,7 @@ void main() { var startedPlaying = false; final camera = Camera( - textureId: 1, + textureId: textureId, cameraSettings: cameraSettings, ); @@ -154,9 +179,8 @@ void main() { }); testWidgets( - 'assigns a media stream ' + 'initializes the camera stream ' 'from CameraSettings.getMediaStreamForOptions ' - 'to the video element\'s source ' 'if it does not exist', (tester) async { final options = CameraOptions( video: VideoConstraints( @@ -165,7 +189,7 @@ void main() { ); final camera = Camera( - textureId: 1, + textureId: textureId, options: options, cameraSettings: cameraSettings, ); @@ -182,18 +206,19 @@ void main() { verify( () => cameraSettings.getMediaStreamForOptions( options, - cameraId: 1, + cameraId: textureId, ), ).called(2); expect(camera.videoElement.srcObject, mediaStream); + expect(camera.stream, mediaStream); }); }); group('stop', () { - testWidgets('resets the video element\'s source', (tester) async { + testWidgets('resets the camera stream', (tester) async { final camera = Camera( - textureId: 1, + textureId: textureId, cameraSettings: cameraSettings, ); @@ -203,13 +228,14 @@ void main() { camera.stop(); expect(camera.videoElement.srcObject, isNull); + expect(camera.stream, isNull); }); }); group('takePicture', () { testWidgets('returns a captured picture', (tester) async { final camera = Camera( - textureId: 1, + textureId: textureId, cameraSettings: cameraSettings, ); @@ -220,6 +246,99 @@ void main() { expect(pictureFile, isNotNull); }); + + group( + 'enables the torch mode ' + 'when taking a picture', () { + late List videoTracks; + late MediaStream videoStream; + late VideoElement videoElement; + + setUp(() { + videoTracks = [MockMediaStreamTrack(), MockMediaStreamTrack()]; + videoStream = FakeMediaStream(videoTracks); + + videoElement = getVideoElementWithBlankStream(Size(100, 100)) + ..muted = true; + + when(() => videoTracks.first.applyConstraints(any())) + .thenAnswer((_) async => {}); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': true, + }); + }); + + testWidgets('if the flash mode is auto', (tester) async { + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ) + ..window = window + ..stream = videoStream + ..videoElement = videoElement + ..flashMode = FlashMode.auto; + + await camera.play(); + + final _ = await camera.takePicture(); + + verify( + () => videoTracks.first.applyConstraints({ + "advanced": [ + { + "torch": true, + } + ] + }), + ).called(1); + + verify( + () => videoTracks.first.applyConstraints({ + "advanced": [ + { + "torch": false, + } + ] + }), + ).called(1); + }); + + testWidgets('if the flash mode is always', (tester) async { + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ) + ..window = window + ..stream = videoStream + ..videoElement = videoElement + ..flashMode = FlashMode.always; + + await camera.play(); + + final _ = await camera.takePicture(); + + verify( + () => videoTracks.first.applyConstraints({ + "advanced": [ + { + "torch": true, + } + ] + }), + ).called(1); + + verify( + () => videoTracks.first.applyConstraints({ + "advanced": [ + { + "torch": false, + } + ] + }), + ).called(1); + }); + }); }); group('getVideoSize', () { @@ -232,7 +351,7 @@ void main() { mediaStream = videoElement.captureStream(); final camera = Camera( - textureId: 1, + textureId: textureId, cameraSettings: cameraSettings, ); @@ -252,7 +371,7 @@ void main() { mediaStream = videoElement.captureStream(); final camera = Camera( - textureId: 1, + textureId: textureId, cameraSettings: cameraSettings, ); @@ -265,10 +384,251 @@ void main() { }); }); + group('setFlashMode', () { + late List videoTracks; + late MediaStream videoStream; + + setUp(() { + videoTracks = [MockMediaStreamTrack(), MockMediaStreamTrack()]; + videoStream = FakeMediaStream(videoTracks); + + when(() => videoTracks.first.applyConstraints(any())) + .thenAnswer((_) async => {}); + + when(videoTracks.first.getCapabilities).thenReturn({}); + }); + + testWidgets('sets the camera flash mode', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'torch': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': true, + }); + + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ) + ..window = window + ..stream = videoStream; + + const flashMode = FlashMode.always; + + camera.setFlashMode(flashMode); + + expect( + camera.flashMode, + equals(flashMode), + ); + }); + + testWidgets( + 'enables the torch mode ' + 'if the flash mode is torch', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'torch': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': true, + }); + + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ) + ..window = window + ..stream = videoStream; + + camera.setFlashMode(FlashMode.torch); + + verify( + () => videoTracks.first.applyConstraints({ + "advanced": [ + { + "torch": true, + } + ] + }), + ).called(1); + }); + + testWidgets( + 'disables the torch mode ' + 'if the flash mode is not torch', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'torch': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': true, + }); + + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ) + ..window = window + ..stream = videoStream; + + camera.setFlashMode(FlashMode.auto); + + verify( + () => videoTracks.first.applyConstraints({ + "advanced": [ + { + "torch": false, + } + ] + }), + ).called(1); + }); + + group('throws CameraWebException', () { + testWidgets( + 'with torchModeNotSupported error ' + 'when there are no media devices', (tester) async { + when(() => navigator.mediaDevices).thenReturn(null); + + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ) + ..window = window + ..stream = videoStream; + + expect( + () => camera.setFlashMode(FlashMode.always), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.torchModeNotSupported, + ), + ), + ); + }); + + testWidgets( + 'with torchModeNotSupported error ' + 'when the torch mode is not supported ' + 'in the browser', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'torch': false, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': true, + }); + + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ) + ..window = window + ..stream = videoStream; + + expect( + () => camera.setFlashMode(FlashMode.always), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.torchModeNotSupported, + ), + ), + ); + }); + + testWidgets( + 'with torchModeNotSupported error ' + 'when the torch mode is not supported ' + 'by the camera', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'torch': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': false, + }); + + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ) + ..window = window + ..stream = videoStream; + + expect( + () => camera.setFlashMode(FlashMode.always), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.torchModeNotSupported, + ), + ), + ); + }); + + testWidgets( + 'with notStarted error ' + 'when the camera stream has not been initialized', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'torch': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': true, + }); + + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + )..window = window; + + expect( + () => camera.setFlashMode(FlashMode.always), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.notStarted, + ), + ), + ); + }); + }); + }); + group('getViewType', () { testWidgets('returns a correct view type', (tester) async { - const textureId = 1; - final camera = Camera( textureId: textureId, cameraSettings: cameraSettings, @@ -286,7 +646,7 @@ void main() { group('dispose', () { testWidgets('resets the video element\'s source', (tester) async { final camera = Camera( - textureId: 1, + textureId: textureId, cameraSettings: cameraSettings, ); diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index e11634d83fce..fda35dd088c1 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -78,6 +78,7 @@ void main() { setUpAll(() { registerFallbackValue(MockMediaStreamTrack()); registerFallbackValue(MockCameraOptions()); + registerFallbackValue(FlashMode.off); }); testWidgets('CameraPlugin is the live instance', (tester) async { @@ -981,14 +982,79 @@ void main() { ); }); - testWidgets('setFlashMode throws UnimplementedError', (tester) async { - expect( - () => CameraPlatform.instance.setFlashMode( - cameraId, - FlashMode.auto, - ), - throwsUnimplementedError, - ); + group('setFlashMode', () { + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.setFlashMode( + cameraId, + FlashMode.always, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when setFlashMode throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.NOT_SUPPORTED); + + when(() => camera.setFlashMode(any())).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.setFlashMode( + cameraId, + FlashMode.always, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when setFlashMode throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(() => camera.setFlashMode(any())).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.setFlashMode( + cameraId, + FlashMode.torch, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); }); testWidgets('setExposureMode throws UnimplementedError', (tester) async { @@ -1345,7 +1411,7 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on video error ' + 'on initialize video error ' 'with a message', (tester) async { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); @@ -1380,7 +1446,7 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on video error ' + 'on initialize video error ' 'with no message', (tester) async { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); @@ -1411,7 +1477,7 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on abort error', (tester) async { + 'on initialize abort error', (tester) async { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); @@ -1433,6 +1499,45 @@ void main() { await streamQueue.cancel(); }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on setFlashMode error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(() => camera.setFlashMode(any())).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => await CameraPlatform.instance.setFlashMode( + cameraId, + FlashMode.always, + ), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); }); testWidgets('onVideoRecordedEvent throws UnimplementedError', diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index 9e469033dfc4..1cd007b917bb 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -8,6 +8,7 @@ import 'dart:ui'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/camera_settings.dart'; import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/foundation.dart'; import 'shims/dart_ui.dart' as ui; @@ -39,6 +40,10 @@ class Camera { this.options = const CameraOptions(), }) : _cameraSettings = cameraSettings; + // A torch mode constraint name. + // See: https://w3c.github.io/mediacapture-image/#dom-mediatracksupportedconstraints-torch + static const _torchModeKey = "torch"; + /// The texture id used to register the camera view. final int textureId; @@ -47,20 +52,32 @@ class Camera { /// The video element that displays the camera stream. /// Initialized in [initialize]. - late html.VideoElement videoElement; + late final html.VideoElement videoElement; /// The wrapping element for the [videoElement] to avoid overriding /// the custom styles applied in [_applyDefaultVideoStyles]. /// Initialized in [initialize]. - late html.DivElement divElement; + late final html.DivElement divElement; + + /// The camera stream displayed in the [videoElement]. + /// Initialized in [initialize] and [play], reset in [stop]. + html.MediaStream? stream; + + /// The camera flash mode. + @visibleForTesting + FlashMode? flashMode; /// The camera settings used to get the media stream for the camera. final CameraSettings _cameraSettings; + /// The current browser window used to access media devices. + @visibleForTesting + html.Window? window = html.window; + /// Initializes the camera stream displayed in the [videoElement]. /// Registers the camera view with [textureId] under [_getViewType] type. Future initialize() async { - final stream = await _cameraSettings.getMediaStreamForOptions( + stream = await _cameraSettings.getMediaStreamForOptions( options, cameraId: textureId, ); @@ -89,7 +106,7 @@ class Camera { /// Initializes the camera source if the camera was previously stopped. Future play() async { if (videoElement.srcObject == null) { - final stream = await _cameraSettings.getMediaStreamForOptions( + stream = await _cameraSettings.getMediaStreamForOptions( options, cameraId: textureId, ); @@ -107,18 +124,36 @@ class Camera { } } videoElement.srcObject = null; + stream = null; } /// Captures a picture and returns the saved file in a JPEG format. + /// + /// Enables the device flash when taking a picture if the flash mode + /// is either [FlashMode.auto] or [FlashMode.always]. Future takePicture() async { + final shouldEnableTorchMode = + flashMode == FlashMode.auto || flashMode == FlashMode.always; + + if (shouldEnableTorchMode) { + _setTorchMode(enabled: true); + } + final videoWidth = videoElement.videoWidth; final videoHeight = videoElement.videoHeight; final canvas = html.CanvasElement(width: videoWidth, height: videoHeight); + canvas.context2D ..translate(videoWidth, 0) ..scale(-1, 1) ..drawImageScaled(videoElement, 0, 0, videoWidth, videoHeight); + final blob = await canvas.toBlob('image/jpeg'); + + if (shouldEnableTorchMode) { + _setTorchMode(enabled: false); + } + return XFile(html.Url.createObjectUrl(blob)); } @@ -146,6 +181,61 @@ class Camera { } } + /// Sets the camera flash mode to [mode]. + void setFlashMode(FlashMode mode) { + final mediaDevices = window?.navigator.mediaDevices; + final supportedConstraints = mediaDevices?.getSupportedConstraints(); + final torchModeSupported = supportedConstraints?[_torchModeKey] ?? false; + + if (!torchModeSupported) { + throw CameraWebException( + textureId, + CameraErrorCode.torchModeNotSupported, + 'The torch mode is not supported in the current browser.', + ); + } + + // Save the updated flash mode to be used later when taking a picture. + flashMode = mode; + + // Enable the torch mode only if the flash mode is torch. + _setTorchMode(enabled: mode == FlashMode.torch); + } + + /// Sets the camera torch mode constraint to [enabled]. + void _setTorchMode({required bool enabled}) { + final videoTracks = stream?.getVideoTracks() ?? []; + + if (videoTracks.isNotEmpty) { + final defaultVideoTrack = videoTracks.first; + + final bool canEnableTorchMode = + defaultVideoTrack.getCapabilities()[_torchModeKey] ?? false; + + if (canEnableTorchMode) { + defaultVideoTrack.applyConstraints({ + "advanced": [ + { + _torchModeKey: enabled, + } + ] + }); + } else { + throw CameraWebException( + textureId, + CameraErrorCode.torchModeNotSupported, + 'The torch mode is not supported by the current camera.', + ); + } + } else { + throw CameraWebException( + textureId, + CameraErrorCode.notStarted, + 'The camera has not been initialized or started.', + ); + } + } + /// Returns the registered view type of the camera. String getViewType() => _getViewType(textureId); diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 01fc0a23aa34..0ae0d9e75c24 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -429,8 +429,15 @@ class CameraPlugin extends CameraPlatform { } @override - Future setFlashMode(int cameraId, FlashMode mode) { - throw UnimplementedError('setFlashMode() is not implemented.'); + Future setFlashMode(int cameraId, FlashMode mode) async { + try { + getCamera(cameraId).setFlashMode(mode); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } } @override diff --git a/packages/camera/camera_web/lib/src/types/camera_error_code.dart b/packages/camera/camera_web/lib/src/types/camera_error_code.dart index 9a70663c4aaf..904920db6ac6 100644 --- a/packages/camera/camera_web/lib/src/types/camera_error_code.dart +++ b/packages/camera/camera_web/lib/src/types/camera_error_code.dart @@ -52,6 +52,14 @@ class CameraErrorCode { static const CameraErrorCode orientationNotSupported = CameraErrorCode._('orientationNotSupported'); + /// The camera torch mode is not supported. + static const CameraErrorCode torchModeNotSupported = + CameraErrorCode._('torchModeNotSupported'); + + /// The camera has not been initialized or started. + static const CameraErrorCode notStarted = + CameraErrorCode._('cameraNotStarted'); + /// An unknown camera error. static const CameraErrorCode unknown = CameraErrorCode._('cameraUnknown'); diff --git a/packages/camera/camera_web/test/types/camera_error_code_test.dart b/packages/camera/camera_web/test/types/camera_error_code_test.dart index 6f2d7dd1cd09..1fec82b16f8d 100644 --- a/packages/camera/camera_web/test/types/camera_error_code_test.dart +++ b/packages/camera/camera_web/test/types/camera_error_code_test.dart @@ -82,6 +82,20 @@ void main() { ); }); + test('torchModeNotSupported', () { + expect( + CameraErrorCode.torchModeNotSupported.toString(), + equals('torchModeNotSupported'), + ); + }); + + test('notStarted', () { + expect( + CameraErrorCode.notStarted.toString(), + equals('cameraNotStarted'), + ); + }); + test('unknown', () { expect( CameraErrorCode.unknown.toString(), From 19f2ff71a8ba86b4cf4a52e9d18b4c650dcb9285 Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Mon, 9 Aug 2021 12:40:05 -0700 Subject: [PATCH 05/57] Add `buildViewWithTextDirection` to platform interface (#4121) --- .../CHANGELOG.md | 4 + .../method_channel_google_maps_flutter.dart | 160 ++++++++---------- .../google_maps_flutter_platform.dart | 38 ++++- .../pubspec.yaml | 2 +- .../google_maps_flutter_platform_test.dart | 40 +++++ 5 files changed, 157 insertions(+), 87 deletions(-) diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md index 2dc533fe1dfa..5d361d8e0c7c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.1 + +* Method `buildViewWithTextDirection` has been added to the platform interface. + ## 2.1.0 * Add support for Hybrid Composition when building the Google Maps widget on Android. Set diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart index 41aedc759b15..2b9c71ee85bd 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart @@ -456,11 +456,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { /// Defaults to false. bool useAndroidViewSurface = false; - /// Returns a widget displaying the map view. - /// - /// This method includes a parameter for platforms that require a text - /// direction. For example, this should be used when using hybrid composition - /// on Android. + @override Widget buildViewWithTextDirection( int creationId, PlatformViewCreatedCallback onPlatformViewCreated, { @@ -473,79 +469,6 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { Set tileOverlays = const {}, Set>? gestureRecognizers, Map mapOptions = const {}, - }) { - if (defaultTargetPlatform == TargetPlatform.android && - useAndroidViewSurface) { - final Map creationParams = { - 'initialCameraPosition': initialCameraPosition.toMap(), - 'options': mapOptions, - 'markersToAdd': serializeMarkerSet(markers), - 'polygonsToAdd': serializePolygonSet(polygons), - 'polylinesToAdd': serializePolylineSet(polylines), - 'circlesToAdd': serializeCircleSet(circles), - 'tileOverlaysToAdd': serializeTileOverlaySet(tileOverlays), - }; - return PlatformViewLink( - viewType: 'plugins.flutter.io/google_maps', - surfaceFactory: ( - BuildContext context, - PlatformViewController controller, - ) { - return AndroidViewSurface( - controller: controller as AndroidViewController, - gestureRecognizers: gestureRecognizers ?? - const >{}, - hitTestBehavior: PlatformViewHitTestBehavior.opaque, - ); - }, - onCreatePlatformView: (PlatformViewCreationParams params) { - final SurfaceAndroidViewController controller = - PlatformViewsService.initSurfaceAndroidView( - id: params.id, - viewType: 'plugins.flutter.io/google_maps', - layoutDirection: textDirection, - creationParams: creationParams, - creationParamsCodec: const StandardMessageCodec(), - onFocus: () => params.onFocusChanged(true), - ); - controller.addOnPlatformViewCreatedListener( - params.onPlatformViewCreated, - ); - controller.addOnPlatformViewCreatedListener( - onPlatformViewCreated, - ); - - controller.create(); - return controller; - }, - ); - } - return buildView( - creationId, - onPlatformViewCreated, - initialCameraPosition: initialCameraPosition, - markers: markers, - polygons: polygons, - polylines: polylines, - circles: circles, - tileOverlays: tileOverlays, - gestureRecognizers: gestureRecognizers, - mapOptions: mapOptions, - ); - } - - @override - Widget buildView( - int creationId, - PlatformViewCreatedCallback onPlatformViewCreated, { - required CameraPosition initialCameraPosition, - Set markers = const {}, - Set polygons = const {}, - Set polylines = const {}, - Set circles = const {}, - Set tileOverlays = const {}, - Set>? gestureRecognizers, - Map mapOptions = const {}, }) { final Map creationParams = { 'initialCameraPosition': initialCameraPosition.toMap(), @@ -556,14 +479,52 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { 'circlesToAdd': serializeCircleSet(circles), 'tileOverlaysToAdd': serializeTileOverlaySet(tileOverlays), }; + if (defaultTargetPlatform == TargetPlatform.android) { - return AndroidView( - viewType: 'plugins.flutter.io/google_maps', - onPlatformViewCreated: onPlatformViewCreated, - gestureRecognizers: gestureRecognizers, - creationParams: creationParams, - creationParamsCodec: const StandardMessageCodec(), - ); + if (useAndroidViewSurface) { + return PlatformViewLink( + viewType: 'plugins.flutter.io/google_maps', + surfaceFactory: ( + BuildContext context, + PlatformViewController controller, + ) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: gestureRecognizers ?? + const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + final SurfaceAndroidViewController controller = + PlatformViewsService.initSurfaceAndroidView( + id: params.id, + viewType: 'plugins.flutter.io/google_maps', + layoutDirection: textDirection, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + onFocus: () => params.onFocusChanged(true), + ); + controller.addOnPlatformViewCreatedListener( + params.onPlatformViewCreated, + ); + controller.addOnPlatformViewCreatedListener( + onPlatformViewCreated, + ); + + controller.create(); + return controller; + }, + ); + } else { + return AndroidView( + viewType: 'plugins.flutter.io/google_maps', + onPlatformViewCreated: onPlatformViewCreated, + gestureRecognizers: gestureRecognizers, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + ); + } } else if (defaultTargetPlatform == TargetPlatform.iOS) { return UiKitView( viewType: 'plugins.flutter.io/google_maps', @@ -573,7 +534,36 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { creationParamsCodec: const StandardMessageCodec(), ); } + return Text( '$defaultTargetPlatform is not yet supported by the maps plugin'); } + + @override + Widget buildView( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers, + Map mapOptions = const {}, + }) { + return buildViewWithTextDirection( + creationId, + onPlatformViewCreated, + initialCameraPosition: initialCameraPosition, + textDirection: TextDirection.ltr, + markers: markers, + polygons: polygons, + polylines: polylines, + circles: circles, + tileOverlays: tileOverlays, + gestureRecognizers: gestureRecognizers, + mapOptions: mapOptions, + ); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart index 425e040ee812..2bb0ab2588f9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart @@ -338,7 +338,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { throw UnimplementedError('dispose() has not been implemented.'); } - /// Returns a widget displaying the map view + /// Returns a widget displaying the map view. Widget buildView( int creationId, PlatformViewCreatedCallback onPlatformViewCreated, { @@ -356,4 +356,40 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { }) { throw UnimplementedError('buildView() has not been implemented.'); } + + /// Returns a widget displaying the map view. + /// + /// This method is similar to [buildView], but contains a parameter for + /// platforms that require a text direction. + /// + /// Default behavior passes all parameters except `textDirection` to + /// [buildView]. This is for backward compatibility with existing + /// implementations. Platforms that use the text direction should override + /// this as the primary implementation, and delegate to it from buildView. + Widget buildViewWithTextDirection( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + required TextDirection textDirection, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers, + Map mapOptions = const {}, + }) { + return buildView( + creationId, + onPlatformViewCreated, + initialCameraPosition: initialCameraPosition, + markers: markers, + polygons: polygons, + polylines: polylines, + circles: circles, + tileOverlays: tileOverlays, + gestureRecognizers: gestureRecognizers, + mapOptions: mapOptions, + ); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml index 1ea425ea0273..1dc73f442d2e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/google_maps_ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.1.0 +version: 2.1.1 environment: sdk: '>=2.12.0 <3.0.0' diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart index 2c50313ab8a6..de4edf375696 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart @@ -2,6 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; import 'package:mockito/mockito.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; @@ -34,6 +38,23 @@ void main() { test('Can be extended', () { GoogleMapsFlutterPlatform.instance = ExtendsGoogleMapsFlutterPlatform(); }); + + test( + 'default implementation of `buildViewWithTextDirection` delegates to `buildView`', + () { + final GoogleMapsFlutterPlatform platform = + BuildViewGoogleMapsFlutterPlatform(); + expect( + platform.buildViewWithTextDirection( + 0, + (_) {}, + initialCameraPosition: CameraPosition(target: LatLng(0.0, 0.0)), + textDirection: TextDirection.ltr, + ), + isA(), + ); + }, + ); }); } @@ -45,3 +66,22 @@ class ImplementsGoogleMapsFlutterPlatform extends Mock implements GoogleMapsFlutterPlatform {} class ExtendsGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform {} + +class BuildViewGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { + @override + Widget buildView( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers = + const >{}, + Map mapOptions = const {}, + }) { + return const Text(''); + } +} From 256d37dd2198b71edeeb4e6fe42ec657045c1516 Mon Sep 17 00:00:00 2001 From: Marcus Date: Tue, 10 Aug 2021 00:05:07 +0200 Subject: [PATCH 06/57] [image_picker] fix camera on Android 11 (#3194) --- .../image_picker/image_picker/CHANGELOG.md | 5 ++ .../imagepicker/ImagePickerDelegate.java | 54 +++++++++---------- .../imagepicker/ImagePickerDelegateTest.java | 16 +++--- .../image_picker/image_picker/pubspec.yaml | 2 +- 4 files changed, 37 insertions(+), 40 deletions(-) diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index f9c7640183d5..9d89389cb105 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.3+2 + +* Fix using Camera as image source on Android 11+ + ## 0.8.3+1 * Fixed README Example. @@ -26,6 +30,7 @@ * Fix image picker causing a crash when the cache directory is deleted. ## 0.8.1+2 + * Update the example app to support the multi-image feature. ## 0.8.1+1 diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java index 8b904f5d769d..dbd0f70af936 100644 --- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java +++ b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java @@ -6,6 +6,7 @@ import android.Manifest; import android.app.Activity; +import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -88,7 +89,6 @@ public class ImagePickerDelegate private final ImageResizer imageResizer; private final ImagePickerCache cache; private final PermissionManager permissionManager; - private final IntentResolver intentResolver; private final FileUriResolver fileUriResolver; private final FileUtils fileUtils; private CameraDevice cameraDevice; @@ -101,10 +101,6 @@ interface PermissionManager { boolean needRequestCameraPermission(); } - interface IntentResolver { - boolean resolveActivity(Intent intent); - } - interface FileUriResolver { Uri resolveFileProviderUriForFile(String fileProviderName, File imageFile); @@ -148,12 +144,6 @@ public boolean needRequestCameraPermission() { return ImagePickerUtils.needRequestCameraPermission(activity); } }, - new IntentResolver() { - @Override - public boolean resolveActivity(Intent intent) { - return intent.resolveActivity(activity.getPackageManager()) != null; - } - }, new FileUriResolver() { @Override public Uri resolveFileProviderUriForFile(String fileProviderName, File file) { @@ -190,7 +180,6 @@ public void onScanCompleted(String path, Uri uri) { final MethodCall methodCall, final ImagePickerCache cache, final PermissionManager permissionManager, - final IntentResolver intentResolver, final FileUriResolver fileUriResolver, final FileUtils fileUtils) { this.activity = activity; @@ -200,7 +189,6 @@ public void onScanCompleted(String path, Uri uri) { this.pendingResult = result; this.methodCall = methodCall; this.permissionManager = permissionManager; - this.intentResolver = intentResolver; this.fileUriResolver = fileUriResolver; this.fileUtils = fileUtils; this.cache = cache; @@ -291,13 +279,6 @@ private void launchTakeVideoWithCameraIntent() { useFrontCamera(intent); } - boolean canTakePhotos = intentResolver.resolveActivity(intent); - - if (!canTakePhotos) { - finishWithError("no_available_camera", "No cameras available for taking pictures."); - return; - } - File videoFile = createTemporaryWritableVideoFile(); pendingCameraMediaUri = Uri.parse("file:" + videoFile.getAbsolutePath()); @@ -305,7 +286,18 @@ private void launchTakeVideoWithCameraIntent() { intent.putExtra(MediaStore.EXTRA_OUTPUT, videoUri); grantUriPermissions(intent, videoUri); - activity.startActivityForResult(intent, REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA); + try { + activity.startActivityForResult(intent, REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA); + } catch (ActivityNotFoundException e) { + try { + // If we can't delete the file again here, there's not really anything we can do about it. + //noinspection ResultOfMethodCallIgnored + videoFile.delete(); + } catch (SecurityException exception) { + exception.printStackTrace(); + } + finishWithError("no_available_camera", "No cameras available for taking pictures."); + } } public void chooseImageFromGallery(MethodCall methodCall, MethodChannel.Result result) { @@ -371,13 +363,6 @@ private void launchTakeImageWithCameraIntent() { useFrontCamera(intent); } - boolean canTakePhotos = intentResolver.resolveActivity(intent); - - if (!canTakePhotos) { - finishWithError("no_available_camera", "No cameras available for taking pictures."); - return; - } - File imageFile = createTemporaryWritableImageFile(); pendingCameraMediaUri = Uri.parse("file:" + imageFile.getAbsolutePath()); @@ -385,7 +370,18 @@ private void launchTakeImageWithCameraIntent() { intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); grantUriPermissions(intent, imageUri); - activity.startActivityForResult(intent, REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA); + try { + activity.startActivityForResult(intent, REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA); + } catch (ActivityNotFoundException e) { + try { + // If we can't delete the file again here, there's not really anything we can do about it. + //noinspection ResultOfMethodCallIgnored + imageFile.delete(); + } catch (SecurityException exception) { + exception.printStackTrace(); + } + finishWithError("no_available_camera", "No cameras available for taking pictures."); + } } private File createTemporaryWritableImageFile() { diff --git a/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java index 1b55a7569eac..ebd58d05fee4 100644 --- a/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java +++ b/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java @@ -7,7 +7,9 @@ import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -16,6 +18,7 @@ import android.Manifest; import android.app.Activity; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -42,7 +45,6 @@ public class ImagePickerDelegateTest { @Mock MethodCall mockMethodCall; @Mock MethodChannel.Result mockResult; @Mock ImagePickerDelegate.PermissionManager mockPermissionManager; - @Mock ImagePickerDelegate.IntentResolver mockIntentResolver; @Mock FileUtils mockFileUtils; @Mock Intent mockIntent; @Mock ImagePickerCache cache; @@ -164,7 +166,6 @@ public void takeImageWithCamera_WhenHasNoCameraPermission_RequestsForPermission( @Test public void takeImageWithCamera_WhenCameraPermissionNotPresent_RequestsForPermission() { when(mockPermissionManager.needRequestCameraPermission()).thenReturn(false); - when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true); ImagePickerDelegate delegate = createDelegate(); delegate.takeImageWithCamera(mockMethodCall, mockResult); @@ -178,7 +179,6 @@ public void takeImageWithCamera_WhenCameraPermissionNotPresent_RequestsForPermis public void takeImageWithCamera_WhenHasCameraPermission_AndAnActivityCanHandleCameraIntent_LaunchesTakeWithCameraIntent() { when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true); - when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true); ImagePickerDelegate delegate = createDelegate(); delegate.takeImageWithCamera(mockMethodCall, mockResult); @@ -192,8 +192,9 @@ public void takeImageWithCamera_WhenCameraPermissionNotPresent_RequestsForPermis public void takeImageWithCamera_WhenHasCameraPermission_AndNoActivityToHandleCameraIntent_FinishesWithNoCamerasAvailableError() { when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true); - when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(false); - + doThrow(ActivityNotFoundException.class) + .when(mockActivity) + .startActivityForResult(any(Intent.class), anyInt()); ImagePickerDelegate delegate = createDelegate(); delegate.takeImageWithCamera(mockMethodCall, mockResult); @@ -205,7 +206,6 @@ public void takeImageWithCamera_WhenCameraPermissionNotPresent_RequestsForPermis @Test public void takeImageWithCamera_WritesImageToCacheDirectory() { when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true); - when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true); ImagePickerDelegate delegate = createDelegate(); delegate.takeImageWithCamera(mockMethodCall, mockResult); @@ -231,7 +231,6 @@ public void onRequestPermissionsResult_WhenCameraPermissionDenied_FinishesWithEr @Test public void onRequestTakeVideoPermissionsResult_WhenCameraPermissionGranted_LaunchesTakeVideoWithCameraIntent() { - when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true); ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); delegate.onRequestPermissionsResult( @@ -247,7 +246,6 @@ public void onRequestPermissionsResult_WhenCameraPermissionDenied_FinishesWithEr @Test public void onRequestTakeImagePermissionsResult_WhenCameraPermissionGranted_LaunchesTakeWithCameraIntent() { - when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true); ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); delegate.onRequestPermissionsResult( @@ -379,7 +377,6 @@ private ImagePickerDelegate createDelegate() { null, cache, mockPermissionManager, - mockIntentResolver, mockFileUriResolver, mockFileUtils); } @@ -393,7 +390,6 @@ private ImagePickerDelegate createDelegateWithPendingResultAndMethodCall() { mockMethodCall, cache, mockPermissionManager, - mockIntentResolver, mockFileUriResolver, mockFileUtils); } diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index e67e79fbba14..e167d8ab891c 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/master/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.3+1 +version: 0.8.3+2 environment: sdk: ">=2.12.0 <3.0.0" From d31bd7db62cb141ce98d2bf1286a606f017b607f Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Tue, 10 Aug 2021 11:57:06 -0700 Subject: [PATCH 07/57] [webview_flutter] Only call onWebResourceError for main frame (#3078) --- .../webview_flutter/CHANGELOG.md | 4 +- .../webviewflutter/FlutterWebViewClient.java | 17 ++++--- .../webview_flutter_test.dart | 44 +++++++++++++++++++ .../webview_flutter/lib/webview_flutter.dart | 3 +- .../webview_flutter/pubspec.yaml | 2 +- 5 files changed, 61 insertions(+), 9 deletions(-) diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index fcfaf4e5720d..df7d9cb87457 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,6 +1,8 @@ -## NEXT +## 2.0.12 * Improved the documentation on using the different Android Platform View modes. +* So that Android and iOS behave the same, `onWebResourceError` is now only called for the main + page. ## 2.0.11 diff --git a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java index 4e7056f1468c..adc84671a701 100644 --- a/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java +++ b/packages/webview_flutter/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java @@ -14,6 +14,7 @@ import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.webkit.WebViewClient; +import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.webkit.WebResourceErrorCompat; import androidx.webkit.WebViewClientCompat; @@ -192,8 +193,10 @@ public void onPageFinished(WebView view, String url) { @Override public void onReceivedError( WebView view, WebResourceRequest request, WebResourceError error) { - FlutterWebViewClient.this.onWebResourceError( - error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); + if (request.isForMainFrame()) { + FlutterWebViewClient.this.onWebResourceError( + error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); + } } @Override @@ -239,9 +242,13 @@ public void onPageFinished(WebView view, String url) { @SuppressLint("RequiresFeature") @Override public void onReceivedError( - WebView view, WebResourceRequest request, WebResourceErrorCompat error) { - FlutterWebViewClient.this.onWebResourceError( - error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); + @NonNull WebView view, + @NonNull WebResourceRequest request, + @NonNull WebResourceErrorCompat error) { + if (request.isForMainFrame()) { + FlutterWebViewClient.this.onWebResourceError( + error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); + } } @Override diff --git a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart index 876f961a353b..f3eeee156421 100644 --- a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -1139,6 +1139,7 @@ void main() { (WidgetTester tester) async { final Completer errorCompleter = Completer(); + final Completer pageFinishCompleter = Completer(); await tester.pumpWidget( Directionality( @@ -1150,13 +1151,56 @@ void main() { onWebResourceError: (WebResourceError error) { errorCompleter.complete(error); }, + onPageFinished: (_) => pageFinishCompleter.complete(), ), ), ); expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; }); + testWidgets( + 'onWebResourceError only called for main frame', + (WidgetTester tester) async { + final String iframeTest = ''' + + + + WebResourceError test + + + + + + '''; + final String iframeTestBase64 = + base64Encode(const Utf8Encoder().convert(iframeTest)); + + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,$iframeTestBase64', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }, + ); + testWidgets('can block requests', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); diff --git a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart index 74d8af8d4687..398ac876bf3e 100644 --- a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart @@ -369,8 +369,7 @@ class WebView extends StatefulWidget { /// Invoked when a web resource has failed to load. /// - /// This can be called for any resource (iframe, image, etc.), not just for - /// the main page. + /// This callback is only called for the main page. final WebResourceErrorCallback? onWebResourceError; /// Controls whether WebView debugging is enabled. diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 2f00071e772e..cc5d9cdc8b96 100644 --- a/packages/webview_flutter/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.0.11 +version: 2.0.12 environment: sdk: ">=2.12.0 <3.0.0" From e3d5ef0ddb77c880bc37a7530cd5cb0ba7523a92 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Wed, 11 Aug 2021 01:02:05 +0200 Subject: [PATCH 08/57] [camera_web] Add support for a zoom level (#4224) --- .../camera_settings_test.dart | 153 +++++++ .../example/integration_test/camera_test.dart | 174 +++++++ .../integration_test/camera_web_test.dart | 425 +++++++++++++++++- .../camera/camera_web/lib/src/camera.dart | 50 +++ .../camera_web/lib/src/camera_settings.dart | 61 +++ .../camera/camera_web/lib/src/camera_web.dart | 35 +- .../lib/src/types/camera_error_code.dart | 8 + .../camera_web/lib/src/types/types.dart | 1 + .../lib/src/types/zoom_level_capability.dart | 45 ++ packages/camera/camera_web/pubspec.yaml | 1 + .../camera/camera_web/test/helpers/mocks.dart | 3 + .../test/types/camera_error_code_test.dart | 14 + .../types/zoom_level_capability_test.dart | 47 ++ 13 files changed, 993 insertions(+), 24 deletions(-) create mode 100644 packages/camera/camera_web/lib/src/types/zoom_level_capability.dart create mode 100644 packages/camera/camera_web/test/types/zoom_level_capability_test.dart diff --git a/packages/camera/camera_web/example/integration_test/camera_settings_test.dart b/packages/camera/camera_web/example/integration_test/camera_settings_test.dart index bc228b2e35c6..0e1d78789f08 100644 --- a/packages/camera/camera_web/example/integration_test/camera_settings_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_settings_test.dart @@ -4,8 +4,10 @@ import 'dart:html'; import 'dart:ui'; +import 'dart:js_util' as js_util; import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/src/camera.dart'; import 'package:camera_web/src/camera_settings.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter/services.dart'; @@ -342,6 +344,157 @@ void main() { }); }); + group('getZoomLevelCapabilityForCamera', () { + late Camera camera; + late List videoTracks; + + setUp(() { + camera = MockCamera(); + videoTracks = [MockMediaStreamTrack(), MockMediaStreamTrack()]; + + when(() => camera.textureId).thenReturn(0); + when(() => camera.stream).thenReturn(FakeMediaStream(videoTracks)); + }); + + testWidgets( + 'returns the zoom level capability ' + 'based on the first video track', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'zoom': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'zoom': js_util.jsify({ + 'min': 100, + 'max': 400, + 'step': 2, + }), + }); + + final zoomLevelCapability = + settings.getZoomLevelCapabilityForCamera(camera); + + expect(zoomLevelCapability.minimum, equals(100.0)); + expect(zoomLevelCapability.maximum, equals(400.0)); + expect(zoomLevelCapability.videoTrack, equals(videoTracks.first)); + }); + + group('throws CameraWebException', () { + testWidgets( + 'with zoomLevelNotSupported error ' + 'when there are no media devices', (tester) async { + when(() => navigator.mediaDevices).thenReturn(null); + + expect( + () => settings.getZoomLevelCapabilityForCamera(camera), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + camera.textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.zoomLevelNotSupported, + ), + ), + ); + }); + + testWidgets( + 'with zoomLevelNotSupported error ' + 'when the zoom level is not supported ' + 'in the browser', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'zoom': false, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'zoom': { + 'min': 100, + 'max': 400, + 'step': 2, + }, + }); + + expect( + () => settings.getZoomLevelCapabilityForCamera(camera), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + camera.textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.zoomLevelNotSupported, + ), + ), + ); + }); + + testWidgets( + 'with zoomLevelNotSupported error ' + 'when the zoom level is not supported ' + 'by the camera', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'zoom': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({}); + + expect( + () => settings.getZoomLevelCapabilityForCamera(camera), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + camera.textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.zoomLevelNotSupported, + ), + ), + ); + }); + + testWidgets( + 'with notStarted error ' + 'when the camera stream has not been initialized', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'zoom': true, + }); + + // Create a camera stream with no video tracks. + when(() => camera.stream).thenReturn(FakeMediaStream([])); + + expect( + () => settings.getZoomLevelCapabilityForCamera(camera), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + camera.textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.notStarted, + ), + ), + ); + }); + }); + }); + group('getFacingModeForVideoTrack', () { testWidgets( 'throws PlatformException ' diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart index 740e24f87819..03ffe81cad64 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -627,6 +627,180 @@ void main() { }); }); + group('zoomLevel', () { + group('getMaxZoomLevel', () { + testWidgets( + 'returns maximum ' + 'from CameraSettings.getZoomLevelCapabilityForCamera', + (tester) async { + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ); + + final zoomLevelCapability = ZoomLevelCapability( + minimum: 50.0, + maximum: 100.0, + videoTrack: MockMediaStreamTrack(), + ); + + when(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + .thenReturn(zoomLevelCapability); + + final maximumZoomLevel = camera.getMaxZoomLevel(); + + verify(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + .called(1); + + expect( + maximumZoomLevel, + equals(zoomLevelCapability.maximum), + ); + }); + }); + + group('getMinZoomLevel', () { + testWidgets( + 'returns minimum ' + 'from CameraSettings.getZoomLevelCapabilityForCamera', + (tester) async { + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ); + + final zoomLevelCapability = ZoomLevelCapability( + minimum: 50.0, + maximum: 100.0, + videoTrack: MockMediaStreamTrack(), + ); + + when(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + .thenReturn(zoomLevelCapability); + + final minimumZoomLevel = camera.getMinZoomLevel(); + + verify(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + .called(1); + + expect( + minimumZoomLevel, + equals(zoomLevelCapability.minimum), + ); + }); + }); + + group('setZoomLevel', () { + testWidgets( + 'applies zoom on the video track ' + 'from CameraSettings.getZoomLevelCapabilityForCamera', + (tester) async { + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ); + + final videoTrack = MockMediaStreamTrack(); + + final zoomLevelCapability = ZoomLevelCapability( + minimum: 50.0, + maximum: 100.0, + videoTrack: videoTrack, + ); + + when(() => videoTrack.applyConstraints(any())) + .thenAnswer((_) async {}); + + when(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + .thenReturn(zoomLevelCapability); + + const zoom = 75.0; + + camera.setZoomLevel(zoom); + + verify( + () => videoTrack.applyConstraints({ + "advanced": [ + { + ZoomLevelCapability.constraintName: zoom, + } + ] + }), + ).called(1); + }); + + group('throws CameraWebException', () { + testWidgets( + 'with zoomLevelInvalid error ' + 'when the provided zoom level is below minimum', (tester) async { + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ); + + final zoomLevelCapability = ZoomLevelCapability( + minimum: 50.0, + maximum: 100.0, + videoTrack: MockMediaStreamTrack(), + ); + + when(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + .thenReturn(zoomLevelCapability); + + expect( + () => camera.setZoomLevel(45.0), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.zoomLevelInvalid, + ), + )); + }); + + testWidgets( + 'with zoomLevelInvalid error ' + 'when the provided zoom level is below minimum', (tester) async { + final camera = Camera( + textureId: textureId, + cameraSettings: cameraSettings, + ); + + final zoomLevelCapability = ZoomLevelCapability( + minimum: 50.0, + maximum: 100.0, + videoTrack: MockMediaStreamTrack(), + ); + + when(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + .thenReturn(zoomLevelCapability); + + expect( + () => camera.setZoomLevel(105.0), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.zoomLevelInvalid, + ), + )); + }); + }); + }); + }); + group('getViewType', () { testWidgets('returns a correct view type', (tester) async { final camera = Camera( diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index fda35dd088c1..eb988f49ab87 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -1131,28 +1131,302 @@ void main() { ); }); - testWidgets('getMaxZoomLevel throws UnimplementedError', (tester) async { - expect( - () => CameraPlatform.instance.getMaxZoomLevel(cameraId), - throwsUnimplementedError, - ); + group('getMaxZoomLevel', () { + testWidgets('calls getMaxZoomLevel on the camera', (tester) async { + final camera = MockCamera(); + const maximumZoomLevel = 100.0; + + when(camera.getMaxZoomLevel).thenReturn(maximumZoomLevel); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + await CameraPlatform.instance.getMaxZoomLevel( + cameraId, + ), + equals(maximumZoomLevel), + ); + + verify(camera.getMaxZoomLevel).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () async => await CameraPlatform.instance.getMaxZoomLevel( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when getMaxZoomLevel throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.NOT_SUPPORTED); + + when(camera.getMaxZoomLevel).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.getMaxZoomLevel( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when getMaxZoomLevel throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.getMaxZoomLevel).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.getMaxZoomLevel( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); }); - testWidgets('getMinZoomLevel throws UnimplementedError', (tester) async { - expect( - () => CameraPlatform.instance.getMinZoomLevel(cameraId), - throwsUnimplementedError, - ); + group('getMinZoomLevel', () { + testWidgets('calls getMinZoomLevel on the camera', (tester) async { + final camera = MockCamera(); + const minimumZoomLevel = 100.0; + + when(camera.getMinZoomLevel).thenReturn(minimumZoomLevel); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + await CameraPlatform.instance.getMinZoomLevel( + cameraId, + ), + equals(minimumZoomLevel), + ); + + verify(camera.getMinZoomLevel).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () async => await CameraPlatform.instance.getMinZoomLevel( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when getMinZoomLevel throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.NOT_SUPPORTED); + + when(camera.getMinZoomLevel).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.getMinZoomLevel( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when getMinZoomLevel throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.getMinZoomLevel).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.getMinZoomLevel( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); }); - testWidgets('setZoomLevel throws UnimplementedError', (tester) async { - expect( - () => CameraPlatform.instance.setZoomLevel( - cameraId, - 1.0, - ), - throwsUnimplementedError, - ); + group('setZoomLevel', () { + testWidgets('calls setZoomLevel on the camera', (tester) async { + final camera = MockCamera(); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + const zoom = 100.0; + + await CameraPlatform.instance.setZoomLevel(cameraId, zoom); + + verify(() => camera.setZoomLevel(zoom)).called(1); + }); + + group('throws CameraException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () async => await CameraPlatform.instance.setZoomLevel( + cameraId, + 100.0, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when setZoomLevel throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.NOT_SUPPORTED); + + when(() => camera.setZoomLevel(any())).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.setZoomLevel( + cameraId, + 100.0, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when setZoomLevel throws PlatformException', + (tester) async { + final camera = MockCamera(); + final exception = PlatformException( + code: CameraErrorCode.notSupported.toString(), + message: 'message', + ); + + when(() => camera.setZoomLevel(any())).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.setZoomLevel( + cameraId, + 100.0, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code, + ), + ), + ); + }); + + testWidgets('when setZoomLevel throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(() => camera.setZoomLevel(any())).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.setZoomLevel( + cameraId, + 100.0, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); }); testWidgets( @@ -1538,6 +1812,121 @@ void main() { await streamQueue.cancel(); }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on getMaxZoomLevel error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.zoomLevelNotSupported, + 'description', + ); + + when(camera.getMaxZoomLevel).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => await CameraPlatform.instance.getMaxZoomLevel( + cameraId, + ), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on getMinZoomLevel error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.zoomLevelNotSupported, + 'description', + ); + + when(camera.getMinZoomLevel).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => await CameraPlatform.instance.getMinZoomLevel( + cameraId, + ), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on setZoomLevel error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.zoomLevelNotSupported, + 'description', + ); + + when(() => camera.setZoomLevel(any())).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => await CameraPlatform.instance.setZoomLevel( + cameraId, + 100.0, + ), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); }); testWidgets('onVideoRecordedEvent throws UnimplementedError', diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index 1cd007b917bb..c77d36023058 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -28,6 +28,10 @@ String _getViewType(int cameraId) => 'plugins.flutter.io/camera_$cameraId'; /// The camera can be played/stopped by calling [play]/[stop] /// or may capture a picture by calling [takePicture]. /// +/// The camera zoom may be adjusted with [setZoomLevel]. The provided +/// zoom level must be a value in the range of [getMinZoomLevel] to +/// [getMaxZoomLevel]. +/// /// The [textureId] is used to register a camera view with the id /// defined by [_getViewType]. class Camera { @@ -182,6 +186,9 @@ class Camera { } /// Sets the camera flash mode to [mode]. + /// + /// Throws a [CameraWebException] if the torch mode is not supported + /// or the camera has not been initialized or started. void setFlashMode(FlashMode mode) { final mediaDevices = window?.navigator.mediaDevices; final supportedConstraints = mediaDevices?.getSupportedConstraints(); @@ -203,6 +210,9 @@ class Camera { } /// Sets the camera torch mode constraint to [enabled]. + /// + /// Throws a [CameraWebException] if the torch mode is not supported + /// or the camera has not been initialized or started. void _setTorchMode({required bool enabled}) { final videoTracks = stream?.getVideoTracks() ?? []; @@ -236,6 +246,46 @@ class Camera { } } + /// Returns the camera maximum zoom level. + /// + /// Throws a [CameraWebException] if the zoom level is not supported + /// or the camera has not been initialized or started. + double getMaxZoomLevel() => + _cameraSettings.getZoomLevelCapabilityForCamera(this).maximum; + + /// Returns the camera minimum zoom level. + /// + /// Throws a [CameraWebException] if the zoom level is not supported + /// or the camera has not been initialized or started. + double getMinZoomLevel() => + _cameraSettings.getZoomLevelCapabilityForCamera(this).minimum; + + /// Sets the camera zoom level to [zoom]. + /// + /// Throws a [CameraWebException] if the zoom level is invalid, + /// not supported or the camera has not been initialized or started. + void setZoomLevel(double zoom) { + final zoomLevelCapability = + _cameraSettings.getZoomLevelCapabilityForCamera(this); + + if (zoom < zoomLevelCapability.minimum || + zoom > zoomLevelCapability.maximum) { + throw CameraWebException( + textureId, + CameraErrorCode.zoomLevelInvalid, + 'The provided zoom level must be in the range of ${zoomLevelCapability.minimum} to ${zoomLevelCapability.maximum}.', + ); + } + + zoomLevelCapability.videoTrack.applyConstraints({ + "advanced": [ + { + ZoomLevelCapability.constraintName: zoom, + } + ] + }); + } + /// Returns the registered view type of the camera. String getViewType() => _getViewType(textureId); diff --git a/packages/camera/camera_web/lib/src/camera_settings.dart b/packages/camera/camera_web/lib/src/camera_settings.dart index ce713bc52468..7d35fff84112 100644 --- a/packages/camera/camera_web/lib/src/camera_settings.dart +++ b/packages/camera/camera_web/lib/src/camera_settings.dart @@ -4,8 +4,10 @@ import 'dart:html' as html; import 'dart:ui'; +import 'dart:js_util' as js_util; import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/src/camera.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -107,6 +109,65 @@ class CameraSettings { } } + /// Returns the zoom level capability for the given [camera]. + /// + /// Throws a [CameraWebException] if the zoom level is not supported + /// or the camera has not been initialized or started. + ZoomLevelCapability getZoomLevelCapabilityForCamera( + Camera camera, + ) { + final mediaDevices = window?.navigator.mediaDevices; + final supportedConstraints = mediaDevices?.getSupportedConstraints(); + final zoomLevelSupported = + supportedConstraints?[ZoomLevelCapability.constraintName] ?? false; + + if (!zoomLevelSupported) { + throw CameraWebException( + camera.textureId, + CameraErrorCode.zoomLevelNotSupported, + 'The zoom level is not supported in the current browser.', + ); + } + + final videoTracks = camera.stream?.getVideoTracks() ?? []; + + if (videoTracks.isNotEmpty) { + final defaultVideoTrack = videoTracks.first; + + /// The zoom level capability is represented by MediaSettingsRange. + /// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaSettingsRange + final zoomLevelCapability = defaultVideoTrack + .getCapabilities()[ZoomLevelCapability.constraintName] ?? + {}; + + // The zoom level capability is a nested JS object, therefore + // we need to access its properties with the js_util library. + // See: https://api.dart.dev/stable/2.13.4/dart-js_util/getProperty.html + final minimumZoomLevel = js_util.getProperty(zoomLevelCapability, 'min'); + final maximumZoomLevel = js_util.getProperty(zoomLevelCapability, 'max'); + + if (minimumZoomLevel != null && maximumZoomLevel != null) { + return ZoomLevelCapability( + minimum: minimumZoomLevel.toDouble(), + maximum: maximumZoomLevel.toDouble(), + videoTrack: defaultVideoTrack, + ); + } else { + throw CameraWebException( + camera.textureId, + CameraErrorCode.zoomLevelNotSupported, + 'The zoom level is not supported by the current camera.', + ); + } + } else { + throw CameraWebException( + camera.textureId, + CameraErrorCode.notStarted, + 'The camera has not been initialized or started.', + ); + } + } + /// Returns a facing mode of the [videoTrack] /// (null if the facing mode is not available). String? getFacingModeForVideoTrack(html.MediaStreamTrack videoTrack) { diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 0ae0d9e75c24..ecea3a76e74a 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -481,18 +481,41 @@ class CameraPlugin extends CameraPlatform { } @override - Future getMaxZoomLevel(int cameraId) { - throw UnimplementedError('getMaxZoomLevel() is not implemented.'); + Future getMaxZoomLevel(int cameraId) async { + try { + return getCamera(cameraId).getMaxZoomLevel(); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } } @override - Future getMinZoomLevel(int cameraId) { - throw UnimplementedError('getMinZoomLevel() is not implemented.'); + Future getMinZoomLevel(int cameraId) async { + try { + return getCamera(cameraId).getMinZoomLevel(); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } } @override - Future setZoomLevel(int cameraId, double zoom) { - throw UnimplementedError('setZoomLevel() is not implemented.'); + Future setZoomLevel(int cameraId, double zoom) async { + try { + getCamera(cameraId).setZoomLevel(zoom); + } on html.DomException catch (e) { + throw CameraException(e.name, e.message); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw CameraException(e.code.toString(), e.description); + } } @override diff --git a/packages/camera/camera_web/lib/src/types/camera_error_code.dart b/packages/camera/camera_web/lib/src/types/camera_error_code.dart index 904920db6ac6..210fa2baa9d2 100644 --- a/packages/camera/camera_web/lib/src/types/camera_error_code.dart +++ b/packages/camera/camera_web/lib/src/types/camera_error_code.dart @@ -56,6 +56,14 @@ class CameraErrorCode { static const CameraErrorCode torchModeNotSupported = CameraErrorCode._('torchModeNotSupported'); + /// The camera zoom level is not supported. + static const CameraErrorCode zoomLevelNotSupported = + CameraErrorCode._('zoomLevelNotSupported'); + + /// The camera zoom level is invalid. + static const CameraErrorCode zoomLevelInvalid = + CameraErrorCode._('zoomLevelInvalid'); + /// The camera has not been initialized or started. static const CameraErrorCode notStarted = CameraErrorCode._('cameraNotStarted'); diff --git a/packages/camera/camera_web/lib/src/types/types.dart b/packages/camera/camera_web/lib/src/types/types.dart index 4e3902fcb3ee..72d7fb85af14 100644 --- a/packages/camera/camera_web/lib/src/types/types.dart +++ b/packages/camera/camera_web/lib/src/types/types.dart @@ -7,3 +7,4 @@ export 'camera_options.dart'; export 'camera_web_exception.dart'; export 'media_device_kind.dart'; export 'orientation_type.dart'; +export 'zoom_level_capability.dart'; diff --git a/packages/camera/camera_web/lib/src/types/zoom_level_capability.dart b/packages/camera/camera_web/lib/src/types/zoom_level_capability.dart new file mode 100644 index 000000000000..ace57140d956 --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/zoom_level_capability.dart @@ -0,0 +1,45 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:html' as html; +import 'dart:ui' show hashValues; + +/// The possible range of values for the zoom level configurable +/// on the camera video track. +class ZoomLevelCapability { + /// Creates a new instance of [ZoomLevelCapability] with the given + /// zoom level range of [minimum] to [maximum] configurable + /// on the [videoTrack]. + ZoomLevelCapability({ + required this.minimum, + required this.maximum, + required this.videoTrack, + }); + + /// The zoom level constraint name. + /// See: https://w3c.github.io/mediacapture-image/#dom-mediatracksupportedconstraints-zoom + static const constraintName = "zoom"; + + /// The minimum zoom level. + final double minimum; + + /// The maximum zoom level. + final double maximum; + + /// The video track capable of configuring the zoom level. + final html.MediaStreamTrack videoTrack; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is ZoomLevelCapability && + other.minimum == minimum && + other.maximum == maximum && + other.videoTrack == videoTrack; + } + + @override + int get hashCode => hashValues(minimum, maximum, videoTrack); +} diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index a2aa43c22d65..ec674f375164 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -30,4 +30,5 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + mocktail: ^0.1.4 pedantic: ^1.11.1 \ No newline at end of file diff --git a/packages/camera/camera_web/test/helpers/mocks.dart b/packages/camera/camera_web/test/helpers/mocks.dart index 0398ad33f126..34c56632b60f 100644 --- a/packages/camera/camera_web/test/helpers/mocks.dart +++ b/packages/camera/camera_web/test/helpers/mocks.dart @@ -5,6 +5,9 @@ import 'dart:html'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockMediaStreamTrack extends Mock implements MediaStreamTrack {} /// A fake [MediaError] that returns the provided error [_code]. class FakeMediaError extends Fake implements MediaError { diff --git a/packages/camera/camera_web/test/types/camera_error_code_test.dart b/packages/camera/camera_web/test/types/camera_error_code_test.dart index 1fec82b16f8d..c31dc6a9ffb0 100644 --- a/packages/camera/camera_web/test/types/camera_error_code_test.dart +++ b/packages/camera/camera_web/test/types/camera_error_code_test.dart @@ -89,6 +89,20 @@ void main() { ); }); + test('zoomLevelNotSupported', () { + expect( + CameraErrorCode.zoomLevelNotSupported.toString(), + equals('zoomLevelNotSupported'), + ); + }); + + test('zoomLevelInvalid', () { + expect( + CameraErrorCode.zoomLevelInvalid.toString(), + equals('zoomLevelInvalid'), + ); + }); + test('notStarted', () { expect( CameraErrorCode.notStarted.toString(), diff --git a/packages/camera/camera_web/test/types/zoom_level_capability_test.dart b/packages/camera/camera_web/test/types/zoom_level_capability_test.dart new file mode 100644 index 000000000000..c382b4b76cc4 --- /dev/null +++ b/packages/camera/camera_web/test/types/zoom_level_capability_test.dart @@ -0,0 +1,47 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../helpers/helpers.dart'; + +void main() { + group('ZoomLevelCapability', () { + test('sets all properties', () { + const minimum = 100.0; + const maximum = 400.0; + final videoTrack = MockMediaStreamTrack(); + + final capability = ZoomLevelCapability( + minimum: minimum, + maximum: maximum, + videoTrack: videoTrack, + ); + + expect(capability.minimum, equals(minimum)); + expect(capability.maximum, equals(maximum)); + expect(capability.videoTrack, equals(videoTrack)); + }); + + test('supports value equality', () { + final videoTrack = MockMediaStreamTrack(); + + expect( + ZoomLevelCapability( + minimum: 0.0, + maximum: 100.0, + videoTrack: videoTrack, + ), + equals( + ZoomLevelCapability( + minimum: 0.0, + maximum: 100.0, + videoTrack: videoTrack, + ), + ), + ); + }); + }); +} From b9ac641ee37c5aa0c0d03dab88e3c302af4d1350 Mon Sep 17 00:00:00 2001 From: Casey Hillers Date: Wed, 11 Aug 2021 10:57:06 -0700 Subject: [PATCH 09/57] [ci.yaml] Auto-generate LUCI configs (#4223) --- .ci.yaml | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/.ci.yaml b/.ci.yaml index 92bfc040eecb..c2b7deebab14 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -8,9 +8,39 @@ enabled_branches: - master +platform_properties: + windows: + properties: + caches: >- + [ + {"name": "vsbuild", "path": "vsbuild"}, + {"name": "pub_cache", "path": ".pub-cache"} + ] + dependencies: > + [ + {"dependency": "certs"} + ] + device_type: none + os: Windows + targets: - - name: Windows Plugins - builder: Windows Plugins - postsubmit: false + - name: Windows Plugins master channel + recipe: plugins/plugins + timeout: 30 + properties: + dependencies: > + [ + {"dependency": "vs_build"} + ] scheduler: luci + - name: Windows Plugins stable channel + recipe: plugins/plugins + timeout: 30 + properties: + channel: stable + dependencies: > + [ + {"dependency": "vs_build"} + ] + scheduler: luci From 77029678d2f2ae462f04de0ad96d361e516fd453 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 12 Aug 2021 00:32:06 +0200 Subject: [PATCH 10/57] [camera_web] Rename `CameraSettings` to `CameraService` (#4225) --- .../camera_error_code_test.dart | 45 +++---- .../camera_metadata_test.dart | 5 +- .../camera_options_test.dart | 39 +++--- ...ngs_test.dart => camera_service_test.dart} | 115 +++++++++--------- .../example/integration_test/camera_test.dart | 95 +++++++-------- .../camera_web_exception_test.dart | 7 +- .../integration_test/camera_web_test.dart | 85 +++++++------ .../integration_test/helpers/mocks.dart | 4 +- .../zoom_level_capability_test.dart | 9 +- .../camera/camera_web/lib/src/camera.dart | 22 ++-- ...mera_settings.dart => camera_service.dart} | 4 +- .../camera/camera_web/lib/src/camera_web.dart | 31 +++-- packages/camera/camera_web/pubspec.yaml | 1 - .../camera_web/test/helpers/helpers.dart | 5 - .../camera/camera_web/test/helpers/mocks.dart | 20 --- 15 files changed, 239 insertions(+), 248 deletions(-) rename packages/camera/camera_web/{test/types => example/integration_test}/camera_error_code_test.dart (73%) rename packages/camera/camera_web/{test/types => example/integration_test}/camera_metadata_test.dart (76%) rename packages/camera/camera_web/{test/types => example/integration_test}/camera_options_test.dart (82%) rename packages/camera/camera_web/example/integration_test/{camera_settings_test.dart => camera_service_test.dart} (86%) rename packages/camera/camera_web/{test/types => example/integration_test}/camera_web_exception_test.dart (80%) rename packages/camera/camera_web/{test/types => example/integration_test}/zoom_level_capability_test.dart (80%) rename packages/camera/camera_web/lib/src/{camera_settings.dart => camera_service.dart} (99%) delete mode 100644 packages/camera/camera_web/test/helpers/helpers.dart delete mode 100644 packages/camera/camera_web/test/helpers/mocks.dart diff --git a/packages/camera/camera_web/test/types/camera_error_code_test.dart b/packages/camera/camera_web/example/integration_test/camera_error_code_test.dart similarity index 73% rename from packages/camera/camera_web/test/types/camera_error_code_test.dart rename to packages/camera/camera_web/example/integration_test/camera_error_code_test.dart index c31dc6a9ffb0..d0250c6e4e26 100644 --- a/packages/camera/camera_web/test/types/camera_error_code_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_error_code_test.dart @@ -6,111 +6,114 @@ import 'dart:html'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; -import '../helpers/helpers.dart'; +import 'helpers/helpers.dart'; void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + group('CameraErrorCode', () { group('toString returns a correct type for', () { - test('notSupported', () { + testWidgets('notSupported', (tester) async { expect( CameraErrorCode.notSupported.toString(), equals('cameraNotSupported'), ); }); - test('notFound', () { + testWidgets('notFound', (tester) async { expect( CameraErrorCode.notFound.toString(), equals('cameraNotFound'), ); }); - test('notReadable', () { + testWidgets('notReadable', (tester) async { expect( CameraErrorCode.notReadable.toString(), equals('cameraNotReadable'), ); }); - test('overconstrained', () { + testWidgets('overconstrained', (tester) async { expect( CameraErrorCode.overconstrained.toString(), equals('cameraOverconstrained'), ); }); - test('permissionDenied', () { + testWidgets('permissionDenied', (tester) async { expect( CameraErrorCode.permissionDenied.toString(), equals('cameraPermission'), ); }); - test('type', () { + testWidgets('type', (tester) async { expect( CameraErrorCode.type.toString(), equals('cameraType'), ); }); - test('abort', () { + testWidgets('abort', (tester) async { expect( CameraErrorCode.abort.toString(), equals('cameraAbort'), ); }); - test('security', () { + testWidgets('security', (tester) async { expect( CameraErrorCode.security.toString(), equals('cameraSecurity'), ); }); - test('missingMetadata', () { + testWidgets('missingMetadata', (tester) async { expect( CameraErrorCode.missingMetadata.toString(), equals('cameraMissingMetadata'), ); }); - test('orientationNotSupported', () { + testWidgets('orientationNotSupported', (tester) async { expect( CameraErrorCode.orientationNotSupported.toString(), equals('orientationNotSupported'), ); }); - test('torchModeNotSupported', () { + testWidgets('torchModeNotSupported', (tester) async { expect( CameraErrorCode.torchModeNotSupported.toString(), equals('torchModeNotSupported'), ); }); - test('zoomLevelNotSupported', () { + testWidgets('zoomLevelNotSupported', (tester) async { expect( CameraErrorCode.zoomLevelNotSupported.toString(), equals('zoomLevelNotSupported'), ); }); - test('zoomLevelInvalid', () { + testWidgets('zoomLevelInvalid', (tester) async { expect( CameraErrorCode.zoomLevelInvalid.toString(), equals('zoomLevelInvalid'), ); }); - test('notStarted', () { + testWidgets('notStarted', (tester) async { expect( CameraErrorCode.notStarted.toString(), equals('cameraNotStarted'), ); }); - test('unknown', () { + testWidgets('unknown', (tester) async { expect( CameraErrorCode.unknown.toString(), equals('cameraUnknown'), @@ -118,7 +121,7 @@ void main() { }); group('fromMediaError', () { - test('with aborted error code', () { + testWidgets('with aborted error code', (tester) async { expect( CameraErrorCode.fromMediaError( FakeMediaError(MediaError.MEDIA_ERR_ABORTED), @@ -127,7 +130,7 @@ void main() { ); }); - test('with network error code', () { + testWidgets('with network error code', (tester) async { expect( CameraErrorCode.fromMediaError( FakeMediaError(MediaError.MEDIA_ERR_NETWORK), @@ -136,7 +139,7 @@ void main() { ); }); - test('with decode error code', () { + testWidgets('with decode error code', (tester) async { expect( CameraErrorCode.fromMediaError( FakeMediaError(MediaError.MEDIA_ERR_DECODE), @@ -145,7 +148,7 @@ void main() { ); }); - test('with source not supported error code', () { + testWidgets('with source not supported error code', (tester) async { expect( CameraErrorCode.fromMediaError( FakeMediaError(MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED), @@ -154,7 +157,7 @@ void main() { ); }); - test('with unknown error code', () { + testWidgets('with unknown error code', (tester) async { expect( CameraErrorCode.fromMediaError( FakeMediaError(5), diff --git a/packages/camera/camera_web/test/types/camera_metadata_test.dart b/packages/camera/camera_web/example/integration_test/camera_metadata_test.dart similarity index 76% rename from packages/camera/camera_web/test/types/camera_metadata_test.dart rename to packages/camera/camera_web/example/integration_test/camera_metadata_test.dart index c76688f768d7..36ecb3e47f31 100644 --- a/packages/camera/camera_web/test/types/camera_metadata_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_metadata_test.dart @@ -4,10 +4,13 @@ import 'package:camera_web/src/types/types.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + group('CameraMetadata', () { - test('supports value equality', () { + testWidgets('supports value equality', (tester) async { expect( CameraMetadata( deviceId: 'deviceId', diff --git a/packages/camera/camera_web/test/types/camera_options_test.dart b/packages/camera/camera_web/example/integration_test/camera_options_test.dart similarity index 82% rename from packages/camera/camera_web/test/types/camera_options_test.dart rename to packages/camera/camera_web/example/integration_test/camera_options_test.dart index 6f60bfd5aeda..a74ba3088394 100644 --- a/packages/camera/camera_web/test/types/camera_options_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_options_test.dart @@ -4,10 +4,13 @@ import 'package:camera_web/src/types/types.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + group('CameraOptions', () { - test('serializes correctly', () { + testWidgets('serializes correctly', (tester) async { final cameraOptions = CameraOptions( audio: AudioConstraints(enabled: true), video: VideoConstraints( @@ -24,7 +27,7 @@ void main() { ); }); - test('supports value equality', () { + testWidgets('supports value equality', (tester) async { expect( CameraOptions( audio: AudioConstraints(enabled: false), @@ -51,14 +54,14 @@ void main() { }); group('AudioConstraints', () { - test('serializes correctly', () { + testWidgets('serializes correctly', (tester) async { expect( AudioConstraints(enabled: true).toJson(), equals(true), ); }); - test('supports value equality', () { + testWidgets('supports value equality', (tester) async { expect( AudioConstraints(enabled: true), equals(AudioConstraints(enabled: true)), @@ -67,7 +70,7 @@ void main() { }); group('VideoConstraints', () { - test('serializes correctly', () { + testWidgets('serializes correctly', (tester) async { final videoConstraints = VideoConstraints( facingMode: FacingModeConstraint.exact(CameraType.user), width: VideoSizeConstraint(ideal: 100, maximum: 100), @@ -88,7 +91,7 @@ void main() { ); }); - test('supports value equality', () { + testWidgets('supports value equality', (tester) async { expect( VideoConstraints( facingMode: FacingModeConstraint.exact(CameraType.environment), @@ -110,25 +113,25 @@ void main() { group('FacingModeConstraint', () { group('ideal', () { - test( + testWidgets( 'serializes correctly ' - 'for environment camera type', () { + 'for environment camera type', (tester) async { expect( FacingModeConstraint(CameraType.environment).toJson(), equals({'ideal': 'environment'}), ); }); - test( + testWidgets( 'serializes correctly ' - 'for user camera type', () { + 'for user camera type', (tester) async { expect( FacingModeConstraint(CameraType.user).toJson(), equals({'ideal': 'user'}), ); }); - test('supports value equality', () { + testWidgets('supports value equality', (tester) async { expect( FacingModeConstraint(CameraType.user), equals(FacingModeConstraint(CameraType.user)), @@ -137,25 +140,25 @@ void main() { }); group('exact', () { - test( + testWidgets( 'serializes correctly ' - 'for environment camera type', () { + 'for environment camera type', (tester) async { expect( FacingModeConstraint.exact(CameraType.environment).toJson(), equals({'exact': 'environment'}), ); }); - test( + testWidgets( 'serializes correctly ' - 'for user camera type', () { + 'for user camera type', (tester) async { expect( FacingModeConstraint.exact(CameraType.user).toJson(), equals({'exact': 'user'}), ); }); - test('supports value equality', () { + testWidgets('supports value equality', (tester) async { expect( FacingModeConstraint.exact(CameraType.environment), equals(FacingModeConstraint.exact(CameraType.environment)), @@ -165,7 +168,7 @@ void main() { }); group('VideoSizeConstraint ', () { - test('serializes correctly', () { + testWidgets('serializes correctly', (tester) async { expect( VideoSizeConstraint( minimum: 200, @@ -180,7 +183,7 @@ void main() { ); }); - test('supports value equality', () { + testWidgets('supports value equality', (tester) async { expect( VideoSizeConstraint( minimum: 100, diff --git a/packages/camera/camera_web/example/integration_test/camera_settings_test.dart b/packages/camera/camera_web/example/integration_test/camera_service_test.dart similarity index 86% rename from packages/camera/camera_web/example/integration_test/camera_settings_test.dart rename to packages/camera/camera_web/example/integration_test/camera_service_test.dart index 0e1d78789f08..161aeb4a595e 100644 --- a/packages/camera/camera_web/example/integration_test/camera_settings_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_service_test.dart @@ -8,7 +8,7 @@ import 'dart:js_util' as js_util; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/camera.dart'; -import 'package:camera_web/src/camera_settings.dart'; +import 'package:camera_web/src/camera_service.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -20,13 +20,13 @@ import 'helpers/helpers.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('CameraSettings', () { + group('CameraService', () { const cameraId = 0; late Window window; late Navigator navigator; late MediaDevices mediaDevices; - late CameraSettings settings; + late CameraService cameraService; setUp(() async { window = MockWindow(); @@ -36,7 +36,7 @@ void main() { when(() => window.navigator).thenReturn(navigator); when(() => navigator.mediaDevices).thenReturn(mediaDevices); - settings = CameraSettings()..window = window; + cameraService = CameraService()..window = window; }); group('getMediaStreamForOptions', () { @@ -53,7 +53,7 @@ void main() { ), ); - await settings.getMediaStreamForOptions(options); + await cameraService.getMediaStreamForOptions(options); verify( () => mediaDevices.getUserMedia(options.toJson()), @@ -67,7 +67,7 @@ void main() { when(() => navigator.mediaDevices).thenReturn(null); expect( - () => settings.getMediaStreamForOptions(CameraOptions()), + () => cameraService.getMediaStreamForOptions(CameraOptions()), throwsA( isA().having( (e) => e.code, @@ -87,7 +87,7 @@ void main() { .thenThrow(FakeDomException('NotFoundError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -107,7 +107,7 @@ void main() { .thenThrow(FakeDomException('DevicesNotFoundError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -127,7 +127,7 @@ void main() { .thenThrow(FakeDomException('NotReadableError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -147,7 +147,7 @@ void main() { .thenThrow(FakeDomException('TrackStartError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -167,7 +167,7 @@ void main() { .thenThrow(FakeDomException('OverconstrainedError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -188,7 +188,7 @@ void main() { .thenThrow(FakeDomException('ConstraintNotSatisfiedError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -209,7 +209,7 @@ void main() { .thenThrow(FakeDomException('NotAllowedError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -230,7 +230,7 @@ void main() { .thenThrow(FakeDomException('PermissionDeniedError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -251,7 +251,7 @@ void main() { .thenThrow(FakeDomException('TypeError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -271,7 +271,7 @@ void main() { .thenThrow(FakeDomException('AbortError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -291,7 +291,7 @@ void main() { .thenThrow(FakeDomException('SecurityError')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -311,7 +311,7 @@ void main() { .thenThrow(FakeDomException('Unknown')); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -330,7 +330,7 @@ void main() { when(() => mediaDevices.getUserMedia(any())).thenThrow(Exception()); expect( - () => settings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions(), cameraId: cameraId, ), @@ -372,7 +372,7 @@ void main() { }); final zoomLevelCapability = - settings.getZoomLevelCapabilityForCamera(camera); + cameraService.getZoomLevelCapabilityForCamera(camera); expect(zoomLevelCapability.minimum, equals(100.0)); expect(zoomLevelCapability.maximum, equals(400.0)); @@ -386,7 +386,7 @@ void main() { when(() => navigator.mediaDevices).thenReturn(null); expect( - () => settings.getZoomLevelCapabilityForCamera(camera), + () => cameraService.getZoomLevelCapabilityForCamera(camera), throwsA( isA() .having( @@ -420,7 +420,7 @@ void main() { }); expect( - () => settings.getZoomLevelCapabilityForCamera(camera), + () => cameraService.getZoomLevelCapabilityForCamera(camera), throwsA( isA() .having( @@ -448,7 +448,7 @@ void main() { when(videoTracks.first.getCapabilities).thenReturn({}); expect( - () => settings.getZoomLevelCapabilityForCamera(camera), + () => cameraService.getZoomLevelCapabilityForCamera(camera), throwsA( isA() .having( @@ -476,7 +476,7 @@ void main() { when(() => camera.stream).thenReturn(FakeMediaStream([])); expect( - () => settings.getZoomLevelCapabilityForCamera(camera), + () => cameraService.getZoomLevelCapabilityForCamera(camera), throwsA( isA() .having( @@ -503,7 +503,8 @@ void main() { when(() => navigator.mediaDevices).thenReturn(null); expect( - () => settings.getFacingModeForVideoTrack(MockMediaStreamTrack()), + () => + cameraService.getFacingModeForVideoTrack(MockMediaStreamTrack()), throwsA( isA().having( (e) => e.code, @@ -522,7 +523,7 @@ void main() { }); final facingMode = - settings.getFacingModeForVideoTrack(MockMediaStreamTrack()); + cameraService.getFacingModeForVideoTrack(MockMediaStreamTrack()); expect( facingMode, @@ -544,7 +545,8 @@ void main() { when(videoTrack.getSettings).thenReturn({'facingMode': 'user'}); - final facingMode = settings.getFacingModeForVideoTrack(videoTrack); + final facingMode = + cameraService.getFacingModeForVideoTrack(videoTrack); expect( facingMode, @@ -563,7 +565,8 @@ void main() { 'facingMode': ['environment', 'left'] }); - final facingMode = settings.getFacingModeForVideoTrack(videoTrack); + final facingMode = + cameraService.getFacingModeForVideoTrack(videoTrack); expect( facingMode, @@ -580,7 +583,8 @@ void main() { when(videoTrack.getSettings).thenReturn({}); when(videoTrack.getCapabilities).thenReturn({'facingMode': []}); - final facingMode = settings.getFacingModeForVideoTrack(videoTrack); + final facingMode = + cameraService.getFacingModeForVideoTrack(videoTrack); expect( facingMode, @@ -597,7 +601,8 @@ void main() { when(videoTrack.getSettings).thenReturn({}); when(videoTrack.getCapabilities).thenThrow(JSNoSuchMethodError()); - final facingMode = settings.getFacingModeForVideoTrack(videoTrack); + final facingMode = + cameraService.getFacingModeForVideoTrack(videoTrack); expect( facingMode, @@ -616,7 +621,7 @@ void main() { when(videoTrack.getCapabilities).thenThrow(Exception('Unknown')); expect( - () => settings.getFacingModeForVideoTrack(videoTrack), + () => cameraService.getFacingModeForVideoTrack(videoTrack), throwsA( isA().having( (e) => e.code, @@ -634,7 +639,7 @@ void main() { 'returns front ' 'when the facing mode is user', (tester) async { expect( - settings.mapFacingModeToLensDirection('user'), + cameraService.mapFacingModeToLensDirection('user'), equals(CameraLensDirection.front), ); }); @@ -643,7 +648,7 @@ void main() { 'returns back ' 'when the facing mode is environment', (tester) async { expect( - settings.mapFacingModeToLensDirection('environment'), + cameraService.mapFacingModeToLensDirection('environment'), equals(CameraLensDirection.back), ); }); @@ -652,7 +657,7 @@ void main() { 'returns external ' 'when the facing mode is left', (tester) async { expect( - settings.mapFacingModeToLensDirection('left'), + cameraService.mapFacingModeToLensDirection('left'), equals(CameraLensDirection.external), ); }); @@ -661,7 +666,7 @@ void main() { 'returns external ' 'when the facing mode is right', (tester) async { expect( - settings.mapFacingModeToLensDirection('right'), + cameraService.mapFacingModeToLensDirection('right'), equals(CameraLensDirection.external), ); }); @@ -672,7 +677,7 @@ void main() { 'returns user ' 'when the facing mode is user', (tester) async { expect( - settings.mapFacingModeToCameraType('user'), + cameraService.mapFacingModeToCameraType('user'), equals(CameraType.user), ); }); @@ -681,7 +686,7 @@ void main() { 'returns environment ' 'when the facing mode is environment', (tester) async { expect( - settings.mapFacingModeToCameraType('environment'), + cameraService.mapFacingModeToCameraType('environment'), equals(CameraType.environment), ); }); @@ -690,7 +695,7 @@ void main() { 'returns user ' 'when the facing mode is left', (tester) async { expect( - settings.mapFacingModeToCameraType('left'), + cameraService.mapFacingModeToCameraType('left'), equals(CameraType.user), ); }); @@ -699,7 +704,7 @@ void main() { 'returns user ' 'when the facing mode is right', (tester) async { expect( - settings.mapFacingModeToCameraType('right'), + cameraService.mapFacingModeToCameraType('right'), equals(CameraType.user), ); }); @@ -710,7 +715,7 @@ void main() { 'returns 3840x2160 ' 'when the resolution preset is max', (tester) async { expect( - settings.mapResolutionPresetToSize(ResolutionPreset.max), + cameraService.mapResolutionPresetToSize(ResolutionPreset.max), equals(Size(3840, 2160)), ); }); @@ -719,7 +724,7 @@ void main() { 'returns 3840x2160 ' 'when the resolution preset is ultraHigh', (tester) async { expect( - settings.mapResolutionPresetToSize(ResolutionPreset.ultraHigh), + cameraService.mapResolutionPresetToSize(ResolutionPreset.ultraHigh), equals(Size(3840, 2160)), ); }); @@ -728,7 +733,7 @@ void main() { 'returns 1920x1080 ' 'when the resolution preset is veryHigh', (tester) async { expect( - settings.mapResolutionPresetToSize(ResolutionPreset.veryHigh), + cameraService.mapResolutionPresetToSize(ResolutionPreset.veryHigh), equals(Size(1920, 1080)), ); }); @@ -737,7 +742,7 @@ void main() { 'returns 1280x720 ' 'when the resolution preset is high', (tester) async { expect( - settings.mapResolutionPresetToSize(ResolutionPreset.high), + cameraService.mapResolutionPresetToSize(ResolutionPreset.high), equals(Size(1280, 720)), ); }); @@ -746,7 +751,7 @@ void main() { 'returns 720x480 ' 'when the resolution preset is medium', (tester) async { expect( - settings.mapResolutionPresetToSize(ResolutionPreset.medium), + cameraService.mapResolutionPresetToSize(ResolutionPreset.medium), equals(Size(720, 480)), ); }); @@ -755,7 +760,7 @@ void main() { 'returns 320x240 ' 'when the resolution preset is low', (tester) async { expect( - settings.mapResolutionPresetToSize(ResolutionPreset.low), + cameraService.mapResolutionPresetToSize(ResolutionPreset.low), equals(Size(320, 240)), ); }); @@ -766,7 +771,7 @@ void main() { 'returns portraitPrimary ' 'when the device orientation is portraitUp', (tester) async { expect( - settings.mapDeviceOrientationToOrientationType( + cameraService.mapDeviceOrientationToOrientationType( DeviceOrientation.portraitUp, ), equals(OrientationType.portraitPrimary), @@ -777,7 +782,7 @@ void main() { 'returns landscapePrimary ' 'when the device orientation is landscapeLeft', (tester) async { expect( - settings.mapDeviceOrientationToOrientationType( + cameraService.mapDeviceOrientationToOrientationType( DeviceOrientation.landscapeLeft, ), equals(OrientationType.landscapePrimary), @@ -788,7 +793,7 @@ void main() { 'returns portraitSecondary ' 'when the device orientation is portraitDown', (tester) async { expect( - settings.mapDeviceOrientationToOrientationType( + cameraService.mapDeviceOrientationToOrientationType( DeviceOrientation.portraitDown, ), equals(OrientationType.portraitSecondary), @@ -799,7 +804,7 @@ void main() { 'returns landscapeSecondary ' 'when the device orientation is landscapeRight', (tester) async { expect( - settings.mapDeviceOrientationToOrientationType( + cameraService.mapDeviceOrientationToOrientationType( DeviceOrientation.landscapeRight, ), equals(OrientationType.landscapeSecondary), @@ -812,7 +817,7 @@ void main() { 'returns portraitUp ' 'when the orientation type is portraitPrimary', (tester) async { expect( - settings.mapOrientationTypeToDeviceOrientation( + cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.portraitPrimary, ), equals(DeviceOrientation.portraitUp), @@ -823,7 +828,7 @@ void main() { 'returns landscapeLeft ' 'when the orientation type is landscapePrimary', (tester) async { expect( - settings.mapOrientationTypeToDeviceOrientation( + cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.landscapePrimary, ), equals(DeviceOrientation.landscapeLeft), @@ -834,7 +839,7 @@ void main() { 'returns portraitDown ' 'when the orientation type is portraitSecondary', (tester) async { expect( - settings.mapOrientationTypeToDeviceOrientation( + cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.portraitSecondary, ), equals(DeviceOrientation.portraitDown), @@ -845,7 +850,7 @@ void main() { 'returns portraitDown ' 'when the orientation type is portraitSecondary', (tester) async { expect( - settings.mapOrientationTypeToDeviceOrientation( + cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.portraitSecondary, ), equals(DeviceOrientation.portraitDown), @@ -856,7 +861,7 @@ void main() { 'returns landscapeRight ' 'when the orientation type is landscapeSecondary', (tester) async { expect( - settings.mapOrientationTypeToDeviceOrientation( + cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.landscapeSecondary, ), equals(DeviceOrientation.landscapeRight), @@ -867,7 +872,7 @@ void main() { 'returns portraitUp ' 'for an unknown orientation type', (tester) async { expect( - settings.mapOrientationTypeToDeviceOrientation( + cameraService.mapOrientationTypeToDeviceOrientation( 'unknown', ), equals(DeviceOrientation.portraitUp), diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart index 03ffe81cad64..5c3d842502ba 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -7,7 +7,7 @@ import 'dart:ui'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/camera.dart'; -import 'package:camera_web/src/camera_settings.dart'; +import 'package:camera_web/src/camera_service.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -26,7 +26,7 @@ void main() { late MediaDevices mediaDevices; late MediaStream mediaStream; - late CameraSettings cameraSettings; + late CameraService cameraService; setUp(() { window = MockWindow(); @@ -36,13 +36,13 @@ void main() { when(() => window.navigator).thenReturn(navigator); when(() => navigator.mediaDevices).thenReturn(mediaDevices); - cameraSettings = MockCameraSettings(); + cameraService = MockCameraService(); final videoElement = getVideoElementWithBlankStream(Size(10, 10)); mediaStream = videoElement.captureStream(); when( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( any(), cameraId: any(named: 'cameraId'), ), @@ -55,7 +55,7 @@ void main() { group('initialize', () { testWidgets( - 'calls CameraSettings.getMediaStreamForOptions ' + 'calls CameraService.getMediaStreamForOptions ' 'with provided options', (tester) async { final options = CameraOptions( video: VideoConstraints( @@ -67,13 +67,13 @@ void main() { final camera = Camera( textureId: textureId, options: options, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); verify( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( options, cameraId: textureId, ), @@ -90,7 +90,7 @@ void main() { options: CameraOptions( audio: audioConstraints, ), - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); @@ -115,7 +115,7 @@ void main() { 'with correct properties', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); @@ -128,7 +128,7 @@ void main() { testWidgets('initializes the camera stream', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); @@ -138,16 +138,15 @@ void main() { testWidgets( 'throws an exception ' - 'when CameraSettings.getMediaStreamForOptions throws', - (tester) async { + 'when CameraService.getMediaStreamForOptions throws', (tester) async { final exception = Exception('A media stream exception occured.'); - when(() => cameraSettings.getMediaStreamForOptions(any(), + when(() => cameraService.getMediaStreamForOptions(any(), cameraId: any(named: 'cameraId'))).thenThrow(exception); final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); expect( @@ -163,7 +162,7 @@ void main() { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); @@ -180,7 +179,7 @@ void main() { testWidgets( 'initializes the camera stream ' - 'from CameraSettings.getMediaStreamForOptions ' + 'from CameraService.getMediaStreamForOptions ' 'if it does not exist', (tester) async { final options = CameraOptions( video: VideoConstraints( @@ -191,7 +190,7 @@ void main() { final camera = Camera( textureId: textureId, options: options, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); @@ -204,7 +203,7 @@ void main() { // Should be called twice: for initialize and play. verify( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( options, cameraId: textureId, ), @@ -219,7 +218,7 @@ void main() { testWidgets('resets the camera stream', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); @@ -236,7 +235,7 @@ void main() { testWidgets('returns a captured picture', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); @@ -272,7 +271,7 @@ void main() { testWidgets('if the flash mode is auto', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ) ..window = window ..stream = videoStream @@ -307,7 +306,7 @@ void main() { testWidgets('if the flash mode is always', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ) ..window = window ..stream = videoStream @@ -352,7 +351,7 @@ void main() { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); @@ -372,7 +371,7 @@ void main() { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); @@ -409,7 +408,7 @@ void main() { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ) ..window = window ..stream = videoStream; @@ -437,7 +436,7 @@ void main() { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ) ..window = window ..stream = videoStream; @@ -468,7 +467,7 @@ void main() { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ) ..window = window ..stream = videoStream; @@ -494,7 +493,7 @@ void main() { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ) ..window = window ..stream = videoStream; @@ -531,7 +530,7 @@ void main() { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ) ..window = window ..stream = videoStream; @@ -568,7 +567,7 @@ void main() { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ) ..window = window ..stream = videoStream; @@ -604,7 +603,7 @@ void main() { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, )..window = window; expect( @@ -631,11 +630,11 @@ void main() { group('getMaxZoomLevel', () { testWidgets( 'returns maximum ' - 'from CameraSettings.getZoomLevelCapabilityForCamera', + 'from CameraService.getZoomLevelCapabilityForCamera', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); final zoomLevelCapability = ZoomLevelCapability( @@ -644,12 +643,12 @@ void main() { videoTrack: MockMediaStreamTrack(), ); - when(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + when(() => cameraService.getZoomLevelCapabilityForCamera(camera)) .thenReturn(zoomLevelCapability); final maximumZoomLevel = camera.getMaxZoomLevel(); - verify(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + verify(() => cameraService.getZoomLevelCapabilityForCamera(camera)) .called(1); expect( @@ -662,11 +661,11 @@ void main() { group('getMinZoomLevel', () { testWidgets( 'returns minimum ' - 'from CameraSettings.getZoomLevelCapabilityForCamera', + 'from CameraService.getZoomLevelCapabilityForCamera', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); final zoomLevelCapability = ZoomLevelCapability( @@ -675,12 +674,12 @@ void main() { videoTrack: MockMediaStreamTrack(), ); - when(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + when(() => cameraService.getZoomLevelCapabilityForCamera(camera)) .thenReturn(zoomLevelCapability); final minimumZoomLevel = camera.getMinZoomLevel(); - verify(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + verify(() => cameraService.getZoomLevelCapabilityForCamera(camera)) .called(1); expect( @@ -693,11 +692,11 @@ void main() { group('setZoomLevel', () { testWidgets( 'applies zoom on the video track ' - 'from CameraSettings.getZoomLevelCapabilityForCamera', + 'from CameraService.getZoomLevelCapabilityForCamera', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); final videoTrack = MockMediaStreamTrack(); @@ -711,7 +710,7 @@ void main() { when(() => videoTrack.applyConstraints(any())) .thenAnswer((_) async {}); - when(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + when(() => cameraService.getZoomLevelCapabilityForCamera(camera)) .thenReturn(zoomLevelCapability); const zoom = 75.0; @@ -735,7 +734,7 @@ void main() { 'when the provided zoom level is below minimum', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); final zoomLevelCapability = ZoomLevelCapability( @@ -744,7 +743,7 @@ void main() { videoTrack: MockMediaStreamTrack(), ); - when(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + when(() => cameraService.getZoomLevelCapabilityForCamera(camera)) .thenReturn(zoomLevelCapability); expect( @@ -769,7 +768,7 @@ void main() { 'when the provided zoom level is below minimum', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); final zoomLevelCapability = ZoomLevelCapability( @@ -778,7 +777,7 @@ void main() { videoTrack: MockMediaStreamTrack(), ); - when(() => cameraSettings.getZoomLevelCapabilityForCamera(camera)) + when(() => cameraService.getZoomLevelCapabilityForCamera(camera)) .thenReturn(zoomLevelCapability); expect( @@ -805,7 +804,7 @@ void main() { testWidgets('returns a correct view type', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); @@ -821,7 +820,7 @@ void main() { testWidgets('resets the video element\'s source', (tester) async { final camera = Camera( textureId: textureId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); await camera.initialize(); diff --git a/packages/camera/camera_web/test/types/camera_web_exception_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_exception_test.dart similarity index 80% rename from packages/camera/camera_web/test/types/camera_web_exception_test.dart rename to packages/camera/camera_web/example/integration_test/camera_web_exception_test.dart index d58512b460e2..6f8531b6f4af 100644 --- a/packages/camera/camera_web/test/types/camera_web_exception_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_exception_test.dart @@ -4,10 +4,13 @@ import 'package:camera_web/src/types/types.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + group('CameraWebException', () { - test('sets all properties', () { + testWidgets('sets all properties', (tester) async { final cameraId = 1; final code = CameraErrorCode.notFound; final description = 'The camera is not found.'; @@ -19,7 +22,7 @@ void main() { expect(exception.description, equals(description)); }); - test('toString includes all properties', () { + testWidgets('toString includes all properties', (tester) async { final cameraId = 2; final code = CameraErrorCode.notReadable; final description = 'The camera is not readable.'; diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index eb988f49ab87..ada5c2da1a29 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -10,10 +10,10 @@ import 'package:async/async.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/camera_web.dart'; import 'package:camera_web/src/camera.dart'; -import 'package:camera_web/src/camera_settings.dart'; +import 'package:camera_web/src/camera_service.dart'; import 'package:camera_web/src/types/types.dart'; -import 'package:flutter/widgets.dart' as widgets; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart' as widgets; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -35,7 +35,7 @@ void main() { late Document document; late Element documentElement; - late CameraSettings cameraSettings; + late CameraService cameraService; setUp(() async { window = MockWindow(); @@ -59,10 +59,10 @@ void main() { when(() => document.documentElement).thenReturn(documentElement); when(() => window.document).thenReturn(document); - cameraSettings = MockCameraSettings(); + cameraService = MockCameraService(); when( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( any(), cameraId: any(named: 'cameraId'), ), @@ -71,7 +71,7 @@ void main() { ); CameraPlatform.instance = CameraPlugin( - cameraSettings: cameraSettings, + cameraService: cameraService, )..window = window; }); @@ -88,7 +88,7 @@ void main() { group('availableCameras', () { setUp(() { when( - () => cameraSettings.getFacingModeForVideoTrack( + () => cameraService.getFacingModeForVideoTrack( any(), ), ).thenReturn(null); @@ -102,7 +102,7 @@ void main() { final _ = await CameraPlatform.instance.availableCameras(); verify( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions( audio: AudioConstraints(enabled: true), ), @@ -126,7 +126,7 @@ void main() { final _ = await CameraPlatform.instance.availableCameras(); verify( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions( video: VideoConstraints( deviceId: videoDevice.deviceId, @@ -153,7 +153,7 @@ void main() { final _ = await CameraPlatform.instance.availableCameras(); verifyNever( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions( video: VideoConstraints( deviceId: videoDevice.deviceId, @@ -177,7 +177,7 @@ void main() { FakeMediaStream([MockMediaStreamTrack(), MockMediaStreamTrack()]); when( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions( video: VideoConstraints(deviceId: videoDevice.deviceId), ), @@ -191,7 +191,7 @@ void main() { final _ = await CameraPlatform.instance.availableCameras(); verify( - () => cameraSettings.getFacingModeForVideoTrack( + () => cameraService.getFacingModeForVideoTrack( videoStream.getVideoTracks().first, ), ).called(1); @@ -239,46 +239,46 @@ void main() { ]), ); - // Mock camera settings to return the first video stream + // Mock camera service to return the first video stream // for the first video device. when( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions( video: VideoConstraints(deviceId: firstVideoDevice.deviceId), ), ), ).thenAnswer((_) => Future.value(firstVideoStream)); - // Mock camera settings to return the second video stream + // Mock camera service to return the second video stream // for the second video device. when( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions( video: VideoConstraints(deviceId: secondVideoDevice.deviceId), ), ), ).thenAnswer((_) => Future.value(secondVideoStream)); - // Mock camera settings to return a user facing mode + // Mock camera service to return a user facing mode // for the first video stream. when( - () => cameraSettings.getFacingModeForVideoTrack( + () => cameraService.getFacingModeForVideoTrack( firstVideoStream.getVideoTracks().first, ), ).thenReturn('user'); - when(() => cameraSettings.mapFacingModeToLensDirection('user')) + when(() => cameraService.mapFacingModeToLensDirection('user')) .thenReturn(CameraLensDirection.front); - // Mock camera settings to return an environment facing mode + // Mock camera service to return an environment facing mode // for the second video stream. when( - () => cameraSettings.getFacingModeForVideoTrack( + () => cameraService.getFacingModeForVideoTrack( secondVideoStream.getVideoTracks().first, ), ).thenReturn('environment'); - when(() => cameraSettings.mapFacingModeToLensDirection('environment')) + when(() => cameraService.mapFacingModeToLensDirection('environment')) .thenReturn(CameraLensDirection.back); final cameras = await CameraPlatform.instance.availableCameras(); @@ -318,7 +318,7 @@ void main() { ); when( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( CameraOptions( video: VideoConstraints(deviceId: videoDevice.deviceId), ), @@ -326,12 +326,12 @@ void main() { ).thenAnswer((_) => Future.value(videoStream)); when( - () => cameraSettings.getFacingModeForVideoTrack( + () => cameraService.getFacingModeForVideoTrack( videoStream.getVideoTracks().first, ), ).thenReturn('left'); - when(() => cameraSettings.mapFacingModeToLensDirection('left')) + when(() => cameraService.mapFacingModeToLensDirection('left')) .thenReturn(CameraLensDirection.external); final camera = (await CameraPlatform.instance.availableCameras()).first; @@ -384,7 +384,7 @@ void main() { }); testWidgets( - 'when CameraSettings.getMediaStreamForOptions ' + 'when CameraService.getMediaStreamForOptions ' 'throws CameraWebException', (tester) async { final exception = CameraWebException( cameraId, @@ -392,7 +392,7 @@ void main() { 'description', ); - when(() => cameraSettings.getMediaStreamForOptions(any())) + when(() => cameraService.getMediaStreamForOptions(any())) .thenThrow(exception); expect( @@ -408,14 +408,14 @@ void main() { }); testWidgets( - 'when CameraSettings.getMediaStreamForOptions ' + 'when CameraService.getMediaStreamForOptions ' 'throws PlatformException', (tester) async { final exception = PlatformException( code: CameraErrorCode.notSupported.toString(), message: 'message', ); - when(() => cameraSettings.getMediaStreamForOptions(any())) + when(() => cameraService.getMediaStreamForOptions(any())) .thenThrow(exception); expect( @@ -454,13 +454,13 @@ void main() { .camerasMetadata[cameraDescription] = cameraMetadata; when( - () => cameraSettings.mapFacingModeToCameraType('user'), + () => cameraService.mapFacingModeToCameraType('user'), ).thenReturn(CameraType.user); }); testWidgets('with appropriate options', (tester) async { when( - () => cameraSettings + () => cameraService .mapResolutionPresetToSize(ResolutionPreset.ultraHigh), ).thenReturn(ultraHighResolutionSize); @@ -503,8 +503,7 @@ void main() { 'and enabled audio set to false ' 'when no options are specified', (tester) async { when( - () => - cameraSettings.mapResolutionPresetToSize(ResolutionPreset.max), + () => cameraService.mapResolutionPresetToSize(ResolutionPreset.max), ).thenReturn(maxResolutionSize); final cameraId = await CameraPlatform.instance.createCamera( @@ -657,7 +656,7 @@ void main() { group('lockCaptureOrientation', () { setUp(() { when( - () => cameraSettings.mapDeviceOrientationToOrientationType(any()), + () => cameraService.mapDeviceOrientationToOrientationType(any()), ).thenReturn(OrientationType.portraitPrimary); }); @@ -676,7 +675,7 @@ void main() { 'locks the capture orientation ' 'based on the given device orientation', (tester) async { when( - () => cameraSettings.mapDeviceOrientationToOrientationType( + () => cameraService.mapDeviceOrientationToOrientationType( DeviceOrientation.landscapeRight, ), ).thenReturn(OrientationType.landscapeSecondary); @@ -687,7 +686,7 @@ void main() { ); verify( - () => cameraSettings.mapDeviceOrientationToOrientationType( + () => cameraService.mapDeviceOrientationToOrientationType( DeviceOrientation.landscapeRight, ), ).called(1); @@ -785,7 +784,7 @@ void main() { group('unlockCaptureOrientation', () { setUp(() { when( - () => cameraSettings.mapDeviceOrientationToOrientationType(any()), + () => cameraService.mapDeviceOrientationToOrientationType(any()), ).thenReturn(OrientationType.portraitPrimary); }); @@ -1434,7 +1433,7 @@ void main() { 'with an appropriate view type', (tester) async { final camera = Camera( textureId: cameraId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); // Save the camera in the camera plugin. @@ -1560,7 +1559,7 @@ void main() { testWidgets('returns the correct camera', (tester) async { final camera = Camera( textureId: cameraId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); // Save the camera in the camera plugin. @@ -1599,7 +1598,7 @@ void main() { videoElement = getVideoElementWithBlankStream(videoSize); when( - () => cameraSettings.getMediaStreamForOptions( + () => cameraService.getMediaStreamForOptions( any(), cameraId: cameraId, ), @@ -1607,7 +1606,7 @@ void main() { final camera = Camera( textureId: cameraId, - cameraSettings: cameraSettings, + cameraService: cameraService, ); // Save the camera in the camera plugin. @@ -1963,13 +1962,13 @@ void main() { 'emits a DeviceOrientationChangedEvent ' 'when the screen orientation is changed', (tester) async { when( - () => cameraSettings.mapOrientationTypeToDeviceOrientation( + () => cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.landscapePrimary, ), ).thenReturn(DeviceOrientation.landscapeLeft); when( - () => cameraSettings.mapOrientationTypeToDeviceOrientation( + () => cameraService.mapOrientationTypeToDeviceOrientation( OrientationType.portraitSecondary, ), ).thenReturn(DeviceOrientation.portraitDown); diff --git a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart index 5fa52dd3398d..436f2065aaf5 100644 --- a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart +++ b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart @@ -7,7 +7,7 @@ import 'dart:html'; import 'dart:ui'; import 'package:camera_web/src/camera.dart'; -import 'package:camera_web/src/camera_settings.dart'; +import 'package:camera_web/src/camera_service.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:cross_file/cross_file.dart'; import 'package:mocktail/mocktail.dart'; @@ -26,7 +26,7 @@ class MockNavigator extends Mock implements Navigator {} class MockMediaDevices extends Mock implements MediaDevices {} -class MockCameraSettings extends Mock implements CameraSettings {} +class MockCameraService extends Mock implements CameraService {} class MockMediaStreamTrack extends Mock implements MediaStreamTrack {} diff --git a/packages/camera/camera_web/test/types/zoom_level_capability_test.dart b/packages/camera/camera_web/example/integration_test/zoom_level_capability_test.dart similarity index 80% rename from packages/camera/camera_web/test/types/zoom_level_capability_test.dart rename to packages/camera/camera_web/example/integration_test/zoom_level_capability_test.dart index c382b4b76cc4..09de03100871 100644 --- a/packages/camera/camera_web/test/types/zoom_level_capability_test.dart +++ b/packages/camera/camera_web/example/integration_test/zoom_level_capability_test.dart @@ -4,12 +4,15 @@ import 'package:camera_web/src/types/types.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; -import '../helpers/helpers.dart'; +import 'helpers/helpers.dart'; void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + group('ZoomLevelCapability', () { - test('sets all properties', () { + testWidgets('sets all properties', (tester) async { const minimum = 100.0; const maximum = 400.0; final videoTrack = MockMediaStreamTrack(); @@ -25,7 +28,7 @@ void main() { expect(capability.videoTrack, equals(videoTrack)); }); - test('supports value equality', () { + testWidgets('supports value equality', (tester) async { final videoTrack = MockMediaStreamTrack(); expect( diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index c77d36023058..6f758843a047 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -6,7 +6,7 @@ import 'dart:html' as html; import 'dart:ui'; import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:camera_web/src/camera_settings.dart'; +import 'package:camera_web/src/camera_service.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter/foundation.dart'; @@ -18,7 +18,7 @@ String _getViewType(int cameraId) => 'plugins.flutter.io/camera_$cameraId'; /// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices /// /// The obtained camera stream is constrained by [options] and fetched -/// with [CameraSettings.getMediaStreamForOptions]. +/// with [CameraService.getMediaStreamForOptions]. /// /// The camera stream is displayed in the [videoElement] wrapped in the /// [divElement] to avoid overriding the custom styles applied to @@ -40,9 +40,9 @@ class Camera { /// [options] and [window]. Camera({ required this.textureId, - required CameraSettings cameraSettings, + required CameraService cameraService, this.options = const CameraOptions(), - }) : _cameraSettings = cameraSettings; + }) : _cameraService = cameraService; // A torch mode constraint name. // See: https://w3c.github.io/mediacapture-image/#dom-mediatracksupportedconstraints-torch @@ -71,8 +71,8 @@ class Camera { @visibleForTesting FlashMode? flashMode; - /// The camera settings used to get the media stream for the camera. - final CameraSettings _cameraSettings; + /// The camera service used to get the media stream for the camera. + final CameraService _cameraService; /// The current browser window used to access media devices. @visibleForTesting @@ -81,7 +81,7 @@ class Camera { /// Initializes the camera stream displayed in the [videoElement]. /// Registers the camera view with [textureId] under [_getViewType] type. Future initialize() async { - stream = await _cameraSettings.getMediaStreamForOptions( + stream = await _cameraService.getMediaStreamForOptions( options, cameraId: textureId, ); @@ -110,7 +110,7 @@ class Camera { /// Initializes the camera source if the camera was previously stopped. Future play() async { if (videoElement.srcObject == null) { - stream = await _cameraSettings.getMediaStreamForOptions( + stream = await _cameraService.getMediaStreamForOptions( options, cameraId: textureId, ); @@ -251,14 +251,14 @@ class Camera { /// Throws a [CameraWebException] if the zoom level is not supported /// or the camera has not been initialized or started. double getMaxZoomLevel() => - _cameraSettings.getZoomLevelCapabilityForCamera(this).maximum; + _cameraService.getZoomLevelCapabilityForCamera(this).maximum; /// Returns the camera minimum zoom level. /// /// Throws a [CameraWebException] if the zoom level is not supported /// or the camera has not been initialized or started. double getMinZoomLevel() => - _cameraSettings.getZoomLevelCapabilityForCamera(this).minimum; + _cameraService.getZoomLevelCapabilityForCamera(this).minimum; /// Sets the camera zoom level to [zoom]. /// @@ -266,7 +266,7 @@ class Camera { /// not supported or the camera has not been initialized or started. void setZoomLevel(double zoom) { final zoomLevelCapability = - _cameraSettings.getZoomLevelCapabilityForCamera(this); + _cameraService.getZoomLevelCapabilityForCamera(this); if (zoom < zoomLevelCapability.minimum || zoom > zoomLevelCapability.maximum) { diff --git a/packages/camera/camera_web/lib/src/camera_settings.dart b/packages/camera/camera_web/lib/src/camera_service.dart similarity index 99% rename from packages/camera/camera_web/lib/src/camera_settings.dart rename to packages/camera/camera_web/lib/src/camera_service.dart index 7d35fff84112..c1a4ad1038ab 100644 --- a/packages/camera/camera_web/lib/src/camera_settings.dart +++ b/packages/camera/camera_web/lib/src/camera_service.dart @@ -12,9 +12,9 @@ import 'package:camera_web/src/types/types.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -/// A utility to fetch, map camera settings and +/// A service to fetch, map camera settings and /// obtain the camera stream. -class CameraSettings { +class CameraService { // A facing mode constraint name. static const _facingModeKey = "facingMode"; diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index ecea3a76e74a..fda33996f474 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -8,7 +8,7 @@ import 'dart:math'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/camera.dart'; -import 'package:camera_web/src/camera_settings.dart'; +import 'package:camera_web/src/camera_service.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -25,18 +25,18 @@ const String _kDefaultErrorMessage = /// This class implements the `package:camera` functionality for the web. class CameraPlugin extends CameraPlatform { /// Creates a new instance of [CameraPlugin] - /// with the given [cameraSettings] utility. - CameraPlugin({required CameraSettings cameraSettings}) - : _cameraSettings = cameraSettings; + /// with the given [cameraService]. + CameraPlugin({required CameraService cameraService}) + : _cameraService = cameraService; /// Registers this class as the default instance of [CameraPlatform]. static void registerWith(Registrar registrar) { CameraPlatform.instance = CameraPlugin( - cameraSettings: CameraSettings(), + cameraService: CameraService(), ); } - final CameraSettings _cameraSettings; + final CameraService _cameraService; /// The cameras managed by the [CameraPlugin]. @visibleForTesting @@ -86,7 +86,7 @@ class CameraPlugin extends CameraPlatform { } // Request video and audio permissions. - await _cameraSettings.getMediaStreamForOptions( + await _cameraService.getMediaStreamForOptions( CameraOptions( audio: AudioConstraints(enabled: true), ), @@ -121,13 +121,13 @@ class CameraPlugin extends CameraPlatform { if (videoTracks.isNotEmpty) { // Get the facing mode from the first available video track. final facingMode = - _cameraSettings.getFacingModeForVideoTrack(videoTracks.first); + _cameraService.getFacingModeForVideoTrack(videoTracks.first); // Get the lens direction based on the facing mode. // Fallback to the external lens direction // if the facing mode is not available. final lensDirection = facingMode != null - ? _cameraSettings.mapFacingModeToLensDirection(facingMode) + ? _cameraService.mapFacingModeToLensDirection(facingMode) : CameraLensDirection.external; // Create a camera description. @@ -191,20 +191,19 @@ class CameraPlugin extends CameraPlatform { final cameraMetadata = camerasMetadata[cameraDescription]!; final cameraType = cameraMetadata.facingMode != null - ? _cameraSettings - .mapFacingModeToCameraType(cameraMetadata.facingMode!) + ? _cameraService.mapFacingModeToCameraType(cameraMetadata.facingMode!) : null; // Use the highest resolution possible // if the resolution preset is not specified. - final videoSize = _cameraSettings + final videoSize = _cameraService .mapResolutionPresetToSize(resolutionPreset ?? ResolutionPreset.max); // Create a camera with the given audio and video constraints. // Sensor orientation is currently not supported. final camera = Camera( textureId: textureId, - cameraSettings: _cameraSettings, + cameraService: _cameraService, options: CameraOptions( audio: AudioConstraints(enabled: enableAudio), video: VideoConstraints( @@ -334,7 +333,7 @@ class CameraPlugin extends CameraPlatform { if (orientation != null) { return orientation.onChange.map( (html.Event _) { - final deviceOrientation = _cameraSettings + final deviceOrientation = _cameraService .mapOrientationTypeToDeviceOrientation(orientation.type!); return DeviceOrientationChangedEvent(deviceOrientation); }, @@ -354,7 +353,7 @@ class CameraPlugin extends CameraPlatform { final documentElement = window?.document.documentElement; if (orientation != null && documentElement != null) { - final orientationType = _cameraSettings + final orientationType = _cameraService .mapDeviceOrientationToOrientationType(deviceOrientation); // Full-screen mode may be required to modify the device orientation. @@ -549,7 +548,7 @@ class CameraPlugin extends CameraPlatform { video: VideoConstraints(deviceId: deviceId), ); - return _cameraSettings.getMediaStreamForOptions(cameraOptions); + return _cameraService.getMediaStreamForOptions(cameraOptions); } /// Returns a camera for the given [cameraId]. diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index ec674f375164..a2aa43c22d65 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -30,5 +30,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - mocktail: ^0.1.4 pedantic: ^1.11.1 \ No newline at end of file diff --git a/packages/camera/camera_web/test/helpers/helpers.dart b/packages/camera/camera_web/test/helpers/helpers.dart deleted file mode 100644 index 7094f55bb62e..000000000000 --- a/packages/camera/camera_web/test/helpers/helpers.dart +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -export 'mocks.dart'; diff --git a/packages/camera/camera_web/test/helpers/mocks.dart b/packages/camera/camera_web/test/helpers/mocks.dart deleted file mode 100644 index 34c56632b60f..000000000000 --- a/packages/camera/camera_web/test/helpers/mocks.dart +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:html'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockMediaStreamTrack extends Mock implements MediaStreamTrack {} - -/// A fake [MediaError] that returns the provided error [_code]. -class FakeMediaError extends Fake implements MediaError { - FakeMediaError(this._code); - - final int _code; - - @override - int get code => _code; -} From d6ba532344a62b19026a8d4f25a70fc9bdc10e95 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 12 Aug 2021 02:22:05 +0200 Subject: [PATCH 11/57] fix: don't request full-screen mode in unlockCaptureOrientation (#4226) --- .../example/integration_test/camera_web_test.dart | 10 ---------- packages/camera/camera_web/lib/src/camera_web.dart | 3 --- 2 files changed, 13 deletions(-) diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index ada5c2da1a29..555e20040a3c 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -788,16 +788,6 @@ void main() { ).thenReturn(OrientationType.portraitPrimary); }); - testWidgets( - 'requests full-screen mode ' - 'on documentElement', (tester) async { - await CameraPlatform.instance.unlockCaptureOrientation( - cameraId, - ); - - verify(documentElement.requestFullscreen).called(1); - }); - testWidgets('unlocks the capture orientation', (tester) async { await CameraPlatform.instance.unlockCaptureOrientation( cameraId, diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index fda33996f474..1038d227e23e 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -378,9 +378,6 @@ class CameraPlugin extends CameraPlatform { final documentElement = window?.document.documentElement; if (orientation != null && documentElement != null) { - // Full-screen mode may be required to modify the device orientation. - // See: https://w3c.github.io/screen-orientation/#interaction-with-fullscreen-api - documentElement.requestFullscreen(); orientation.unlock(); } else { throw PlatformException( From 1ac46e159c5d560df0cb716b36e622ad4f8f188c Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 12 Aug 2021 02:24:52 +0200 Subject: [PATCH 12/57] [camera_web] docs: add `setFlashMode` comments (#4227) --- packages/camera/camera_web/lib/src/camera.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index 6f758843a047..237f9858855e 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -133,8 +133,8 @@ class Camera { /// Captures a picture and returns the saved file in a JPEG format. /// - /// Enables the device flash when taking a picture if the flash mode - /// is either [FlashMode.auto] or [FlashMode.always]. + /// Enables the camera flash (torch mode) for a period of taking a picture + /// if the flash mode is either [FlashMode.auto] or [FlashMode.always]. Future takePicture() async { final shouldEnableTorchMode = flashMode == FlashMode.auto || flashMode == FlashMode.always; @@ -185,7 +185,14 @@ class Camera { } } - /// Sets the camera flash mode to [mode]. + /// Sets the camera flash mode to [mode] by modifying the camera + /// torch mode constraint. + /// + /// The torch mode is enabled for [FlashMode.torch] and + /// disabled for [FlashMode.off]. + /// + /// For [FlashMode.auto] and [FlashMode.always] the torch mode is enabled + /// only for a period of taking a picture in [takePicture]. /// /// Throws a [CameraWebException] if the torch mode is not supported /// or the camera has not been initialized or started. From 4383bb15d91384efb22dbe409c3fd58e4552267c Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 12 Aug 2021 02:27:05 +0200 Subject: [PATCH 13/57] [camera_web] Handle camera errors in `takePicture` (#4230) --- .../integration_test/camera_web_test.dart | 62 +++++++++++++++++++ .../camera/camera_web/lib/src/camera_web.dart | 3 + 2 files changed, 65 insertions(+) diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index 555e20040a3c..57c3de83ba04 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -929,6 +929,32 @@ void main() { ), ); }); + + testWidgets('when takePicture throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.takePicture).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.takePicture(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); }); }); @@ -1763,6 +1789,42 @@ void main() { await streamQueue.cancel(); }); + testWidgets( + 'emits a CameraErrorEvent ' + 'on takePicture error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.takePicture).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => await CameraPlatform.instance.takePicture(cameraId), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + testWidgets( 'emits a CameraErrorEvent ' 'on setFlashMode error', (tester) async { diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 1038d227e23e..134e0726ba4b 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -396,6 +396,9 @@ class CameraPlugin extends CameraPlatform { return getCamera(cameraId).takePicture(); } on html.DomException catch (e) { throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); } } From e5fc83d2b9024802e3e7712854b9789d7cbcee3e Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Fri, 13 Aug 2021 01:40:58 +0200 Subject: [PATCH 14/57] [camera_web] Fix `getCapabilities` not supported error thrown when selecting a camera on Firefox (#4234) * feat: check if getCapabilities can be called rather than catching an exception --- .../integration_test/camera_service_test.dart | 86 ++++++++----------- .../integration_test/helpers/mocks.dart | 3 + .../camera_web/lib/src/camera_service.dart | 68 +++++++-------- .../lib/src/shims/dart_js_util.dart | 14 +++ 4 files changed, 85 insertions(+), 86 deletions(-) create mode 100644 packages/camera/camera_web/lib/src/shims/dart_js_util.dart diff --git a/packages/camera/camera_web/example/integration_test/camera_service_test.dart b/packages/camera/camera_web/example/integration_test/camera_service_test.dart index 161aeb4a595e..937f023f4b36 100644 --- a/packages/camera/camera_web/example/integration_test/camera_service_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_service_test.dart @@ -9,6 +9,7 @@ import 'dart:js_util' as js_util; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/camera.dart'; import 'package:camera_web/src/camera_service.dart'; +import 'package:camera_web/src/shims/dart_js_util.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -27,15 +28,25 @@ void main() { late Navigator navigator; late MediaDevices mediaDevices; late CameraService cameraService; + late JsUtil jsUtil; setUp(() async { window = MockWindow(); navigator = MockNavigator(); mediaDevices = MockMediaDevices(); + jsUtil = MockJsUtil(); when(() => window.navigator).thenReturn(navigator); when(() => navigator.mediaDevices).thenReturn(mediaDevices); + // Mock JsUtil to return the real getProperty from dart:js_util. + when(() => jsUtil.getProperty(any(), any())).thenAnswer( + (invocation) => js_util.getProperty( + invocation.positionalArguments[0], + invocation.positionalArguments[1], + ), + ); + cameraService = CameraService()..window = window; }); @@ -354,6 +365,8 @@ void main() { when(() => camera.textureId).thenReturn(0); when(() => camera.stream).thenReturn(FakeMediaStream(videoTracks)); + + cameraService.jsUtil = jsUtil; }); testWidgets( @@ -496,6 +509,10 @@ void main() { }); group('getFacingModeForVideoTrack', () { + setUp(() { + cameraService.jsUtil = jsUtil; + }); + testWidgets( 'throws PlatformException ' 'with notSupported error ' @@ -525,14 +542,18 @@ void main() { final facingMode = cameraService.getFacingModeForVideoTrack(MockMediaStreamTrack()); - expect( - facingMode, - equals(null), - ); + expect(facingMode, isNull); }); group('when the facing mode is supported', () { + late MediaStreamTrack videoTrack; + setUp(() { + videoTrack = MockMediaStreamTrack(); + + when(() => jsUtil.hasProperty(videoTrack, 'getCapabilities')) + .thenReturn(true); + when(mediaDevices.getSupportedConstraints).thenReturn({ 'facingMode': true, }); @@ -541,95 +562,58 @@ void main() { testWidgets( 'returns an appropriate facing mode ' 'based on the video track settings', (tester) async { - final videoTrack = MockMediaStreamTrack(); - when(videoTrack.getSettings).thenReturn({'facingMode': 'user'}); final facingMode = cameraService.getFacingModeForVideoTrack(videoTrack); - expect( - facingMode, - equals('user'), - ); + expect(facingMode, equals('user')); }); testWidgets( 'returns an appropriate facing mode ' 'based on the video track capabilities ' 'when the facing mode setting is empty', (tester) async { - final videoTrack = MockMediaStreamTrack(); - when(videoTrack.getSettings).thenReturn({}); when(videoTrack.getCapabilities).thenReturn({ 'facingMode': ['environment', 'left'] }); + when(() => jsUtil.hasProperty(videoTrack, 'getCapabilities')) + .thenReturn(true); + final facingMode = cameraService.getFacingModeForVideoTrack(videoTrack); - expect( - facingMode, - equals('environment'), - ); + expect(facingMode, equals('environment')); }); testWidgets( 'returns null ' 'when the facing mode setting ' 'and capabilities are empty', (tester) async { - final videoTrack = MockMediaStreamTrack(); - when(videoTrack.getSettings).thenReturn({}); when(videoTrack.getCapabilities).thenReturn({'facingMode': []}); final facingMode = cameraService.getFacingModeForVideoTrack(videoTrack); - expect( - facingMode, - equals(null), - ); + expect(facingMode, isNull); }); testWidgets( 'returns null ' 'when the facing mode setting is empty and ' 'the video track capabilities are not supported', (tester) async { - final videoTrack = MockMediaStreamTrack(); - when(videoTrack.getSettings).thenReturn({}); - when(videoTrack.getCapabilities).thenThrow(JSNoSuchMethodError()); + + when(() => jsUtil.hasProperty(videoTrack, 'getCapabilities')) + .thenReturn(false); final facingMode = cameraService.getFacingModeForVideoTrack(videoTrack); - expect( - facingMode, - equals(null), - ); - }); - - testWidgets( - 'throws PlatformException ' - 'with unknown error ' - 'when getting the video track capabilities ' - 'throws an unknown error', (tester) async { - final videoTrack = MockMediaStreamTrack(); - - when(videoTrack.getSettings).thenReturn({}); - when(videoTrack.getCapabilities).thenThrow(Exception('Unknown')); - - expect( - () => cameraService.getFacingModeForVideoTrack(videoTrack), - throwsA( - isA().having( - (e) => e.code, - 'code', - CameraErrorCode.unknown.toString(), - ), - ), - ); + expect(facingMode, isNull); }); }); }); diff --git a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart index 436f2065aaf5..e6a11cc0b454 100644 --- a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart +++ b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart @@ -8,6 +8,7 @@ import 'dart:ui'; import 'package:camera_web/src/camera.dart'; import 'package:camera_web/src/camera_service.dart'; +import 'package:camera_web/src/shims/dart_js_util.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:cross_file/cross_file.dart'; import 'package:mocktail/mocktail.dart'; @@ -38,6 +39,8 @@ class MockVideoElement extends Mock implements VideoElement {} class MockXFile extends Mock implements XFile {} +class MockJsUtil extends Mock implements JsUtil {} + /// A fake [MediaStream] that returns the provided [_videoTracks]. class FakeMediaStream extends Fake implements MediaStream { FakeMediaStream(this._videoTracks); diff --git a/packages/camera/camera_web/lib/src/camera_service.dart b/packages/camera/camera_web/lib/src/camera_service.dart index c1a4ad1038ab..612b2b138fdb 100644 --- a/packages/camera/camera_web/lib/src/camera_service.dart +++ b/packages/camera/camera_web/lib/src/camera_service.dart @@ -4,10 +4,10 @@ import 'dart:html' as html; import 'dart:ui'; -import 'dart:js_util' as js_util; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/camera.dart'; +import 'package:camera_web/src/shims/dart_js_util.dart'; import 'package:camera_web/src/types/types.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -22,6 +22,10 @@ class CameraService { @visibleForTesting html.Window? window = html.window; + /// The utility to manipulate JavaScript interop objects. + @visibleForTesting + JsUtil jsUtil = JsUtil(); + /// Returns a media stream associated with the camera device /// with [cameraId] and constrained by [options]. Future getMediaStreamForOptions( @@ -143,8 +147,8 @@ class CameraService { // The zoom level capability is a nested JS object, therefore // we need to access its properties with the js_util library. // See: https://api.dart.dev/stable/2.13.4/dart-js_util/getProperty.html - final minimumZoomLevel = js_util.getProperty(zoomLevelCapability, 'min'); - final maximumZoomLevel = js_util.getProperty(zoomLevelCapability, 'max'); + final minimumZoomLevel = jsUtil.getProperty(zoomLevelCapability, 'min'); + final maximumZoomLevel = jsUtil.getProperty(zoomLevelCapability, 'max'); if (minimumZoomLevel != null && maximumZoomLevel != null) { return ZoomLevelCapability( @@ -201,40 +205,34 @@ class CameraService { final facingMode = videoTrackSettings[_facingModeKey]; if (facingMode == null) { - try { - // If the facing mode does not exist in the video track settings, - // check for the facing mode in the video track capabilities. - // - // MediaTrackCapabilities: - // https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackcapabilities - // - // This may throw a not supported error on Firefox. - final videoTrackCapabilities = videoTrack.getCapabilities(); + // If the facing mode does not exist in the video track settings, + // check for the facing mode in the video track capabilities. + // + // MediaTrackCapabilities: + // https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackcapabilities + + // Check if getting the video track capabilities is supported. + // + // The method may not be supported on Firefox. + // See: https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/getCapabilities#browser_compatibility + if (!jsUtil.hasProperty(videoTrack, 'getCapabilities')) { + // Return null if the video track capabilites are not supported. + return null; + } + + final videoTrackCapabilities = videoTrack.getCapabilities(); - // A list of facing mode capabilities as - // the camera may support multiple facing modes. - final facingModeCapabilities = - List.from(videoTrackCapabilities[_facingModeKey] ?? []); + // A list of facing mode capabilities as + // the camera may support multiple facing modes. + final facingModeCapabilities = + List.from(videoTrackCapabilities[_facingModeKey] ?? []); - if (facingModeCapabilities.isNotEmpty) { - final facingModeCapability = facingModeCapabilities.first; - return facingModeCapability; - } else { - // Return null if there are no facing mode capabilities. - return null; - } - } catch (e) { - switch (e.runtimeType.toString()) { - case 'JSNoSuchMethodError': - // Return null if getting capabilities is currently not supported. - return null; - default: - throw PlatformException( - code: CameraErrorCode.unknown.toString(), - message: - 'An unknown error occured when getting the video track capabilities.', - ); - } + if (facingModeCapabilities.isNotEmpty) { + final facingModeCapability = facingModeCapabilities.first; + return facingModeCapability; + } else { + // Return null if there are no facing mode capabilities. + return null; } } diff --git a/packages/camera/camera_web/lib/src/shims/dart_js_util.dart b/packages/camera/camera_web/lib/src/shims/dart_js_util.dart new file mode 100644 index 000000000000..6601bec6f529 --- /dev/null +++ b/packages/camera/camera_web/lib/src/shims/dart_js_util.dart @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:js_util' as js_util; + +/// A utility that shims dart:js_util to manipulate JavaScript interop objects. +class JsUtil { + /// Returns true if the object [o] has the property [name]. + bool hasProperty(Object o, Object name) => js_util.hasProperty(o, name); + + /// Returns the value of the property [name] in the object [o]. + dynamic getProperty(Object o, Object name) => js_util.getProperty(o, name); +} From 1e497d62039b8d668bb82c2bdde76ecb9e1a6af1 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Fri, 13 Aug 2021 01:41:19 +0200 Subject: [PATCH 15/57] [camera_web] Add missing setFlashMode test (#4235) --- .../example/integration_test/camera_web_test.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index 57c3de83ba04..6b03b6d77035 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -998,6 +998,21 @@ void main() { }); group('setFlashMode', () { + testWidgets('calls setFlashMode on the camera', (tester) async { + final camera = MockCamera(); + const flashMode = FlashMode.always; + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.setFlashMode( + cameraId, + flashMode, + ); + + verify(() => camera.setFlashMode(flashMode)).called(1); + }); + group('throws PlatformException', () { testWidgets( 'with notFound error ' From b1c65df43c5a0e664ebc75eabd78b16b49f51c39 Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Fri, 13 Aug 2021 01:41:43 +0200 Subject: [PATCH 16/57] [camera_web] Update the web plugin README (#4237) * docs: update web plugin README * docs: update web plugin missing implementation README --- packages/camera/camera_web/README.md | 76 ++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera_web/README.md b/packages/camera/camera_web/README.md index d57bd7446d17..8c216b3f4e0e 100644 --- a/packages/camera/camera_web/README.md +++ b/packages/camera/camera_web/README.md @@ -1,7 +1,77 @@ # Camera Web Plugin -A Flutter plugin for Web allowing access to the device cameras. +The web implementation of [`camera`][camera]. -*Note*: This plugin is under development. +*Note*: This plugin is under development. See [missing implementation](#missing-implementation). -In order to use this plugin, your app should depend both on `camera` and `camera_web`. This is a temporary solution until a plugin is released. \ No newline at end of file +## Usage + +This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), which means you can simply use `camera` normally. This package will be automatically included in your app when you do. + +## Example + +Find the example in the [`camera` package](https://pub.dev/packages/camera#example). + +## Limitations on the web platform + +### Camera devices + +The camera devices are accessed with [Stream Web API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Streams_API) with the following [browser support](https://caniuse.com/stream): + +![Data on support for the Stream feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/stream.png) + +Accessing camera devices requires a [secure browsing context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts). This means that you might need to serve your web application over HTTPS. For insecure contexts `CameraPlatform.availableCameras` might throw a `CameraException` with the `permissionDenied` error code. + +### Device orientation + +The device orientation implementation is backed by [`Screen Orientation Web API`](https://www.w3.org/TR/screen-orientation/) with the following [browser support](https://caniuse.com/screen-orientation): + +![Data on support for the Screen Orientation feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/screen-orientation.png) + +For the browsers that do not support the device orientation: +- `CameraPlatform.onDeviceOrientationChanged` returns an empty stream. +- `CameraPlatform.lockCaptureOrientation` and `CameraPlatform.unlockCaptureOrientation` throw a `PlatformException` with the `orientationNotSupported` error code. + +### Flash mode and zoom level + +The flash mode and zoom level implementation is backed by [Image Capture Web API](https://w3c.github.io/mediacapture-image/) with the following [browser support](https://caniuse.com/mdn-api_imagecapture) (as of 12 August 2021): + +![Data on support for the Image Capture feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/static/v1/mdn-api__ImageCapture-1628778966589.png) + +For the browsers that do not support the flash mode: +- `CameraPlatform.setFlashMode` throws a `PlatformException` with the `torchModeNotSupported` error code. + +For the browsers that do not support the zoom level: +- `CameraPlatform.getMaxZoomLevel`, `CameraPlatform.getMinZoomLevel` and `CameraPlatform.setZoomLevel` throw a `PlatformException` with the `zoomLevelNotSupported` error code. + +### Taking a picture + +The image capturing implementation is backed by [`URL.createObjectUrl` Web API](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL) with the following [browser support](https://caniuse.com/bloburls): + +![Data on support for the Blob URLs feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/bloburls.png) + +The web platform does not support `dart:io`. Attempts to display a captured image using `Image.file` will throw an error. The capture image contains a network-accessible URL pointing to a location within the browser and should be displayed using `Image.network` or `Image.memory` after loading the image bytes to memory. + +See the example below: + +```dart +if (kIsWeb) { + Image.network(capturedImage.path); +} else { + Image.file(File(capturedImage.path)); +} +``` + +## Missing implementation + +The web implementation of [`camera`][camera] is missing the following features: +- Video recording +- Exposure mode, point and offset +- Focus mode and point +- Camera closing events +- Camera sensor orientation +- Camera image format group +- Camera image streaming + + +[camera]: https://pub.dev/packages/camera \ No newline at end of file From c8570fc681d83c1086f30ecf6843a958f63323ba Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Fri, 13 Aug 2021 08:52:06 +0200 Subject: [PATCH 17/57] fix: disposed CameraController error thrown when changing a camera (#4236) --- packages/camera/camera/example/lib/main.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 00ac2251ba2a..2314aecbece3 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -594,17 +594,21 @@ class _CameraExampleHomeState extends State } void onNewCameraSelected(CameraDescription cameraDescription) async { - if (controller != null) { - await controller!.dispose(); - } + final previousCameraController = controller; + final CameraController cameraController = CameraController( cameraDescription, ResolutionPreset.medium, enableAudio: enableAudio, imageFormatGroup: ImageFormatGroup.jpeg, ); + controller = cameraController; + if (mounted) { + setState(() {}); + } + // If the controller is updated then update the UI. cameraController.addListener(() { if (mounted) setState(() {}); @@ -637,6 +641,8 @@ class _CameraExampleHomeState extends State if (mounted) { setState(() {}); } + + await previousCameraController?.dispose(); } void onTakePictureButtonPressed() { From ebf4d59543a998d25c90b4a3f72c2eb42d628b7c Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Sat, 14 Aug 2021 01:04:33 +0200 Subject: [PATCH 18/57] [camera_web] Add support for pausing and resuming the camera preview (#4239) * chore: update camera_platform_interface to 2.1.0 * feat: add pause to Camera * test: add Camera pause test * feat: add pausePreview and resumePreview implementation * test: add pausePreview and resumePreview tests --- .../example/integration_test/camera_test.dart | 18 ++ .../integration_test/camera_web_test.dart | 165 ++++++++++++++++++ .../camera/camera_web/lib/src/camera.dart | 5 + .../camera/camera_web/lib/src/camera_web.dart | 21 +++ packages/camera/camera_web/pubspec.yaml | 2 +- 5 files changed, 210 insertions(+), 1 deletion(-) diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart index 5c3d842502ba..1d1659352f26 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -214,6 +214,24 @@ void main() { }); }); + group('pause', () { + testWidgets('pauses the camera stream', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + await camera.play(); + + expect(camera.videoElement.paused, isFalse); + + camera.pause(); + + expect(camera.videoElement.paused, isTrue); + }); + }); + group('stop', () { testWidgets('resets the camera stream', (tester) async { final camera = Camera( diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index 6b03b6d77035..d48df122277f 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -1459,6 +1459,135 @@ void main() { }); }); + group('pausePreview', () { + testWidgets('calls pause on the camera', (tester) async { + final camera = MockCamera(); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.pausePreview(cameraId); + + verify(camera.pause).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () async => await CameraPlatform.instance.pausePreview(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when pause throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.NOT_SUPPORTED); + + when(camera.pause).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.pausePreview(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); + }); + + group('resumePreview', () { + testWidgets('calls play on the camera', (tester) async { + final camera = MockCamera(); + + when(camera.play).thenAnswer((_) async => {}); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.resumePreview(cameraId); + + verify(camera.play).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () async => await CameraPlatform.instance.resumePreview(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when play throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.NOT_SUPPORTED); + + when(camera.play).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.resumePreview(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when play throws CameraWebException', (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.unknown, + 'description', + ); + + when(camera.play).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.resumePreview(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + testWidgets( 'buildPreview returns an HtmlElementView ' 'with an appropriate view type', (tester) async { @@ -1993,6 +2122,42 @@ void main() { await streamQueue.cancel(); }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on resumePreview error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.unknown, + 'description', + ); + + when(camera.play).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => await CameraPlatform.instance.resumePreview(cameraId), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); }); testWidgets('onVideoRecordedEvent throws UnimplementedError', diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index 237f9858855e..c1343ceccf49 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -119,6 +119,11 @@ class Camera { await videoElement.play(); } + /// Pauses the camera stream on the current frame. + void pause() async { + videoElement.pause(); + } + /// Stops the camera stream and resets the camera source. void stop() { final tracks = videoElement.srcObject?.getTracks(); diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 134e0726ba4b..8b131f5d4f6e 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -517,6 +517,27 @@ class CameraPlugin extends CameraPlatform { } } + @override + Future pausePreview(int cameraId) async { + try { + getCamera(cameraId).pause(); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } + } + + @override + Future resumePreview(int cameraId) async { + try { + await getCamera(cameraId).play(); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } + } + @override Widget buildPreview(int cameraId) { return HtmlElementView( diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index a2aa43c22d65..c4d78999f273 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -21,7 +21,7 @@ flutter: fileName: camera_web.dart dependencies: - camera_platform_interface: ^2.0.1 + camera_platform_interface: ^2.1.0 flutter: sdk: flutter flutter_web_plugins: From 0209a2fadefe2950ffaaac5aa184ccb1d2a18f3b Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 13 Aug 2021 16:22:24 -0700 Subject: [PATCH 19/57] Eliminate build_all_plugins_app.sh (#4232) Removes the `build_all_plugins_app.sh` bash script, in support of the goal of eliminating all use of bash from the repository (for maintainability, and for better Windows compatibility). - The exclusion list moves to a config file, match other recent repo changes - The exclusion logging moves into the tool itself, consistent with the tool doing more logging of skipped and excluded plugins - The bulk of the logic moves to a Cirrus task template. This was done instead of rewriting the script in Dart, even though it will mean more work for alternate CI support (e.g., bringing this up on a Windows LUCI bot), because breaking it into components makes it easier to pinpoint failures from the CI UI rather than having all the steps smashed together. --- .cirrus.yml | 36 ++++++--- script/build_all_plugins_app.sh | 73 ------------------- script/common.sh | 14 ---- script/configs/exclude_all_plugins_app.yaml | 10 +++ .../tool/lib/src/common/plugin_command.dart | 4 +- .../src/create_all_plugins_app_command.dart | 28 +++++-- .../create_all_plugins_app_command_test.dart | 42 +++++++++-- script/tool_runner.sh | 6 +- 8 files changed, 99 insertions(+), 114 deletions(-) delete mode 100755 script/build_all_plugins_app.sh delete mode 100644 script/common.sh create mode 100644 script/configs/exclude_all_plugins_app.yaml diff --git a/.cirrus.yml b/.cirrus.yml index f978cc729799..ffdd71daebc4 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -28,6 +28,20 @@ flutter_upgrade_template: &FLUTTER_UPGRADE_TEMPLATE - flutter doctor -v << : *TOOL_SETUP_TEMPLATE +build_all_plugins_app_template: &BUILD_ALL_PLUGINS_APP_TEMPLATE + create_all_plugins_app_script: + - dart $PLUGIN_TOOL all-plugins-app --output-dir=. --exclude script/configs/exclude_all_plugins_app.yaml + build_all_plugins_debug_script: + - cd all_plugins + - if [[ "$BUILD_ALL_ARGS" == "web" ]]; then + - echo "Skipping; web does not support debug builds" + - else + - flutter build $BUILD_ALL_ARGS --debug + - fi + build_all_plugins_release_script: + - cd all_plugins + - flutter build $BUILD_ALL_ARGS --release + macos_template: &MACOS_TEMPLATE # Only one macOS task can run in parallel without credits, so use them for # PRs on macOS. @@ -82,28 +96,29 @@ task: ### Android tasks ### - name: build_all_plugins_apk env: + BUILD_ALL_ARGS: "apk" matrix: CHANNEL: "master" CHANNEL: "stable" - script: - - ./script/build_all_plugins_app.sh apk + << : *BUILD_ALL_PLUGINS_APP_TEMPLATE ### Web tasks ### - name: build_all_plugins_web env: + BUILD_ALL_ARGS: "web" matrix: CHANNEL: "master" CHANNEL: "stable" - script: - - ./script/build_all_plugins_app.sh web + << : *BUILD_ALL_PLUGINS_APP_TEMPLATE ### Linux desktop tasks ### - name: build_all_plugins_linux env: + BUILD_ALL_ARGS: "linux" matrix: CHANNEL: "master" CHANNEL: "stable" - script: + setup_script: - flutter config --enable-linux-desktop - - ./script/build_all_plugins_app.sh linux + << : *BUILD_ALL_PLUGINS_APP_TEMPLATE - name: build-linux+drive-examples env: matrix: @@ -200,11 +215,11 @@ task: ### iOS tasks ### - name: build_all_plugins_ipa env: + BUILD_ALL_ARGS: "ios --no-codesign" matrix: CHANNEL: "master" CHANNEL: "stable" - script: - - ./script/build_all_plugins_app.sh ios --no-codesign + << : *BUILD_ALL_PLUGINS_APP_TEMPLATE - name: build-ipas+drive-examples env: PATH: $PATH:/usr/local/bin @@ -234,12 +249,13 @@ task: ### macOS desktop tasks ### - name: build_all_plugins_macos env: + BUILD_ALL_ARGS: "macos" matrix: CHANNEL: "master" CHANNEL: "stable" - script: + setup_script: - flutter config --enable-macos-desktop - - ./script/build_all_plugins_app.sh macos + << : *BUILD_ALL_PLUGINS_APP_TEMPLATE - name: build-macos+drive-examples env: matrix: diff --git a/script/build_all_plugins_app.sh b/script/build_all_plugins_app.sh deleted file mode 100755 index 3b3416021a42..000000000000 --- a/script/build_all_plugins_app.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/bash -# Copyright 2013 The Flutter Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -# Usage: -# -# ./script/build_all_plugins_app.sh apk -# ./script/build_all_plugins_app.sh ios - -# This script builds the app in flutter/plugins/example/all_plugins to make -# sure all first party plugins can be compiled together. - -# So that users can run this script from anywhere and it will work as expected. -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null && pwd)" - -readonly REPO_DIR="$(dirname "$SCRIPT_DIR")" - -source "$SCRIPT_DIR/common.sh" - -# This list should be kept as short as possible, and things should remain here -# only as long as necessary, since in general the goal is for all of the latest -# versions of plugins to be mutually compatible. -# -# An example use case for this list would be to temporarily add plugins while -# updating multiple plugins for a breaking change in a common dependency in -# cases where using a relaxed version constraint isn't possible. -readonly EXCLUDED_PLUGINS_LIST=( - "plugin_platform_interface" # This should never be a direct app dependency. -) -# Comma-separated string of the list above -readonly EXCLUDED=$(IFS=, ; echo "${EXCLUDED_PLUGINS_LIST[*]}") - -ALL_EXCLUDED=($EXCLUDED) - -echo "Excluding the following plugins: $ALL_EXCLUDED" - -(cd "$REPO_DIR" && plugin_tools all-plugins-app --exclude $ALL_EXCLUDED) - -# Master now creates null-safe app code by default; migrate stable so both -# branches are building in the same mode. -if [[ "${CHANNEL}" == "stable" ]]; then - (cd $REPO_DIR/all_plugins && dart migrate --apply-changes) -fi - -function error() { - echo "$@" 1>&2 -} - -failures=0 - -BUILD_MODES=("debug" "release") -# Web doesn't support --debug for builds. -if [[ "$1" == "web" ]]; then - BUILD_MODES=("release") -fi - -for version in "${BUILD_MODES[@]}"; do - echo "Building $version..." - (cd $REPO_DIR/all_plugins && flutter build $@ --$version) - - if [ $? -eq 0 ]; then - echo "Successfully built $version all_plugins app." - echo "All first-party plugins compile together." - else - error "Failed to build $version all_plugins app." - error "This indicates a conflict between two or more first-party plugins." - failures=$(($failures + 1)) - fi -done - -rm -rf $REPO_DIR/all_plugins/ -exit $failures diff --git a/script/common.sh b/script/common.sh deleted file mode 100644 index 11eb64101f2b..000000000000 --- a/script/common.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -# Copyright 2013 The Flutter Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -function error() { - echo "$@" 1>&2 -} - -# Runs the plugin tools from the plugin_tools git submodule. -function plugin_tools() { - (pushd "$REPO_DIR/script/tool" && dart pub get && popd) >/dev/null - dart run "$REPO_DIR/script/tool/bin/flutter_plugin_tools.dart" "$@" -} diff --git a/script/configs/exclude_all_plugins_app.yaml b/script/configs/exclude_all_plugins_app.yaml new file mode 100644 index 000000000000..8dd0fde5ef5f --- /dev/null +++ b/script/configs/exclude_all_plugins_app.yaml @@ -0,0 +1,10 @@ +# This list should be kept as short as possible, and things should remain here +# only as long as necessary, since in general the goal is for all of the latest +# versions of plugins to be mutually compatible. +# +# An example use case for this list would be to temporarily add plugins while +# updating multiple plugins for a breaking change in a common dependency in +# cases where using a relaxed version constraint isn't possible. + +# This is a permament entry, as it should never be a direct app dependency. +- plugin_platform_interface diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index db0a821fd2d7..10f423360878 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -191,7 +191,7 @@ abstract class PluginCommand extends Command { } /// Returns the set of plugins to exclude based on the `--exclude` argument. - Set _getExcludedPackageName() { + Set getExcludedPackageNames() { final Set excludedPackages = _excludedPackages ?? getStringListArg(_excludeArg).expand((String item) { if (item.endsWith('.yaml')) { @@ -265,7 +265,7 @@ abstract class PluginCommand extends Command { Stream _getAllPackages() async* { Set plugins = Set.from(getStringListArg(_packagesArg)); - final Set excludedPluginNames = _getExcludedPackageName(); + final Set excludedPluginNames = getExcludedPackageNames(); final bool runOnChangedPackages = getBoolArg(_runOnChangedPackagesArg); if (plugins.isEmpty && diff --git a/script/tool/lib/src/create_all_plugins_app_command.dart b/script/tool/lib/src/create_all_plugins_app_command.dart index d4eccb8a313e..e1cee6f3fe7d 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -12,22 +12,27 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/plugin_command.dart'; +const String _outputDirectoryFlag = 'output-dir'; + /// A command to create an application that builds all in a single application. class CreateAllPluginsAppCommand extends PluginCommand { /// Creates an instance of the builder command. CreateAllPluginsAppCommand( Directory packagesDir, { Directory? pluginsRoot, - }) : pluginsRoot = pluginsRoot ?? packagesDir.fileSystem.currentDirectory, - super(packagesDir) { - appDirectory = this.pluginsRoot.childDirectory('all_plugins'); + }) : super(packagesDir) { + final Directory defaultDir = + pluginsRoot ?? packagesDir.fileSystem.currentDirectory; + argParser.addOption(_outputDirectoryFlag, + defaultsTo: defaultDir.path, + help: 'The path the directory to create the "all_plugins" project in.\n' + 'Defaults to the repository root.'); } - /// The root directory of the plugin repository. - Directory pluginsRoot; - /// The location of the synthesized app project. - late Directory appDirectory; + Directory get appDirectory => packagesDir.fileSystem + .directory(getStringArg(_outputDirectoryFlag)) + .childDirectory('all_plugins'); @override String get description => @@ -43,6 +48,15 @@ class CreateAllPluginsAppCommand extends PluginCommand { throw ToolExit(exitCode); } + final Set excluded = getExcludedPackageNames(); + if (excluded.isNotEmpty) { + print('Exluding the following plugins from the combined build:'); + for (final String plugin in excluded) { + print(' $plugin'); + } + print(''); + } + await Future.wait(>[ _genPubspecWithAllPlugins(), _updateAppGradle(), diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart index 073024a17bb3..4439d13c3625 100644 --- a/script/tool/test/create_all_plugins_app_command_test.dart +++ b/script/tool/test/create_all_plugins_app_command_test.dart @@ -13,10 +13,10 @@ import 'util.dart'; void main() { group('$CreateAllPluginsAppCommand', () { late CommandRunner runner; - FileSystem fileSystem; + late CreateAllPluginsAppCommand command; + late FileSystem fileSystem; late Directory testRoot; late Directory packagesDir; - late Directory appDir; setUp(() { // Since the core of this command is a call to 'flutter create', the test @@ -26,11 +26,10 @@ void main() { testRoot = fileSystem.systemTempDirectory.createTempSync(); packagesDir = testRoot.childDirectory('packages'); - final CreateAllPluginsAppCommand command = CreateAllPluginsAppCommand( + command = CreateAllPluginsAppCommand( packagesDir, pluginsRoot: testRoot, ); - appDir = command.appDirectory; runner = CommandRunner( 'create_all_test', 'Test for $CreateAllPluginsAppCommand'); runner.addCommand(command); @@ -47,7 +46,7 @@ void main() { await runCapturingPrint(runner, ['all-plugins-app']); final List pubspec = - appDir.childFile('pubspec.yaml').readAsLinesSync(); + command.appDirectory.childFile('pubspec.yaml').readAsLinesSync(); expect( pubspec, @@ -65,7 +64,7 @@ void main() { await runCapturingPrint(runner, ['all-plugins-app']); final List pubspec = - appDir.childFile('pubspec.yaml').readAsLinesSync(); + command.appDirectory.childFile('pubspec.yaml').readAsLinesSync(); expect( pubspec, @@ -82,9 +81,38 @@ void main() { await runCapturingPrint(runner, ['all-plugins-app']); final String pubspec = - appDir.childFile('pubspec.yaml').readAsStringSync(); + command.appDirectory.childFile('pubspec.yaml').readAsStringSync(); expect(pubspec, contains(RegExp('sdk:\\s*(?:["\']>=|[^])2\\.12\\.'))); }); + + test('handles --output-dir', () async { + createFakePlugin('plugina', packagesDir); + + final Directory customOutputDir = + fileSystem.systemTempDirectory.createTempSync(); + await runCapturingPrint(runner, + ['all-plugins-app', '--output-dir=${customOutputDir.path}']); + + expect(command.appDirectory.path, + customOutputDir.childDirectory('all_plugins').path); + }); + + test('logs exclusions', () async { + createFakePlugin('plugina', packagesDir); + createFakePlugin('pluginb', packagesDir); + createFakePlugin('pluginc', packagesDir); + + final List output = await runCapturingPrint( + runner, ['all-plugins-app', '--exclude=pluginb,pluginc']); + + expect( + output, + containsAllInOrder([ + 'Exluding the following plugins from the combined build:', + ' pluginb', + ' pluginc', + ])); + }); }); } diff --git a/script/tool_runner.sh b/script/tool_runner.sh index 11a54ce435a4..93a7776d0a35 100755 --- a/script/tool_runner.sh +++ b/script/tool_runner.sh @@ -8,7 +8,11 @@ set -e readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" readonly REPO_DIR="$(dirname "$SCRIPT_DIR")" -source "$SCRIPT_DIR/common.sh" +# Runs the plugin tools from the in-tree source. +function plugin_tools() { + (pushd "$REPO_DIR/script/tool" && dart pub get && popd) >/dev/null + dart run "$REPO_DIR/script/tool/bin/flutter_plugin_tools.dart" "$@" +} ACTIONS=("$@") From 3ae3a027e40df088b1a354d9f27b840791473aac Mon Sep 17 00:00:00 2001 From: Balvinder Singh Gambhir Date: Sat, 14 Aug 2021 06:32:03 +0530 Subject: [PATCH 20/57] [video_player] removed video player is not functional on ios simulators warning (#4241) --- packages/video_player/video_player/CHANGELOG.md | 4 ++++ packages/video_player/video_player/README.md | 2 -- packages/video_player/video_player/pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index bfed1615f8a6..8898ba665cf9 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.13 + +* Removed obsolete warning about not working in iOS simulators from README. + ## 2.1.12 * Update the video url in the readme code sample diff --git a/packages/video_player/video_player/README.md b/packages/video_player/video_player/README.md index a1d3d935e71c..4d2bf80a2628 100644 --- a/packages/video_player/video_player/README.md +++ b/packages/video_player/video_player/README.md @@ -12,8 +12,6 @@ First, add `video_player` as a [dependency in your pubspec.yaml file](https://fl ### iOS -Warning: The video player is not functional on iOS simulators. An iOS device must be used during development/testing. - Add the following entry to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: ```xml diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 960f0c6ce63a..c4c616cc751f 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, and web. repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.1.12 +version: 2.1.13 environment: sdk: ">=2.12.0 <3.0.0" From 99c5f6139a196171116de058a68166dc9d0325dc Mon Sep 17 00:00:00 2001 From: nt4f04uNd Date: Sun, 15 Aug 2021 20:32:03 +0300 Subject: [PATCH 21/57] Move test packages from `dependencies` to `dev_dependencies` (#4231) --- .../in_app_purchase/in_app_purchase_android/CHANGELOG.md | 4 ++++ .../in_app_purchase/in_app_purchase_android/pubspec.yaml | 4 ++-- packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md | 4 ++++ packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml | 4 ++-- packages/video_player/video_player/CHANGELOG.md | 4 ++++ packages/video_player/video_player/pubspec.yaml | 6 +++--- 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md index 32f9aa60e4ca..d67d1efd61b5 100644 --- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.1.4+4 + +* Removed dependency on the `test` package. + # 0.1.4+3 - Updated installation instructions in README. diff --git a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml index f8e63821657a..3969e34c052b 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_android description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.4+3 +version: 0.1.4+4 environment: sdk: ">=2.12.0 <3.0.0" @@ -22,10 +22,10 @@ dependencies: in_app_purchase_platform_interface: ^1.1.0 json_annotation: ^4.0.1 meta: ^1.3.0 - test: ^1.16.0 dev_dependencies: build_runner: ^1.11.1 flutter_test: sdk: flutter json_serializable: ^4.1.1 + test: ^1.16.0 diff --git a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md index 305d5a13647c..c76409521e2f 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.1.3+2 + +* Removed dependency on the `test` package. + # 0.1.3+1 - Updated installation instructions in README. diff --git a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml index 5f3b08520eb6..8fc42371f405 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_ios description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the iOS StoreKit Framework. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.3+1 +version: 0.1.3+2 environment: sdk: ">=2.12.0 <3.0.0" @@ -21,10 +21,10 @@ dependencies: in_app_purchase_platform_interface: ^1.1.0 json_annotation: ^4.0.1 meta: ^1.3.0 - test: ^1.16.0 dev_dependencies: build_runner: ^1.11.1 flutter_test: sdk: flutter json_serializable: ^4.1.1 + test: ^1.16.0 diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 8898ba665cf9..f2029622f0ee 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.14 + +* Removed dependency on the `flutter_test` package. + ## 2.1.13 * Removed obsolete warning about not working in iOS simulators from README. diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index c4c616cc751f..0d0cdb1cb436 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, and web. repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.1.13 +version: 2.1.14 environment: sdk: ">=2.12.0 <3.0.0" @@ -23,8 +23,6 @@ flutter: dependencies: flutter: sdk: flutter - flutter_test: - sdk: flutter meta: ^1.3.0 video_player_platform_interface: ^4.1.0 # The design on https://flutter.dev/go/federated-plugins was to leave @@ -36,5 +34,7 @@ dependencies: video_player_web: ^2.0.0 dev_dependencies: + flutter_test: + sdk: flutter pedantic: ^1.10.0 pigeon: ^0.1.21 From 954041d5bc76a9747899d0c7b3c66fc941e27c3f Mon Sep 17 00:00:00 2001 From: Maurice Parrish Date: Mon, 16 Aug 2021 16:27:05 -0700 Subject: [PATCH 22/57] Add unit tests to `quick_actions` plugin (#4245) --- .../quick_actions/android/build.gradle | 9 ++++ .../quickactions/QuickActionsTest.java | 54 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java diff --git a/packages/quick_actions/quick_actions/android/build.gradle b/packages/quick_actions/quick_actions/android/build.gradle index 038f9e99048a..0bce642f3e60 100644 --- a/packages/quick_actions/quick_actions/android/build.gradle +++ b/packages/quick_actions/quick_actions/android/build.gradle @@ -32,6 +32,15 @@ android { disable 'InvalidPackage' } + dependencies { + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:3.2.4' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } testOptions { unitTests.includeAndroidResources = true diff --git a/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java b/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java new file mode 100644 index 000000000000..d437444a53be --- /dev/null +++ b/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java @@ -0,0 +1,54 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.quickactions; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.StandardMethodCodec; +import java.nio.ByteBuffer; +import org.junit.Test; + +public class QuickActionsTest { + private static class TestBinaryMessenger implements BinaryMessenger { + public MethodCall lastMethodCall; + + @Override + public void send(@NonNull String channel, @Nullable ByteBuffer message) { + send(channel, message, null); + } + + @Override + public void send( + @NonNull String channel, + @Nullable ByteBuffer message, + @Nullable final BinaryReply callback) { + if (channel.equals("plugins.flutter.io/quick_actions")) { + lastMethodCall = + StandardMethodCodec.INSTANCE.decodeMethodCall((ByteBuffer) message.position(0)); + } + } + + @Override + public void setMessageHandler(@NonNull String channel, @Nullable BinaryMessageHandler handler) { + // Do nothing. + } + } + + @Test + public void canAttachToEngine() { + final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); + final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class); + when(mockPluginBinding.getBinaryMessenger()).thenReturn(testBinaryMessenger); + + final QuickActionsPlugin plugin = new QuickActionsPlugin(); + plugin.onAttachedToEngine(mockPluginBinding); + } +} From c52ae9fdf1751cb86ef301b48fab69d4fd6cf832 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 17 Aug 2021 08:36:40 -0700 Subject: [PATCH 23/57] [flutter_plugin_tool] Don't allow NEXT on version bumps (#4246) The special "NEXT" entry in a CHANGELOG should never be present in a commit that bumped the version. This validates that this is true even if the CHANGELOG would be correct for a non-version-change state, to catch someone incorrectly resolving a merge conflict by leaving both parts of the conflict, rather than folding the 'NEXT' entry's list into the new version's notes. Fixes https://github.com/flutter/flutter/issues/85584 --- script/tool/CHANGELOG.md | 2 + .../tool/lib/src/version_check_command.dart | 84 ++++++++++++++----- .../tool/test/version_check_command_test.dart | 54 +++++++++++- 3 files changed, 115 insertions(+), 25 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 7f326ff3c8f7..584ea571f0e1 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -23,6 +23,8 @@ the new `native-test` command. - Commands that print a run summary at the end now track and log exclusions similarly to skips for easier auditing. +- `version-check` now validates that `NEXT` is not present when changing + the version. ## 0.4.1 diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index c08600c3f669..67c563782888 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -32,6 +32,21 @@ enum NextVersionType { RELEASE, } +/// The state of a package's version relative to the comparison base. +enum _CurrentVersionState { + /// The version is unchanged. + unchanged, + + /// The version has changed, and the transition is valid. + validChange, + + /// The version has changed, and the transition is invalid. + invalidChange, + + /// There was an error determining the version state. + unknown, +} + /// Returns the set of allowed next versions, with their change type, for /// [version]. /// @@ -140,11 +155,28 @@ class VersionCheckCommand extends PackageLoopingCommand { final List errors = []; - if (!await _hasValidVersionChange(package, pubspec: pubspec)) { - errors.add('Disallowed version change.'); + bool versionChanged; + final _CurrentVersionState versionState = + await _getVersionState(package, pubspec: pubspec); + switch (versionState) { + case _CurrentVersionState.unchanged: + versionChanged = false; + break; + case _CurrentVersionState.validChange: + versionChanged = true; + break; + case _CurrentVersionState.invalidChange: + versionChanged = true; + errors.add('Disallowed version change.'); + break; + case _CurrentVersionState.unknown: + versionChanged = false; + errors.add('Unable to determine previous version.'); + break; } - if (!(await _hasConsistentVersion(package, pubspec: pubspec))) { + if (!(await _validateChangelogVersion(package, + pubspec: pubspec, pubspecVersionChanged: versionChanged))) { errors.add('pubspec.yaml and CHANGELOG.md have different versions'); } @@ -195,10 +227,9 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} return await gitVersionFinder.getPackageVersion(gitPath); } - /// Returns true if the version of [package] is either unchanged relative to - /// the comparison base (git or pub, depending on flags), or is a valid - /// version transition. - Future _hasValidVersionChange( + /// Returns the state of the verison of [package] relative to the comparison + /// base (git or pub, depending on flags). + Future<_CurrentVersionState> _getVersionState( Directory package, { required Pubspec pubspec, }) async { @@ -208,7 +239,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} if (getBoolArg(_againstPubFlag)) { previousVersion = await _fetchPreviousVersionFromPub(pubspec.name); if (previousVersion == null) { - return false; + return _CurrentVersionState.unknown; } if (previousVersion != Version.none) { print( @@ -225,12 +256,12 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} '${getBoolArg(_againstPubFlag) ? 'on pub server' : 'at git base'}.'); logWarning( '${indentation}If this plugin is not new, something has gone wrong.'); - return true; + return _CurrentVersionState.validChange; // Assume new, thus valid. } if (previousVersion == currentVersion) { print('${indentation}No version change.'); - return true; + return _CurrentVersionState.unchanged; } // Check for reverts when doing local validation. @@ -241,9 +272,9 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} // to be a revert rather than a typo by checking that the transition // from the lower version to the new version would have been valid. if (possibleVersionsFromNewVersion.containsKey(previousVersion)) { - print('${indentation}New version is lower than previous version. ' + logWarning('${indentation}New version is lower than previous version. ' 'This is assumed to be a revert.'); - return true; + return _CurrentVersionState.validChange; } } @@ -257,7 +288,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} printError('${indentation}Incorrectly updated version.\n' '${indentation}HEAD: $currentVersion, $source: $previousVersion.\n' '${indentation}Allowed versions: $allowedNextVersions'); - return false; + return _CurrentVersionState.invalidChange; } final bool isPlatformInterface = @@ -268,16 +299,20 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} allowedNextVersions[currentVersion] == NextVersionType.BREAKING_MAJOR) { printError('${indentation}Breaking change detected.\n' '${indentation}Breaking changes to platform interfaces are strongly discouraged.\n'); - return false; + return _CurrentVersionState.invalidChange; } - return true; + return _CurrentVersionState.validChange; } - /// Returns whether or not the pubspec version and CHANGELOG version for - /// [plugin] match. - Future _hasConsistentVersion( + /// Checks whether or not [package]'s CHANGELOG's versioning is correct, + /// both that it matches [pubspec] and that NEXT is used correctly, printing + /// the results of its checks. + /// + /// Returns false if the CHANGELOG fails validation. + Future _validateChangelogVersion( Directory package, { required Pubspec pubspec, + required bool pubspecVersionChanged, }) async { // This method isn't called unless `version` is non-null. final Version fromPubspec = pubspec.version!; @@ -296,10 +331,19 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} // Remove all leading mark down syntax from the version line. String? versionString = firstLineWithText?.split(' ').last; + final String badNextErrorMessage = '${indentation}When bumping the version ' + 'for release, the NEXT section should be incorporated into the new ' + 'version\'s release notes.'; + // Skip validation for the special NEXT version that's used to accumulate // changes that don't warrant publishing on their own. final bool hasNextSection = versionString == 'NEXT'; if (hasNextSection) { + // NEXT should not be present in a commit that changes the version. + if (pubspecVersionChanged) { + printError(badNextErrorMessage); + return false; + } print( '${indentation}Found NEXT; validating next version in the CHANGELOG.'); // Ensure that the version in pubspec hasn't changed without updating @@ -334,9 +378,7 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. if (!hasNextSection) { final RegExp nextRegex = RegExp(r'^#+\s*NEXT\s*$'); if (lines.any((String line) => nextRegex.hasMatch(line))) { - printError('${indentation}When bumping the version for release, the ' - 'NEXT section should be incorporated into the new version\'s ' - 'release notes.'); + printError(badNextErrorMessage); return false; } } diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 587de1a58cd9..7765073feb08 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -373,6 +373,10 @@ void main() { * Some other changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + final List output = await runCapturingPrint( runner, ['version-check', '--base-sha=master']); await expectLater( @@ -384,8 +388,7 @@ void main() { ); }); - test('Fail if NEXT is left in the CHANGELOG when adding a version bump', - () async { + test('Fail if NEXT appears after a version', () async { const String version = '1.0.1'; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, version: version); @@ -419,6 +422,45 @@ void main() { ); }); + test('Fail if NEXT is left in the CHANGELOG when adding a version bump', + () async { + const String version = '1.0.1'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: version); + + const String changelog = ''' +## NEXT +* Some changes that should have been folded in 1.0.1. +## $version +* Some changes. +## 1.0.0 +* Some other changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + + bool hasError = false; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--against-pub' + ], errorHandler: (Error e) { + expect(e, isA()); + hasError = true; + }); + expect(hasError, isTrue); + + expect( + output, + containsAllInOrder([ + contains('When bumping the version for release, the NEXT section ' + 'should be incorporated into the new version\'s release notes.') + ]), + ); + }); + test('Fail if the version changes without replacing NEXT', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, version: '1.0.1'); @@ -430,6 +472,10 @@ void main() { * Some other changes. '''; createFakeCHANGELOG(pluginDirectory, changelog); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + bool hasError = false; final List output = await runCapturingPrint(runner, [ 'version-check', @@ -444,8 +490,8 @@ void main() { expect( output, containsAllInOrder([ - contains('Found NEXT; validating next version in the CHANGELOG.'), - contains('Versions in CHANGELOG.md and pubspec.yaml do not match.'), + contains('When bumping the version for release, the NEXT section ' + 'should be incorporated into the new version\'s release notes.') ]), ); }); From d58036f45d825ffbf311ab48a53d993d40468d5a Mon Sep 17 00:00:00 2001 From: Monika Manuela Hengki Date: Wed, 18 Aug 2021 00:20:08 +0800 Subject: [PATCH 24/57] [quick_actions] Android support only calling initialize once (#4204) Fixes flutter/flutter#87259 --- .../quick_actions/quick_actions/CHANGELOG.md | 4 + .../quickactions/MethodCallHandlerImpl.java | 3 +- .../quickactions/QuickActionsPlugin.java | 21 +++- .../quickactions/QuickActionsTest.java | 111 ++++++++++++++++++ .../quick_actions/lib/quick_actions.dart | 2 +- .../quick_actions/quick_actions/pubspec.yaml | 2 +- .../quick_actions_platform.dart | 2 +- 7 files changed, 139 insertions(+), 6 deletions(-) diff --git a/packages/quick_actions/quick_actions/CHANGELOG.md b/packages/quick_actions/quick_actions/CHANGELOG.md index 4f8943845cf7..5d040f4fd74e 100644 --- a/packages/quick_actions/quick_actions/CHANGELOG.md +++ b/packages/quick_actions/quick_actions/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.0+5 + +* Support only calling initialize once. + ## 0.6.0+4 * Remove references to the Android V1 embedding. diff --git a/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java b/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java index 465283053442..2d89352f3e09 100644 --- a/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java +++ b/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java @@ -20,9 +20,8 @@ import java.util.Map; class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { - + protected static final String EXTRA_ACTION = "some unique action key"; private static final String CHANNEL_ID = "plugins.flutter.io/quick_actions"; - private static final String EXTRA_ACTION = "some unique action key"; private final Context context; private Activity activity; diff --git a/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java b/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java index ab3431325503..b2f80ad0a271 100644 --- a/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java +++ b/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java @@ -6,14 +6,17 @@ import android.app.Activity; import android.content.Context; +import android.content.Intent; +import android.os.Build; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.PluginRegistry.NewIntentListener; /** QuickActionsPlugin */ -public class QuickActionsPlugin implements FlutterPlugin, ActivityAware { +public class QuickActionsPlugin implements FlutterPlugin, ActivityAware, NewIntentListener { private static final String CHANNEL_ID = "plugins.flutter.io/quick_actions"; private MethodChannel channel; @@ -43,6 +46,8 @@ public void onDetachedFromEngine(FlutterPluginBinding binding) { @Override public void onAttachedToActivity(ActivityPluginBinding binding) { handler.setActivity(binding.getActivity()); + binding.addOnNewIntentListener(this); + onNewIntent(binding.getActivity().getIntent()); } @Override @@ -52,6 +57,7 @@ public void onDetachedFromActivity() { @Override public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { + binding.removeOnNewIntentListener(this); onAttachedToActivity(binding); } @@ -60,6 +66,19 @@ public void onDetachedFromActivityForConfigChanges() { onDetachedFromActivity(); } + @Override + public boolean onNewIntent(Intent intent) { + // Do nothing for anything lower than API 25 as the functionality isn't supported. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { + return false; + } + // Notify the Dart side if the launch intent has the intent extra relevant to quick actions. + if (intent.hasExtra(MethodCallHandlerImpl.EXTRA_ACTION) && channel != null) { + channel.invokeMethod("launch", intent.getStringExtra(MethodCallHandlerImpl.EXTRA_ACTION)); + } + return false; + } + private void setupChannel(BinaryMessenger messenger, Context context, Activity activity) { channel = new MethodChannel(messenger, CHANNEL_ID); handler = new MethodCallHandlerImpl(context, activity); diff --git a/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java b/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java index d437444a53be..208a119efafe 100644 --- a/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java +++ b/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java @@ -4,17 +4,30 @@ package io.flutter.plugins.quickactions; +import static io.flutter.plugins.quickactions.MethodCallHandlerImpl.EXTRA_ACTION; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import android.app.Activity; +import android.content.Intent; +import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.StandardMethodCodec; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.nio.ByteBuffer; +import org.junit.After; import org.junit.Test; +import org.mockito.internal.util.reflection.FieldSetter; public class QuickActionsTest { private static class TestBinaryMessenger implements BinaryMessenger { @@ -42,6 +55,10 @@ public void setMessageHandler(@NonNull String channel, @Nullable BinaryMessageHa } } + static final int SUPPORTED_BUILD = 25; + static final int UNSUPPORTED_BUILD = 24; + static final String SHORTCUT_TYPE = "action_one"; + @Test public void canAttachToEngine() { final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); @@ -51,4 +68,98 @@ public void canAttachToEngine() { final QuickActionsPlugin plugin = new QuickActionsPlugin(); plugin.onAttachedToEngine(mockPluginBinding); } + + @Test + public void onAttachedToActivity_buildVersionSupported_invokesLaunchMethod() + throws NoSuchFieldException, IllegalAccessException { + // Arrange + final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); + final QuickActionsPlugin plugin = new QuickActionsPlugin(); + setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin); + setBuildVersion(SUPPORTED_BUILD); + FieldSetter.setField( + plugin, + QuickActionsPlugin.class.getDeclaredField("handler"), + mock(MethodCallHandlerImpl.class)); + final Intent mockIntent = createMockIntentWithQuickActionExtra(); + final Activity mockMainActivity = mock(Activity.class); + when(mockMainActivity.getIntent()).thenReturn(mockIntent); + final ActivityPluginBinding mockActivityPluginBinding = mock(ActivityPluginBinding.class); + when(mockActivityPluginBinding.getActivity()).thenReturn(mockMainActivity); + + // Act + plugin.onAttachedToActivity(mockActivityPluginBinding); + + // Assert + assertNotNull(testBinaryMessenger.lastMethodCall); + assertEquals(testBinaryMessenger.lastMethodCall.method, "launch"); + assertEquals(testBinaryMessenger.lastMethodCall.arguments, SHORTCUT_TYPE); + } + + @Test + public void onNewIntent_buildVersionUnsupported_doesNotInvokeMethod() + throws NoSuchFieldException, IllegalAccessException { + // Arrange + final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); + final QuickActionsPlugin plugin = new QuickActionsPlugin(); + setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin); + setBuildVersion(UNSUPPORTED_BUILD); + final Intent mockIntent = createMockIntentWithQuickActionExtra(); + + // Act + final boolean onNewIntentReturn = plugin.onNewIntent(mockIntent); + + // Assert + assertNull(testBinaryMessenger.lastMethodCall); + assertFalse(onNewIntentReturn); + } + + @Test + public void onNewIntent_buildVersionSupported_invokesLaunchMethod() + throws NoSuchFieldException, IllegalAccessException { + // Arrange + final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); + final QuickActionsPlugin plugin = new QuickActionsPlugin(); + setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin); + setBuildVersion(SUPPORTED_BUILD); + final Intent mockIntent = createMockIntentWithQuickActionExtra(); + + // Act + final boolean onNewIntentReturn = plugin.onNewIntent(mockIntent); + + // Assert + assertNotNull(testBinaryMessenger.lastMethodCall); + assertEquals(testBinaryMessenger.lastMethodCall.method, "launch"); + assertEquals(testBinaryMessenger.lastMethodCall.arguments, SHORTCUT_TYPE); + assertFalse(onNewIntentReturn); + } + + private void setUpMessengerAndFlutterPluginBinding( + TestBinaryMessenger testBinaryMessenger, QuickActionsPlugin plugin) { + final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class); + when(mockPluginBinding.getBinaryMessenger()).thenReturn(testBinaryMessenger); + plugin.onAttachedToEngine(mockPluginBinding); + } + + private Intent createMockIntentWithQuickActionExtra() { + final Intent mockIntent = mock(Intent.class); + when(mockIntent.hasExtra(EXTRA_ACTION)).thenReturn(true); + when(mockIntent.getStringExtra(EXTRA_ACTION)).thenReturn(QuickActionsTest.SHORTCUT_TYPE); + return mockIntent; + } + + private void setBuildVersion(int buildVersion) + throws NoSuchFieldException, IllegalAccessException { + Field buildSdkField = Build.VERSION.class.getField("SDK_INT"); + buildSdkField.setAccessible(true); + final Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(buildSdkField, buildSdkField.getModifiers() & ~Modifier.FINAL); + buildSdkField.set(null, buildVersion); + } + + @After + public void tearDown() throws NoSuchFieldException, IllegalAccessException { + setBuildVersion(0); + } } diff --git a/packages/quick_actions/quick_actions/lib/quick_actions.dart b/packages/quick_actions/quick_actions/lib/quick_actions.dart index 6907f25729ab..7d3d4ad1ef3b 100644 --- a/packages/quick_actions/quick_actions/lib/quick_actions.dart +++ b/packages/quick_actions/quick_actions/lib/quick_actions.dart @@ -16,7 +16,7 @@ class QuickActions { /// Initializes this plugin. /// - /// Call this once before any further interaction with the the plugin. + /// Call this once before any further interaction with the plugin. Future initialize(QuickActionHandler handler) async => QuickActionsPlatform.instance.initialize(handler); diff --git a/packages/quick_actions/quick_actions/pubspec.yaml b/packages/quick_actions/quick_actions/pubspec.yaml index 657c2f001a83..e52ab515432f 100644 --- a/packages/quick_actions/quick_actions/pubspec.yaml +++ b/packages/quick_actions/quick_actions/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for creating shortcuts on home screen, also known as Quick Actions on iOS and App Shortcuts on Android. repository: https://github.com/flutter/plugins/tree/master/packages/quick_actions issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+quick_actions%22 -version: 0.6.0+4 +version: 0.6.0+5 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/quick_actions/quick_actions_platform_interface/lib/platform_interface/quick_actions_platform.dart b/packages/quick_actions/quick_actions_platform_interface/lib/platform_interface/quick_actions_platform.dart index b15fb8b43233..2e06935ccb09 100644 --- a/packages/quick_actions/quick_actions_platform_interface/lib/platform_interface/quick_actions_platform.dart +++ b/packages/quick_actions/quick_actions_platform_interface/lib/platform_interface/quick_actions_platform.dart @@ -38,7 +38,7 @@ abstract class QuickActionsPlatform extends PlatformInterface { /// Initializes this plugin. /// - /// Call this once before any further interaction with the the plugin. + /// Call this once before any further interaction with the plugin. Future initialize(QuickActionHandler handler) async { throw UnimplementedError("initialize() has not been implemented."); } From 04ea39acd6d8c2ec0ed9bb022ae225470285060a Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 17 Aug 2021 09:43:31 -0700 Subject: [PATCH 25/57] [flutter_plugin_tools] Add Android native UI test support (#4188) Adds integration test support for Android to `native-test`. Also fixes an issue where the existing unit test support was not honoring `--no-unit`. Fixes https://github.com/flutter/flutter/issues/86490 --- .../java/io/plugins/DartIntegrationTest.java | 14 ++ .../androidalarmmanager/MainActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../MainActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../plugins/battery/FlutterActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../cameraexample/FlutterActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../FlutterActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../MainActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../googlemapsexample/MainActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../FlutterActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../FlutterActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../FlutterActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../FlutterActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../FlutterFragmentActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../packageinfoexample/MainActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../pathprovider}/MainActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../FlutterActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../sensorsexample/FlutterActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../shareexample/FlutterActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../urllauncherexample/MainActivityTest.java | 2 + .../flutter/plugins/DartIntegrationTest.java | 14 ++ .../MainActivityTest.java | 2 + script/tool/CHANGELOG.md | 4 + script/tool/lib/src/native_test_command.dart | 124 +++++++--- script/tool/pubspec.yaml | 2 +- .../tool/test/native_test_command_test.dart | 227 +++++++++++++++++- 42 files changed, 623 insertions(+), 38 deletions(-) create mode 100644 packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/DartIntegrationTest.java create mode 100644 packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/path_provider/path_provider/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java rename packages/path_provider/path_provider/example/android/app/src/androidTest/java/{ => io/flutter/plugins/pathprovider}/MainActivityTest.java (89%) create mode 100644 packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/sensors/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/share/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java create mode 100644 packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java diff --git a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/DartIntegrationTest.java b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/MainActivityTest.java b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/MainActivityTest.java index 0272c14a8328..a5bb72415f14 100644 --- a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/MainActivityTest.java +++ b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/MainActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class MainActivityTest { @Rule diff --git a/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java b/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/androidintentexample/MainActivityTest.java b/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/androidintentexample/MainActivityTest.java index d9ba10729001..358fc78bfcfd 100644 --- a/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/androidintentexample/MainActivityTest.java +++ b/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/androidintentexample/MainActivityTest.java @@ -6,9 +6,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class MainActivityTest { @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); diff --git a/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/FlutterActivityTest.java b/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/FlutterActivityTest.java index 267271f70f42..5068d043bdfc 100644 --- a/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/FlutterActivityTest.java +++ b/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/FlutterActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java b/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java b/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java index 32acc1ba9c15..39cae489d9fa 100644 --- a/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java +++ b/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/FlutterActivityTest.java b/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/FlutterActivityTest.java index 330f0050a1d8..b4a67622f8dc 100644 --- a/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/FlutterActivityTest.java +++ b/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/FlutterActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/MainActivityTest.java b/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/MainActivityTest.java index 66a606ca00a9..25999995691d 100644 --- a/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/MainActivityTest.java +++ b/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/MainActivityTest.java @@ -6,9 +6,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class MainActivityTest { @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/MainActivityTest.java b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/MainActivityTest.java index fccd4c95c3ac..244a22b6c6c8 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/MainActivityTest.java +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/MainActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class MainActivityTest { @Rule diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java b/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java index 36787ffd9910..edc01de491af 100644 --- a/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java +++ b/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java b/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java index 1ca37ce5feb7..91e068fa8043 100644 --- a/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java +++ b/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java index a60599573d57..03e4066de85e 100644 --- a/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java +++ b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java index a60599573d57..03e4066de85e 100644 --- a/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java index e5ece3edd50d..68c22371d7dd 100644 --- a/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java +++ b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterFragmentActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterFragmentActivityTest { @Rule diff --git a/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/MainActivityTest.java b/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/MainActivityTest.java index cf7252ce19de..fb63f6f8c88b 100644 --- a/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/MainActivityTest.java +++ b/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/MainActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class MainActivityTest { @Rule diff --git a/packages/path_provider/path_provider/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/path_provider/path_provider/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/path_provider/path_provider/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/path_provider/path_provider/example/android/app/src/androidTest/java/MainActivityTest.java b/packages/path_provider/path_provider/example/android/app/src/androidTest/java/io/flutter/plugins/pathprovider/MainActivityTest.java similarity index 89% rename from packages/path_provider/path_provider/example/android/app/src/androidTest/java/MainActivityTest.java rename to packages/path_provider/path_provider/example/android/app/src/androidTest/java/io/flutter/plugins/pathprovider/MainActivityTest.java index 0380a4397ae6..d56458bd753c 100644 --- a/packages/path_provider/path_provider/example/android/app/src/androidTest/java/MainActivityTest.java +++ b/packages/path_provider/path_provider/example/android/app/src/androidTest/java/io/flutter/plugins/pathprovider/MainActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class MainActivityTest { @Rule diff --git a/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java b/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java index 0b60dfa53e1f..e96548da291a 100644 --- a/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java +++ b/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/FlutterActivityTest.java b/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/FlutterActivityTest.java index c1584aab107c..52a6b8bebaf3 100644 --- a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/FlutterActivityTest.java +++ b/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/FlutterActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/share/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/FlutterActivityTest.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/FlutterActivityTest.java index 070749dcff20..aba658887d88 100644 --- a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/FlutterActivityTest.java +++ b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/FlutterActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java b/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/MainActivityTest.java b/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/MainActivityTest.java index 9e343b82a193..67f15efb10aa 100644 --- a/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/MainActivityTest.java +++ b/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/MainActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule diff --git a/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java index b18308ab2feb..a32aaebb0ecd 100644 --- a/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java +++ b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java @@ -7,9 +7,11 @@ import androidx.test.rule.ActivityTestRule; import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class MainActivityTest { @Rule diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 584ea571f0e1..267019fe7359 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,9 @@ ## NEXT +- Added Android native integration test support to `native-test`. + +## 0.5.0 + - `--exclude` and `--custom-analysis` now accept paths to YAML files that contain lists of packages to exclude, in addition to just package names, so that exclude lists can be maintained separately from scripts and CI diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 36b12741f2ce..9fc6a2912ccc 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -96,11 +96,6 @@ this command. throw ToolExit(exitInvalidArguments); } - if (getBoolArg(kPlatformAndroid) && getBoolArg(_integrationTestFlag)) { - logWarning('This command currently only supports unit tests for Android. ' - 'See https://github.com/flutter/flutter/issues/86490.'); - } - // iOS-specific run-level state. if (_requestedPlatforms.contains('ios')) { String destination = getStringArg(_iosDestinationFlag); @@ -178,12 +173,8 @@ this command. } Future<_PlatformResult> _testAndroid(Directory plugin, _TestMode mode) async { - final List examplesWithTests = []; - for (final Directory example in getExamplesForPlugin(plugin)) { - if (!isFlutterPackage(example)) { - continue; - } - if (example + bool exampleHasUnitTests(Directory example) { + return example .childDirectory('android') .childDirectory('app') .childDirectory('src') @@ -193,20 +184,62 @@ this command. .childDirectory('android') .childDirectory('src') .childDirectory('test') - .existsSync()) { - examplesWithTests.add(example); - } else { - _printNoExampleTestsMessage(example, 'Android'); - } + .existsSync(); } - if (examplesWithTests.isEmpty) { - return _PlatformResult(RunState.skipped); + bool exampleHasNativeIntegrationTests(Directory example) { + final Directory integrationTestDirectory = example + .childDirectory('android') + .childDirectory('app') + .childDirectory('src') + .childDirectory('androidTest'); + // There are two types of integration tests that can be in the androidTest + // directory: + // - FlutterTestRunner.class tests, which bridge to Dart integration tests + // - Purely native tests + // Only the latter is supported by this command; the former will hang if + // run here because they will wait for a Dart call that will never come. + // + // This repository uses a convention of putting the former in a + // *ActivityTest.java file, so ignore that file when checking for tests. + // Also ignore DartIntegrationTest.java, which defines the annotation used + // below for filtering the former out when running tests. + // + // If those are the only files, then there are no tests to run here. + return integrationTestDirectory.existsSync() && + integrationTestDirectory + .listSync(recursive: true) + .whereType() + .any((File file) { + final String basename = file.basename; + return !basename.endsWith('ActivityTest.java') && + basename != 'DartIntegrationTest.java'; + }); } + final Iterable examples = getExamplesForPlugin(plugin); + + bool ranTests = false; bool failed = false; bool hasMissingBuild = false; - for (final Directory example in examplesWithTests) { + for (final Directory example in examples) { + final bool hasUnitTests = exampleHasUnitTests(example); + final bool hasIntegrationTests = + exampleHasNativeIntegrationTests(example); + + if (mode.unit && !hasUnitTests) { + _printNoExampleTestsMessage(example, 'Android unit'); + } + if (mode.integration && !hasIntegrationTests) { + _printNoExampleTestsMessage(example, 'Android integration'); + } + + final bool runUnitTests = mode.unit && hasUnitTests; + final bool runIntegrationTests = mode.integration && hasIntegrationTests; + if (!runUnitTests && !runIntegrationTests) { + continue; + } + final String exampleName = getPackageDescription(example); _printRunningExampleTestsMessage(example, 'Android'); @@ -221,17 +254,52 @@ this command. continue; } - final int exitCode = await processRunner.runAndStream( - gradleFile.path, ['testDebugUnitTest'], - workingDir: androidDirectory); - if (exitCode != 0) { - printError('$exampleName tests failed.'); - failed = true; + if (runUnitTests) { + print('Running unit tests...'); + final int exitCode = await processRunner.runAndStream( + gradleFile.path, ['testDebugUnitTest'], + workingDir: androidDirectory); + if (exitCode != 0) { + printError('$exampleName unit tests failed.'); + failed = true; + } + ranTests = true; } + + if (runIntegrationTests) { + // FlutterTestRunner-based tests will hang forever if run in a normal + // app build, since they wait for a Dart call from integration_test that + // will never come. Those tests have an extra annotation to allow + // filtering them out. + const String filter = + 'notAnnotation=io.flutter.plugins.DartIntegrationTest'; + + print('Running integration tests...'); + final int exitCode = await processRunner.runAndStream( + gradleFile.path, + [ + 'app:connectedAndroidTest', + '-Pandroid.testInstrumentationRunnerArguments.$filter', + ], + workingDir: androidDirectory); + if (exitCode != 0) { + printError('$exampleName integration tests failed.'); + failed = true; + } + ranTests = true; + } + } + + if (failed) { + return _PlatformResult(RunState.failed, + error: hasMissingBuild + ? 'Examples must be built before testing.' + : null); + } + if (!ranTests) { + return _PlatformResult(RunState.skipped); } - return _PlatformResult(failed ? RunState.failed : RunState.succeeded, - error: - hasMissingBuild ? 'Examples must be built before testing.' : null); + return _PlatformResult(RunState.succeeded); } Future<_PlatformResult> _testIos(Directory plugin, _TestMode mode) { diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 7b2cdd4f4101..02b3ca624b96 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.4.1 +version: 0.5.0 dependencies: args: ^2.1.0 diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index e656e2f23721..59ca17b25c0b 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -16,6 +16,10 @@ import 'package:test/test.dart'; import 'mocks.dart'; import 'util.dart'; +const String _androidIntegrationTestFilter = + '-Pandroid.testInstrumentationRunnerArguments.' + 'notAnnotation=io.flutter.plugins.DartIntegrationTest'; + final Map _kDeviceListMap = { 'runtimes': >[ { @@ -353,7 +357,7 @@ void main() { }); group('Android', () { - test('runs Java tests in Android implementation folder', () async { + test('runs Java unit tests in Android implementation folder', () async { final Directory plugin = createFakePlugin( 'plugin', packagesDir, @@ -383,7 +387,7 @@ void main() { ); }); - test('runs Java tests in example folder', () async { + test('runs Java unit tests in example folder', () async { final Directory plugin = createFakePlugin( 'plugin', packagesDir, @@ -413,6 +417,172 @@ void main() { ); }); + test('runs Java integration tests', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + await runCapturingPrint(runner, ['native-test', '--android']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const [ + 'app:connectedAndroidTest', + _androidIntegrationTestFilter, + ], + androidFolder.path, + ), + ]), + ); + }); + + test( + 'ignores Java integration test files associated with integration_test', + () async { + createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java', + 'example/android/app/src/androidTest/java/io/flutter/plugins/plugin/FlutterActivityTest.java', + 'example/android/app/src/androidTest/java/io/flutter/plugins/plugin/MainActivityTest.java', + ], + ); + + await runCapturingPrint(runner, ['native-test', '--android']); + + // Nothing should run since those files are all + // integration_test-specific. + expect( + processRunner.recordedCalls, + orderedEquals([]), + ); + }); + + test('runs all tests when present', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'android/src/test/example_test.java', + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + await runCapturingPrint(runner, ['native-test', '--android']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest'], + androidFolder.path, + ), + ProcessCall( + androidFolder.childFile('gradlew').path, + const [ + 'app:connectedAndroidTest', + _androidIntegrationTestFilter, + ], + androidFolder.path, + ), + ]), + ); + }); + + test('honors --no-unit', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'android/src/test/example_test.java', + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + await runCapturingPrint( + runner, ['native-test', '--android', '--no-unit']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const [ + 'app:connectedAndroidTest', + _androidIntegrationTestFilter, + ], + androidFolder.path, + ), + ]), + ); + }); + + test('honors --no-integration', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'android/src/test/example_test.java', + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + await runCapturingPrint( + runner, ['native-test', '--android', '--no-integration']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest'], + androidFolder.path, + ), + ]), + ); + }); + test('fails when the app needs to be built', () async { createFakePlugin( 'plugin', @@ -444,6 +614,46 @@ void main() { ); }); + test('logs missing test types', () async { + // No unit tests. + createFakePlugin( + 'plugin1', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + // No integration tests. + createFakePlugin( + 'plugin2', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'android/src/test/example_test.java', + 'example/android/gradlew', + ], + ); + + final List output = await runCapturingPrint( + runner, ['native-test', '--android']); + + expect( + output, + containsAllInOrder([ + contains('No Android unit tests found for plugin1/example'), + contains('Running integration tests...'), + contains( + 'No Android integration tests found for plugin2/example'), + contains('Running unit tests...'), + ])); + }); + test('fails when a test fails', () async { final Directory pluginDir = createFakePlugin( 'plugin', @@ -478,7 +688,7 @@ void main() { expect( output, containsAllInOrder([ - contains('plugin/example tests failed.'), + contains('plugin/example unit tests failed.'), contains('The following packages had errors:'), contains('plugin') ]), @@ -518,7 +728,8 @@ void main() { expect( output, containsAllInOrder([ - contains('No Android tests found for plugin/example'), + contains('No Android unit tests found for plugin/example'), + contains('No Android integration tests found for plugin/example'), contains('SKIPPING: No tests found.'), ]), ); @@ -810,10 +1021,8 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest'], - androidFolder.path), + ProcessCall(androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest'], androidFolder.path), ProcessCall( 'xcrun', const [ @@ -1003,7 +1212,7 @@ void main() { output, containsAllInOrder([ contains('Running tests for Android...'), - contains('plugin/example tests failed.'), + contains('plugin/example unit tests failed.'), contains('Running tests for iOS...'), contains('Successfully ran iOS xctest for plugin/example'), contains('The following packages had errors:'), From 90fd90ed62571fb765df8c49000b14e3c8c643ec Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 17 Aug 2021 12:09:03 -0700 Subject: [PATCH 26/57] [url_launcher] Add native unit tests for Windows (#4156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a unit test target based on googletest. This is intended to be both a set of unit tests for this plugin, and also a model of changes that can be made to the `flutter create` template for Windows plugins to include better testing out of the box (https://github.com/flutter/flutter/issues/82458). In addition to the test binary being directly runnable, the integration between CMake, VS, and googletest means that these tests are visible—and runnable—in the VS Test Explorer UI after opening the generated .sln file. Changes for testing in general: - Moved the plugin class declaration to a header. - Moved the C registration API implementation to a separate file. - Added (opt-in, so it won't affect plugin client builds) plugin CMake rules to download googletest and build a new executable target that builds all the plugin sources, plus gtest and gmock. - Added a line to the example app CMake rules to enable the unit tests. - Added a unit test file. url_launcher-specific changes: - Wrapped all Win32 calls in a thin class for mockability in unit tests. - Factored some logic into helpers for better maintainability while I was refactoring anyway. Note: This unit test is not yet being run by CI. A tools command to run Windows plugin unit tests will be a separate PR. Part of https://github.com/flutter/flutter/issues/82445 --- .../url_launcher_windows/CHANGELOG.md | 4 + .../example/windows/CMakeLists.txt | 3 + .../example/windows/flutter/CMakeLists.txt | 1 + .../flutter/generated_plugin_registrant.cc | 6 +- .../url_launcher_windows/pubspec.yaml | 2 +- .../windows/CMakeLists.txt | 55 +++++- ...uncher_plugin.h => url_launcher_windows.h} | 2 +- .../windows/system_apis.cpp | 38 ++++ .../windows/system_apis.h | 56 ++++++ .../test/url_launcher_windows_test.cpp | 162 ++++++++++++++++++ .../windows/url_launcher_plugin.cpp | 97 ++++++----- .../windows/url_launcher_plugin.h | 48 ++++++ .../windows/url_launcher_windows.cpp | 15 ++ 13 files changed, 436 insertions(+), 53 deletions(-) rename packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/{url_launcher_plugin.h => url_launcher_windows.h} (92%) create mode 100644 packages/url_launcher/url_launcher_windows/windows/system_apis.cpp create mode 100644 packages/url_launcher/url_launcher_windows/windows/system_apis.h create mode 100644 packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp create mode 100644 packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h create mode 100644 packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp diff --git a/packages/url_launcher/url_launcher_windows/CHANGELOG.md b/packages/url_launcher/url_launcher_windows/CHANGELOG.md index d26fe19c359e..d095a52341b5 100644 --- a/packages/url_launcher/url_launcher_windows/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Added unit tests. + ## 2.0.2 * Replaced reference to `shared_preferences` plugin with the `url_launcher` in the README. diff --git a/packages/url_launcher/url_launcher_windows/example/windows/CMakeLists.txt b/packages/url_launcher/url_launcher_windows/example/windows/CMakeLists.txt index abf90408efb4..5b1622bcb333 100644 --- a/packages/url_launcher/url_launcher_windows/example/windows/CMakeLists.txt +++ b/packages/url_launcher/url_launcher_windows/example/windows/CMakeLists.txt @@ -46,6 +46,9 @@ add_subdirectory(${FLUTTER_MANAGED_DIR}) # Application build add_subdirectory("runner") +# Enable the test target. +set(include_url_launcher_windows_tests TRUE) + # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) diff --git a/packages/url_launcher/url_launcher_windows/example/windows/flutter/CMakeLists.txt b/packages/url_launcher/url_launcher_windows/example/windows/flutter/CMakeLists.txt index c7a8c7607d81..744f08a9389b 100644 --- a/packages/url_launcher/url_launcher_windows/example/windows/flutter/CMakeLists.txt +++ b/packages/url_launcher/url_launcher_windows/example/windows/flutter/CMakeLists.txt @@ -91,6 +91,7 @@ add_custom_command( ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" windows-x64 $ + VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" diff --git a/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.cc b/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.cc index d9fdd53925c5..4f7884874da7 100644 --- a/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.cc +++ b/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,9 @@ #include "generated_plugin_registrant.h" -#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { - UrlLauncherPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UrlLauncherPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/packages/url_launcher/url_launcher_windows/pubspec.yaml b/packages/url_launcher/url_launcher_windows/pubspec.yaml index 6435eda4564a..a92e91ee4568 100644 --- a/packages/url_launcher/url_launcher_windows/pubspec.yaml +++ b/packages/url_launcher/url_launcher_windows/pubspec.yaml @@ -13,7 +13,7 @@ flutter: implements: url_launcher platforms: windows: - pluginClass: UrlLauncherPlugin + pluginClass: UrlLauncherWindows dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt b/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt index 57d87e3f6f85..a4185acff6a1 100644 --- a/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt +++ b/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt @@ -4,12 +4,20 @@ project(${PROJECT_NAME} LANGUAGES CXX) set(PLUGIN_NAME "${PROJECT_NAME}_plugin") -add_library(${PLUGIN_NAME} SHARED +list(APPEND PLUGIN_SOURCES + "system_apis.cpp" + "system_apis.h" "url_launcher_plugin.cpp" + "url_launcher_plugin.h" +) + +add_library(${PLUGIN_NAME} SHARED + "include/url_launcher_windows/url_launcher_windows.h" + "url_launcher_windows.cpp" + ${PLUGIN_SOURCES} ) apply_standard_settings(${PLUGIN_NAME}) -set_target_properties(${PLUGIN_NAME} PROPERTIES - CXX_VISIBILITY_PRESET hidden) +set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden) target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) target_include_directories(${PLUGIN_NAME} INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") @@ -20,3 +28,44 @@ set(file_chooser_bundled_libraries "" PARENT_SCOPE ) + + +# === Tests === + +if (${include_${PROJECT_NAME}_tests}) +set(TEST_RUNNER "${PROJECT_NAME}_test") +enable_testing() +# TODO(stuartmorgan): Consider using a single shared, pre-checked-in googletest +# instance rather than downloading for each plugin. This approach makes sense +# for a template, but not for a monorepo with many plugins. +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/release-1.11.0.zip +) +# Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +# Disable install commands for gtest so it doesn't end up in the bundle. +set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE) + +FetchContent_MakeAvailable(googletest) + +# The plugin's C API is not very useful for unit testing, so build the sources +# directly into the test binary rather than using the DLL. +add_executable(${TEST_RUNNER} + test/url_launcher_windows_test.cpp + ${PLUGIN_SOURCES} +) +apply_standard_settings(${TEST_RUNNER}) +target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin) +target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock) +# flutter_wrapper_plugin has link dependencies on the Flutter DLL. +add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${FLUTTER_LIBRARY}" $ +) + +include(GoogleTest) +gtest_discover_tests(${TEST_RUNNER}) +endif() diff --git a/packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/url_launcher_plugin.h b/packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/url_launcher_windows.h similarity index 92% rename from packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/url_launcher_plugin.h rename to packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/url_launcher_windows.h index 8af3924ded81..251471c9fe56 100644 --- a/packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/url_launcher_plugin.h +++ b/packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/url_launcher_windows.h @@ -16,7 +16,7 @@ extern "C" { #endif -FLUTTER_PLUGIN_EXPORT void UrlLauncherPluginRegisterWithRegistrar( +FLUTTER_PLUGIN_EXPORT void UrlLauncherWindowsRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar); #if defined(__cplusplus) diff --git a/packages/url_launcher/url_launcher_windows/windows/system_apis.cpp b/packages/url_launcher/url_launcher_windows/windows/system_apis.cpp new file mode 100644 index 000000000000..abd690b6e47f --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/system_apis.cpp @@ -0,0 +1,38 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#include "system_apis.h" + +#include + +namespace url_launcher_plugin { + +SystemApis::SystemApis() {} + +SystemApis::~SystemApis() {} + +SystemApisImpl::SystemApisImpl() {} + +SystemApisImpl::~SystemApisImpl() {} + +LSTATUS SystemApisImpl::RegCloseKey(HKEY key) { return ::RegCloseKey(key); } + +LSTATUS SystemApisImpl::RegOpenKeyExW(HKEY key, LPCWSTR sub_key, DWORD options, + REGSAM desired, PHKEY result) { + return ::RegOpenKeyExW(key, sub_key, options, desired, result); +} + +LSTATUS SystemApisImpl::RegQueryValueExW(HKEY key, LPCWSTR value_name, + LPDWORD type, LPBYTE data, + LPDWORD data_size) { + return ::RegQueryValueExW(key, value_name, nullptr, type, data, data_size); +} + +HINSTANCE SystemApisImpl::ShellExecuteW(HWND hwnd, LPCWSTR operation, + LPCWSTR file, LPCWSTR parameters, + LPCWSTR directory, int show_flags) { + return ::ShellExecuteW(hwnd, operation, file, parameters, directory, + show_flags); +} + +} // namespace url_launcher_plugin diff --git a/packages/url_launcher/url_launcher_windows/windows/system_apis.h b/packages/url_launcher/url_launcher_windows/windows/system_apis.h new file mode 100644 index 000000000000..7b56704d8e04 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/system_apis.h @@ -0,0 +1,56 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#include + +namespace url_launcher_plugin { + +// An interface wrapping system APIs used by the plugin, for mocking. +class SystemApis { + public: + SystemApis(); + virtual ~SystemApis(); + + // Disallow copy and move. + SystemApis(const SystemApis&) = delete; + SystemApis& operator=(const SystemApis&) = delete; + + // Wrapper for RegCloseKey. + virtual LSTATUS RegCloseKey(HKEY key) = 0; + + // Wrapper for RegQueryValueEx. + virtual LSTATUS RegQueryValueExW(HKEY key, LPCWSTR value_name, LPDWORD type, + LPBYTE data, LPDWORD data_size) = 0; + + // Wrapper for RegOpenKeyEx. + virtual LSTATUS RegOpenKeyExW(HKEY key, LPCWSTR sub_key, DWORD options, + REGSAM desired, PHKEY result) = 0; + + // Wrapper for ShellExecute. + virtual HINSTANCE ShellExecuteW(HWND hwnd, LPCWSTR operation, LPCWSTR file, + LPCWSTR parameters, LPCWSTR directory, + int show_flags) = 0; +}; + +// Implementation of SystemApis using the Win32 APIs. +class SystemApisImpl : public SystemApis { + public: + SystemApisImpl(); + virtual ~SystemApisImpl(); + + // Disallow copy and move. + SystemApisImpl(const SystemApisImpl&) = delete; + SystemApisImpl& operator=(const SystemApisImpl&) = delete; + + // SystemApis Implementation: + virtual LSTATUS RegCloseKey(HKEY key); + virtual LSTATUS RegOpenKeyExW(HKEY key, LPCWSTR sub_key, DWORD options, + REGSAM desired, PHKEY result); + virtual LSTATUS RegQueryValueExW(HKEY key, LPCWSTR value_name, LPDWORD type, + LPBYTE data, LPDWORD data_size); + virtual HINSTANCE ShellExecuteW(HWND hwnd, LPCWSTR operation, LPCWSTR file, + LPCWSTR parameters, LPCWSTR directory, + int show_flags); +}; + +} // namespace url_launcher_plugin diff --git a/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp b/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp new file mode 100644 index 000000000000..191d51a0caa8 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp @@ -0,0 +1,162 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "url_launcher_plugin.h" + +namespace url_launcher_plugin { +namespace test { + +namespace { + +using flutter::EncodableMap; +using flutter::EncodableValue; +using ::testing::DoAll; +using ::testing::Pointee; +using ::testing::Return; +using ::testing::SetArgPointee; + +class MockSystemApis : public SystemApis { + public: + MOCK_METHOD(LSTATUS, RegCloseKey, (HKEY key), (override)); + MOCK_METHOD(LSTATUS, RegQueryValueExW, + (HKEY key, LPCWSTR value_name, LPDWORD type, LPBYTE data, + LPDWORD data_size), + (override)); + MOCK_METHOD(LSTATUS, RegOpenKeyExW, + (HKEY key, LPCWSTR sub_key, DWORD options, REGSAM desired, + PHKEY result), + (override)); + MOCK_METHOD(HINSTANCE, ShellExecuteW, + (HWND hwnd, LPCWSTR operation, LPCWSTR file, LPCWSTR parameters, + LPCWSTR directory, int show_flags), + (override)); +}; + +class MockMethodResult : public flutter::MethodResult<> { + public: + MOCK_METHOD(void, SuccessInternal, (const EncodableValue* result), + (override)); + MOCK_METHOD(void, ErrorInternal, + (const std::string& error_code, const std::string& error_message, + const EncodableValue* details), + (override)); + MOCK_METHOD(void, NotImplementedInternal, (), (override)); +}; + +std::unique_ptr CreateArgumentsWithUrl(const std::string& url) { + EncodableMap args = { + {EncodableValue("url"), EncodableValue(url)}, + }; + return std::make_unique(args); +} + +} // namespace + +TEST(UrlLauncherPlugin, CanLaunchSuccessTrue) { + std::unique_ptr system = std::make_unique(); + std::unique_ptr result = + std::make_unique(); + + // Return success values from the registery commands. + HKEY fake_key = reinterpret_cast(1); + EXPECT_CALL(*system, RegOpenKeyExW) + .WillOnce(DoAll(SetArgPointee<4>(fake_key), Return(ERROR_SUCCESS))); + EXPECT_CALL(*system, RegQueryValueExW).WillOnce(Return(ERROR_SUCCESS)); + EXPECT_CALL(*system, RegCloseKey(fake_key)).WillOnce(Return(ERROR_SUCCESS)); + // Expect a success response. + EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(true)))); + + UrlLauncherPlugin plugin(std::move(system)); + plugin.HandleMethodCall( + flutter::MethodCall("canLaunch", + CreateArgumentsWithUrl("https://some.url.com")), + std::move(result)); +} + +TEST(UrlLauncherPlugin, CanLaunchQueryFailure) { + std::unique_ptr system = std::make_unique(); + std::unique_ptr result = + std::make_unique(); + + // Return success values from the registery commands, except for the query, + // to simulate a scheme that is in the registry, but has no URL handler. + HKEY fake_key = reinterpret_cast(1); + EXPECT_CALL(*system, RegOpenKeyExW) + .WillOnce(DoAll(SetArgPointee<4>(fake_key), Return(ERROR_SUCCESS))); + EXPECT_CALL(*system, RegQueryValueExW).WillOnce(Return(ERROR_FILE_NOT_FOUND)); + EXPECT_CALL(*system, RegCloseKey(fake_key)).WillOnce(Return(ERROR_SUCCESS)); + // Expect a success response. + EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(false)))); + + UrlLauncherPlugin plugin(std::move(system)); + plugin.HandleMethodCall( + flutter::MethodCall("canLaunch", + CreateArgumentsWithUrl("https://some.url.com")), + std::move(result)); +} + +TEST(UrlLauncherPlugin, CanLaunchHandlesOpenFailure) { + std::unique_ptr system = std::make_unique(); + std::unique_ptr result = + std::make_unique(); + + // Return failure for opening. + EXPECT_CALL(*system, RegOpenKeyExW).WillOnce(Return(ERROR_BAD_PATHNAME)); + // Expect a success response. + EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(false)))); + + UrlLauncherPlugin plugin(std::move(system)); + plugin.HandleMethodCall( + flutter::MethodCall("canLaunch", + CreateArgumentsWithUrl("https://some.url.com")), + std::move(result)); +} + +TEST(UrlLauncherPlugin, LaunchSuccess) { + std::unique_ptr system = std::make_unique(); + std::unique_ptr result = + std::make_unique(); + + // Return a success value (>32) from launching. + EXPECT_CALL(*system, ShellExecuteW) + .WillOnce(Return(reinterpret_cast(33))); + // Expect a success response. + EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(true)))); + + UrlLauncherPlugin plugin(std::move(system)); + plugin.HandleMethodCall( + flutter::MethodCall("launch", + CreateArgumentsWithUrl("https://some.url.com")), + std::move(result)); +} + +TEST(UrlLauncherPlugin, LaunchReportsFailure) { + std::unique_ptr system = std::make_unique(); + std::unique_ptr result = + std::make_unique(); + + // Return a faile value (<=32) from launching. + EXPECT_CALL(*system, ShellExecuteW) + .WillOnce(Return(reinterpret_cast(32))); + // Expect an error response. + EXPECT_CALL(*result, ErrorInternal); + + UrlLauncherPlugin plugin(std::move(system)); + plugin.HandleMethodCall( + flutter::MethodCall("launch", + CreateArgumentsWithUrl("https://some.url.com")), + std::move(result)); +} + +} // namespace test +} // namespace url_launcher_plugin diff --git a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp index 51740a3a4b04..748c75ddd243 100644 --- a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp +++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "include/url_launcher_windows/url_launcher_plugin.h" +#include "url_launcher_plugin.h" #include #include @@ -9,9 +9,12 @@ #include #include +#include #include #include +namespace url_launcher_plugin { + namespace { using flutter::EncodableMap; @@ -54,19 +57,7 @@ std::string GetUrlArgument(const flutter::MethodCall<>& method_call) { return url; } -class UrlLauncherPlugin : public flutter::Plugin { - public: - static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar); - - virtual ~UrlLauncherPlugin(); - - private: - UrlLauncherPlugin(); - - // Called when a method is called on plugin channel; - void HandleMethodCall(const flutter::MethodCall<>& method_call, - std::unique_ptr> result); -}; +} // namespace // static void UrlLauncherPlugin::RegisterWithRegistrar( @@ -75,8 +66,8 @@ void UrlLauncherPlugin::RegisterWithRegistrar( registrar->messenger(), "plugins.flutter.io/url_launcher", &flutter::StandardMethodCodec::GetInstance()); - // Uses new instead of make_unique due to private constructor. - std::unique_ptr plugin(new UrlLauncherPlugin()); + std::unique_ptr plugin = + std::make_unique(); channel->SetMethodCallHandler( [plugin_pointer = plugin.get()](const auto& call, auto result) { @@ -86,7 +77,11 @@ void UrlLauncherPlugin::RegisterWithRegistrar( registrar->AddPlugin(std::move(plugin)); } -UrlLauncherPlugin::UrlLauncherPlugin() = default; +UrlLauncherPlugin::UrlLauncherPlugin() + : system_apis_(std::make_unique()) {} + +UrlLauncherPlugin::UrlLauncherPlugin(std::unique_ptr system_apis) + : system_apis_(std::move(system_apis)) {} UrlLauncherPlugin::~UrlLauncherPlugin() = default; @@ -99,17 +94,10 @@ void UrlLauncherPlugin::HandleMethodCall( result->Error("argument_error", "No URL provided"); return; } - std::wstring url_wide = Utf16FromUtf8(url); - - int status = static_cast(reinterpret_cast( - ::ShellExecute(nullptr, TEXT("open"), url_wide.c_str(), nullptr, - nullptr, SW_SHOWNORMAL))); - if (status <= 32) { - std::ostringstream error_message; - error_message << "Failed to open " << url << ": ShellExecute error code " - << status; - result->Error("open_error", error_message.str()); + std::optional error = LaunchUrl(url); + if (error) { + result->Error("open_error", error.value()); return; } result->Success(EncodableValue(true)); @@ -120,29 +108,48 @@ void UrlLauncherPlugin::HandleMethodCall( return; } - bool can_launch = false; - size_t separator_location = url.find(":"); - if (separator_location != std::string::npos) { - std::wstring scheme = Utf16FromUtf8(url.substr(0, separator_location)); - HKEY key = nullptr; - if (::RegOpenKeyEx(HKEY_CLASSES_ROOT, scheme.c_str(), 0, KEY_QUERY_VALUE, - &key) == ERROR_SUCCESS) { - can_launch = ::RegQueryValueEx(key, L"URL Protocol", nullptr, nullptr, - nullptr, nullptr) == ERROR_SUCCESS; - ::RegCloseKey(key); - } - } + bool can_launch = CanLaunchUrl(url); result->Success(EncodableValue(can_launch)); } else { result->NotImplemented(); } } -} // namespace +bool UrlLauncherPlugin::CanLaunchUrl(const std::string& url) { + size_t separator_location = url.find(":"); + if (separator_location == std::string::npos) { + return false; + } + std::wstring scheme = Utf16FromUtf8(url.substr(0, separator_location)); + + HKEY key = nullptr; + if (system_apis_->RegOpenKeyExW(HKEY_CLASSES_ROOT, scheme.c_str(), 0, + KEY_QUERY_VALUE, &key) != ERROR_SUCCESS) { + return false; + } + bool has_handler = + system_apis_->RegQueryValueExW(key, L"URL Protocol", nullptr, nullptr, + nullptr) == ERROR_SUCCESS; + system_apis_->RegCloseKey(key); + return has_handler; +} -void UrlLauncherPluginRegisterWithRegistrar( - FlutterDesktopPluginRegistrarRef registrar) { - UrlLauncherPlugin::RegisterWithRegistrar( - flutter::PluginRegistrarManager::GetInstance() - ->GetRegistrar(registrar)); +std::optional UrlLauncherPlugin::LaunchUrl( + const std::string& url) { + std::wstring url_wide = Utf16FromUtf8(url); + + int status = static_cast(reinterpret_cast( + system_apis_->ShellExecuteW(nullptr, TEXT("open"), url_wide.c_str(), + nullptr, nullptr, SW_SHOWNORMAL))); + + // Per ::ShellExecuteW documentation, anything >32 indicates success. + if (status <= 32) { + std::ostringstream error_message; + error_message << "Failed to open " << url << ": ShellExecute error code " + << status; + return std::optional(error_message.str()); + } + return std::nullopt; } + +} // namespace url_launcher_plugin diff --git a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h new file mode 100644 index 000000000000..45e70e5fc067 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h @@ -0,0 +1,48 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#include +#include +#include + +#include +#include +#include +#include + +#include "system_apis.h" + +namespace url_launcher_plugin { + +class UrlLauncherPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar); + + UrlLauncherPlugin(); + + // Creates a plugin instance with the given SystemApi instance. + // + // Exists for unit testing with mock implementations. + UrlLauncherPlugin(std::unique_ptr system_apis); + + virtual ~UrlLauncherPlugin(); + + // Disallow copy and move. + UrlLauncherPlugin(const UrlLauncherPlugin&) = delete; + UrlLauncherPlugin& operator=(const UrlLauncherPlugin&) = delete; + + // Called when a method is called on the plugin channel. + void HandleMethodCall(const flutter::MethodCall<>& method_call, + std::unique_ptr> result); + + private: + // Returns whether or not the given URL has a registered handler. + bool CanLaunchUrl(const std::string& url); + + // Attempts to launch the given URL. On failure, returns an error string. + std::optional LaunchUrl(const std::string& url); + + std::unique_ptr system_apis_; +}; + +} // namespace url_launcher_plugin diff --git a/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp b/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp new file mode 100644 index 000000000000..05de586d8fe0 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#include "include/url_launcher_windows/url_launcher_windows.h" + +#include + +#include "url_launcher_plugin.h" + +void UrlLauncherWindowsRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + url_launcher_plugin::UrlLauncherPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} From af2896b199ecc3cc5e1ed3000b009f65fc05c9cd Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 18 Aug 2021 06:51:10 -0700 Subject: [PATCH 27/57] [flutter_plugin_tools] Add a command to lint Android code (#4206) Adds a new `lint-android` command to run `gradlew lint` on Android plugins. Also standardizes the names of the Cirrus tasks that run all the build and platform-specific (i.e., not Dart unit test) tests for each platform, as they were getting unnecessarily long and complex in some cases. Fixes https://github.com/flutter/flutter/issues/87071 --- .cirrus.yml | 23 +- packages/android_alarm_manager/CHANGELOG.md | 1 + .../android/build.gradle | 2 + .../android/lint-baseline.xml | 59 +++ packages/android_intent/CHANGELOG.md | 1 + packages/android_intent/android/build.gradle | 1 + packages/battery/battery/CHANGELOG.md | 1 + packages/battery/battery/android/build.gradle | 1 + packages/camera/camera/CHANGELOG.md | 4 + packages/camera/camera/android/build.gradle | 2 + .../camera/camera/android/lint-baseline.xml | 114 +++++ .../connectivity/connectivity/CHANGELOG.md | 1 + .../connectivity/android/build.gradle | 1 + packages/device_info/device_info/CHANGELOG.md | 1 + .../device_info/android/build.gradle | 1 + packages/espresso/CHANGELOG.md | 4 + packages/espresso/android/build.gradle | 2 + packages/espresso/android/lint-baseline.xml | 389 ++++++++++++++++++ .../CHANGELOG.md | 4 + .../android/build.gradle | 1 + .../google_maps_flutter/android/build.gradle | 1 + .../google_sign_in/CHANGELOG.md | 4 + .../google_sign_in/android/build.gradle | 1 + .../image_picker/image_picker/CHANGELOG.md | 4 + .../image_picker/android/build.gradle | 1 + .../in_app_purchase_android/CHANGELOG.md | 8 +- .../android/build.gradle | 1 + packages/local_auth/CHANGELOG.md | 4 + packages/local_auth/android/build.gradle | 2 + packages/local_auth/android/lint-baseline.xml | 59 +++ packages/package_info/CHANGELOG.md | 1 + packages/package_info/android/build.gradle | 1 + .../path_provider/path_provider/CHANGELOG.md | 4 + .../path_provider/android/build.gradle | 1 + .../quick_actions/quick_actions/CHANGELOG.md | 4 + .../quick_actions/android/build.gradle | 1 + packages/sensors/CHANGELOG.md | 1 + packages/sensors/android/build.gradle | 1 + packages/share/CHANGELOG.md | 1 + packages/share/android/build.gradle | 1 + .../shared_preferences/CHANGELOG.md | 1 + .../shared_preferences/android/build.gradle | 2 + .../android/lint-baseline.xml | 81 ++++ .../url_launcher/url_launcher/CHANGELOG.md | 4 + .../url_launcher/android/build.gradle | 1 + .../video_player/video_player/CHANGELOG.md | 4 + .../video_player/android/build.gradle | 1 + .../webview_flutter/CHANGELOG.md | 4 + .../webview_flutter/android/build.gradle | 1 + .../wifi_info_flutter/CHANGELOG.md | 4 + .../wifi_info_flutter/android/build.gradle | 1 + script/tool/CHANGELOG.md | 1 + script/tool/lib/src/common/gradle.dart | 57 +++ script/tool/lib/src/common/xcode.dart | 2 +- .../lib/src/firebase_test_lab_command.dart | 46 +-- script/tool/lib/src/lint_android_command.dart | 61 +++ script/tool/lib/src/main.dart | 2 + script/tool/lib/src/native_test_command.dart | 29 +- script/tool/test/common/gradle_test.dart | 179 ++++++++ .../tool/test/lint_android_command_test.dart | 158 +++++++ 60 files changed, 1306 insertions(+), 47 deletions(-) create mode 100644 packages/android_alarm_manager/android/lint-baseline.xml create mode 100644 packages/camera/camera/android/lint-baseline.xml create mode 100644 packages/espresso/android/lint-baseline.xml create mode 100644 packages/local_auth/android/lint-baseline.xml create mode 100644 packages/shared_preferences/shared_preferences/android/lint-baseline.xml create mode 100644 script/tool/lib/src/common/gradle.dart create mode 100644 script/tool/lib/src/lint_android_command.dart create mode 100644 script/tool/test/common/gradle_test.dart create mode 100644 script/tool/test/lint_android_command_test.dart diff --git a/.cirrus.yml b/.cirrus.yml index ffdd71daebc4..d830a2a15913 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -119,7 +119,7 @@ task: setup_script: - flutter config --enable-linux-desktop << : *BUILD_ALL_PLUGINS_APP_TEMPLATE - - name: build-linux+drive-examples + - name: linux-build+platform-tests env: matrix: CHANNEL: "master" @@ -146,7 +146,7 @@ task: memory: 12G matrix: ### Android tasks ### - - name: build-apks+android-unit+firebase-test-lab + - name: android-build+platform-tests env: matrix: PLUGIN_SHARDING: "--shardIndex 0 --shardCount 4" @@ -165,6 +165,13 @@ task: - export CIRRUS_CHANGE_MESSAGE="" - export CIRRUS_COMMIT_MESSAGE="" - ./script/tool_runner.sh build-examples --apk + lint_script: + # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they + # might include non-ASCII characters which makes Gradle crash. + # TODO(stuartmorgan): See https://github.com/flutter/flutter/issues/24935 + - export CIRRUS_CHANGE_MESSAGE="" + - export CIRRUS_COMMIT_MESSAGE="" + - ./script/tool_runner.sh lint-android # must come after build-examples native_unit_test_script: # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they # might include non-ASCII characters which makes Gradle crash. @@ -186,8 +193,14 @@ task: - else - echo "This user does not have permission to run Firebase Test Lab tests." - fi + # Upload the full lint results to Cirrus to display in the results UI. + always: + android-lint_artifacts: + path: "**/reports/lint-results-debug.xml" + type: text/xml + format: android-lint ### Web tasks ### - - name: build-web+drive-examples + - name: web-build+platform-tests env: matrix: CHANNEL: "master" @@ -220,7 +233,7 @@ task: CHANNEL: "master" CHANNEL: "stable" << : *BUILD_ALL_PLUGINS_APP_TEMPLATE - - name: build-ipas+drive-examples + - name: ios-build+platform-tests env: PATH: $PATH:/usr/local/bin matrix: @@ -256,7 +269,7 @@ task: setup_script: - flutter config --enable-macos-desktop << : *BUILD_ALL_PLUGINS_APP_TEMPLATE - - name: build-macos+drive-examples + - name: macos-build+platform-tests env: matrix: CHANNEL: "master" diff --git a/packages/android_alarm_manager/CHANGELOG.md b/packages/android_alarm_manager/CHANGELOG.md index 71f47cede66e..d53b932e3f0f 100644 --- a/packages/android_alarm_manager/CHANGELOG.md +++ b/packages/android_alarm_manager/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Remove support for the V1 Android embedding. +* Updated Android lint settings. ## 2.0.2 diff --git a/packages/android_alarm_manager/android/build.gradle b/packages/android_alarm_manager/android/build.gradle index be741097f362..b173137786a9 100644 --- a/packages/android_alarm_manager/android/build.gradle +++ b/packages/android_alarm_manager/android/build.gradle @@ -38,6 +38,8 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + baseline file("lint-baseline.xml") } diff --git a/packages/android_alarm_manager/android/lint-baseline.xml b/packages/android_alarm_manager/android/lint-baseline.xml new file mode 100644 index 000000000000..de588614fdb2 --- /dev/null +++ b/packages/android_alarm_manager/android/lint-baseline.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/android_intent/CHANGELOG.md b/packages/android_intent/CHANGELOG.md index 71428c53cea8..82cd5db3e4e4 100644 --- a/packages/android_intent/CHANGELOG.md +++ b/packages/android_intent/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Remove references to the V1 Android embedding. +* Updated Android lint settings. ## 2.0.2 diff --git a/packages/android_intent/android/build.gradle b/packages/android_intent/android/build.gradle index b0238b7db4f3..e8b9f3810146 100644 --- a/packages/android_intent/android/build.gradle +++ b/packages/android_intent/android/build.gradle @@ -35,6 +35,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } diff --git a/packages/battery/battery/CHANGELOG.md b/packages/battery/battery/CHANGELOG.md index 8590e646564e..ddc912d2ba2a 100644 --- a/packages/battery/battery/CHANGELOG.md +++ b/packages/battery/battery/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Remove references to the Android v1 embedding. +* Updated Android lint settings. ## 2.0.3 diff --git a/packages/battery/battery/android/build.gradle b/packages/battery/battery/android/build.gradle index 1e484897c2ad..14f503813f7e 100644 --- a/packages/battery/battery/android/build.gradle +++ b/packages/battery/battery/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index d455ddb2fad1..694898092d7a 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 0.8.1+7 * Fix device orientation sometimes not affecting the camera preview orientation. diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera/android/build.gradle index 6ceed97c9a17..9bbafb653ef8 100644 --- a/packages/camera/camera/android/build.gradle +++ b/packages/camera/camera/android/build.gradle @@ -35,6 +35,8 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + baseline file("lint-baseline.xml") } compileOptions { sourceCompatibility = '1.8' diff --git a/packages/camera/camera/android/lint-baseline.xml b/packages/camera/camera/android/lint-baseline.xml new file mode 100644 index 000000000000..4ddaafa87988 --- /dev/null +++ b/packages/camera/camera/android/lint-baseline.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/connectivity/connectivity/CHANGELOG.md b/packages/connectivity/connectivity/CHANGELOG.md index 58047482fcb7..f5489692bee9 100644 --- a/packages/connectivity/connectivity/CHANGELOG.md +++ b/packages/connectivity/connectivity/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Remove references to the Android V1 embedding. +* Updated Android lint settings. ## 3.0.6 diff --git a/packages/connectivity/connectivity/android/build.gradle b/packages/connectivity/connectivity/android/build.gradle index 53a390bd74f0..983f29b142de 100644 --- a/packages/connectivity/connectivity/android/build.gradle +++ b/packages/connectivity/connectivity/android/build.gradle @@ -35,6 +35,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } diff --git a/packages/device_info/device_info/CHANGELOG.md b/packages/device_info/device_info/CHANGELOG.md index 669423cc4efb..97349d450cf1 100644 --- a/packages/device_info/device_info/CHANGELOG.md +++ b/packages/device_info/device_info/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Remove references to the Android V1 embedding. +* Updated Android lint settings. ## 2.0.2 diff --git a/packages/device_info/device_info/android/build.gradle b/packages/device_info/device_info/android/build.gradle index 51ec2a7fb567..ed89da419d4a 100644 --- a/packages/device_info/device_info/android/build.gradle +++ b/packages/device_info/device_info/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } diff --git a/packages/espresso/CHANGELOG.md b/packages/espresso/CHANGELOG.md index 10e5ae59f71a..e00ea7065ce0 100644 --- a/packages/espresso/CHANGELOG.md +++ b/packages/espresso/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 0.1.0+3 * Remove references to the Android v1 embedding. diff --git a/packages/espresso/android/build.gradle b/packages/espresso/android/build.gradle index 8cd54811afa0..da0cd2ebfee8 100644 --- a/packages/espresso/android/build.gradle +++ b/packages/espresso/android/build.gradle @@ -30,6 +30,8 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + baseline file("lint-baseline.xml") } diff --git a/packages/espresso/android/lint-baseline.xml b/packages/espresso/android/lint-baseline.xml new file mode 100644 index 000000000000..19b349f044bf --- /dev/null +++ b/packages/espresso/android/lint-baseline.xml @@ -0,0 +1,389 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md index 6a05ed01e2de..7e567d8cce5c 100644 --- a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md +++ b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 2.0.3 * Remove references to the Android V1 embedding. diff --git a/packages/flutter_plugin_android_lifecycle/android/build.gradle b/packages/flutter_plugin_android_lifecycle/android/build.gradle index ba3a54b235e6..5a584b4e366f 100644 --- a/packages/flutter_plugin_android_lifecycle/android/build.gradle +++ b/packages/flutter_plugin_android_lifecycle/android/build.gradle @@ -31,6 +31,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } dependencies { diff --git a/packages/google_maps_flutter/google_maps_flutter/android/build.gradle b/packages/google_maps_flutter/google_maps_flutter/android/build.gradle index 6c5ea76ae61e..e3cf6ffe8818 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } dependencies { diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index e4207de117fa..8ac07ae1793b 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 5.0.7 * Mark iOS arm64 simulators as unsupported. diff --git a/packages/google_sign_in/google_sign_in/android/build.gradle b/packages/google_sign_in/google_sign_in/android/build.gradle index 7d1825defa84..ea98b315f147 100644 --- a/packages/google_sign_in/google_sign_in/android/build.gradle +++ b/packages/google_sign_in/google_sign_in/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 9d89389cb105..4f21ed3cc398 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 0.8.3+2 * Fix using Camera as image source on Android 11+ diff --git a/packages/image_picker/image_picker/android/build.gradle b/packages/image_picker/image_picker/android/build.gradle index 607b3c1523a1..1e6439e6a4eb 100755 --- a/packages/image_picker/image_picker/android/build.gradle +++ b/packages/image_picker/image_picker/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } dependencies { implementation 'androidx.core:core:1.0.2' diff --git a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md index d67d1efd61b5..60dae1be6d86 100644 --- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -1,8 +1,12 @@ -# 0.1.4+4 +## NEXT + +* Updated Android lint settings. + +## 0.1.4+4 * Removed dependency on the `test` package. -# 0.1.4+3 +## 0.1.4+3 - Updated installation instructions in README. diff --git a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle index 349f9eeb734c..656f7c34bf7a 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle +++ b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/packages/local_auth/CHANGELOG.md b/packages/local_auth/CHANGELOG.md index c33fa7778b94..c0d04fb5688a 100644 --- a/packages/local_auth/CHANGELOG.md +++ b/packages/local_auth/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 1.1.7 * Remove references to the Android V1 embedding. diff --git a/packages/local_auth/android/build.gradle b/packages/local_auth/android/build.gradle index 4fcb77cf6c98..dc282e78ced0 100644 --- a/packages/local_auth/android/build.gradle +++ b/packages/local_auth/android/build.gradle @@ -30,6 +30,8 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + baseline file("lint-baseline.xml") } diff --git a/packages/local_auth/android/lint-baseline.xml b/packages/local_auth/android/lint-baseline.xml new file mode 100644 index 000000000000..e89eaadb3e6d --- /dev/null +++ b/packages/local_auth/android/lint-baseline.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/package_info/CHANGELOG.md b/packages/package_info/CHANGELOG.md index 2ec20b3fe775..0fe91175cf6b 100644 --- a/packages/package_info/CHANGELOG.md +++ b/packages/package_info/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Remove references to the Android v1 embedding. +* Updated Android lint settings. ## 2.0.2 diff --git a/packages/package_info/android/build.gradle b/packages/package_info/android/build.gradle index d2846f260556..e21d911ff490 100644 --- a/packages/package_info/android/build.gradle +++ b/packages/package_info/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } diff --git a/packages/path_provider/path_provider/CHANGELOG.md b/packages/path_provider/path_provider/CHANGELOG.md index 5e08c520dcd7..ba7bb3dc7ada 100644 --- a/packages/path_provider/path_provider/CHANGELOG.md +++ b/packages/path_provider/path_provider/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 2.0.3 * Add iOS unit test target. diff --git a/packages/path_provider/path_provider/android/build.gradle b/packages/path_provider/path_provider/android/build.gradle index db2c79c15796..3458140bd0eb 100644 --- a/packages/path_provider/path_provider/android/build.gradle +++ b/packages/path_provider/path_provider/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } android { compileOptions { diff --git a/packages/quick_actions/quick_actions/CHANGELOG.md b/packages/quick_actions/quick_actions/CHANGELOG.md index 5d040f4fd74e..9087c2807061 100644 --- a/packages/quick_actions/quick_actions/CHANGELOG.md +++ b/packages/quick_actions/quick_actions/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 0.6.0+5 * Support only calling initialize once. diff --git a/packages/quick_actions/quick_actions/android/build.gradle b/packages/quick_actions/quick_actions/android/build.gradle index 0bce642f3e60..ec3f84eab4cf 100644 --- a/packages/quick_actions/quick_actions/android/build.gradle +++ b/packages/quick_actions/quick_actions/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } dependencies { diff --git a/packages/sensors/CHANGELOG.md b/packages/sensors/CHANGELOG.md index 5ac0943333fa..acea470855fb 100644 --- a/packages/sensors/CHANGELOG.md +++ b/packages/sensors/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Remove references to the Android V1 embedding. +* Updated Android lint settings. ## 2.0.3 diff --git a/packages/sensors/android/build.gradle b/packages/sensors/android/build.gradle index a16ebd2ee459..7e1087764dee 100644 --- a/packages/sensors/android/build.gradle +++ b/packages/sensors/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } diff --git a/packages/share/CHANGELOG.md b/packages/share/CHANGELOG.md index 9074f59f05b7..c9a468d925a7 100644 --- a/packages/share/CHANGELOG.md +++ b/packages/share/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Remove references to the Android V1 embedding. +* Updated Android lint settings. ## 2.0.4 diff --git a/packages/share/android/build.gradle b/packages/share/android/build.gradle index 1b95bf592fb6..b2ea363a3e11 100644 --- a/packages/share/android/build.gradle +++ b/packages/share/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } dependencies { diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index 3476f4eff3f0..48abf9ad4045 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Add iOS unit test target. +* Updated Android lint settings. ## 2.0.6 diff --git a/packages/shared_preferences/shared_preferences/android/build.gradle b/packages/shared_preferences/shared_preferences/android/build.gradle index 6a66eba508fb..9284f1c36143 100644 --- a/packages/shared_preferences/shared_preferences/android/build.gradle +++ b/packages/shared_preferences/shared_preferences/android/build.gradle @@ -38,6 +38,8 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + baseline file("lint-baseline.xml") } dependencies { testImplementation 'junit:junit:4.12' diff --git a/packages/shared_preferences/shared_preferences/android/lint-baseline.xml b/packages/shared_preferences/shared_preferences/android/lint-baseline.xml new file mode 100644 index 000000000000..6b2f35f5a151 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/android/lint-baseline.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index dc67a2142ec2..237f0b139475 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 6.0.10 * Remove references to the Android v1 embedding. diff --git a/packages/url_launcher/url_launcher/android/build.gradle b/packages/url_launcher/url_launcher/android/build.gradle index 5dd7e773a1ca..d374d40534c3 100644 --- a/packages/url_launcher/url_launcher/android/build.gradle +++ b/packages/url_launcher/url_launcher/android/build.gradle @@ -30,6 +30,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index f2029622f0ee..f07bb5f66f8c 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 2.1.14 * Removed dependency on the `flutter_test` package. diff --git a/packages/video_player/video_player/android/build.gradle b/packages/video_player/video_player/android/build.gradle index f2f18bff9798..9d9984439370 100644 --- a/packages/video_player/video_player/android/build.gradle +++ b/packages/video_player/video_player/android/build.gradle @@ -35,6 +35,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } android { compileOptions { diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index df7d9cb87457..361bfd24f3af 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 2.0.12 * Improved the documentation on using the different Android Platform View modes. diff --git a/packages/webview_flutter/webview_flutter/android/build.gradle b/packages/webview_flutter/webview_flutter/android/build.gradle index cd1b4188a1eb..4a164317c60f 100644 --- a/packages/webview_flutter/webview_flutter/android/build.gradle +++ b/packages/webview_flutter/webview_flutter/android/build.gradle @@ -31,6 +31,7 @@ android { lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } dependencies { diff --git a/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md b/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md index 925745faa22a..86f3f67af103 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md +++ b/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updated Android lint settings. + ## 2.0.2 * Update README to point to Plus Plugins version. diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle b/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle index 2b5a8a7fc209..661ee82da4d0 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle +++ b/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle @@ -29,6 +29,7 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 267019fe7359..87917d63d3fc 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT - Added Android native integration test support to `native-test`. +- Added a new `android-lint` command to lint Android plugin native code. ## 0.5.0 diff --git a/script/tool/lib/src/common/gradle.dart b/script/tool/lib/src/common/gradle.dart new file mode 100644 index 000000000000..e7214bf29714 --- /dev/null +++ b/script/tool/lib/src/common/gradle.dart @@ -0,0 +1,57 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:platform/platform.dart'; + +import 'process_runner.dart'; + +const String _gradleWrapperWindows = 'gradlew.bat'; +const String _gradleWrapperNonWindows = 'gradlew'; + +/// A utility class for interacting with Gradle projects. +class GradleProject { + /// Creates an instance that runs commands for [project] with the given + /// [processRunner]. + /// + /// If [log] is true, commands run by this instance will long various status + /// messages. + GradleProject( + this.flutterProject, { + this.processRunner = const ProcessRunner(), + this.platform = const LocalPlatform(), + }); + + /// The directory of a Flutter project to run Gradle commands in. + final Directory flutterProject; + + /// The [ProcessRunner] used to run commands. Overridable for testing. + final ProcessRunner processRunner; + + /// The platform that commands are being run on. + final Platform platform; + + /// The project's 'android' directory. + Directory get androidDirectory => flutterProject.childDirectory('android'); + + /// The path to the Gradle wrapper file for the project. + File get gradleWrapper => androidDirectory.childFile( + platform.isWindows ? _gradleWrapperWindows : _gradleWrapperNonWindows); + + /// Whether or not the project is ready to have Gradle commands run on it + /// (i.e., whether the `flutter` tool has generated the necessary files). + bool isConfigured() => gradleWrapper.existsSync(); + + /// Runs a `gradlew` command with the given parameters. + Future runCommand( + String target, { + List arguments = const [], + }) { + return processRunner.runAndStream( + gradleWrapper.path, + [target, ...arguments], + workingDir: androidDirectory, + ); + } +} diff --git a/script/tool/lib/src/common/xcode.dart b/script/tool/lib/src/common/xcode.dart index d6bbae419eda..83f681bcb492 100644 --- a/script/tool/lib/src/common/xcode.dart +++ b/script/tool/lib/src/common/xcode.dart @@ -15,7 +15,7 @@ const String _xcRunCommand = 'xcrun'; /// A utility class for interacting with the installed version of Xcode. class Xcode { - /// Creates an instance that runs commends with the given [processRunner]. + /// Creates an instance that runs commands with the given [processRunner]. /// /// If [log] is true, commands run by this instance will long various status /// messages. diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 8459f6c70153..fd2de97be4b3 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -10,6 +10,7 @@ import 'package:platform/platform.dart'; import 'package:uuid/uuid.dart'; import 'common/core.dart'; +import 'common/gradle.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; @@ -74,8 +75,6 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { 'Runs tests in test_instrumentation folder using the ' 'instrumentation_test package.'; - static const String _gradleWrapper = 'gradlew'; - bool _firebaseProjectConfigured = false; Future _configureFirebaseProject() async { @@ -138,13 +137,15 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { } // Ensures that gradle wrapper exists - if (!await _ensureGradleWrapperExists(androidDirectory)) { + final GradleProject project = GradleProject(exampleDirectory, + processRunner: processRunner, platform: platform); + if (!await _ensureGradleWrapperExists(project)) { return PackageResult.fail(['Unable to build example apk']); } await _configureFirebaseProject(); - if (!await _runGradle(androidDirectory, 'app:assembleAndroidTest')) { + if (!await _runGradle(project, 'app:assembleAndroidTest')) { return PackageResult.fail(['Unable to assemble androidTest']); } @@ -156,8 +157,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { for (final File test in _findIntegrationTestFiles(package)) { final String testName = getRelativePosixPath(test, from: package); print('Testing $testName...'); - if (!await _runGradle(androidDirectory, 'app:assembleDebug', - testFile: test)) { + if (!await _runGradle(project, 'app:assembleDebug', testFile: test)) { printError('Could not build $testName'); errors.add('$testName failed to build'); continue; @@ -204,12 +204,12 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { : PackageResult.fail(errors); } - /// Checks that 'gradlew' exists in [androidDirectory], and if not runs a + /// Checks that Gradle has been configured for [project], and if not runs a /// Flutter build to generate it. /// /// Returns true if either gradlew was already present, or the build succeeds. - Future _ensureGradleWrapperExists(Directory androidDirectory) async { - if (!androidDirectory.childFile(_gradleWrapper).existsSync()) { + Future _ensureGradleWrapperExists(GradleProject project) async { + if (!project.isConfigured()) { print('Running flutter build apk...'); final String experiment = getStringArg(kEnableExperiment); final int exitCode = await processRunner.runAndStream( @@ -219,7 +219,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { 'apk', if (experiment.isNotEmpty) '--enable-experiment=$experiment', ], - workingDir: androidDirectory); + workingDir: project.androidDirectory); if (exitCode != 0) { return false; @@ -228,15 +228,15 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { return true; } - /// Builds [target] using 'gradlew' in the given [directory]. Assumes - /// 'gradlew' already exists. + /// Builds [target] using Gradle in the given [project]. Assumes Gradle is + /// already configured. /// /// [testFile] optionally does the Flutter build with the given test file as /// the build target. /// /// Returns true if the command succeeds. Future _runGradle( - Directory directory, + GradleProject project, String target, { File? testFile, }) async { @@ -245,17 +245,15 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { ? Uri.encodeComponent('--enable-experiment=$experiment') : null; - final int exitCode = await processRunner.runAndStream( - directory.childFile(_gradleWrapper).path, - [ - target, - '-Pverbose=true', - if (testFile != null) '-Ptarget=${testFile.path}', - if (extraOptions != null) '-Pextra-front-end-options=$extraOptions', - if (extraOptions != null) - '-Pextra-gen-snapshot-options=$extraOptions', - ], - workingDir: directory); + final int exitCode = await project.runCommand( + target, + arguments: [ + '-Pverbose=true', + if (testFile != null) '-Ptarget=${testFile.path}', + if (extraOptions != null) '-Pextra-front-end-options=$extraOptions', + if (extraOptions != null) '-Pextra-gen-snapshot-options=$extraOptions', + ], + ); if (exitCode != 0) { return false; diff --git a/script/tool/lib/src/lint_android_command.dart b/script/tool/lib/src/lint_android_command.dart new file mode 100644 index 000000000000..be6c6ed32415 --- /dev/null +++ b/script/tool/lib/src/lint_android_command.dart @@ -0,0 +1,61 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:platform/platform.dart'; + +import 'common/core.dart'; +import 'common/gradle.dart'; +import 'common/package_looping_command.dart'; +import 'common/process_runner.dart'; + +/// Lint the CocoaPod podspecs and run unit tests. +/// +/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. +class LintAndroidCommand extends PackageLoopingCommand { + /// Creates an instance of the linter command. + LintAndroidCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform); + + @override + final String name = 'lint-android'; + + @override + final String description = 'Runs "gradlew lint" on Android plugins.\n\n' + 'Requires the example to have been build at least once before running.'; + + @override + Future runForPackage(Directory package) async { + if (!pluginSupportsPlatform(kPlatformAndroid, package, + requiredMode: PlatformSupport.inline)) { + return PackageResult.skip( + 'Plugin does not have an Android implemenatation.'); + } + + final Directory exampleDirectory = package.childDirectory('example'); + final GradleProject project = GradleProject(exampleDirectory, + processRunner: processRunner, platform: platform); + + if (!project.isConfigured()) { + return PackageResult.fail(['Build example before linting']); + } + + final String packageName = package.basename; + + // Only lint one build mode to avoid extra work. + // Only lint the plugin project itself, to avoid failing due to errors in + // dependencies. + // + // TODO(stuartmorgan): Consider adding an XML parser to read and summarize + // all results. Currently, only the first three errors will be shown inline, + // and the rest have to be checked via the CI-uploaded artifact. + final int exitCode = await project.runCommand('$packageName:lintDebug'); + + return exitCode == 0 ? PackageResult.success() : PackageResult.fail(); + } +} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 6001c5df7f0a..e70cba24cc5e 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -16,6 +16,7 @@ import 'drive_examples_command.dart'; import 'firebase_test_lab_command.dart'; import 'format_command.dart'; import 'license_check_command.dart'; +import 'lint_android_command.dart'; import 'lint_podspecs_command.dart'; import 'list_command.dart'; import 'native_test_command.dart'; @@ -51,6 +52,7 @@ void main(List args) { ..addCommand(FirebaseTestLabCommand(packagesDir)) ..addCommand(FormatCommand(packagesDir)) ..addCommand(LicenseCheckCommand(packagesDir)) + ..addCommand(LintAndroidCommand(packagesDir)) ..addCommand(LintPodspecsCommand(packagesDir)) ..addCommand(ListCommand(packagesDir)) ..addCommand(NativeTestCommand(packagesDir)) diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 9fc6a2912ccc..0bd2ab45f634 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -6,6 +6,7 @@ import 'package:file/file.dart'; import 'package:platform/platform.dart'; import 'common/core.dart'; +import 'common/gradle.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; @@ -47,8 +48,6 @@ class NativeTestCommand extends PackageLoopingCommand { help: 'Runs native integration (UI) tests', defaultsTo: true); } - static const String _gradleWrapper = 'gradlew'; - // The device destination flags for iOS tests. List _iosDestinationFlags = []; @@ -243,9 +242,12 @@ this command. final String exampleName = getPackageDescription(example); _printRunningExampleTestsMessage(example, 'Android'); - final Directory androidDirectory = example.childDirectory('android'); - final File gradleFile = androidDirectory.childFile(_gradleWrapper); - if (!gradleFile.existsSync()) { + final GradleProject project = GradleProject( + example, + processRunner: processRunner, + platform: platform, + ); + if (!project.isConfigured()) { printError('ERROR: Run "flutter build apk" on $exampleName, or run ' 'this tool\'s "build-examples --apk" command, ' 'before executing tests.'); @@ -256,9 +258,7 @@ this command. if (runUnitTests) { print('Running unit tests...'); - final int exitCode = await processRunner.runAndStream( - gradleFile.path, ['testDebugUnitTest'], - workingDir: androidDirectory); + final int exitCode = await project.runCommand('testDebugUnitTest'); if (exitCode != 0) { printError('$exampleName unit tests failed.'); failed = true; @@ -275,13 +275,12 @@ this command. 'notAnnotation=io.flutter.plugins.DartIntegrationTest'; print('Running integration tests...'); - final int exitCode = await processRunner.runAndStream( - gradleFile.path, - [ - 'app:connectedAndroidTest', - '-Pandroid.testInstrumentationRunnerArguments.$filter', - ], - workingDir: androidDirectory); + final int exitCode = await project.runCommand( + 'app:connectedAndroidTest', + arguments: [ + '-Pandroid.testInstrumentationRunnerArguments.$filter', + ], + ); if (exitCode != 0) { printError('$exampleName integration tests failed.'); failed = true; diff --git a/script/tool/test/common/gradle_test.dart b/script/tool/test/common/gradle_test.dart new file mode 100644 index 000000000000..c24887d3d469 --- /dev/null +++ b/script/tool/test/common/gradle_test.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. + +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/gradle.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; +import '../util.dart'; + +void main() { + late FileSystem fileSystem; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + processRunner = RecordingProcessRunner(); + }); + + group('isConfigured', () { + test('reports true when configured on Windows', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew.bat']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isWindows: true), + ); + + expect(project.isConfigured(), true); + }); + + test('reports true when configured on non-Windows', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isMacOS: true), + ); + + expect(project.isConfigured(), true); + }); + + test('reports false when not configured on Windows', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/foo']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isWindows: true), + ); + + expect(project.isConfigured(), false); + }); + + test('reports true when configured on non-Windows', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/foo']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isMacOS: true), + ); + + expect(project.isConfigured(), false); + }); + }); + + group('runXcodeBuild', () { + test('runs without arguments', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isMacOS: true), + ); + + final int exitCode = await project.runCommand('foo'); + + expect(exitCode, 0); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + plugin.childDirectory('android').childFile('gradlew').path, + const [ + 'foo', + ], + plugin.childDirectory('android').path), + ])); + }); + + test('runs with arguments', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isMacOS: true), + ); + + final int exitCode = await project.runCommand( + 'foo', + arguments: ['--bar', '--baz'], + ); + + expect(exitCode, 0); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + plugin.childDirectory('android').childFile('gradlew').path, + const [ + 'foo', + '--bar', + '--baz', + ], + plugin.childDirectory('android').path), + ])); + }); + + test('runs with the correct wrapper on Windows', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew.bat']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isWindows: true), + ); + + final int exitCode = await project.runCommand('foo'); + + expect(exitCode, 0); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + plugin.childDirectory('android').childFile('gradlew.bat').path, + const [ + 'foo', + ], + plugin.childDirectory('android').path), + ])); + }); + + test('returns error codes', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew.bat']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isWindows: true), + ); + + processRunner.mockProcessesForExecutable[project.gradleWrapper.path] = + [ + MockProcess.failing(), + ]; + + final int exitCode = await project.runCommand('foo'); + + expect(exitCode, 1); + }); + }); +} diff --git a/script/tool/test/lint_android_command_test.dart b/script/tool/test/lint_android_command_test.dart new file mode 100644 index 000000000000..05ead220c15b --- /dev/null +++ b/script/tool/test/lint_android_command_test.dart @@ -0,0 +1,158 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:flutter_plugin_tools/src/lint_android_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$LintAndroidCommand', () { + FileSystem fileSystem; + late Directory packagesDir; + late CommandRunner runner; + late MockPlatform mockPlatform; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(style: FileSystemStyle.posix); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + mockPlatform = MockPlatform(); + processRunner = RecordingProcessRunner(); + final LintAndroidCommand command = LintAndroidCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner( + 'lint_android_test', 'Test for $LintAndroidCommand'); + runner.addCommand(command); + }); + + test('runs gradle lint', () async { + final Directory pluginDir = + createFakePlugin('plugin1', packagesDir, extraFiles: [ + 'example/android/gradlew', + ], platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }); + + final Directory androidDir = + pluginDir.childDirectory('example').childDirectory('android'); + + final List output = + await runCapturingPrint(runner, ['lint-android']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidDir.childFile('gradlew').path, + const ['plugin1:lintDebug'], + androidDir.path, + ), + ]), + ); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin1'), + contains('No issues found!'), + ])); + }); + + test('fails if gradlew is missing', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['lint-android'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder( + [ + contains('Build example before linting'), + ], + )); + }); + + test('fails if linting finds issues', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }); + + processRunner.mockProcessesForExecutable['gradlew'] = [ + MockProcess.failing(), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['lint-android'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder( + [ + contains('Build example before linting'), + ], + )); + }); + + test('skips non-Android plugins', () async { + createFakePlugin('plugin1', packagesDir); + + final List output = + await runCapturingPrint(runner, ['lint-android']); + + expect( + output, + containsAllInOrder( + [ + contains( + 'SKIPPING: Plugin does not have an Android implemenatation.') + ], + )); + }); + + test('skips non-inline plugins', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.federated + }); + + final List output = + await runCapturingPrint(runner, ['lint-android']); + + expect( + output, + containsAllInOrder( + [ + contains( + 'SKIPPING: Plugin does not have an Android implemenatation.') + ], + )); + }); + }); +} From a22f5912f6ba5f6b15378172c507388991d50de3 Mon Sep 17 00:00:00 2001 From: Andrew Zuo Date: Thu, 19 Aug 2021 10:16:27 -0400 Subject: [PATCH 28/57] [in_app_purchase] Add toString() to IAPError (#4162) This adds toString() to the IAPError class. This is so it is easier to see what is causing IAPError's in error logs. --- .../CHANGELOG.md | 4 +++ .../lib/src/errors/errors.dart | 1 + .../in_app_purchase_error.dart | 5 ++++ .../src/types/product_details_response.dart | 2 +- .../lib/src/types/purchase_details.dart | 2 +- .../lib/src/types/types.dart | 1 - .../pubspec.yaml | 2 +- .../errors/in_app_purchase_error_test.dart | 29 +++++++++++++++++++ 8 files changed, 42 insertions(+), 4 deletions(-) rename packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/{types => errors}/in_app_purchase_error.dart (88%) create mode 100644 packages/in_app_purchase/in_app_purchase_platform_interface/test/src/errors/in_app_purchase_error_test.dart diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md index ec619d2fdc37..cd4b86d7f39a 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.2.0 + +* Added `toString()` to `IAPError` + ## 1.1.0 * Added `currencySymbol` in ProductDetails. diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart index 7b788aaef490..8e10997aaedc 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart @@ -2,4 +2,5 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'in_app_purchase_error.dart'; export 'in_app_purchase_exception.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_error.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/in_app_purchase_error.dart similarity index 88% rename from packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_error.dart rename to packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/in_app_purchase_error.dart index f305f578f54a..166646d35b24 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_error.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/in_app_purchase_error.dart @@ -28,4 +28,9 @@ class IAPError { /// Error details, possibly null. final dynamic details; + + @override + String toString() { + return 'IAPError(code: $code, source: $source, message: $message, details: $details)'; + } } diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details_response.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details_response.dart index 11b244a84ae3..3a9d7c3c976e 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details_response.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details_response.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'in_app_purchase_error.dart'; +import '../errors/in_app_purchase_error.dart'; import 'product_details.dart'; /// The response returned by [InAppPurchasePlatform.queryProductDetails]. diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_details.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_details.dart index 08d0efe09878..8c98beb591ef 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_details.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_details.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'in_app_purchase_error.dart'; +import '../errors/in_app_purchase_error.dart'; import 'purchase_status.dart'; import 'purchase_verification_data.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart index 33d183c51d04..7cb666408249 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -export 'in_app_purchase_error.dart'; export 'product_details.dart'; export 'product_details_response.dart'; export 'purchase_details.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml index d15e5f40fc6f..64574e0cf306 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purch issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 1.1.0 +version: 1.2.0 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/errors/in_app_purchase_error_test.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/errors/in_app_purchase_error_test.dart new file mode 100644 index 000000000000..ed63f495b4c2 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/errors/in_app_purchase_error_test.dart @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase_platform_interface/src/errors/in_app_purchase_error.dart'; + +void main() { + test('toString: Should return a description of the error', () { + final IAPError exceptionNoDetails = IAPError( + code: 'error_code', + message: 'dummy_message', + source: 'dummy_source', + ); + + expect(exceptionNoDetails.toString(), + 'IAPError(code: error_code, source: dummy_source, message: dummy_message, details: null)'); + + final IAPError exceptionWithDetails = IAPError( + code: 'error_code', + message: 'dummy_message', + source: 'dummy_source', + details: 'dummy_details', + ); + + expect(exceptionWithDetails.toString(), + 'IAPError(code: error_code, source: dummy_source, message: dummy_message, details: dummy_details)'); + }); +} From f93314bb3779ebb0151bc326a0e515ca5f46533c Mon Sep 17 00:00:00 2001 From: Yusuf <82844127+ydag@users.noreply.github.com> Date: Fri, 20 Aug 2021 16:17:06 +0200 Subject: [PATCH 29/57] [image_picker] Platform interface update cache (#4123) --- .../CHANGELOG.md | 4 ++++ .../method_channel_image_picker.dart | 14 +++++++++++++- .../image_picker_platform.dart | 6 +++--- .../lib/src/types/lost_data_response.dart | 17 +++++++++++++++-- .../pubspec.yaml | 2 +- .../new_method_channel_image_picker_test.dart | 16 ++++++++++++++++ 6 files changed, 52 insertions(+), 7 deletions(-) diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index bd56f0ca77a6..97480e044284 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.3.0 + +* Updated `LostDataResponse` to include a `files` property, in case more than one file was recovered. + ## 2.2.0 * Added new methods that return `XFile` (from `package:cross_file`) 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 bb9e18e78d83..292cb814ddeb 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 @@ -227,7 +227,9 @@ class MethodChannelImagePicker extends ImagePickerPlatform { @override Future getLostData() async { - final Map? result = + List? pickedFileList; + + Map? result = await _channel.invokeMapMethod('retrieve'); if (result == null) { @@ -254,10 +256,20 @@ class MethodChannelImagePicker extends ImagePickerPlatform { final String? path = result['path']; + final pathList = result['pathList']; + if (pathList != null) { + pickedFileList = []; + // In this case, multiRetrieve is invoked. + for (String path in pathList) { + pickedFileList.add(XFile(path)); + } + } + return LostDataResponse( file: path != null ? XFile(path) : null, exception: exception, type: retrieveType, + files: pickedFileList, ); } } 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 8f9ab99eae06..5c1c8b698442 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 @@ -128,7 +128,8 @@ abstract class ImagePickerPlatform extends PlatformInterface { throw UnimplementedError('pickVideo() has not been implemented.'); } - /// Retrieve the lost [PickedFile] file when [pickImage] or [pickVideo] failed because the MainActivity is destroyed. (Android only) + /// Retrieves any previously picked file, that was lost due to the MainActivity being destroyed. + /// In case multiple files were lost, only the last file will be recovered. (Android only). /// /// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive. /// Call this method to retrieve the lost data and process the data according to your APP's business logic. @@ -233,8 +234,7 @@ abstract class ImagePickerPlatform extends PlatformInterface { throw UnimplementedError('getVideo() has not been implemented.'); } - /// Retrieve the lost [XFile] file when [getImage], [getMultiImage] or [getVideo] failed because the MainActivity is - /// destroyed. (Android only) + /// Retrieves any previously picked files, that were lost due to the MainActivity being destroyed. (Android only) /// /// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is /// always alive. Call this method to retrieve the lost data and process the data according to your APP's business logic. diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart index 576ad334bd35..65f5d7e15c90 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart @@ -14,7 +14,12 @@ import 'package:image_picker_platform_interface/src/types/types.dart'; class LostDataResponse { /// Creates an instance with the given [file], [exception], and [type]. Any of /// the params may be null, but this is never considered to be empty. - LostDataResponse({this.file, this.exception, this.type}); + LostDataResponse({ + this.file, + this.exception, + this.type, + this.files, + }); /// Initializes an instance with all member params set to null and considered /// to be empty. @@ -22,7 +27,8 @@ class LostDataResponse { : file = null, exception = null, type = null, - _empty = true; + _empty = true, + files = null; /// Whether it is an empty response. /// @@ -50,4 +56,11 @@ class LostDataResponse { final RetrieveType? type; bool _empty = false; + + /// The list of files that were lost in a previous [getMultiImage] call due to MainActivity being destroyed. + /// + /// When [files] is populated, [file] will refer to the last item in the [files] list. + /// + /// Can be null if [exception] exists. + final List? files; } diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index 0953e76f03ee..2168ff0f778a 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.2.0 +version: 2.3.0 environment: sdk: ">=2.12.0 <3.0.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 e5321abc0121..17caa8456621 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 @@ -929,6 +929,22 @@ void main() { expect(response.file!.path, '/example/path'); }); + test('getLostData should successfully retrieve multiple files', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'image', + 'path': '/example/path1', + 'pathList': ['/example/path0', '/example/path1'], + }; + }); + final LostDataResponse response = await picker.getLostData(); + expect(response.type, RetrieveType.image); + expect(response.file, isNotNull); + expect(response.file!.path, '/example/path1'); + expect(response.files!.first.path, '/example/path0'); + expect(response.files!.length, 2); + }); + test('getLostData get error response', () async { picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { return { From 5ea96e51de83ae5cfcfecf5ae4af15fdd5f8c9b8 Mon Sep 17 00:00:00 2001 From: Cobinja Date: Fri, 20 Aug 2021 16:57:08 +0200 Subject: [PATCH 30/57] [shared_preferences] Fix possible clash of string with double entry (#3895) --- .../shared_preferences/CHANGELOG.md | 3 +- .../MethodCallHandlerImpl.java | 4 ++- .../shared_preferences_test.dart | 36 +++++++++++++++++++ .../lib/shared_preferences.dart | 7 ++++ .../shared_preferences/pubspec.yaml | 2 +- 5 files changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index 48abf9ad4045..57b35a81255b 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,7 +1,8 @@ -## NEXT +## 2.0.7 * Add iOS unit test target. * Updated Android lint settings. +* Fix string clash with double entries on Android ## 2.0.6 diff --git a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java index 71ec14e7d06b..cea3f34b9b96 100644 --- a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java +++ b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java @@ -86,7 +86,9 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { break; case "setString": String value = (String) call.argument("value"); - if (value.startsWith(LIST_IDENTIFIER) || value.startsWith(BIG_INTEGER_PREFIX)) { + if (value.startsWith(LIST_IDENTIFIER) + || value.startsWith(BIG_INTEGER_PREFIX) + || value.startsWith(DOUBLE_PREFIX)) { result.error( "StorageError", "This string cannot be stored as it clashes with special identifier prefixes.", diff --git a/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart index 1d46ed5751b0..e8498f473a2c 100644 --- a/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart @@ -3,6 +3,8 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:integration_test/integration_test.dart'; @@ -102,5 +104,39 @@ void main() { // The last write should win. expect(preferences.getInt('int'), writeCount); }); + + testWidgets( + 'string clash with lists, big integers and doubles (Android only)', + (WidgetTester _) async { + await preferences.clear(); + // special prefixes plus a string value + expect( + // prefix for lists + preferences.setString( + 'String', + 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu' + + kTestValues2['flutter.String']), + throwsA(isA())); + await preferences.reload(); + expect(preferences.getString('String'), null); + expect( + // prefix for big integers + preferences.setString( + 'String', + 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy' + + kTestValues2['flutter.String']), + throwsA(isA())); + await preferences.reload(); + expect(preferences.getString('String'), null); + expect( + // prefix for doubles + preferences.setString( + 'String', + 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu' + + kTestValues2['flutter.String']), + throwsA(isA())); + await preferences.reload(); + expect(preferences.getString('String'), null); + }, skip: !Platform.isAndroid); }); } diff --git a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart index 3d2dd051f61c..841d615262de 100644 --- a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart +++ b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart @@ -128,6 +128,13 @@ class SharedPreferences { _setValue('Double', key, value); /// Saves a string [value] to persistent storage in the background. + /// + /// Note: Due to limitations in Android's SharedPreferences, + /// values cannot start with any one of the following: + /// + /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu' + /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy' + /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu' Future setString(String key, String value) => _setValue('String', key, value); diff --git a/packages/shared_preferences/shared_preferences/pubspec.yaml b/packages/shared_preferences/shared_preferences/pubspec.yaml index c3039a98588b..e3cdfe4f87b3 100644 --- a/packages/shared_preferences/shared_preferences/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. repository: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.6 +version: 2.0.7 environment: sdk: ">=2.12.0 <3.0.0" From f2b42f78b17ea5edc8f1078aee97a4438f06a6ad Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 20 Aug 2021 07:58:24 -0700 Subject: [PATCH 31/57] Fix and test for 'implements' pubspec entry (#4242) The federated plugin spec calls for implementation packages to include an `implements` entry in the `plugins` section of the `pubspec.yaml` indicating what app-facing package it implements. Most of the described behaviors of the `flutter` tool aren't implemented yet, and the pub.dev features have `default_plugin` as a backstop, so we haven't noticed that they are mostly missing (or in one case, incorrect). To better future-proof the plugins, and to provide a better example to people looking at our plugins as examples of federation, this adds a CI check to make sure that we are correctly adding it, and fixes all of the missing/incorrect values it turned up. Fixes https://github.com/flutter/flutter/issues/88222 --- packages/camera/camera_web/CHANGELOG.md | 4 + packages/camera/camera_web/pubspec.yaml | 4 +- .../connectivity_for_web/CHANGELOG.md | 4 + .../connectivity_for_web/pubspec.yaml | 3 +- .../connectivity_macos/CHANGELOG.md | 3 +- .../connectivity_macos/pubspec.yaml | 4 +- .../file_selector_web/CHANGELOG.md | 4 + .../file_selector_web/pubspec.yaml | 3 +- .../google_maps_flutter_web/CHANGELOG.md | 4 + .../google_maps_flutter_web/pubspec.yaml | 3 +- .../google_sign_in_web/CHANGELOG.md | 4 + .../google_sign_in_web/pubspec.yaml | 3 +- .../image_picker_for_web/CHANGELOG.md | 4 + .../image_picker_for_web/pubspec.yaml | 3 +- .../in_app_purchase_android/CHANGELOG.md | 3 +- .../in_app_purchase_android/pubspec.yaml | 3 +- .../in_app_purchase_ios/CHANGELOG.md | 4 + .../in_app_purchase_ios/pubspec.yaml | 3 +- .../shared_preferences_web/CHANGELOG.md | 4 + .../shared_preferences_web/pubspec.yaml | 3 +- .../url_launcher_web/CHANGELOG.md | 4 + .../url_launcher_web/pubspec.yaml | 3 +- .../video_player_web/CHANGELOG.md | 4 + .../video_player_web/pubspec.yaml | 3 +- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/pubspec_check_command.dart | 80 ++++- .../tool/test/pubspec_check_command_test.dart | 273 +++++++++++++++--- 27 files changed, 369 insertions(+), 69 deletions(-) diff --git a/packages/camera/camera_web/CHANGELOG.md b/packages/camera/camera_web/CHANGELOG.md index 68bc5f4e1a1e..a481554b540c 100644 --- a/packages/camera/camera_web/CHANGELOG.md +++ b/packages/camera/camera_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.0+1 + +* Add `implements` to pubspec. + ## 0.1.0 * Initial release diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index c4d78999f273..822af60a979b 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_web description: A Flutter plugin for getting information about and controlling the camera on Web. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.1.0 +version: 0.1.0+1 # This plugin is under development and will be published # when the first working web camera implementation is added. @@ -30,4 +30,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.11.1 \ No newline at end of file + pedantic: ^1.11.1 diff --git a/packages/connectivity/connectivity_for_web/CHANGELOG.md b/packages/connectivity/connectivity_for_web/CHANGELOG.md index ccd689760b84..97e5032c8dd4 100644 --- a/packages/connectivity/connectivity_for_web/CHANGELOG.md +++ b/packages/connectivity/connectivity_for_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.0+1 + +* Add `implements` to pubspec. + ## 0.4.0 * Migrate to null-safety diff --git a/packages/connectivity/connectivity_for_web/pubspec.yaml b/packages/connectivity/connectivity_for_web/pubspec.yaml index 5b05dd80d088..2aaa8bd978fa 100644 --- a/packages/connectivity/connectivity_for_web/pubspec.yaml +++ b/packages/connectivity/connectivity_for_web/pubspec.yaml @@ -2,7 +2,7 @@ name: connectivity_for_web description: An implementation for the web platform of the Flutter `connectivity` plugin. This uses the NetworkInformation Web API, with a fallback to Navigator.onLine. repository: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_for_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+connectivity%22 -version: 0.4.0 +version: 0.4.0+1 environment: sdk: ">=2.12.0 <3.0.0" @@ -10,6 +10,7 @@ environment: flutter: plugin: + implements: connectivity platforms: web: pluginClass: ConnectivityPlugin diff --git a/packages/connectivity/connectivity_macos/CHANGELOG.md b/packages/connectivity/connectivity_macos/CHANGELOG.md index c7bc5b4cf469..46a4038f91ee 100644 --- a/packages/connectivity/connectivity_macos/CHANGELOG.md +++ b/packages/connectivity/connectivity_macos/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 0.2.1+2 * Add Swift language version to podspec. +* Fix `implements` package name in pubspec. ## 0.2.1+1 diff --git a/packages/connectivity/connectivity_macos/pubspec.yaml b/packages/connectivity/connectivity_macos/pubspec.yaml index 1e8842c7417a..b98f23d34eb7 100644 --- a/packages/connectivity/connectivity_macos/pubspec.yaml +++ b/packages/connectivity/connectivity_macos/pubspec.yaml @@ -2,7 +2,7 @@ name: connectivity_macos description: macOS implementation of the connectivity plugin. repository: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+connectivity%22 -version: 0.2.1+1 +version: 0.2.1+2 environment: sdk: ">=2.12.0 <3.0.0" @@ -10,7 +10,7 @@ environment: flutter: plugin: - implements: connectivity_platform_interface + implements: connectivity platforms: macos: pluginClass: ConnectivityPlugin diff --git a/packages/file_selector/file_selector_web/CHANGELOG.md b/packages/file_selector/file_selector_web/CHANGELOG.md index dadf5ffdc3fc..e2a863643027 100644 --- a/packages/file_selector/file_selector_web/CHANGELOG.md +++ b/packages/file_selector/file_selector_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.1+2 + +* Add `implements` to pubspec. + # 0.8.1+1 - Updated installation instructions in README. diff --git a/packages/file_selector/file_selector_web/pubspec.yaml b/packages/file_selector/file_selector_web/pubspec.yaml index 9753f9216694..bbad45bf2d6b 100644 --- a/packages/file_selector/file_selector_web/pubspec.yaml +++ b/packages/file_selector/file_selector_web/pubspec.yaml @@ -2,7 +2,7 @@ name: file_selector_web description: Web platform implementation of file_selector repository: https://github.com/flutter/plugins/tree/master/packages/file_selector/file_selector_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.8.1+1 +version: 0.8.1+2 environment: sdk: ">=2.12.0 <3.0.0" @@ -10,6 +10,7 @@ environment: flutter: plugin: + implements: file_selector platforms: web: pluginClass: FileSelectorWeb diff --git a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md index d587c16f9207..83ffe09b357d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.0+4 + +* Add `implements` to pubspec. + ## 0.3.0+3 * Update the `README.md` usage instructions to not be tied to explicit package versions. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml index c4323fc6486f..82605f8fd070 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_web description: Web platform implementation of google_maps_flutter repository: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 0.3.0+3 +version: 0.3.0+4 environment: sdk: ">=2.12.0 <3.0.0" @@ -10,6 +10,7 @@ environment: flutter: plugin: + implements: google_maps_flutter platforms: web: pluginClass: GoogleMapsPlugin diff --git a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md index 8a2f1dbf56d2..7b9eb6b747ec 100644 --- a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.10.0+2 + +* Add `implements` to pubspec. + ## 0.10.0+1 * Updated installation instructions in README. diff --git a/packages/google_sign_in/google_sign_in_web/pubspec.yaml b/packages/google_sign_in/google_sign_in_web/pubspec.yaml index 0de229e795ce..7075f43151a6 100644 --- a/packages/google_sign_in/google_sign_in_web/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android, iOS and Web. repository: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 0.10.0+1 +version: 0.10.0+2 environment: sdk: ">=2.12.0 <3.0.0" @@ -11,6 +11,7 @@ environment: flutter: plugin: + implements: google_sign_in platforms: web: pluginClass: GoogleSignInPlugin diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md index 01d13f900d2d..d11ead3bb64e 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.3 + +* Add `implements` to pubspec. + ## 2.1.2 * Updated installation instructions in README. diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index 6296992c46d0..895486f3de06 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/master/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.2 +version: 2.1.3 environment: sdk: ">=2.12.0 <3.0.0" @@ -10,6 +10,7 @@ environment: flutter: plugin: + implements: image_picker platforms: web: pluginClass: ImagePickerPlugin diff --git a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md index 60dae1be6d86..8e342a65422c 100644 --- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.1.4+5 +* Add `implements` to pubspec. * Updated Android lint settings. ## 0.1.4+4 diff --git a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml index 3969e34c052b..745b651e5828 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_android description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.4+4 +version: 0.1.4+5 environment: sdk: ">=2.12.0 <3.0.0" @@ -10,6 +10,7 @@ environment: flutter: plugin: + implements: in_app_purchase platforms: android: package: io.flutter.plugins.inapppurchase diff --git a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md index c76409521e2f..e66b5dee6295 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.3+3 + +* Add `implements` to pubspec. + # 0.1.3+2 * Removed dependency on the `test` package. diff --git a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml index 8fc42371f405..07eae3ccc702 100644 --- a/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_ios description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the iOS StoreKit Framework. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.3+2 +version: 0.1.3+3 environment: sdk: ">=2.12.0 <3.0.0" @@ -10,6 +10,7 @@ environment: flutter: plugin: + implements: in_app_purchase platforms: ios: pluginClass: InAppPurchasePlugin diff --git a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md index 0a00e7d66a2a..dd68f5321541 100644 --- a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.2 + +* Add `implements` to pubspec. + ## 2.0.1 * Updated installation instructions in README. diff --git a/packages/shared_preferences/shared_preferences_web/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/pubspec.yaml index 2e67be20e427..c878903ac236 100644 --- a/packages/shared_preferences/shared_preferences_web/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_web/pubspec.yaml @@ -2,7 +2,7 @@ name: shared_preferences_web description: Web platform implementation of shared_preferences repository: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.1 +version: 2.0.2 environment: sdk: ">=2.12.0 <3.0.0" @@ -10,6 +10,7 @@ environment: flutter: plugin: + implements: shared_preferences platforms: web: pluginClass: SharedPreferencesPlugin diff --git a/packages/url_launcher/url_launcher_web/CHANGELOG.md b/packages/url_launcher/url_launcher_web/CHANGELOG.md index 64830f5e4481..f5338e62a775 100644 --- a/packages/url_launcher/url_launcher_web/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.4 + +* Add `implements` to pubspec. + ## 2.0.3 - Replaced reference to `shared_preferences` plugin with the `url_launcher` in the README. diff --git a/packages/url_launcher/url_launcher_web/pubspec.yaml b/packages/url_launcher/url_launcher_web/pubspec.yaml index cba098daceb7..77e8068f1396 100644 --- a/packages/url_launcher/url_launcher_web/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher_web description: Web platform implementation of url_launcher repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 2.0.3 +version: 2.0.4 environment: sdk: ">=2.12.0 <3.0.0" @@ -10,6 +10,7 @@ environment: flutter: plugin: + implements: url_launcher platforms: web: pluginClass: UrlLauncherPlugin diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index 398ec02ba743..a7a198db21e1 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.3 + +* Add `implements` to pubspec. + ## 2.0.2 * Updated installation instructions in README. diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index f101543598b8..c5eb57c1fc6e 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_web description: Web platform implementation of video_player. repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.0.2 +version: 2.0.3 environment: sdk: ">=2.12.0 <3.0.0" @@ -10,6 +10,7 @@ environment: flutter: plugin: + implements: video_player platforms: web: pluginClass: VideoPlayerPlugin diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 87917d63d3fc..063ae82c386d 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -2,6 +2,7 @@ - Added Android native integration test support to `native-test`. - Added a new `android-lint` command to lint Android plugin native code. +- Pubspec validation now checks for `implements` in implementation packages. ## 0.5.0 diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 539b170dbea1..58aeca1447a8 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -7,6 +7,7 @@ import 'package:git/git.dart'; import 'package:platform/platform.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; +import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; @@ -65,8 +66,8 @@ class PubspecCheckCommand extends PackageLoopingCommand { @override Future runForPackage(Directory package) async { final File pubspec = package.childFile('pubspec.yaml'); - final bool passesCheck = !pubspec.existsSync() || - await _checkPubspec(pubspec, packageName: package.basename); + final bool passesCheck = + !pubspec.existsSync() || await _checkPubspec(pubspec, package: package); if (!passesCheck) { return PackageResult.fail(); } @@ -75,7 +76,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { Future _checkPubspec( File pubspecFile, { - required String packageName, + required Directory package, }) async { final String contents = pubspecFile.readAsStringSync(); final Pubspec? pubspec = _tryParsePubspec(contents); @@ -84,34 +85,43 @@ class PubspecCheckCommand extends PackageLoopingCommand { } final List pubspecLines = contents.split('\n'); - final List sectionOrder = pubspecLines.contains(' plugin:') - ? _majorPluginSections - : _majorPackageSections; + final bool isPlugin = pubspec.flutter?.containsKey('plugin') ?? false; + final List sectionOrder = + isPlugin ? _majorPluginSections : _majorPackageSections; bool passing = _checkSectionOrder(pubspecLines, sectionOrder); if (!passing) { - print('${indentation}Major sections should follow standard ' + printError('${indentation}Major sections should follow standard ' 'repository ordering:'); final String listIndentation = indentation * 2; - print('$listIndentation${sectionOrder.join('\n$listIndentation')}'); + printError('$listIndentation${sectionOrder.join('\n$listIndentation')}'); } if (pubspec.publishTo != 'none') { final List repositoryErrors = - _checkForRepositoryLinkErrors(pubspec, packageName: packageName); + _checkForRepositoryLinkErrors(pubspec, packageName: package.basename); if (repositoryErrors.isNotEmpty) { for (final String error in repositoryErrors) { - print('$indentation$error'); + printError('$indentation$error'); } passing = false; } if (!_checkIssueLink(pubspec)) { - print( + printError( '${indentation}A package should have an "issue_tracker" link to a ' 'search for open flutter/flutter bugs with the relevant label:\n' '${indentation * 2}$_expectedIssueLinkFormat'); passing = false; } + + if (isPlugin) { + final String? error = + _checkForImplementsError(pubspec, package: package); + if (error != null) { + printError('$indentation$error'); + passing = false; + } + } } return passing; @@ -168,4 +178,52 @@ class PubspecCheckCommand extends PackageLoopingCommand { .startsWith(_expectedIssueLinkFormat) == true; } + + // Validates the "implements" keyword for a plugin, returning an error + // string if there are any issues. + // + // Should only be called on plugin packages. + String? _checkForImplementsError( + Pubspec pubspec, { + required Directory package, + }) { + if (_isImplementationPackage(package)) { + final String? implements = + pubspec.flutter!['plugin']!['implements'] as String?; + final String expectedImplements = package.parent.basename; + if (implements == null) { + return 'Missing "implements: $expectedImplements" in "plugin" section.'; + } else if (implements != expectedImplements) { + return 'Expecetd "implements: $expectedImplements"; ' + 'found "implements: $implements".'; + } + } + return null; + } + + // Returns true if [packageName] appears to be an implementation package + // according to repository conventions. + bool _isImplementationPackage(Directory package) { + // An implementation package should be in a group folder... + final Directory parentDir = package.parent; + if (parentDir.path == packagesDir.path) { + return false; + } + final String packageName = package.basename; + final String parentName = parentDir.basename; + // ... whose name is a prefix of the package name. + if (!packageName.startsWith(parentName)) { + return false; + } + // A few known package names are not implementation packages; assume + // anything else is. (This is done instead of listing known implementation + // suffixes to allow for non-standard suffixes; e.g., to put several + // platforms in one package for code-sharing purposes.) + const Set nonImplementationSuffixes = { + '', // App-facing package. + '_platform_interface', // Platform interface package. + }; + final String suffix = packageName.substring(parentName.length); + return !nonImplementationSuffixes.contains(suffix); + } } diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 177ed7f25b4e..a038e0c58fb0 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -66,9 +66,13 @@ environment: '''; } - String flutterSection({bool isPlugin = false}) { - const String pluginEntry = ''' + String flutterSection({ + bool isPlugin = false, + String? implementedPackage, + }) { + final String pluginEntry = ''' plugin: +${implementedPackage == null ? '' : ' implements: $implementedPackage'} platforms: '''; return ''' @@ -177,12 +181,19 @@ ${dependenciesSection()} ${devDependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Found a "homepage" entry; only "repository" should be used.'), + ]), ); }); @@ -197,12 +208,18 @@ ${dependenciesSection()} ${devDependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Missing "repository"'), + ]), ); }); @@ -217,12 +234,19 @@ ${dependenciesSection()} ${devDependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Found a "homepage" entry; only "repository" should be used.'), + ]), ); }); @@ -237,12 +261,18 @@ ${dependenciesSection()} ${devDependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('A package should have an "issue_tracker" link'), + ]), ); }); @@ -257,12 +287,19 @@ ${devDependenciesSection()} ${environmentSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + ]), ); }); @@ -277,12 +314,19 @@ ${dependenciesSection()} ${devDependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + ]), ); }); @@ -297,12 +341,19 @@ ${devDependenciesSection()} ${dependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); - await expectLater( - result, - throwsA(isA()), + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + ]), ); }); @@ -317,12 +368,150 @@ ${flutterSection(isPlugin: true)} ${dependenciesSection()} '''); - final Future> result = - runCapturingPrint(runner, ['pubspec-check']); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + ]), + ); + }); - await expectLater( - result, - throwsA(isA()), + test('fails when an implemenation package is missing "implements"', + () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a_foo', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Missing "implements: plugin_a" in "plugin" section.'), + ]), + ); + }); + + test('fails when an implemenation package has the wrong "implements"', + () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a_foo', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true, implementedPackage: 'plugin_a_foo')} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Expecetd "implements: plugin_a"; ' + 'found "implements: plugin_a_foo".'), + ]), + ); + }); + + test('passes for a correct implemenation package', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a_foo', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true, implementedPackage: 'plugin_a')} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final List output = + await runCapturingPrint(runner, ['pubspec-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin_a_foo...'), + contains('No issues found!'), + ]), + ); + }); + + test('passes for an app-facing package without "implements"', () async { + final Directory pluginDirectory = + createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final List output = + await runCapturingPrint(runner, ['pubspec-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin_a/plugin_a...'), + contains('No issues found!'), + ]), + ); + }); + + test('passes for a platform interface package without "implements"', + () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin_a_platform_interface', + packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a_platform_interface', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final List output = + await runCapturingPrint(runner, ['pubspec-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin_a_platform_interface...'), + contains('No issues found!'), + ]), ); }); }); From b1fe1912e016f5566a7b4d171cb06f826bd98bbb Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 20 Aug 2021 11:50:56 -0700 Subject: [PATCH 32/57] [flutter_plugin_tools] Improve 'repository' check (#4244) Ensures that the full relative path in the 'repository' link is correct, not just the last segment. This ensure that path-level errors (e.g., linking to the group directory rather than the package itself for app-facing packages) are caught. Also fixes the errors that this improved check catches, including several cases where a previously unfederated package wasn't fixed when it was moved to a subdirectory. --- .../in_app_purchase/CHANGELOG.md | 4 ++ .../in_app_purchase/pubspec.yaml | 4 +- packages/ios_platform_images/CHANGELOG.md | 3 +- packages/ios_platform_images/pubspec.yaml | 4 +- .../quick_actions/quick_actions/CHANGELOG.md | 3 +- .../quick_actions/quick_actions/pubspec.yaml | 4 +- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/pubspec_check_command.dart | 14 +++-- .../tool/test/pubspec_check_command_test.dart | 61 +++++++++++++++++-- 9 files changed, 81 insertions(+), 17 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md index 228fcddb6370..95ba4f27d10a 100644 --- a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.8 + +* Fix repository link in pubspec.yaml. + ## 1.0.7 * Remove references to the Android V1 embedding. diff --git a/packages/in_app_purchase/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/in_app_purchase/pubspec.yaml index a37ae07baa86..8b4510b3fce4 100644 --- a/packages/in_app_purchase/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase/pubspec.yaml @@ -1,8 +1,8 @@ name: in_app_purchase description: A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store and Google Play. -repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase +repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 1.0.7 +version: 1.0.8 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/ios_platform_images/CHANGELOG.md b/packages/ios_platform_images/CHANGELOG.md index 60db21a450d8..a7270eed0576 100644 --- a/packages/ios_platform_images/CHANGELOG.md +++ b/packages/ios_platform_images/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 0.2.0+1 * Add iOS unit test target. +* Fix repository link in pubspec.yaml. ## 0.2.0 diff --git a/packages/ios_platform_images/pubspec.yaml b/packages/ios_platform_images/pubspec.yaml index e90937f4f0b5..c3938856e386 100644 --- a/packages/ios_platform_images/pubspec.yaml +++ b/packages/ios_platform_images/pubspec.yaml @@ -1,8 +1,8 @@ name: ios_platform_images description: A plugin to share images between Flutter and iOS in add-to-app setups. -repository: https://github.com/flutter/plugins/tree/master/packages/ios_platform_images/ios_platform_images +repository: https://github.com/flutter/plugins/tree/master/packages/ios_platform_images issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+ios_platform_images%22 -version: 0.2.0 +version: 0.2.0+1 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/quick_actions/quick_actions/CHANGELOG.md b/packages/quick_actions/quick_actions/CHANGELOG.md index 9087c2807061..d893b67b10dc 100644 --- a/packages/quick_actions/quick_actions/CHANGELOG.md +++ b/packages/quick_actions/quick_actions/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 0.6.0+6 * Updated Android lint settings. +* Fix repository link in pubspec.yaml. ## 0.6.0+5 diff --git a/packages/quick_actions/quick_actions/pubspec.yaml b/packages/quick_actions/quick_actions/pubspec.yaml index e52ab515432f..c5d3fe4d4cbe 100644 --- a/packages/quick_actions/quick_actions/pubspec.yaml +++ b/packages/quick_actions/quick_actions/pubspec.yaml @@ -1,9 +1,9 @@ name: quick_actions description: Flutter plugin for creating shortcuts on home screen, also known as Quick Actions on iOS and App Shortcuts on Android. -repository: https://github.com/flutter/plugins/tree/master/packages/quick_actions +repository: https://github.com/flutter/plugins/tree/master/packages/quick_actions/quick_actions issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+quick_actions%22 -version: 0.6.0+5 +version: 0.6.0+6 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 063ae82c386d..1881d1bb6689 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -3,6 +3,7 @@ - Added Android native integration test support to `native-test`. - Added a new `android-lint` command to lint Android plugin native code. - Pubspec validation now checks for `implements` in implementation packages. +- Pubspec valitation now checks the full relative path of `repository` entries. ## 0.5.0 diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 58aeca1447a8..0a066ab72baf 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -98,7 +98,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { if (pubspec.publishTo != 'none') { final List repositoryErrors = - _checkForRepositoryLinkErrors(pubspec, packageName: package.basename); + _checkForRepositoryLinkErrors(pubspec, package: package); if (repositoryErrors.isNotEmpty) { for (final String error in repositoryErrors) { printError('$indentation$error'); @@ -154,14 +154,18 @@ class PubspecCheckCommand extends PackageLoopingCommand { List _checkForRepositoryLinkErrors( Pubspec pubspec, { - required String packageName, + required Directory package, }) { final List errorMessages = []; if (pubspec.repository == null) { errorMessages.add('Missing "repository"'); - } else if (!pubspec.repository!.path.endsWith(packageName)) { - errorMessages - .add('The "repository" link should end with the package name.'); + } else { + final String relativePackagePath = + path.relative(package.path, from: packagesDir.parent.path); + if (!pubspec.repository!.path.endsWith(relativePackagePath)) { + errorMessages + .add('The "repository" link should end with the package path.'); + } } if (pubspec.homepage != null) { diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index a038e0c58fb0..833f7b601e50 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -37,15 +37,29 @@ void main() { runner.addCommand(command); }); + /// Returns the top section of a pubspec.yaml for a package named [name], + /// for either a flutter/packages or flutter/plugins package depending on + /// the values of [isPlugin]. + /// + /// By default it will create a header that includes all of the expected + /// values, elements can be changed via arguments to create incorrect + /// entries. + /// + /// If [includeRepository] is true, by default the path in the link will + /// be "packages/[name]"; a different "packages"-relative path can be + /// provided with [repositoryPackagesDirRelativePath]. String headerSection( String name, { bool isPlugin = false, bool includeRepository = true, + String? repositoryPackagesDirRelativePath, bool includeHomepage = false, bool includeIssueTracker = true, }) { + final String repositoryPath = repositoryPackagesDirRelativePath ?? name; final String repoLink = 'https://github.com/flutter/' - '${isPlugin ? 'plugins' : 'packages'}/tree/master/packages/$name'; + '${isPlugin ? 'plugins' : 'packages'}/tree/master/' + 'packages/$repositoryPath'; final String issueTrackerLink = 'https://github.com/flutter/flutter/issues?' 'q=is%3Aissue+is%3Aopen+label%3A%22p%3A+$name%22'; @@ -250,6 +264,32 @@ ${devDependenciesSection()} ); }); + test('fails when repository is incorrect', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, repositoryPackagesDirRelativePath: 'different_plugin')} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The "repository" link should end with the package path.'), + ]), + ); + }); + test('fails when issue tracker is missing', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); @@ -446,7 +486,11 @@ ${devDependenciesSection()} 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin_a_foo', isPlugin: true)} +${headerSection( + 'plugin_a_foo', + isPlugin: true, + repositoryPackagesDirRelativePath: 'plugin_a/plugin_a_foo', + )} ${environmentSection()} ${flutterSection(isPlugin: true, implementedPackage: 'plugin_a')} ${dependenciesSection()} @@ -470,7 +514,11 @@ ${devDependenciesSection()} createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin_a', isPlugin: true)} +${headerSection( + 'plugin_a', + isPlugin: true, + repositoryPackagesDirRelativePath: 'plugin_a/plugin_a', + )} ${environmentSection()} ${flutterSection(isPlugin: true)} ${dependenciesSection()} @@ -496,7 +544,12 @@ ${devDependenciesSection()} packagesDir.childDirectory('plugin_a')); pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' -${headerSection('plugin_a_platform_interface', isPlugin: true)} +${headerSection( + 'plugin_a_platform_interface', + isPlugin: true, + repositoryPackagesDirRelativePath: + 'plugin_a/plugin_a_platform_interface', + )} ${environmentSection()} ${flutterSection(isPlugin: true)} ${dependenciesSection()} From 6a8681e7ac18ed625ced9e92c740fb55bd739222 Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Mon, 23 Aug 2021 10:57:03 +0200 Subject: [PATCH 33/57] [image_picker] Fix pickImage not returning on iOS when dismissing the PHPicker view by swiping down. (#4228) --- .../image_picker/image_picker/CHANGELOG.md | 3 ++- .../ios/Classes/FLTImagePickerPlugin.m | 24 ++++++++++++++----- .../image_picker/image_picker/pubspec.yaml | 2 +- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 4f21ed3cc398..a9255976c526 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.8.3+3 +* Fix pickImage not returning a value on iOS when dismissing PHPicker sheet by swiping. * Updated Android lint settings. ## 0.8.3+2 diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m index 4084ae65b5e0..cf3103195482 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m @@ -18,7 +18,8 @@ @interface FLTImagePickerPlugin () + PHPickerViewControllerDelegate, + UIAdaptivePresentationControllerDelegate> @property(copy, nonatomic) FlutterResult result; @@ -92,6 +93,7 @@ - (void)pickImageWithPHPicker:(int)maxImagesAllowed API_AVAILABLE(ios(14)) { _pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:config]; _pickerViewController.delegate = self; + _pickerViewController.presentationController.delegate = self; self.maxImagesAllowed = maxImagesAllowed; @@ -373,18 +375,28 @@ - (NSNumber *)getDesiredImageQuality:(NSNumber *)imageQuality { return imageQuality; } +- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController { + if (self.result != nil) { + self.result(nil); + self.result = nil; + self->_arguments = nil; + } +} + - (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray *)results API_AVAILABLE(ios(14)) { [picker dismissViewControllerAnimated:YES completion:nil]; - dispatch_queue_t backgroundQueue = - dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); - dispatch_async(backgroundQueue, ^{ - if (results.count == 0) { + if (results.count == 0) { + if (self.result != nil) { self.result(nil); self.result = nil; self->_arguments = nil; - return; } + return; + } + dispatch_queue_t backgroundQueue = + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); + dispatch_async(backgroundQueue, ^{ NSNumber *maxWidth = [self->_arguments objectForKey:@"maxWidth"]; NSNumber *maxHeight = [self->_arguments objectForKey:@"maxHeight"]; NSNumber *imageQuality = [self->_arguments objectForKey:@"imageQuality"]; diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index e167d8ab891c..4becca930261 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/master/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.3+2 +version: 0.8.3+3 environment: sdk: ">=2.12.0 <3.0.0" From 0a86ac866b8b322a37d5fac36c7b15856a2b37e8 Mon Sep 17 00:00:00 2001 From: Bodhi Mulders Date: Mon, 23 Aug 2021 18:30:36 +0200 Subject: [PATCH 34/57] [camera] android-rework part 9: Final implementation of camera class (#4059) This PR adds the final implementation for the Camera class that incorporates all the features from previous parts. --- packages/camera/camera/CHANGELOG.md | 5 +- .../io/flutter/plugins/camera/Camera.java | 1275 ++++++++--------- .../flutter/plugins/camera/CameraPlugin.java | 28 +- .../plugins/camera/CameraRegionUtils.java | 29 +- .../flutter/plugins/camera/CameraRegions.java | 76 - .../flutter/plugins/camera/CameraUtils.java | 120 +- .../flutter/plugins/camera/DartMessenger.java | 28 +- .../camera/DeviceOrientationManager.java | 200 --- .../plugins/camera/MethodCallHandlerImpl.java | 40 +- .../plugins/camera/PictureCaptureRequest.java | 96 -- .../camera/features/CameraFeatureFactory.java | 12 +- .../features/CameraFeatureFactoryImpl.java | 11 +- .../camera/features/CameraFeatures.java | 37 + .../exposurepoint/ExposurePointFeature.java | 15 +- .../focuspoint/FocusPointFeature.java | 15 +- .../noisereduction/NoiseReductionFeature.java | 19 +- .../DeviceOrientationManager.java | 192 ++- .../camera/CameraPropertiesImplTest.java | 7 +- ...s_convertPointToMeteringRectangleTest.java | 197 +++ ...aRegionUtils_getCameraBoundariesTest.java} | 120 +- .../io/flutter/plugins/camera/CameraTest.java | 843 +++++++++++ .../plugins/camera/CameraUtilsTest.java | 53 +- .../plugins/camera/CameraZoomTest.java | 18 +- .../plugins/camera/DartMessengerTest.java | 4 +- .../plugins/camera/ImageSaverTests.java | 6 +- .../camera/PictureCaptureRequestTest.java | 152 -- .../autofocus/AutoFocusFeatureTest.java | 24 +- .../features/autofocus/FocusModeTest.java | 6 +- .../exposurelock/ExposureLockFeatureTest.java | 14 +- .../exposurelock/ExposureModeTest.java | 6 +- .../ExposureOffsetFeatureTest.java | 13 +- .../ExposurePointFeatureTest.java | 121 +- .../features/flash/FlashFeatureTest.java | 22 +- .../focuspoint/FocusPointFeatureTest.java | 119 +- .../fpsrange/FpsRangeFeaturePixel4aTest.java | 2 +- .../fpsrange/FpsRangeFeatureTest.java | 12 +- .../NoiseReductionFeatureTest.java | 25 +- .../resolution/ResolutionFeatureTest.java | 22 +- .../DeviceOrientationManagerTest.java | 115 +- .../SensorOrientationFeatureTest.java | 17 +- .../zoomlevel/ZoomLevelFeatureTest.java | 18 +- .../features/zoomlevel/ZoomUtilsTest.java | 8 +- .../media/MediaRecorderBuilderTest.java | 4 +- .../camera/types/ExposureModeTest.java | 6 +- .../plugins/camera/types/FlashModeTest.java | 6 +- .../plugins/camera/types/FocusModeTest.java | 6 +- .../plugins/camera/utils/TestUtils.java | 10 + .../camera/lib/src/camera_controller.dart | 2 +- .../camera/camera/lib/src/camera_preview.dart | 4 +- packages/camera/camera/pubspec.yaml | 3 +- .../camera/test/camera_preview_test.dart | 4 +- .../lib/src/events/device_event.dart | 3 +- .../platform_interface/camera_platform.dart | 3 +- 53 files changed, 2291 insertions(+), 1902 deletions(-) delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java delete mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_convertPointToMeteringRectangleTest.java rename packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/{CameraRegionUtilsTest.java => CameraRegionUtils_getCameraBoundariesTest.java} (61%) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java delete mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 694898092d7a..68188d6510ff 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,5 +1,8 @@ -## NEXT +## 0.9.0 +* Complete rewrite of Android plugin to fix many capture, focus, flash, orientation and exposure issues. +* Fixed crash when opening front-facing cameras on some legacy android devices like Sony XZ. +* Android Flash mode works with full precapture sequence. * Updated Android lint settings. ## 0.8.1+7 diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 4c1370f2f3cb..4724d22a1bcd 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -4,27 +4,19 @@ package io.flutter.plugins.camera; -import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; - import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.graphics.ImageFormat; -import android.graphics.Rect; import android.graphics.SurfaceTexture; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; -import android.hardware.camera2.CameraCaptureSession.CaptureCallback; -import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraMetadata; -import android.hardware.camera2.CaptureFailure; import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.CaptureResult; import android.hardware.camera2.TotalCaptureResult; -import android.hardware.camera2.params.MeteringRectangle; import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.params.SessionConfiguration; import android.media.CamcorderProfile; @@ -35,27 +27,43 @@ import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Handler; +import android.os.HandlerThread; import android.os.Looper; -import android.os.SystemClock; import android.util.Log; -import android.util.Range; -import android.util.Rational; import android.util.Size; +import android.view.Display; import android.view.Surface; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugins.camera.PictureCaptureRequest.State; +import io.flutter.plugins.camera.features.CameraFeature; +import io.flutter.plugins.camera.features.CameraFeatureFactory; +import io.flutter.plugins.camera.features.CameraFeatures; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; +import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature; +import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature; +import io.flutter.plugins.camera.features.flash.FlashFeature; +import io.flutter.plugins.camera.features.flash.FlashMode; +import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; +import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; +import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; import io.flutter.plugins.camera.media.MediaRecorderBuilder; -import io.flutter.plugins.camera.types.ExposureMode; -import io.flutter.plugins.camera.types.FlashMode; -import io.flutter.plugins.camera.types.FocusMode; -import io.flutter.plugins.camera.types.ResolutionPreset; +import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; import io.flutter.view.TextureRegistry.SurfaceTextureEntry; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -71,151 +79,173 @@ interface ErrorCallback { void onError(String errorCode, String errorMessage); } -public class Camera { +class Camera + implements CameraCaptureCallback.CameraCaptureStateListener, + ImageReader.OnImageAvailableListener, + LifecycleObserver { private static final String TAG = "Camera"; - /** Timeout for the pre-capture sequence. */ - private static final long PRECAPTURE_TIMEOUT_MS = 1000; + private static final HashMap supportedImageFormats; + + // Current supported outputs. + static { + supportedImageFormats = new HashMap<>(); + supportedImageFormats.put("yuv420", ImageFormat.YUV_420_888); + supportedImageFormats.put("jpeg", ImageFormat.JPEG); + } + + /** + * Holds all of the camera features/settings and will be used to update the request builder when + * one changes. + */ + private final CameraFeatures cameraFeatures; private final SurfaceTextureEntry flutterTexture; - private final CameraManager cameraManager; - private final DeviceOrientationManager deviceOrientationListener; - private final boolean isFrontFacing; - private final int sensorOrientation; - private final String cameraName; - private final Size captureSize; - private final Size previewSize; private final boolean enableAudio; private final Context applicationContext; - private final CamcorderProfile recordingProfile; private final DartMessenger dartMessenger; - private final CameraZoom cameraZoom; - private final CameraCharacteristics cameraCharacteristics; + private final CameraProperties cameraProperties; + private final CameraFeatureFactory cameraFeatureFactory; + private final Activity activity; + /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */ + private final CameraCaptureCallback cameraCaptureCallback; + /** A {@link Handler} for running tasks in the background. */ + private Handler backgroundHandler; + + /** An additional thread for running tasks that shouldn't block the UI. */ + private HandlerThread backgroundHandlerThread; private CameraDevice cameraDevice; - private CameraCaptureSession cameraCaptureSession; + private CameraCaptureSession captureSession; private ImageReader pictureImageReader; private ImageReader imageStreamReader; - private CaptureRequest.Builder captureRequestBuilder; + /** {@link CaptureRequest.Builder} for the camera preview */ + private CaptureRequest.Builder previewRequestBuilder; + private MediaRecorder mediaRecorder; + /** True when recording video. */ private boolean recordingVideo; - private File videoRecordingFile; - private FlashMode flashMode; - private ExposureMode exposureMode; - private FocusMode focusMode; - private PictureCaptureRequest pictureCaptureRequest; - private CameraRegions cameraRegions; - private int exposureOffset; - private boolean useAutoFocus = true; - private Range fpsRange; - private PlatformChannel.DeviceOrientation lockedCaptureOrientation; - private long preCaptureStartTime; - private static final HashMap supportedImageFormats; - // Current supported outputs - static { - supportedImageFormats = new HashMap<>(); - supportedImageFormats.put("yuv420", 35); - supportedImageFormats.put("jpeg", 256); - } + private File captureFile; + + /** Holds the current capture timeouts */ + private CaptureTimeoutsWrapper captureTimeouts; + + private MethodChannel.Result flutterResult; public Camera( final Activity activity, final SurfaceTextureEntry flutterTexture, + final CameraFeatureFactory cameraFeatureFactory, final DartMessenger dartMessenger, - final String cameraName, - final String resolutionPreset, - final boolean enableAudio) - throws CameraAccessException { + final CameraProperties cameraProperties, + final ResolutionPreset resolutionPreset, + final boolean enableAudio) { + if (activity == null) { throw new IllegalStateException("No activity available!"); } - this.cameraName = cameraName; + this.activity = activity; this.enableAudio = enableAudio; this.flutterTexture = flutterTexture; this.dartMessenger = dartMessenger; - this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); this.applicationContext = activity.getApplicationContext(); - this.flashMode = FlashMode.auto; - this.exposureMode = ExposureMode.auto; - this.focusMode = FocusMode.auto; - this.exposureOffset = 0; - - cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); - initFps(cameraCharacteristics); - sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); - isFrontFacing = - cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) - == CameraMetadata.LENS_FACING_FRONT; - ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); - recordingProfile = - CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); - captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - previewSize = computeBestPreviewSize(cameraName, preset); - cameraZoom = - new CameraZoom( - cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), - cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); - - deviceOrientationListener = - new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); - deviceOrientationListener.start(); + this.cameraProperties = cameraProperties; + this.cameraFeatureFactory = cameraFeatureFactory; + this.cameraFeatures = + CameraFeatures.init( + cameraFeatureFactory, cameraProperties, activity, dartMessenger, resolutionPreset); + + // Create capture callback. + captureTimeouts = new CaptureTimeoutsWrapper(3000, 3000); + cameraCaptureCallback = CameraCaptureCallback.create(this, captureTimeouts); + + startBackgroundThread(); } - private void initFps(CameraCharacteristics cameraCharacteristics) { - try { - Range[] ranges = - cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); - if (ranges != null) { - for (Range range : ranges) { - int upper = range.getUpper(); - Log.i("Camera", "[FPS Range Available] is:" + range); - if (upper >= 10) { - if (fpsRange == null || upper > fpsRange.getUpper()) { - fpsRange = range; - } - } - } - } - } catch (Exception e) { - e.printStackTrace(); + @Override + public void onConverged() { + takePictureAfterPrecapture(); + } + + @Override + public void onPrecapture() { + runPrecaptureSequence(); + } + + /** + * Updates the builder settings with all of the available features. + * + * @param requestBuilder request builder to update. + */ + private void updateBuilderSettings(CaptureRequest.Builder requestBuilder) { + for (CameraFeature feature : cameraFeatures.getAllFeatures()) { + Log.d(TAG, "Updating builder with feature: " + feature.getDebugName()); + feature.updateBuilder(requestBuilder); } - Log.i("Camera", "[FPS Range] is:" + fpsRange); } private void prepareMediaRecorder(String outputFilePath) throws IOException { + Log.i(TAG, "prepareMediaRecorder"); + if (mediaRecorder != null) { mediaRecorder.release(); } + final PlatformChannel.DeviceOrientation lockedOrientation = + ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) + .getLockedCaptureOrientation(); + mediaRecorder = - new MediaRecorderBuilder(recordingProfile, outputFilePath) + new MediaRecorderBuilder(getRecordingProfile(), outputFilePath) .setEnableAudio(enableAudio) .setMediaOrientation( - lockedCaptureOrientation == null - ? deviceOrientationListener.getMediaOrientation() - : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) + lockedOrientation == null + ? getDeviceOrientationManager().getVideoOrientation() + : getDeviceOrientationManager().getVideoOrientation(lockedOrientation)) .build(); } @SuppressLint("MissingPermission") public void open(String imageFormatGroup) throws CameraAccessException { + final ResolutionFeature resolutionFeature = cameraFeatures.getResolution(); + + if (!resolutionFeature.checkIsSupported()) { + // Tell the user that the camera they are trying to open is not supported, + // as its {@link android.media.CamcorderProfile} cannot be fetched due to the name + // not being a valid parsable integer. + dartMessenger.sendCameraErrorEvent( + "Camera with name \"" + + cameraProperties.getCameraName() + + "\" is not supported by this plugin."); + return; + } + + // Always capture using JPEG format. pictureImageReader = ImageReader.newInstance( - captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); + resolutionFeature.getCaptureSize().getWidth(), + resolutionFeature.getCaptureSize().getHeight(), + ImageFormat.JPEG, + 1); + // For image streaming, use the provided image format or fall back to YUV420. Integer imageFormat = supportedImageFormats.get(imageFormatGroup); if (imageFormat == null) { Log.w(TAG, "The selected imageFormatGroup is not supported by Android. Defaulting to yuv420"); imageFormat = ImageFormat.YUV_420_888; } - - // Used to steam image byte data to dart side. imageStreamReader = - ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 2); + ImageReader.newInstance( + resolutionFeature.getPreviewSize().getWidth(), + resolutionFeature.getPreviewSize().getHeight(), + imageFormat, + 1); + // Open the camera. + CameraManager cameraManager = CameraUtils.getCameraManager(activity); cameraManager.openCamera( - cameraName, + cameraProperties.getCameraName(), new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice device) { @@ -223,12 +253,12 @@ public void onOpened(@NonNull CameraDevice device) { try { startPreview(); dartMessenger.sendCameraInitializedEvent( - previewSize.getWidth(), - previewSize.getHeight(), - exposureMode, - focusMode, - isExposurePointSupported(), - isFocusPointSupported()); + resolutionFeature.getPreviewSize().getWidth(), + resolutionFeature.getPreviewSize().getHeight(), + cameraFeatures.getExposureLock().getValue(), + cameraFeatures.getAutoFocus().getValue(), + cameraFeatures.getExposurePoint().checkIsSupported(), + cameraFeatures.getFocusPoint().checkIsSupported()); } catch (CameraAccessException e) { dartMessenger.sendCameraErrorEvent(e.getMessage()); close(); @@ -237,18 +267,24 @@ public void onOpened(@NonNull CameraDevice device) { @Override public void onClosed(@NonNull CameraDevice camera) { + Log.i(TAG, "open | onClosed"); + dartMessenger.sendCameraClosingEvent(); super.onClosed(camera); } @Override public void onDisconnected(@NonNull CameraDevice cameraDevice) { + Log.i(TAG, "open | onDisconnected"); + close(); dartMessenger.sendCameraErrorEvent("The camera was disconnected."); } @Override public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { + Log.i(TAG, "open | onError"); + close(); String errorDescription; switch (errorCode) { @@ -273,7 +309,7 @@ public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { dartMessenger.sendCameraErrorEvent(errorDescription); } }, - null); + backgroundHandler); } private void createCaptureSession(int templateType, Surface... surfaces) @@ -288,39 +324,45 @@ private void createCaptureSession( closeCaptureSession(); // Create a new capture builder. - captureRequestBuilder = cameraDevice.createCaptureRequest(templateType); + previewRequestBuilder = cameraDevice.createCaptureRequest(templateType); - // Build Flutter surface to render to + // Build Flutter surface to render to. + ResolutionFeature resolutionFeature = cameraFeatures.getResolution(); SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); - surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); + surfaceTexture.setDefaultBufferSize( + resolutionFeature.getPreviewSize().getWidth(), + resolutionFeature.getPreviewSize().getHeight()); Surface flutterSurface = new Surface(surfaceTexture); - captureRequestBuilder.addTarget(flutterSurface); + previewRequestBuilder.addTarget(flutterSurface); List remainingSurfaces = Arrays.asList(surfaces); if (templateType != CameraDevice.TEMPLATE_PREVIEW) { // If it is not preview mode, add all surfaces as targets. for (Surface surface : remainingSurfaces) { - captureRequestBuilder.addTarget(surface); + previewRequestBuilder.addTarget(surface); } } - cameraRegions = new CameraRegions(getRegionBoundaries()); + // Update camera regions. + Size cameraBoundaries = + CameraRegionUtils.getCameraBoundaries(cameraProperties, previewRequestBuilder); + cameraFeatures.getExposurePoint().setCameraBoundaries(cameraBoundaries); + cameraFeatures.getFocusPoint().setCameraBoundaries(cameraBoundaries); - // Prepare the callback + // Prepare the callback. CameraCaptureSession.StateCallback callback = new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession session) { + // Camera was already closed. if (cameraDevice == null) { dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); return; } - cameraCaptureSession = session; + captureSession = session; - updateFpsRange(); - updateFocus(focusMode); - updateFlash(flashMode); - updateExposure(exposureMode); + Log.i(TAG, "Updating builder settings"); + updateBuilderSettings(previewRequestBuilder); refreshPreviewCaptureSession( onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); @@ -332,9 +374,9 @@ public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession } }; - // Start the session + // Start the session. if (VERSION.SDK_INT >= VERSION_CODES.P) { - // Collect all surfaces we want to render to. + // Collect all surfaces to render to. List configs = new ArrayList<>(); configs.add(new OutputConfiguration(flutterSurface)); for (Surface surface : remainingSurfaces) { @@ -342,7 +384,7 @@ public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession } createCaptureSessionWithSessionConfig(configs, callback); } else { - // Collect all surfaces we want to render to. + // Collect all surfaces to render to. List surfaceList = new ArrayList<>(); surfaceList.add(flutterSurface); surfaceList.addAll(remainingSurfaces); @@ -367,276 +409,273 @@ private void createCaptureSessionWithSessionConfig( private void createCaptureSession( List surfaces, CameraCaptureSession.StateCallback callback) throws CameraAccessException { - cameraDevice.createCaptureSession(surfaces, callback, null); + cameraDevice.createCaptureSession(surfaces, callback, backgroundHandler); } + // Send a repeating request to refresh capture session. private void refreshPreviewCaptureSession( @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { - if (cameraCaptureSession == null) { + if (captureSession == null) { + Log.i( + TAG, + "[refreshPreviewCaptureSession] captureSession not yet initialized, " + + "skipping preview capture session refresh."); return; } try { - cameraCaptureSession.setRepeatingRequest( - captureRequestBuilder.build(), - pictureCaptureCallback, - new Handler(Looper.getMainLooper())); + captureSession.setRepeatingRequest( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); if (onSuccessCallback != null) { onSuccessCallback.run(); } - } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { - onErrorCallback.onError("cameraAccess", e.getMessage()); - } - } - private void writeToFile(ByteBuffer buffer, File file) throws IOException { - try (FileOutputStream outputStream = new FileOutputStream(file)) { - while (0 < buffer.remaining()) { - outputStream.getChannel().write(buffer); - } + } catch (CameraAccessException e) { + onErrorCallback.onError("cameraAccess", e.getMessage()); } } public void takePicture(@NonNull final Result result) { - // Only take 1 picture at a time - if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { + // Only take one picture at a time. + if (cameraCaptureCallback.getCameraState() != CameraState.STATE_PREVIEW) { result.error("captureAlreadyActive", "Picture is currently already being captured", null); return; } - // Store the result - this.pictureCaptureRequest = new PictureCaptureRequest(result); - // Create temporary file + flutterResult = result; + + // Create temporary file. final File outputDir = applicationContext.getCacheDir(); - final File file; try { - file = File.createTempFile("CAP", ".jpg", outputDir); + captureFile = File.createTempFile("CAP", ".jpg", outputDir); + captureTimeouts.reset(); } catch (IOException | SecurityException e) { - pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); + dartMessenger.error(flutterResult, "cannotCreateFile", e.getMessage(), null); return; } - // Listen for picture being taken - pictureImageReader.setOnImageAvailableListener( - reader -> { - try (Image image = reader.acquireLatestImage()) { - ByteBuffer buffer = image.getPlanes()[0].getBuffer(); - writeToFile(buffer, file); - pictureCaptureRequest.finish(file.getAbsolutePath()); - } catch (IOException e) { - pictureCaptureRequest.error("IOError", "Failed saving image", null); - } - }, - null); + // Listen for picture being taken. + pictureImageReader.setOnImageAvailableListener(this, backgroundHandler); - if (useAutoFocus) { + final AutoFocusFeature autoFocusFeature = cameraFeatures.getAutoFocus(); + final boolean isAutoFocusSupported = autoFocusFeature.checkIsSupported(); + if (isAutoFocusSupported && autoFocusFeature.getValue() == FocusMode.auto) { runPictureAutoFocus(); } else { - runPicturePreCapture(); + runPrecaptureSequence(); } } - private final CameraCaptureSession.CaptureCallback pictureCaptureCallback = - new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - processCapture(result); - } + /** + * Run the precapture sequence for capturing a still image. This method should be called when a + * response is received in {@link #cameraCaptureCallback} from lockFocus(). + */ + private void runPrecaptureSequence() { + Log.i(TAG, "runPrecaptureSequence"); + try { + // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE_START. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + captureSession.capture( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); + + // Repeating request to refresh preview session. + refreshPreviewCaptureSession( + null, + (code, message) -> dartMessenger.error(flutterResult, "cameraAccess", message, null)); - @Override - public void onCaptureProgressed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureResult partialResult) { - processCapture(partialResult); - } + // Start precapture. + cameraCaptureCallback.setCameraState(CameraState.STATE_WAITING_PRECAPTURE_START); - @Override - public void onCaptureFailed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - if (pictureCaptureRequest == null || pictureCaptureRequest.isFinished()) { - return; - } - String reason; - boolean fatalFailure = false; - switch (failure.getReason()) { - case CaptureFailure.REASON_ERROR: - reason = "An error happened in the framework"; - break; - case CaptureFailure.REASON_FLUSHED: - reason = "The capture has failed due to an abortCaptures() call"; - fatalFailure = true; - break; - default: - reason = "Unknown reason"; - } - Log.w("Camera", "pictureCaptureCallback.onCaptureFailed(): " + reason); - if (fatalFailure) pictureCaptureRequest.error("captureFailure", reason, null); - } - - private void processCapture(CaptureResult result) { - if (pictureCaptureRequest == null) { - return; - } + previewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); - Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); - Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); - switch (pictureCaptureRequest.getState()) { - case focusing: - if (afState == null) { - return; - } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED - || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { - // Some devices might return null here, in which case we will also continue. - if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { - runPictureCapture(); - } else { - runPicturePreCapture(); - } - } - break; - case preCapture: - // Some devices might return null here, in which case we will also continue. - if (aeState == null - || aeState == CaptureRequest.CONTROL_AE_STATE_PRECAPTURE - || aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED - || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { - pictureCaptureRequest.setState(State.waitingPreCaptureReady); - setPreCaptureStartTime(); - } - break; - case waitingPreCaptureReady: - if (aeState == null || aeState != CaptureRequest.CONTROL_AE_STATE_PRECAPTURE) { - runPictureCapture(); - } else { - if (hitPreCaptureTimeout()) { - unlockAutoFocus(); - } - } - } - } - }; + // Trigger one capture to start AE sequence. + captureSession.capture( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); - private void runPictureAutoFocus() { - assert (pictureCaptureRequest != null); - - pictureCaptureRequest.setState(PictureCaptureRequest.State.focusing); - lockAutoFocus(pictureCaptureCallback); + } catch (CameraAccessException e) { + e.printStackTrace(); + } } - private void runPicturePreCapture() { - assert (pictureCaptureRequest != null); - pictureCaptureRequest.setState(PictureCaptureRequest.State.preCapture); + /** + * Capture a still picture. This method should be called when a response is received {@link + * #cameraCaptureCallback} from both lockFocus(). + */ + private void takePictureAfterPrecapture() { + Log.i(TAG, "captureStillPicture"); + cameraCaptureCallback.setCameraState(CameraState.STATE_CAPTURING); - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + if (cameraDevice == null) { + return; + } + // This is the CaptureRequest.Builder that is used to take a picture. + CaptureRequest.Builder stillBuilder; + try { + stillBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + } catch (CameraAccessException e) { + dartMessenger.error(flutterResult, "cameraAccess", e.getMessage(), null); + return; + } + stillBuilder.addTarget(pictureImageReader.getSurface()); + + // Zoom. + stillBuilder.set( + CaptureRequest.SCALER_CROP_REGION, + previewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); + + // Have all features update the builder. + updateBuilderSettings(stillBuilder); + + // Orientation. + final PlatformChannel.DeviceOrientation lockedOrientation = + ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) + .getLockedCaptureOrientation(); + stillBuilder.set( + CaptureRequest.JPEG_ORIENTATION, + lockedOrientation == null + ? getDeviceOrientationManager().getPhotoOrientation() + : getDeviceOrientationManager().getPhotoOrientation(lockedOrientation)); + + CameraCaptureSession.CaptureCallback captureCallback = + new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + unlockAutoFocus(); + } + }; - refreshPreviewCaptureSession( - () -> - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, - CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE), - (code, message) -> pictureCaptureRequest.error(code, message, null)); + try { + captureSession.stopRepeating(); + captureSession.abortCaptures(); + Log.i(TAG, "sending capture request"); + captureSession.capture(stillBuilder.build(), captureCallback, backgroundHandler); + } catch (CameraAccessException e) { + dartMessenger.error(flutterResult, "cameraAccess", e.getMessage(), null); + } + } + + @SuppressWarnings("deprecation") + private Display getDefaultDisplay() { + return activity.getWindowManager().getDefaultDisplay(); } - private void runPictureCapture() { - assert (pictureCaptureRequest != null); - pictureCaptureRequest.setState(PictureCaptureRequest.State.capturing); + /** Starts a background thread and its {@link Handler}. */ + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + public void startBackgroundThread() { + backgroundHandlerThread = new HandlerThread("CameraBackground"); try { - final CaptureRequest.Builder captureBuilder = - cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); - captureBuilder.addTarget(pictureImageReader.getSurface()); - captureBuilder.set( - CaptureRequest.SCALER_CROP_REGION, - captureRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); - captureBuilder.set( - CaptureRequest.JPEG_ORIENTATION, - lockedCaptureOrientation == null - ? deviceOrientationListener.getMediaOrientation() - : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)); - - switch (flashMode) { - case off: - captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); - captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - case auto: - captureBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); - break; - case always: - default: - captureBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); - break; + backgroundHandlerThread.start(); + } catch (IllegalThreadStateException e) { + // Ignore exception in case the thread has already started. + } + backgroundHandler = new Handler(backgroundHandlerThread.getLooper()); + } + + /** Stops the background thread and its {@link Handler}. */ + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + public void stopBackgroundThread() { + if (backgroundHandlerThread != null) { + backgroundHandlerThread.quitSafely(); + try { + backgroundHandlerThread.join(); + } catch (InterruptedException e) { + dartMessenger.error(flutterResult, "cameraAccess", e.getMessage(), null); } - cameraCaptureSession.stopRepeating(); - cameraCaptureSession.capture( - captureBuilder.build(), - new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - unlockAutoFocus(); - } - }, - null); - } catch (CameraAccessException e) { - pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } + backgroundHandlerThread = null; + backgroundHandler = null; + } + + /** Start capturing a picture, doing autofocus first. */ + private void runPictureAutoFocus() { + Log.i(TAG, "runPictureAutoFocus"); + + cameraCaptureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS); + lockAutoFocus(); } - private void lockAutoFocus(CaptureCallback callback) { - captureRequestBuilder.set( + private void lockAutoFocus() { + Log.i(TAG, "lockAutoFocus"); + if (captureSession == null) { + Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); + return; + } + + // Trigger AF to start. + previewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - refreshPreviewCaptureSession( - null, (code, message) -> pictureCaptureRequest.error(code, message, null)); + try { + captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); + } catch (CameraAccessException e) { + dartMessenger.sendCameraErrorEvent(e.getMessage()); + } } + /** Cancel and reset auto focus state and refresh the preview session. */ private void unlockAutoFocus() { - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); - updateFocus(focusMode); + Log.i(TAG, "unlockAutoFocus"); + if (captureSession == null) { + Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); + return; + } try { - cameraCaptureSession.capture(captureRequestBuilder.build(), null, null); - } catch (CameraAccessException ignored) { + // Cancel existing AF state. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); + + // Set AF state to idle again. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + + captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); + } catch (CameraAccessException e) { + dartMessenger.sendCameraErrorEvent(e.getMessage()); + return; } - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); refreshPreviewCaptureSession( null, - (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); + (errorCode, errorMessage) -> + dartMessenger.error(flutterResult, errorCode, errorMessage, null)); } - public void startVideoRecording(Result result) { + public void startVideoRecording(@NonNull Result result) { final File outputDir = applicationContext.getCacheDir(); try { - videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir); + captureFile = File.createTempFile("REC", ".mp4", outputDir); } catch (IOException | SecurityException e) { result.error("cannotCreateFile", e.getMessage(), null); return; } - try { - prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); - recordingVideo = true; + prepareMediaRecorder(captureFile.getAbsolutePath()); + } catch (IOException e) { + recordingVideo = false; + captureFile = null; + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } + // Re-create autofocus feature so it's using video focus mode now. + cameraFeatures.setAutoFocus( + cameraFeatureFactory.createAutoFocusFeature(cameraProperties, true)); + recordingVideo = true; + try { createCaptureSession( CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); result.success(null); - } catch (CameraAccessException | IOException e) { + } catch (CameraAccessException e) { recordingVideo = false; - videoRecordingFile = null; + captureFile = null; result.error("videoRecordingFailed", e.getMessage(), null); } } @@ -646,24 +685,25 @@ public void stopVideoRecording(@NonNull final Result result) { result.success(null); return; } - + // Re-create autofocus feature so it's using continuous capture focus mode now. + cameraFeatures.setAutoFocus( + cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false)); + recordingVideo = false; + try { + captureSession.abortCaptures(); + mediaRecorder.stop(); + } catch (CameraAccessException | IllegalStateException e) { + // Ignore exceptions and try to continue (changes are camera session already aborted capture). + } + mediaRecorder.reset(); try { - recordingVideo = false; - - try { - cameraCaptureSession.abortCaptures(); - mediaRecorder.stop(); - } catch (CameraAccessException | IllegalStateException e) { - // Ignore exceptions and try to continue (changes are camera session already aborted capture) - } - - mediaRecorder.reset(); startPreview(); - result.success(videoRecordingFile.getAbsolutePath()); - videoRecordingFile = null; } catch (CameraAccessException | IllegalStateException e) { result.error("videoRecordingFailed", e.getMessage(), null); + return; } + result.success(captureFile.getAbsolutePath()); + captureFile = null; } public void pauseVideoRecording(@NonNull final Result result) { @@ -709,259 +749,185 @@ public void resumeVideoRecording(@NonNull final Result result) { result.success(null); } - public void setFlashMode(@NonNull final Result result, FlashMode mode) - throws CameraAccessException { - // Get the flash availability - Boolean flashAvailable = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); - - // Check if flash is available. - if (flashAvailable == null || !flashAvailable) { - result.error("setFlashModeFailed", "Device does not have flash capabilities", null); - return; - } + /** + * Method handler for setting new flash modes. + * + * @param result Flutter result. + * @param newMode new mode. + */ + public void setFlashMode(@NonNull final Result result, @NonNull FlashMode newMode) { + // Save the new flash mode setting. + final FlashFeature flashFeature = cameraFeatures.getFlash(); + flashFeature.setValue(newMode); + flashFeature.updateBuilder(previewRequestBuilder); - // If switching directly from torch to auto or on, make sure we turn off the torch. - if (flashMode == FlashMode.torch && mode != FlashMode.torch && mode != FlashMode.off) { - updateFlash(FlashMode.off); - - this.cameraCaptureSession.setRepeatingRequest( - captureRequestBuilder.build(), - new CaptureCallback() { - private boolean isFinished = false; - - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult captureResult) { - if (isFinished) { - return; - } + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); + } - updateFlash(mode); - refreshPreviewCaptureSession( - () -> { - result.success(null); - isFinished = true; - }, - (code, message) -> - result.error("setFlashModeFailed", "Could not set flash mode.", null)); - } + /** + * Method handler for setting new exposure modes. + * + * @param result Flutter result. + * @param newMode new mode. + */ + public void setExposureMode(@NonNull final Result result, @NonNull ExposureMode newMode) { + final ExposureLockFeature exposureLockFeature = cameraFeatures.getExposureLock(); + exposureLockFeature.setValue(newMode); + exposureLockFeature.updateBuilder(previewRequestBuilder); - @Override - public void onCaptureFailed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - if (isFinished) { - return; - } + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> + result.error("setExposureModeFailed", "Could not set exposure mode.", null)); + } - result.error("setFlashModeFailed", "Could not set flash mode.", null); - isFinished = true; - } - }, - null); - } else { - updateFlash(mode); + /** + * Sets new exposure point from dart. + * + * @param result Flutter result. + * @param point The exposure point. + */ + public void setExposurePoint(@NonNull final Result result, @Nullable Point point) { + final ExposurePointFeature exposurePointFeature = cameraFeatures.getExposurePoint(); + exposurePointFeature.setValue(point); + exposurePointFeature.updateBuilder(previewRequestBuilder); - refreshPreviewCaptureSession( - () -> result.success(null), - (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); - } + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> + result.error("setExposurePointFailed", "Could not set exposure point.", null)); } - public void setExposureMode(@NonNull final Result result, ExposureMode mode) - throws CameraAccessException { - updateExposure(mode); - cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); - result.success(null); + /** Return the max exposure offset value supported by the camera to dart. */ + public double getMaxExposureOffset() { + return cameraFeatures.getExposureOffset().getMaxExposureOffset(); } - public void setExposurePoint(@NonNull final Result result, Double x, Double y) - throws CameraAccessException { - // Check if exposure point functionality is available. - if (!isExposurePointSupported()) { - result.error( - "setExposurePointFailed", "Device does not have exposure point capabilities", null); - return; - } - // Check if the current region boundaries are known - if (cameraRegions.getMaxBoundaries() == null) { - result.error("setExposurePointFailed", "Could not determine max region boundaries", null); - return; - } - // Set the metering rectangle - if (x == null || y == null) cameraRegions.resetAutoExposureMeteringRectangle(); - else cameraRegions.setAutoExposureMeteringRectangleFromPoint(y, 1 - x); - // Apply it - updateExposure(exposureMode); - refreshPreviewCaptureSession( - () -> result.success(null), (code, message) -> result.error("CameraAccess", message, null)); + /** Return the min exposure offset value supported by the camera to dart. */ + public double getMinExposureOffset() { + return cameraFeatures.getExposureOffset().getMinExposureOffset(); } - public void setFocusMode(@NonNull final Result result, FocusMode mode) - throws CameraAccessException { - this.focusMode = mode; - - updateFocus(mode); + /** Return the exposure offset step size to dart. */ + public double getExposureOffsetStepSize() { + return cameraFeatures.getExposureOffset().getExposureOffsetStepSize(); + } - switch (mode) { - case auto: - refreshPreviewCaptureSession( - null, (code, message) -> result.error("setFocusMode", message, null)); - break; + /** + * Sets new focus mode from dart. + * + * @param result Flutter result. + * @param newMode New mode. + */ + public void setFocusMode(final Result result, @NonNull FocusMode newMode) { + final AutoFocusFeature autoFocusFeature = cameraFeatures.getAutoFocus(); + autoFocusFeature.setValue(newMode); + autoFocusFeature.updateBuilder(previewRequestBuilder); + + /* + * For focus mode an extra step of actually locking/unlocking the + * focus has to be done, in order to ensure it goes into the correct state. + */ + switch (newMode) { case locked: - lockAutoFocus( - new CaptureCallback() { - @Override - public void onCaptureCompleted( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull TotalCaptureResult result) { - unlockAutoFocus(); - } - }); - break; - } - result.success(null); - } + // Perform a single focus trigger. + lockAutoFocus(); + if (captureSession == null) { + Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); + return; + } - public void setFocusPoint(@NonNull final Result result, Double x, Double y) - throws CameraAccessException { - // Check if focus point functionality is available. - if (!isFocusPointSupported()) { - result.error("setFocusPointFailed", "Device does not have focus point capabilities", null); - return; - } + // Set AF state to idle again. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); - // Check if the current region boundaries are known - if (cameraRegions.getMaxBoundaries() == null) { - result.error("setFocusPointFailed", "Could not determine max region boundaries", null); - return; + try { + captureSession.setRepeatingRequest( + previewRequestBuilder.build(), null, backgroundHandler); + } catch (CameraAccessException e) { + if (result != null) { + result.error("setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); + } + return; + } + break; + case auto: + // Cancel current AF trigger and set AF to idle again. + unlockAutoFocus(); + break; } - // Set the metering rectangle - if (x == null || y == null) { - cameraRegions.resetAutoFocusMeteringRectangle(); - } else { - cameraRegions.setAutoFocusMeteringRectangleFromPoint(y, 1 - x); + if (result != null) { + result.success(null); } - - // Apply the new metering rectangle - setFocusMode(result, focusMode); } - @TargetApi(VERSION_CODES.P) - private boolean supportsDistortionCorrection() throws CameraAccessException { - int[] availableDistortionCorrectionModes = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); - if (availableDistortionCorrectionModes == null) availableDistortionCorrectionModes = new int[0]; - long nonOffModesSupported = - Arrays.stream(availableDistortionCorrectionModes) - .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) - .count(); - return nonOffModesSupported > 0; - } - - private Size getRegionBoundaries() throws CameraAccessException { - // No distortion correction support - if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { - return cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); - } - // Get the current distortion correction mode - Integer distortionCorrectionMode = - captureRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); - // Return the correct boundaries depending on the mode - android.graphics.Rect rect; - if (distortionCorrectionMode == null - || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { - rect = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); - } else { - rect = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); - } - return rect == null ? null : new Size(rect.width(), rect.height()); - } + /** + * Sets new focus point from dart. + * + * @param result Flutter result. + * @param point the new coordinates. + */ + public void setFocusPoint(@NonNull final Result result, @Nullable Point point) { + final FocusPointFeature focusPointFeature = cameraFeatures.getFocusPoint(); + focusPointFeature.setValue(point); + focusPointFeature.updateBuilder(previewRequestBuilder); - private boolean isExposurePointSupported() throws CameraAccessException { - Integer supportedRegions = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); - return supportedRegions != null && supportedRegions > 0; - } + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFocusPointFailed", "Could not set focus point.", null)); - private boolean isFocusPointSupported() throws CameraAccessException { - Integer supportedRegions = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); - return supportedRegions != null && supportedRegions > 0; + this.setFocusMode(null, cameraFeatures.getAutoFocus().getValue()); } - public double getMinExposureOffset() throws CameraAccessException { - Range range = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); - double minStepped = range == null ? 0 : range.getLower(); - double stepSize = getExposureOffsetStepSize(); - return minStepped * stepSize; - } + /** + * Sets a new exposure offset from dart. From dart the offset comes as a double, like +1.3 or + * -1.3. + * + * @param result flutter result. + * @param offset new value. + */ + public void setExposureOffset(@NonNull final Result result, double offset) { + final ExposureOffsetFeature exposureOffsetFeature = cameraFeatures.getExposureOffset(); + exposureOffsetFeature.setValue(offset); + exposureOffsetFeature.updateBuilder(previewRequestBuilder); - public double getMaxExposureOffset() throws CameraAccessException { - Range range = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); - double maxStepped = range == null ? 0 : range.getUpper(); - double stepSize = getExposureOffsetStepSize(); - return maxStepped * stepSize; + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> + result.error("setExposureOffsetFailed", "Could not set exposure offset.", null)); } - public double getExposureOffsetStepSize() throws CameraAccessException { - Rational stepSize = - cameraManager - .getCameraCharacteristics(cameraDevice.getId()) - .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); - return stepSize == null ? 0.0 : stepSize.doubleValue(); + public float getMaxZoomLevel() { + return cameraFeatures.getZoomLevel().getMaximumZoomLevel(); } - public void setExposureOffset(@NonNull final Result result, double offset) - throws CameraAccessException { - // Set the exposure offset - double stepSize = getExposureOffsetStepSize(); - exposureOffset = (int) (offset / stepSize); - // Apply it - updateExposure(exposureMode); - this.cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); - result.success(offset); + public float getMinZoomLevel() { + return cameraFeatures.getZoomLevel().getMinimumZoomLevel(); } - public float getMaxZoomLevel() { - return cameraZoom.maxZoom; + /** Shortcut to get current recording profile. */ + CamcorderProfile getRecordingProfile() { + return cameraFeatures.getResolution().getRecordingProfile(); } - public float getMinZoomLevel() { - return CameraZoom.DEFAULT_ZOOM_FACTOR; + /** Shortut to get deviceOrientationListener. */ + DeviceOrientationManager getDeviceOrientationManager() { + return cameraFeatures.getSensorOrientation().getDeviceOrientationManager(); } + /** + * Sets zoom level from dart. + * + * @param result Flutter result. + * @param zoom new value. + */ public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { - float maxZoom = cameraZoom.maxZoom; - float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; + final ZoomLevelFeature zoomLevel = cameraFeatures.getZoomLevel(); + float maxZoom = zoomLevel.getMaximumZoomLevel(); + float minZoom = zoomLevel.getMinimumZoomLevel(); if (zoom > maxZoom || zoom < minZoom) { String errorMessage = @@ -974,122 +940,31 @@ public void setZoomLevel(@NonNull final Result result, float zoom) throws Camera return; } - //Zoom area is calculated relative to sensor area (activeRect) - if (captureRequestBuilder != null) { - final Rect computedZoom = cameraZoom.computeZoom(zoom); - captureRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); - cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); - } + zoomLevel.setValue(zoom); + zoomLevel.updateBuilder(previewRequestBuilder); - result.success(null); + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setZoomLevelFailed", "Could not set zoom level.", null)); } + /** + * Lock capture orientation from dart. + * + * @param orientation new orientation. + */ public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { - this.lockedCaptureOrientation = orientation; + cameraFeatures.getSensorOrientation().lockCaptureOrientation(orientation); } + /** Unlock capture orientation from dart. */ public void unlockCaptureOrientation() { - this.lockedCaptureOrientation = null; - } - - private void updateFpsRange() { - if (fpsRange == null) { - return; - } - - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); - } - - private void updateFocus(FocusMode mode) { - if (useAutoFocus) { - int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); - // Auto focus is not supported - if (modes == null - || modes.length == 0 - || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)) { - useAutoFocus = false; - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); - } else { - // Applying auto focus - switch (mode) { - case locked: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); - break; - case auto: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, - recordingVideo - ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO - : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); - default: - break; - } - MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle(); - captureRequestBuilder.set( - CaptureRequest.CONTROL_AF_REGIONS, - afRect == null ? null : new MeteringRectangle[] {afRect}); - } - } else { - captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); - } - } - - private void updateExposure(ExposureMode mode) { - exposureMode = mode; - - // Applying auto exposure - MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_REGIONS, - aeRect == null ? null : new MeteringRectangle[] {cameraRegions.getAEMeteringRectangle()}); - - switch (mode) { - case locked: - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); - break; - case auto: - default: - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); - break; - } - - captureRequestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); - } - - private void updateFlash(FlashMode mode) { - // Get flash - flashMode = mode; - - // Applying flash modes - switch (flashMode) { - case off: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - case auto: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - case always: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); - break; - case torch: - default: - captureRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); - captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); - break; - } + cameraFeatures.getSensorOrientation().unlockCaptureOrientation(); } public void startPreview() throws CameraAccessException { if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; + Log.i(TAG, "startPreview"); createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); } @@ -1097,6 +972,7 @@ public void startPreview() throws CameraAccessException { public void startPreviewWithImageStream(EventChannel imageStreamChannel) throws CameraAccessException { createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); + Log.i(TAG, "startPreviewWithImageStream"); imageStreamChannel.setStreamHandler( new EventChannel.StreamHandler() { @@ -1107,15 +983,43 @@ public void onListen(Object o, EventChannel.EventSink imageStreamSink) { @Override public void onCancel(Object o) { - imageStreamReader.setOnImageAvailableListener(null, null); + imageStreamReader.setOnImageAvailableListener(null, backgroundHandler); } }); } + /** + * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a + * still image is ready to be saved. + */ + @Override + public void onImageAvailable(ImageReader reader) { + Log.i(TAG, "onImageAvailable"); + + backgroundHandler.post( + new ImageSaver( + // Use acquireNextImage since image reader is only for one image. + reader.acquireNextImage(), + captureFile, + new ImageSaver.Callback() { + @Override + public void onComplete(String absolutePath) { + dartMessenger.finish(flutterResult, absolutePath); + } + + @Override + public void onError(String errorCode, String errorMessage) { + dartMessenger.error(flutterResult, errorCode, errorMessage, null); + } + })); + cameraCaptureCallback.setCameraState(CameraState.STATE_PREVIEW); + } + private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { imageStreamReader.setOnImageAvailableListener( reader -> { - Image img = reader.acquireLatestImage(); + // Use acquireNextImage since image reader is only for one image. + Image img = reader.acquireNextImage(); if (img == null) return; List> planes = new ArrayList<>(); @@ -1139,41 +1043,24 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i imageBuffer.put("format", img.getFormat()); imageBuffer.put("planes", planes); - imageStreamSink.success(imageBuffer); + final Handler handler = new Handler(Looper.getMainLooper()); + handler.post(() -> imageStreamSink.success(imageBuffer)); img.close(); }, - null); - } - - public void stopImageStream() throws CameraAccessException { - if (imageStreamReader != null) { - imageStreamReader.setOnImageAvailableListener(null, null); - } - startPreview(); - } - - /** Sets the time the pre-capture sequence started. */ - private void setPreCaptureStartTime() { - preCaptureStartTime = SystemClock.elapsedRealtime(); - } - - /** - * Check if the timeout for the pre-capture sequence has been reached. - * - * @return true if the timeout is reached; otherwise false is returned. - */ - private boolean hitPreCaptureTimeout() { - return (SystemClock.elapsedRealtime() - preCaptureStartTime) > PRECAPTURE_TIMEOUT_MS; + backgroundHandler); } private void closeCaptureSession() { - if (cameraCaptureSession != null) { - cameraCaptureSession.close(); - cameraCaptureSession = null; + if (captureSession != null) { + Log.i(TAG, "closeCaptureSession"); + + captureSession.close(); + captureSession = null; } } public void close() { + Log.i(TAG, "close"); closeCaptureSession(); if (cameraDevice != null) { @@ -1193,11 +1080,15 @@ public void close() { mediaRecorder.release(); mediaRecorder = null; } + + stopBackgroundThread(); } public void dispose() { + Log.i(TAG, "dispose"); + close(); flutterTexture.release(); - deviceOrientationListener.stop(); + getDeviceOrientationManager().stop(); } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java index 75730ab41711..ef3a2b9b5d83 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java @@ -8,9 +8,11 @@ import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry; import io.flutter.view.TextureRegistry; @@ -51,7 +53,8 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra registrar.activity(), registrar.messenger(), registrar::addRequestPermissionsResultListener, - registrar.view()); + registrar.view(), + null); } @Override @@ -70,18 +73,17 @@ public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { binding.getActivity(), flutterPluginBinding.getBinaryMessenger(), binding::addRequestPermissionsResultListener, - flutterPluginBinding.getTextureRegistry()); + flutterPluginBinding.getTextureRegistry(), + FlutterLifecycleAdapter.getActivityLifecycle(binding)); } @Override public void onDetachedFromActivity() { - if (methodCallHandler == null) { - // Could be on too low of an SDK to have started listening originally. - return; + // Could be on too low of an SDK to have started listening originally. + if (methodCallHandler != null) { + methodCallHandler.stopListening(); + methodCallHandler = null; } - - methodCallHandler.stopListening(); - methodCallHandler = null; } @Override @@ -98,7 +100,8 @@ private void maybeStartListening( Activity activity, BinaryMessenger messenger, PermissionsRegistry permissionsRegistry, - TextureRegistry textureRegistry) { + TextureRegistry textureRegistry, + @Nullable Lifecycle lifecycle) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { // If the sdk is less than 21 (min sdk for Camera2) we don't register the plugin. return; @@ -106,6 +109,11 @@ private void maybeStartListening( methodCallHandler = new MethodCallHandlerImpl( - activity, messenger, new CameraPermissions(), permissionsRegistry, textureRegistry); + activity, + messenger, + new CameraPermissions(), + permissionsRegistry, + textureRegistry, + lifecycle); } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java index ff8a49f1d148..951a2797d68f 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java @@ -11,6 +11,7 @@ import android.util.Size; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import java.util.Arrays; /** @@ -69,11 +70,32 @@ && supportsDistortionCorrection(cameraProperties)) { * boundaries. */ public static MeteringRectangle convertPointToMeteringRectangle( - @NonNull Size boundaries, double x, double y) { + @NonNull Size boundaries, + double x, + double y, + @NonNull PlatformChannel.DeviceOrientation orientation) { assert (boundaries.getWidth() > 0 && boundaries.getHeight() > 0); assert (x >= 0 && x <= 1); assert (y >= 0 && y <= 1); - + // Rotate the coordinates to match the device orientation. + double oldX = x, oldY = y; + switch (orientation) { + case PORTRAIT_UP: // 90 ccw. + y = 1 - oldX; + x = oldY; + break; + case PORTRAIT_DOWN: // 90 cw. + x = 1 - oldY; + y = oldX; + break; + case LANDSCAPE_LEFT: + // No rotation required. + break; + case LANDSCAPE_RIGHT: // 180. + x = 1 - x; + y = 1 - y; + break; + } // Interpolate the target coordinate. int targetX = (int) Math.round(x * ((double) (boundaries.getWidth() - 1))); int targetY = (int) Math.round(y * ((double) (boundaries.getHeight() - 1))); @@ -98,7 +120,6 @@ public static MeteringRectangle convertPointToMeteringRectangle( if (targetY > maxTargetY) { targetY = maxTargetY; } - // Build the metering rectangle. return MeteringRectangleFactory.create(targetX, targetY, targetWidth, targetHeight, 1); } @@ -130,7 +151,7 @@ static class MeteringRectangleFactory { * @param width width >= 0. * @param height height >= 0. * @param meteringWeight weight between {@value MeteringRectangle#METERING_WEIGHT_MIN} and - * {@value MeteringRectangle#METERING_WEIGHT_MAX} inclusively + * {@value MeteringRectangle#METERING_WEIGHT_MAX} inclusively. * @return new instance of the {@link MeteringRectangle} class. * @throws IllegalArgumentException if any of the parameters were negative. */ diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java deleted file mode 100644 index 60c866cd82d5..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.camera; - -import android.hardware.camera2.params.MeteringRectangle; -import android.util.Size; - -public final class CameraRegions { - private MeteringRectangle aeMeteringRectangle; - private MeteringRectangle afMeteringRectangle; - private Size maxBoundaries; - - public CameraRegions(Size maxBoundaries) { - assert (maxBoundaries == null || maxBoundaries.getWidth() > 0); - assert (maxBoundaries == null || maxBoundaries.getHeight() > 0); - this.maxBoundaries = maxBoundaries; - } - - public MeteringRectangle getAEMeteringRectangle() { - return aeMeteringRectangle; - } - - public MeteringRectangle getAFMeteringRectangle() { - return afMeteringRectangle; - } - - public Size getMaxBoundaries() { - return this.maxBoundaries; - } - - public void resetAutoExposureMeteringRectangle() { - this.aeMeteringRectangle = null; - } - - public void setAutoExposureMeteringRectangleFromPoint(double x, double y) { - this.aeMeteringRectangle = getMeteringRectangleForPoint(maxBoundaries, x, y); - } - - public void resetAutoFocusMeteringRectangle() { - this.afMeteringRectangle = null; - } - - public void setAutoFocusMeteringRectangleFromPoint(double x, double y) { - this.afMeteringRectangle = getMeteringRectangleForPoint(maxBoundaries, x, y); - } - - public MeteringRectangle getMeteringRectangleForPoint(Size maxBoundaries, double x, double y) { - assert (x >= 0 && x <= 1); - assert (y >= 0 && y <= 1); - if (maxBoundaries == null) - throw new IllegalStateException( - "Functionality for managing metering rectangles is unavailable as this CameraRegions instance was initialized with null boundaries."); - - // Interpolate the target coordinate - int targetX = (int) Math.round(x * ((double) (maxBoundaries.getWidth() - 1))); - int targetY = (int) Math.round(y * ((double) (maxBoundaries.getHeight() - 1))); - // Determine the dimensions of the metering triangle (10th of the viewport) - int targetWidth = (int) Math.round(((double) maxBoundaries.getWidth()) / 10d); - int targetHeight = (int) Math.round(((double) maxBoundaries.getHeight()) / 10d); - // Adjust target coordinate to represent top-left corner of metering rectangle - targetX -= targetWidth / 2; - targetY -= targetHeight / 2; - // Adjust target coordinate as to not fall out of bounds - if (targetX < 0) targetX = 0; - if (targetY < 0) targetY = 0; - int maxTargetX = maxBoundaries.getWidth() - 1 - targetWidth; - int maxTargetY = maxBoundaries.getHeight() - 1 - targetHeight; - if (targetX > maxTargetX) targetX = maxTargetX; - if (targetY > maxTargetY) targetY = maxTargetY; - - // Build the metering rectangle - return new MeteringRectangle(targetX, targetY, targetWidth, targetHeight, 1); - } -} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java index b4d4689f2b4e..003d80a6c241 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java @@ -6,20 +6,12 @@ import android.app.Activity; import android.content.Context; -import android.graphics.ImageFormat; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraMetadata; -import android.hardware.camera2.params.StreamConfigurationMap; -import android.media.CamcorderProfile; -import android.util.Size; import io.flutter.embedding.engine.systemchannels.PlatformChannel; -import io.flutter.plugins.camera.types.ResolutionPreset; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -29,23 +21,24 @@ public final class CameraUtils { private CameraUtils() {} - static PlatformChannel.DeviceOrientation getDeviceOrientationFromDegrees(int degrees) { - // Round to the nearest 90 degrees. - degrees = (int) (Math.round(degrees / 90.0) * 90) % 360; - // Determine the corresponding device orientation. - switch (degrees) { - case 90: - return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; - case 180: - return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; - case 270: - return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; - case 0: - default: - return PlatformChannel.DeviceOrientation.PORTRAIT_UP; - } + /** + * Gets the {@link CameraManager} singleton. + * + * @param context The context to get the {@link CameraManager} singleton from. + * @return The {@link CameraManager} singleton. + */ + static CameraManager getCameraManager(Context context) { + return (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); } + /** + * Serializes the {@link PlatformChannel.DeviceOrientation} to a string value. + * + * @param orientation The orientation to serialize. + * @return The serialized orientation. + * @throws UnsupportedOperationException when the provided orientation not have a corresponding + * string value. + */ static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orientation) { if (orientation == null) throw new UnsupportedOperationException("Could not serialize null device orientation."); @@ -64,6 +57,15 @@ static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orien } } + /** + * Deserializes a string value to its corresponding {@link PlatformChannel.DeviceOrientation} + * value. + * + * @param orientation The string value to deserialize. + * @return The deserialized orientation. + * @throws UnsupportedOperationException when the provided string value does not have a + * corresponding {@link PlatformChannel.DeviceOrientation}. + */ static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String orientation) { if (orientation == null) throw new UnsupportedOperationException("Could not deserialize null device orientation."); @@ -82,23 +84,13 @@ static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String ori } } - static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) { - if (preset.ordinal() > ResolutionPreset.high.ordinal()) { - preset = ResolutionPreset.high; - } - - CamcorderProfile profile = - getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); - return new Size(profile.videoFrameWidth, profile.videoFrameHeight); - } - - static Size computeBestCaptureSize(StreamConfigurationMap streamConfigurationMap) { - // For still image captures, we use the largest available size. - return Collections.max( - Arrays.asList(streamConfigurationMap.getOutputSizes(ImageFormat.JPEG)), - new CompareSizesByArea()); - } - + /** + * Gets all the available cameras for the device. + * + * @param activity The current Android activity. + * @return A map of all the available cameras, with their name as their key. + * @throws CameraAccessException when the camera could not be accessed. + */ public static List> getAvailableCameras(Activity activity) throws CameraAccessException { CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); @@ -127,52 +119,4 @@ public static List> getAvailableCameras(Activity activity) } return cameras; } - - static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( - String cameraName, ResolutionPreset preset) { - int cameraId = Integer.parseInt(cameraName); - switch (preset) { - // All of these cases deliberately fall through to get the best available profile. - case max: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH); - } - case ultraHigh: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P); - } - case veryHigh: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P); - } - case high: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P); - } - case medium: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P); - } - case low: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QVGA); - } - default: - if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) { - return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW); - } else { - throw new IllegalArgumentException( - "No capture session available for current capture session."); - } - } - } - - private static class CompareSizesByArea implements Comparator { - @Override - public int compare(Size lhs, Size rhs) { - // We cast here to ensure the multiplications won't overflow. - return Long.signum( - (long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); - } - } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java index 93b963e65821..dc62fce524d3 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java @@ -11,8 +11,8 @@ import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugins.camera.types.ExposureMode; -import io.flutter.plugins.camera.types.FocusMode; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; import java.util.HashMap; import java.util.Map; @@ -178,4 +178,28 @@ public void run() { } }); } + + /** + * Send a success payload to a {@link MethodChannel.Result} on the main thread. + * + * @param payload The payload to send. + */ + public void finish(MethodChannel.Result result, Object payload) { + handler.post(() -> result.success(payload)); + } + + /** + * Send an error payload to a {@link MethodChannel.Result} on the main thread. + * + * @param errorCode error code. + * @param errorMessage error message. + * @param errorDetails error details. + */ + public void error( + MethodChannel.Result result, + String errorCode, + @Nullable String errorMessage, + @Nullable Object errorDetails) { + handler.post(() -> result.error(errorCode, errorMessage, errorDetails)); + } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java deleted file mode 100644 index 634596dde8bb..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.camera; - -import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Configuration; -import android.hardware.SensorManager; -import android.provider.Settings; -import android.view.Display; -import android.view.OrientationEventListener; -import android.view.Surface; -import android.view.WindowManager; -import io.flutter.embedding.engine.systemchannels.PlatformChannel; - -class DeviceOrientationManager { - - private static final IntentFilter orientationIntentFilter = - new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); - - private final Activity activity; - private final DartMessenger messenger; - private final boolean isFrontFacing; - private final int sensorOrientation; - private PlatformChannel.DeviceOrientation lastOrientation; - private OrientationEventListener orientationEventListener; - private BroadcastReceiver broadcastReceiver; - - public DeviceOrientationManager( - Activity activity, DartMessenger messenger, boolean isFrontFacing, int sensorOrientation) { - this.activity = activity; - this.messenger = messenger; - this.isFrontFacing = isFrontFacing; - this.sensorOrientation = sensorOrientation; - } - - public void start() { - startSensorListener(); - startUIListener(); - } - - public void stop() { - stopSensorListener(); - stopUIListener(); - } - - public int getMediaOrientation() { - return this.getMediaOrientation(this.lastOrientation); - } - - public int getMediaOrientation(PlatformChannel.DeviceOrientation orientation) { - int angle = 0; - - // Fallback to device orientation when the orientation value is null - if (orientation == null) { - orientation = getUIOrientation(); - } - - switch (orientation) { - case PORTRAIT_UP: - angle = 0; - break; - case PORTRAIT_DOWN: - angle = 180; - break; - case LANDSCAPE_LEFT: - angle = 90; - break; - case LANDSCAPE_RIGHT: - angle = 270; - break; - } - if (isFrontFacing) angle *= -1; - return (angle + sensorOrientation + 360) % 360; - } - - private void startSensorListener() { - if (orientationEventListener != null) return; - orientationEventListener = - new OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { - @Override - public void onOrientationChanged(int angle) { - if (!isSystemAutoRotationLocked()) { - PlatformChannel.DeviceOrientation newOrientation = calculateSensorOrientation(angle); - if (!newOrientation.equals(lastOrientation)) { - lastOrientation = newOrientation; - messenger.sendDeviceOrientationChangeEvent(newOrientation); - } - } - } - }; - if (orientationEventListener.canDetectOrientation()) { - orientationEventListener.enable(); - } - } - - private void startUIListener() { - if (broadcastReceiver != null) return; - broadcastReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (isSystemAutoRotationLocked()) { - PlatformChannel.DeviceOrientation orientation = getUIOrientation(); - if (!orientation.equals(lastOrientation)) { - lastOrientation = orientation; - messenger.sendDeviceOrientationChangeEvent(orientation); - } - } - } - }; - activity.registerReceiver(broadcastReceiver, orientationIntentFilter); - broadcastReceiver.onReceive(activity, null); - } - - private void stopSensorListener() { - if (orientationEventListener == null) return; - orientationEventListener.disable(); - orientationEventListener = null; - } - - private void stopUIListener() { - if (broadcastReceiver == null) return; - activity.unregisterReceiver(broadcastReceiver); - broadcastReceiver = null; - } - - private boolean isSystemAutoRotationLocked() { - return android.provider.Settings.System.getInt( - activity.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) - != 1; - } - - private PlatformChannel.DeviceOrientation getUIOrientation() { - final int rotation = getDisplay().getRotation(); - final int orientation = activity.getResources().getConfiguration().orientation; - - switch (orientation) { - case Configuration.ORIENTATION_PORTRAIT: - if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { - return PlatformChannel.DeviceOrientation.PORTRAIT_UP; - } else { - return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; - } - case Configuration.ORIENTATION_LANDSCAPE: - if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { - return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; - } else { - return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; - } - default: - return PlatformChannel.DeviceOrientation.PORTRAIT_UP; - } - } - - private PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) { - final int tolerance = 45; - angle += tolerance; - - // Orientation is 0 in the default orientation mode. This is portait-mode for phones - // and landscape for tablets. We have to compensate for this by calculating the default - // orientation, and apply an offset accordingly. - int defaultDeviceOrientation = getDeviceDefaultOrientation(); - if (defaultDeviceOrientation == Configuration.ORIENTATION_LANDSCAPE) { - angle += 90; - } - // Determine the orientation - angle = angle % 360; - return new PlatformChannel.DeviceOrientation[] { - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - } - [angle / 90]; - } - - private int getDeviceDefaultOrientation() { - Configuration config = activity.getResources().getConfiguration(); - int rotation = getDisplay().getRotation(); - if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) - && config.orientation == Configuration.ORIENTATION_LANDSCAPE) - || ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) - && config.orientation == Configuration.ORIENTATION_PORTRAIT)) { - return Configuration.ORIENTATION_LANDSCAPE; - } else { - return Configuration.ORIENTATION_PORTRAIT; - } - } - - @SuppressWarnings("deprecation") - private Display getDisplay() { - return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); - } -} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index 50bca6349217..893785f1a58f 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -10,6 +10,8 @@ import android.os.Looper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; @@ -17,14 +19,17 @@ import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry; -import io.flutter.plugins.camera.types.ExposureMode; -import io.flutter.plugins.camera.types.FlashMode; -import io.flutter.plugins.camera.types.FocusMode; +import io.flutter.plugins.camera.features.CameraFeatureFactoryImpl; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; +import io.flutter.plugins.camera.features.flash.FlashMode; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; import io.flutter.view.TextureRegistry; import java.util.HashMap; import java.util.Map; -final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { +final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler, LifecycleObserver { private final Activity activity; private final BinaryMessenger messenger; private final CameraPermissions cameraPermissions; @@ -32,6 +37,7 @@ final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { private final TextureRegistry textureRegistry; private final MethodChannel methodChannel; private final EventChannel imageStreamChannel; + private final Lifecycle lifecycle; private @Nullable Camera camera; MethodCallHandlerImpl( @@ -39,12 +45,14 @@ final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { BinaryMessenger messenger, CameraPermissions cameraPermissions, PermissionsRegistry permissionsAdder, - TextureRegistry textureRegistry) { + TextureRegistry textureRegistry, + @Nullable Lifecycle lifecycle) { this.activity = activity; this.messenger = messenger; this.cameraPermissions = cameraPermissions; this.permissionsRegistry = permissionsAdder; this.textureRegistry = textureRegistry; + this.lifecycle = lifecycle; methodChannel = new MethodChannel(messenger, "plugins.flutter.io/camera"); imageStreamChannel = new EventChannel(messenger, "plugins.flutter.io/camera/imageStream"); @@ -172,7 +180,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) y = call.argument("y"); } try { - camera.setExposurePoint(result, x, y); + camera.setExposurePoint(result, new Point(x, y)); } catch (Exception e) { handleException(e, result); } @@ -239,7 +247,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) y = call.argument("y"); } try { - camera.setFocusPoint(result, x, y); + camera.setFocusPoint(result, new Point(x, y)); } catch (Exception e) { handleException(e, result); } @@ -351,22 +359,36 @@ void stopListening() { private void instantiateCamera(MethodCall call, Result result) throws CameraAccessException { String cameraName = call.argument("cameraName"); - String resolutionPreset = call.argument("resolutionPreset"); + String preset = call.argument("resolutionPreset"); boolean enableAudio = call.argument("enableAudio"); + TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture = textureRegistry.createSurfaceTexture(); DartMessenger dartMessenger = new DartMessenger( messenger, flutterSurfaceTexture.id(), new Handler(Looper.getMainLooper())); + CameraProperties cameraProperties = + new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity)); + ResolutionPreset resolutionPreset = ResolutionPreset.valueOf(preset); + + if (camera != null && lifecycle != null) { + lifecycle.removeObserver(camera); + } + camera = new Camera( activity, flutterSurfaceTexture, + new CameraFeatureFactoryImpl(), dartMessenger, - cameraName, + cameraProperties, resolutionPreset, enableAudio); + if (lifecycle != null) { + lifecycle.addObserver(camera); + } + Map reply = new HashMap<>(); reply.put("cameraId", flutterSurfaceTexture.id()); result.success(reply); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java deleted file mode 100644 index 4c11e2d40e62..000000000000 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.camera; - -import android.os.Handler; -import android.os.Looper; -import androidx.annotation.Nullable; -import io.flutter.plugin.common.MethodChannel; - -class PictureCaptureRequest { - - enum State { - idle, - focusing, - preCapture, - waitingPreCaptureReady, - capturing, - finished, - error, - } - - private final Runnable timeoutCallback = - new Runnable() { - @Override - public void run() { - error("captureTimeout", "Picture capture request timed out", state.toString()); - } - }; - - private final MethodChannel.Result result; - private final TimeoutHandler timeoutHandler; - private State state; - - public PictureCaptureRequest(MethodChannel.Result result) { - this(result, new TimeoutHandler()); - } - - public PictureCaptureRequest(MethodChannel.Result result, TimeoutHandler timeoutHandler) { - this.result = result; - this.state = State.idle; - this.timeoutHandler = timeoutHandler; - } - - public void setState(State state) { - if (isFinished()) throw new IllegalStateException("Request has already been finished"); - this.state = state; - if (state != State.idle && state != State.finished && state != State.error) { - this.timeoutHandler.resetTimeout(timeoutCallback); - } else { - this.timeoutHandler.clearTimeout(timeoutCallback); - } - } - - public State getState() { - return state; - } - - public boolean isFinished() { - return state == State.finished || state == State.error; - } - - public void finish(String absolutePath) { - if (isFinished()) throw new IllegalStateException("Request has already been finished"); - this.timeoutHandler.clearTimeout(timeoutCallback); - result.success(absolutePath); - state = State.finished; - } - - public void error( - String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { - if (isFinished()) throw new IllegalStateException("Request has already been finished"); - this.timeoutHandler.clearTimeout(timeoutCallback); - result.error(errorCode, errorMessage, errorDetails); - state = State.error; - } - - static class TimeoutHandler { - private static final int REQUEST_TIMEOUT = 5000; - private final Handler handler; - - TimeoutHandler() { - this.handler = new Handler(Looper.getMainLooper()); - } - - public void resetTimeout(Runnable runnable) { - clearTimeout(runnable); - handler.postDelayed(runnable, REQUEST_TIMEOUT); - } - - public void clearTimeout(Runnable runnable) { - handler.removeCallbacks(runnable); - } - } -} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java index 8d10c445788c..b91f9a1c03f7 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java @@ -84,9 +84,13 @@ ResolutionFeature createResolutionFeature( * * @param cameraProperties instance of the CameraProperties class containing information about the * cameras features. + * @param sensorOrientationFeature instance of the SensorOrientationFeature class containing + * information about the sensor and device orientation. * @return newly created instance of the FocusPointFeature class. */ - FocusPointFeature createFocusPointFeature(@NonNull CameraProperties cameraProperties); + FocusPointFeature createFocusPointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature); /** * Creates a new instance of the FPS range feature. @@ -126,9 +130,13 @@ SensorOrientationFeature createSensorOrientationFeature( * * @param cameraProperties instance of the CameraProperties class containing information about the * cameras features. + * @param sensorOrientationFeature instance of the SensorOrientationFeature class containing + * information about the sensor and device orientation. * @return newly created instance of the ExposurePointFeature class. */ - ExposurePointFeature createExposurePointFeature(@NonNull CameraProperties cameraProperties); + ExposurePointFeature createExposurePointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature); /** * Creates a new instance of the noise reduction feature. diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java index b12ad3626226..95a8c06caa0a 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java @@ -59,8 +59,10 @@ public ResolutionFeature createResolutionFeature( } @Override - public FocusPointFeature createFocusPointFeature(@NonNull CameraProperties cameraProperties) { - return new FocusPointFeature(cameraProperties); + public FocusPointFeature createFocusPointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature) { + return new FocusPointFeature(cameraProperties, sensorOrientationFeature); } @Override @@ -83,8 +85,9 @@ public ZoomLevelFeature createZoomLevelFeature(@NonNull CameraProperties cameraP @Override public ExposurePointFeature createExposurePointFeature( - @NonNull CameraProperties cameraProperties) { - return new ExposurePointFeature(cameraProperties); + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature) { + return new ExposurePointFeature(cameraProperties, sensorOrientationFeature); } @Override diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java index 0ee8969071bc..659fd15963e9 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java @@ -4,6 +4,9 @@ package io.flutter.plugins.camera.features; +import android.app.Activity; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.DartMessenger; import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature; import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature; @@ -13,6 +16,7 @@ import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature; import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature; import io.flutter.plugins.camera.features.resolution.ResolutionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; import java.util.Collection; @@ -37,6 +41,39 @@ public class CameraFeatures { private static final String SENSOR_ORIENTATION = "SENSOR_ORIENTATION"; private static final String ZOOM_LEVEL = "ZOOM_LEVEL"; + public static CameraFeatures init( + CameraFeatureFactory cameraFeatureFactory, + CameraProperties cameraProperties, + Activity activity, + DartMessenger dartMessenger, + ResolutionPreset resolutionPreset) { + CameraFeatures cameraFeatures = new CameraFeatures(); + cameraFeatures.setAutoFocus( + cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false)); + cameraFeatures.setExposureLock( + cameraFeatureFactory.createExposureLockFeature(cameraProperties)); + cameraFeatures.setExposureOffset( + cameraFeatureFactory.createExposureOffsetFeature(cameraProperties)); + SensorOrientationFeature sensorOrientationFeature = + cameraFeatureFactory.createSensorOrientationFeature( + cameraProperties, activity, dartMessenger); + cameraFeatures.setSensorOrientation(sensorOrientationFeature); + cameraFeatures.setExposurePoint( + cameraFeatureFactory.createExposurePointFeature( + cameraProperties, sensorOrientationFeature)); + cameraFeatures.setFlash(cameraFeatureFactory.createFlashFeature(cameraProperties)); + cameraFeatures.setFocusPoint( + cameraFeatureFactory.createFocusPointFeature(cameraProperties, sensorOrientationFeature)); + cameraFeatures.setFpsRange(cameraFeatureFactory.createFpsRangeFeature(cameraProperties)); + cameraFeatures.setNoiseReduction( + cameraFeatureFactory.createNoiseReductionFeature(cameraProperties)); + cameraFeatures.setResolution( + cameraFeatureFactory.createResolutionFeature( + cameraProperties, resolutionPreset, cameraProperties.getCameraName())); + cameraFeatures.setZoomLevel(cameraFeatureFactory.createZoomLevelFeature(cameraProperties)); + return cameraFeatures; + } + private Map featureMap = new HashMap<>(); /** diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java index 8c2ee6167846..336e756e9ed8 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java @@ -8,10 +8,12 @@ import android.hardware.camera2.params.MeteringRectangle; import android.util.Size; import androidx.annotation.NonNull; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegionUtils; import io.flutter.plugins.camera.features.CameraFeature; import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; /** Exposure point controls where in the frame exposure metering will come from. */ public class ExposurePointFeature extends CameraFeature { @@ -19,14 +21,17 @@ public class ExposurePointFeature extends CameraFeature { private Size cameraBoundaries; private Point exposurePoint; private MeteringRectangle exposureRectangle; + private final SensorOrientationFeature sensorOrientationFeature; /** * Creates a new instance of the {@link ExposurePointFeature}. * * @param cameraProperties Collection of the characteristics for the current camera device. */ - public ExposurePointFeature(CameraProperties cameraProperties) { + public ExposurePointFeature( + CameraProperties cameraProperties, SensorOrientationFeature sensorOrientationFeature) { super(cameraProperties); + this.sensorOrientationFeature = sensorOrientationFeature; } /** @@ -80,9 +85,15 @@ private void buildExposureRectangle() { if (this.exposurePoint == null) { this.exposureRectangle = null; } else { + PlatformChannel.DeviceOrientation orientation = + this.sensorOrientationFeature.getLockedCaptureOrientation(); + if (orientation == null) { + orientation = + this.sensorOrientationFeature.getDeviceOrientationManager().getLastUIOrientation(); + } this.exposureRectangle = CameraRegionUtils.convertPointToMeteringRectangle( - this.cameraBoundaries, this.exposurePoint.x, this.exposurePoint.y); + this.cameraBoundaries, this.exposurePoint.x, this.exposurePoint.y, orientation); } } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java index 92fcfa9f1132..a3a0172d3c37 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java @@ -8,10 +8,12 @@ import android.hardware.camera2.params.MeteringRectangle; import android.util.Size; import androidx.annotation.NonNull; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegionUtils; import io.flutter.plugins.camera.features.CameraFeature; import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; /** Focus point controls where in the frame focus will come from. */ public class FocusPointFeature extends CameraFeature { @@ -19,14 +21,17 @@ public class FocusPointFeature extends CameraFeature { private Size cameraBoundaries; private Point focusPoint; private MeteringRectangle focusRectangle; + private final SensorOrientationFeature sensorOrientationFeature; /** * Creates a new instance of the {@link FocusPointFeature}. * * @param cameraProperties Collection of the characteristics for the current camera device. */ - public FocusPointFeature(CameraProperties cameraProperties) { + public FocusPointFeature( + CameraProperties cameraProperties, SensorOrientationFeature sensorOrientationFeature) { super(cameraProperties); + this.sensorOrientationFeature = sensorOrientationFeature; } /** @@ -80,9 +85,15 @@ private void buildFocusRectangle() { if (this.focusPoint == null) { this.focusRectangle = null; } else { + PlatformChannel.DeviceOrientation orientation = + this.sensorOrientationFeature.getLockedCaptureOrientation(); + if (orientation == null) { + orientation = + this.sensorOrientationFeature.getDeviceOrientationManager().getLastUIOrientation(); + } this.focusRectangle = CameraRegionUtils.convertPointToMeteringRectangle( - this.cameraBoundaries, this.focusPoint.x, this.focusPoint.y); + this.cameraBoundaries, this.focusPoint.x, this.focusPoint.y, orientation); } } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java index 847a817641ab..408575b375e6 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java @@ -20,9 +20,15 @@ public class NoiseReductionFeature extends CameraFeature { private NoiseReductionMode currentSetting = NoiseReductionMode.fast; - private static final HashMap NOISE_REDUCTION_MODES = new HashMap<>(); + private final HashMap NOISE_REDUCTION_MODES = new HashMap<>(); - static { + /** + * Creates a new instance of the {@link NoiseReductionFeature}. + * + * @param cameraProperties Collection of the characteristics for the current camera device. + */ + public NoiseReductionFeature(CameraProperties cameraProperties) { + super(cameraProperties); NOISE_REDUCTION_MODES.put(NoiseReductionMode.off, CaptureRequest.NOISE_REDUCTION_MODE_OFF); NOISE_REDUCTION_MODES.put(NoiseReductionMode.fast, CaptureRequest.NOISE_REDUCTION_MODE_FAST); NOISE_REDUCTION_MODES.put( @@ -35,15 +41,6 @@ public class NoiseReductionFeature extends CameraFeature { } } - /** - * Creates a new instance of the {@link NoiseReductionFeature}. - * - * @param cameraProperties Collection of the characteristics for the current camera device. - */ - public NoiseReductionFeature(CameraProperties cameraProperties) { - super(cameraProperties); - } - @Override public String getDebugName() { return "NoiseReductionFeature"; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java index 2a04caad743a..dd1e489e6225 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java @@ -10,10 +10,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; -import android.hardware.SensorManager; -import android.provider.Settings; import android.view.Display; -import android.view.OrientationEventListener; import android.view.Surface; import android.view.WindowManager; import androidx.annotation.NonNull; @@ -35,7 +32,6 @@ public class DeviceOrientationManager { private final boolean isFrontFacing; private final int sensorOrientation; private PlatformChannel.DeviceOrientation lastOrientation; - private OrientationEventListener orientationEventListener; private BroadcastReceiver broadcastReceiver; /** Factory method to create a device orientation manager. */ @@ -63,7 +59,7 @@ private DeviceOrientationManager( * *

When orientation information is updated the new orientation is send to the client using the * {@link DartMessenger}. This latest value can also be retrieved through the {@link - * #getMediaOrientation()} accessor. + * #getVideoOrientation()} accessor. * *

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

The orientation is returned in degrees and could be one of the following values: + *

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

    - *
  • 0: Indicates the device is currently in portrait. - *
  • 90: Indicates the device is currently in landscape left. - *
  • 180: Indicates the device is currently in portrait down. - *
  • 270: Indicates the device is currently in landscape right. - *
+ * @return The device's photo orientation in degrees. + */ + public int getPhotoOrientation() { + return this.getPhotoOrientation(this.lastOrientation); + } + + /** + * Returns the device's photo orientation in degrees based on the sensor orientation and the + * supplied {@link PlatformChannel.DeviceOrientation} value. * - * @return The last captured orientation in degrees + *

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

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

+ * @return The device's video orientation in degrees. + */ + public int getVideoOrientation() { + return this.getVideoOrientation(this.lastOrientation); + } + + /** + * Returns the device's video orientation in degrees based on the sensor orientation and the + * supplied {@link PlatformChannel.DeviceOrientation} value. * - *

    - *
  • PORTRAIT_UP: converts to 0 degrees. - *
  • LANDSCAPE_LEFT: converts to 90 degrees. - *
  • PORTRAIT_DOWN: converts to 180 degrees. - *
  • LANDSCAPE_RIGHT: converts to 270 degrees. - *
+ *

Returns one of 0, 90, 180 or 270. * * @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted * into degrees. - * @return The device's orientation in degrees. + * @return The device's video orientation in degrees. */ - public int getMediaOrientation(PlatformChannel.DeviceOrientation orientation) { + public int getVideoOrientation(PlatformChannel.DeviceOrientation orientation) { int angle = 0; - // Fallback to device orientation when the orientation value is null + // Fallback to device orientation when the orientation value is null. if (orientation == null) { orientation = getUIOrientation(); } @@ -146,51 +193,9 @@ public int getMediaOrientation(PlatformChannel.DeviceOrientation orientation) { return (angle + sensorOrientation + 360) % 360; } - private void startSensorListener() { - if (orientationEventListener != null) { - return; - } - orientationEventListener = - new OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { - @Override - public void onOrientationChanged(int angle) { - handleSensorOrientationChange(angle); - } - }; - if (orientationEventListener.canDetectOrientation()) { - orientationEventListener.enable(); - } - } - - private void startUIListener() { - if (broadcastReceiver != null) { - return; - } - broadcastReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - handleUIOrientationChange(); - } - }; - activity.registerReceiver(broadcastReceiver, orientationIntentFilter); - broadcastReceiver.onReceive(activity, null); - } - - /** - * Handles orientation changes based on information from the device's sensors. - * - *

This method is visible for testing purposes only and should never be used outside this - * class. - * - * @param angle of the current orientation. - */ - @VisibleForTesting - void handleSensorOrientationChange(int angle) { - if (!isAccelerometerRotationLocked()) { - PlatformChannel.DeviceOrientation orientation = calculateSensorOrientation(angle); - lastOrientation = handleOrientationChange(orientation, lastOrientation, messenger); - } + /** @return the last received UI orientation. */ + public PlatformChannel.DeviceOrientation getLastUIOrientation() { + return this.lastOrientation; } /** @@ -201,10 +206,9 @@ void handleSensorOrientationChange(int angle) { */ @VisibleForTesting void handleUIOrientationChange() { - if (isAccelerometerRotationLocked()) { - PlatformChannel.DeviceOrientation orientation = getUIOrientation(); - lastOrientation = handleOrientationChange(orientation, lastOrientation, messenger); - } + PlatformChannel.DeviceOrientation orientation = getUIOrientation(); + handleOrientationChange(orientation, lastOrientation, messenger); + lastOrientation = orientation; } /** @@ -215,37 +219,13 @@ void handleUIOrientationChange() { * class. */ @VisibleForTesting - static DeviceOrientation handleOrientationChange( + static void handleOrientationChange( DeviceOrientation newOrientation, DeviceOrientation previousOrientation, DartMessenger messenger) { if (!newOrientation.equals(previousOrientation)) { messenger.sendDeviceOrientationChangeEvent(newOrientation); } - - return newOrientation; - } - - private void stopSensorListener() { - if (orientationEventListener == null) { - return; - } - orientationEventListener.disable(); - orientationEventListener = null; - } - - private void stopUIListener() { - if (broadcastReceiver == null) { - return; - } - activity.unregisterReceiver(broadcastReceiver); - broadcastReceiver = null; - } - - private boolean isAccelerometerRotationLocked() { - return android.provider.Settings.System.getInt( - activity.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) - != 1; } /** diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java index 2c0381744191..40db12ee0fc3 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java @@ -41,7 +41,7 @@ public void before() { } @Test - public void ctor_Should_return_valid_instance() throws CameraAccessException { + public void ctor_shouldReturnValidInstance() throws CameraAccessException { verify(mockCameraManager, times(1)).getCameraCharacteristics(CAMERA_NAME); assertNotNull(cameraProperties); } @@ -76,8 +76,7 @@ public void getControlAutoExposureCompensationRangeTest() { } @Test - public void - getControlAutoExposureCompensationStep_Should_return_double_When_rational_is_not_null() { + public void getControlAutoExposureCompensationStep_shouldReturnDoubleWhenRationalIsNotNull() { double expectedStep = 3.1415926535; Rational mockRational = mock(Rational.class); @@ -92,7 +91,7 @@ public void getControlAutoExposureCompensationRangeTest() { } @Test - public void getControlAutoExposureCompensationStep_Should_return_zero_When_rational_is_null() { + public void getControlAutoExposureCompensationStep_shouldReturnZeroWhenRationalIsNull() { double expectedStep = 0.0; when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP)) diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_convertPointToMeteringRectangleTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_convertPointToMeteringRectangleTest.java new file mode 100644 index 000000000000..2c6d9d9177e9 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_convertPointToMeteringRectangleTest.java @@ -0,0 +1,197 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import android.hardware.camera2.params.MeteringRectangle; +import android.util.Size; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +public class CameraRegionUtils_convertPointToMeteringRectangleTest { + private MockedStatic mockedMeteringRectangleFactory; + private Size mockCameraBoundaries; + + @Before + public void setUp() { + this.mockCameraBoundaries = mock(Size.class); + when(this.mockCameraBoundaries.getWidth()).thenReturn(100); + when(this.mockCameraBoundaries.getHeight()).thenReturn(100); + mockedMeteringRectangleFactory = mockStatic(CameraRegionUtils.MeteringRectangleFactory.class); + + mockedMeteringRectangleFactory + .when( + () -> + CameraRegionUtils.MeteringRectangleFactory.create( + anyInt(), anyInt(), anyInt(), anyInt(), anyInt())) + .thenAnswer( + new Answer() { + @Override + public MeteringRectangle answer(InvocationOnMock createInvocation) throws Throwable { + MeteringRectangle mockMeteringRectangle = mock(MeteringRectangle.class); + when(mockMeteringRectangle.getX()).thenReturn(createInvocation.getArgument(0)); + when(mockMeteringRectangle.getY()).thenReturn(createInvocation.getArgument(1)); + when(mockMeteringRectangle.getWidth()).thenReturn(createInvocation.getArgument(2)); + when(mockMeteringRectangle.getHeight()).thenReturn(createInvocation.getArgument(3)); + when(mockMeteringRectangle.getMeteringWeight()) + .thenReturn(createInvocation.getArgument(4)); + when(mockMeteringRectangle.equals(any())) + .thenAnswer( + new Answer() { + @Override + public Boolean answer(InvocationOnMock equalsInvocation) + throws Throwable { + MeteringRectangle otherMockMeteringRectangle = + equalsInvocation.getArgument(0); + return mockMeteringRectangle.getX() == otherMockMeteringRectangle.getX() + && mockMeteringRectangle.getY() == otherMockMeteringRectangle.getY() + && mockMeteringRectangle.getWidth() + == otherMockMeteringRectangle.getWidth() + && mockMeteringRectangle.getHeight() + == otherMockMeteringRectangle.getHeight() + && mockMeteringRectangle.getMeteringWeight() + == otherMockMeteringRectangle.getMeteringWeight(); + } + }); + return mockMeteringRectangle; + } + }); + } + + @After + public void tearDown() { + mockedMeteringRectangleFactory.close(); + } + + @Test + public void convertPointToMeteringRectangle_shouldReturnValidMeteringRectangleForCenterCoord() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0.5, 0.5, PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(45, 45, 10, 10, 1).equals(r)); + } + + @Test + public void convertPointToMeteringRectangle_shouldReturnValidMeteringRectangleForTopLeftCoord() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0, 0, PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 0, 10, 10, 1).equals(r)); + } + + @Test + public void convertPointToMeteringRectangle_ShouldReturnValidMeteringRectangleForTopRightCoord() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 0, PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 0, 10, 10, 1).equals(r)); + } + + @Test + public void + convertPointToMeteringRectangle_shouldReturnValidMeteringRectangleForBottomLeftCoord() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0, 1, PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 89, 10, 10, 1).equals(r)); + } + + @Test + public void + convertPointToMeteringRectangle_shouldReturnValidMeteringRectangleForBottomRightCoord() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 1, PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 89, 10, 10, 1).equals(r)); + } + + @Test(expected = AssertionError.class) + public void convertPointToMeteringRectangle_shouldThrowForXUpperBound() { + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1.5, 0, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } + + @Test(expected = AssertionError.class) + public void convertPointToMeteringRectangle_shouldThrowForXLowerBound() { + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, -0.5, 0, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } + + @Test(expected = AssertionError.class) + public void convertPointToMeteringRectangle_shouldThrowForYUpperBound() { + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0, 1.5, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } + + @Test(expected = AssertionError.class) + public void convertPointToMeteringRectangle_shouldThrowForYLowerBound() { + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0, -0.5, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } + + @Test() + public void + convertPointToMeteringRectangle_shouldRotateMeteringRectangleAccordingToUiOrientationForPortraitUp() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 1, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 0, 10, 10, 1).equals(r)); + } + + @Test() + public void + convertPointToMeteringRectangle_shouldRotateMeteringRectangleAccordingToUiOrientationForPortraitDown() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 1, PlatformChannel.DeviceOrientation.PORTRAIT_DOWN); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 89, 10, 10, 1).equals(r)); + } + + @Test() + public void + convertPointToMeteringRectangle_shouldRotateMeteringRectangleAccordingToUiOrientationForLandscapeLeft() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 1, PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 89, 10, 10, 1).equals(r)); + } + + @Test() + public void + convertPointToMeteringRectangle_shouldRotateMeteringRectangleAccordingToUiOrientationForLandscapeRight() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 1, PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 0, 10, 10, 1).equals(r)); + } + + @Test(expected = AssertionError.class) + public void convertPointToMeteringRectangle_shouldThrowFor0WidthBoundary() { + Size mockCameraBoundaries = mock(Size.class); + when(mockCameraBoundaries.getWidth()).thenReturn(0); + when(mockCameraBoundaries.getHeight()).thenReturn(50); + CameraRegionUtils.convertPointToMeteringRectangle( + mockCameraBoundaries, 0, -0.5, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } + + @Test(expected = AssertionError.class) + public void convertPointToMeteringRectangle_shouldThrowFor0HeightBoundary() { + Size mockCameraBoundaries = mock(Size.class); + when(mockCameraBoundaries.getWidth()).thenReturn(50); + when(mockCameraBoundaries.getHeight()).thenReturn(0); + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0, -0.5, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_getCameraBoundariesTest.java similarity index 61% rename from packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java rename to packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_getCameraBoundariesTest.java index 2d65c4e0fc05..4c0164981b74 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtilsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_getCameraBoundariesTest.java @@ -4,8 +4,6 @@ package io.flutter.plugins.camera; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; @@ -15,17 +13,15 @@ import android.graphics.Rect; import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.params.MeteringRectangle; import android.os.Build; import android.util.Size; import io.flutter.plugins.camera.utils.TestUtils; import org.junit.Before; import org.junit.Test; import org.mockito.MockedStatic; -import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; -public class CameraRegionUtilsTest { +public class CameraRegionUtils_getCameraBoundariesTest { Size mockCameraBoundaries; @@ -37,8 +33,7 @@ public void setUp() { } @Test - public void - getCameraBoundaries_should_return_sensor_info_pixel_array_size_when_running_pre_android_p() { + public void getCameraBoundaries_shouldReturnSensorInfoPixelArraySizeWhenRunningPreAndroidP() { updateSdkVersion(Build.VERSION_CODES.O_MR1); try { @@ -58,7 +53,7 @@ public void setUp() { @Test public void - getCameraBoundaries_should_return_sensor_info_pixel_array_size_when_distortion_correction_is_null() { + getCameraBoundaries_shouldReturnSensorInfoPixelArraySizeWhenDistortionCorrectionIsNull() { updateSdkVersion(Build.VERSION_CODES.P); try { @@ -80,7 +75,7 @@ public void setUp() { @Test public void - getCameraBoundaries_should_return_sensor_info_pixel_array_size_when_distortion_correction_is_off() { + getCameraBoundaries_shouldReturnSensorInfoPixelArraySizeWhenDistortionCorrectionIsOff() { updateSdkVersion(Build.VERSION_CODES.P); try { @@ -103,7 +98,7 @@ public void setUp() { @Test public void - getCameraBoundaries_should_return_info_pre_correction_active_array_size_when_distortion_correction_mode_is_set_to_null() { + getCameraBoundaries_shouldReturnInfoPreCorrectionActiveArraySizeWhenDistortionCorrectionModeIsSetToNull() { updateSdkVersion(Build.VERSION_CODES.P); try { @@ -150,7 +145,7 @@ public void setUp() { @Test public void - getCameraBoundaries_should_return_info_pre_correction_active_array_size_when_distortion_correction_mode_is_set_to_off() { + getCameraBoundaries_shouldReturnInfoPreCorrectionActiveArraySizeWhenDistortionCorrectionModeIsSetToOff() { updateSdkVersion(Build.VERSION_CODES.P); try { @@ -199,7 +194,7 @@ public void setUp() { @Test public void - getCameraBoundaries_should_return_sensor_info_active_array_size_when_distortion_correction_mode_is_set() { + getCameraBoundaries_shouldReturnSensorInfoActiveArraySizeWhenDistortionCorrectionModeIsSet() { updateSdkVersion(Build.VERSION_CODES.P); try { @@ -246,107 +241,6 @@ public void setUp() { } } - @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_x_upper_bound() { - CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 1.5, 0); - } - - @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_x_lower_bound() { - CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, -0.5, 0); - } - - @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_y_upper_bound() { - CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0, 1.5); - } - - @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_y_lower_bound() { - CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0, -0.5); - } - - @Test - public void getMeteringRectangleForPoint_should_return_valid_MeteringRectangle() { - try (MockedStatic mockedMeteringRectangleFactory = - mockStatic(CameraRegionUtils.MeteringRectangleFactory.class)) { - - mockedMeteringRectangleFactory - .when( - () -> - CameraRegionUtils.MeteringRectangleFactory.create( - anyInt(), anyInt(), anyInt(), anyInt(), anyInt())) - .thenAnswer( - new Answer() { - @Override - public MeteringRectangle answer(InvocationOnMock createInvocation) - throws Throwable { - MeteringRectangle mockMeteringRectangle = mock(MeteringRectangle.class); - when(mockMeteringRectangle.getX()).thenReturn(createInvocation.getArgument(0)); - when(mockMeteringRectangle.getY()).thenReturn(createInvocation.getArgument(1)); - when(mockMeteringRectangle.getWidth()) - .thenReturn(createInvocation.getArgument(2)); - when(mockMeteringRectangle.getHeight()) - .thenReturn(createInvocation.getArgument(3)); - when(mockMeteringRectangle.getMeteringWeight()) - .thenReturn(createInvocation.getArgument(4)); - when(mockMeteringRectangle.equals(any())) - .thenAnswer( - new Answer() { - @Override - public Boolean answer(InvocationOnMock equalsInvocation) - throws Throwable { - MeteringRectangle otherMockMeteringRectangle = - equalsInvocation.getArgument(0); - return mockMeteringRectangle.getX() - == otherMockMeteringRectangle.getX() - && mockMeteringRectangle.getY() - == otherMockMeteringRectangle.getY() - && mockMeteringRectangle.getWidth() - == otherMockMeteringRectangle.getWidth() - && mockMeteringRectangle.getHeight() - == otherMockMeteringRectangle.getHeight() - && mockMeteringRectangle.getMeteringWeight() - == otherMockMeteringRectangle.getMeteringWeight(); - } - }); - return mockMeteringRectangle; - } - }); - - MeteringRectangle r; - // Center - r = CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0.5, 0.5); - assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(45, 45, 10, 10, 1).equals(r)); - - // Top left - r = CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0.0, 0.0); - assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 0, 10, 10, 1).equals(r)); - - // Bottom right - r = CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 1.0, 1.0); - assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 89, 10, 10, 1).equals(r)); - - // Top left - r = CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 0.0, 1.0); - assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 89, 10, 10, 1).equals(r)); - - // Top right - r = CameraRegionUtils.convertPointToMeteringRectangle(this.mockCameraBoundaries, 1.0, 0.0); - assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 0, 10, 10, 1).equals(r)); - } - } - - @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_0_width_boundary() { - new io.flutter.plugins.camera.CameraRegions(new Size(0, 50)); - } - - @Test(expected = AssertionError.class) - public void getMeteringRectangleForPoint_should_throw_for_0_height_boundary() { - new io.flutter.plugins.camera.CameraRegions(new Size(100, 0)); - } - private static void updateSdkVersion(int version) { TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", version); } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java new file mode 100644 index 000000000000..cab2ae8974a4 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java @@ -0,0 +1,843 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureRequest; +import android.media.CamcorderProfile; +import android.media.MediaRecorder; +import android.os.Build; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.camera.features.CameraFeatureFactory; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; +import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature; +import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature; +import io.flutter.plugins.camera.features.flash.FlashFeature; +import io.flutter.plugins.camera.features.flash.FlashMode; +import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; +import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature; +import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; +import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; +import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; +import io.flutter.plugins.camera.utils.TestUtils; +import io.flutter.view.TextureRegistry; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class CameraTest { + private CameraProperties mockCameraProperties; + private CameraFeatureFactory mockCameraFeatureFactory; + private DartMessenger mockDartMessenger; + private Camera camera; + private CameraCaptureSession mockCaptureSession; + private CaptureRequest.Builder mockPreviewRequestBuilder; + + @Before + public void before() { + mockCameraProperties = mock(CameraProperties.class); + mockCameraFeatureFactory = new TestCameraFeatureFactory(); + mockDartMessenger = mock(DartMessenger.class); + mockCaptureSession = mock(CameraCaptureSession.class); + mockPreviewRequestBuilder = mock(CaptureRequest.Builder.class); + + final Activity mockActivity = mock(Activity.class); + final TextureRegistry.SurfaceTextureEntry mockFlutterTexture = + mock(TextureRegistry.SurfaceTextureEntry.class); + final String cameraName = "1"; + final ResolutionPreset resolutionPreset = ResolutionPreset.high; + final boolean enableAudio = false; + + when(mockCameraProperties.getCameraName()).thenReturn(cameraName); + + camera = + new Camera( + mockActivity, + mockFlutterTexture, + mockCameraFeatureFactory, + mockDartMessenger, + mockCameraProperties, + resolutionPreset, + enableAudio); + + TestUtils.setPrivateField(camera, "captureSession", mockCaptureSession); + TestUtils.setPrivateField(camera, "previewRequestBuilder", mockPreviewRequestBuilder); + } + + @After + public void after() { + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 0); + } + + @Test + public void shouldCreateCameraPluginAndSetAllFeatures() { + final Activity mockActivity = mock(Activity.class); + final TextureRegistry.SurfaceTextureEntry mockFlutterTexture = + mock(TextureRegistry.SurfaceTextureEntry.class); + final CameraFeatureFactory mockCameraFeatureFactory = mock(CameraFeatureFactory.class); + final String cameraName = "1"; + final ResolutionPreset resolutionPreset = ResolutionPreset.high; + final boolean enableAudio = false; + + when(mockCameraProperties.getCameraName()).thenReturn(cameraName); + SensorOrientationFeature mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + when(mockCameraFeatureFactory.createSensorOrientationFeature(any(), any(), any())) + .thenReturn(mockSensorOrientationFeature); + + Camera camera = + new Camera( + mockActivity, + mockFlutterTexture, + mockCameraFeatureFactory, + mockDartMessenger, + mockCameraProperties, + resolutionPreset, + enableAudio); + + verify(mockCameraFeatureFactory, times(1)) + .createSensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + verify(mockCameraFeatureFactory, times(1)).createAutoFocusFeature(mockCameraProperties, false); + verify(mockCameraFeatureFactory, times(1)).createExposureLockFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)) + .createExposurePointFeature(eq(mockCameraProperties), eq(mockSensorOrientationFeature)); + verify(mockCameraFeatureFactory, times(1)).createExposureOffsetFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)).createFlashFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)) + .createFocusPointFeature(eq(mockCameraProperties), eq(mockSensorOrientationFeature)); + verify(mockCameraFeatureFactory, times(1)).createFpsRangeFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)).createNoiseReductionFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)) + .createResolutionFeature(mockCameraProperties, resolutionPreset, cameraName); + verify(mockCameraFeatureFactory, times(1)).createZoomLevelFeature(mockCameraProperties); + assertNotNull("should create a camera", camera); + } + + @Test + public void getDeviceOrientationManager() { + SensorOrientationFeature mockSensorOrientationFeature = + mockCameraFeatureFactory.createSensorOrientationFeature(mockCameraProperties, null, null); + DeviceOrientationManager mockDeviceOrientationManager = mock(DeviceOrientationManager.class); + + when(mockSensorOrientationFeature.getDeviceOrientationManager()) + .thenReturn(mockDeviceOrientationManager); + + DeviceOrientationManager actualDeviceOrientationManager = camera.getDeviceOrientationManager(); + + verify(mockSensorOrientationFeature, times(1)).getDeviceOrientationManager(); + assertEquals(mockDeviceOrientationManager, actualDeviceOrientationManager); + } + + @Test + public void getExposureOffsetStepSize() { + ExposureOffsetFeature mockExposureOffsetFeature = + mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); + double stepSize = 2.3; + + when(mockExposureOffsetFeature.getExposureOffsetStepSize()).thenReturn(stepSize); + + double actualSize = camera.getExposureOffsetStepSize(); + + verify(mockExposureOffsetFeature, times(1)).getExposureOffsetStepSize(); + assertEquals(stepSize, actualSize, 0); + } + + @Test + public void getMaxExposureOffset() { + ExposureOffsetFeature mockExposureOffsetFeature = + mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); + double expectedMaxOffset = 42.0; + + when(mockExposureOffsetFeature.getMaxExposureOffset()).thenReturn(expectedMaxOffset); + + double actualMaxOffset = camera.getMaxExposureOffset(); + + verify(mockExposureOffsetFeature, times(1)).getMaxExposureOffset(); + assertEquals(expectedMaxOffset, actualMaxOffset, 0); + } + + @Test + public void getMinExposureOffset() { + ExposureOffsetFeature mockExposureOffsetFeature = + mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); + double expectedMinOffset = 21.5; + + when(mockExposureOffsetFeature.getMinExposureOffset()).thenReturn(21.5); + + double actualMinOffset = camera.getMinExposureOffset(); + + verify(mockExposureOffsetFeature, times(1)).getMinExposureOffset(); + assertEquals(expectedMinOffset, actualMinOffset, 0); + } + + @Test + public void getMaxZoomLevel() { + ZoomLevelFeature mockZoomLevelFeature = + mockCameraFeatureFactory.createZoomLevelFeature(mockCameraProperties); + float expectedMaxZoomLevel = 4.2f; + + when(mockZoomLevelFeature.getMaximumZoomLevel()).thenReturn(expectedMaxZoomLevel); + + float actualMaxZoomLevel = camera.getMaxZoomLevel(); + + verify(mockZoomLevelFeature, times(1)).getMaximumZoomLevel(); + assertEquals(expectedMaxZoomLevel, actualMaxZoomLevel, 0); + } + + @Test + public void getMinZoomLevel() { + ZoomLevelFeature mockZoomLevelFeature = + mockCameraFeatureFactory.createZoomLevelFeature(mockCameraProperties); + float expectedMinZoomLevel = 4.2f; + + when(mockZoomLevelFeature.getMinimumZoomLevel()).thenReturn(expectedMinZoomLevel); + + float actualMinZoomLevel = camera.getMinZoomLevel(); + + verify(mockZoomLevelFeature, times(1)).getMinimumZoomLevel(); + assertEquals(expectedMinZoomLevel, actualMinZoomLevel, 0); + } + + @Test + public void getRecordingProfile() { + ResolutionFeature mockResolutionFeature = + mockCameraFeatureFactory.createResolutionFeature(mockCameraProperties, null, null); + CamcorderProfile mockCamcorderProfile = mock(CamcorderProfile.class); + + when(mockResolutionFeature.getRecordingProfile()).thenReturn(mockCamcorderProfile); + + CamcorderProfile actualRecordingProfile = camera.getRecordingProfile(); + + verify(mockResolutionFeature, times(1)).getRecordingProfile(); + assertEquals(mockCamcorderProfile, actualRecordingProfile); + } + + @Test + public void setExposureMode_shouldUpdateExposureLockFeature() { + ExposureLockFeature mockExposureLockFeature = + mockCameraFeatureFactory.createExposureLockFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + ExposureMode exposureMode = ExposureMode.locked; + + camera.setExposureMode(mockResult, exposureMode); + + verify(mockExposureLockFeature, times(1)).setValue(exposureMode); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(null); + } + + @Test + public void setExposureMode_shouldUpdateBuilder() { + ExposureLockFeature mockExposureLockFeature = + mockCameraFeatureFactory.createExposureLockFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + ExposureMode exposureMode = ExposureMode.locked; + + camera.setExposureMode(mockResult, exposureMode); + + verify(mockExposureLockFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setExposureMode_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + ExposureMode exposureMode = ExposureMode.locked; + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setExposureMode(mockResult, exposureMode); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)) + .error("setExposureModeFailed", "Could not set exposure mode.", null); + } + + @Test + public void setExposurePoint_shouldUpdateExposurePointFeature() { + SensorOrientationFeature mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + ExposurePointFeature mockExposurePointFeature = + mockCameraFeatureFactory.createExposurePointFeature( + mockCameraProperties, mockSensorOrientationFeature); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Point point = new Point(42d, 42d); + + camera.setExposurePoint(mockResult, point); + + verify(mockExposurePointFeature, times(1)).setValue(point); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(null); + } + + @Test + public void setExposurePoint_shouldUpdateBuilder() { + SensorOrientationFeature mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + ExposurePointFeature mockExposurePointFeature = + mockCameraFeatureFactory.createExposurePointFeature( + mockCameraProperties, mockSensorOrientationFeature); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Point point = new Point(42d, 42d); + + camera.setExposurePoint(mockResult, point); + + verify(mockExposurePointFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setExposurePoint_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Point point = new Point(42d, 42d); + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setExposurePoint(mockResult, point); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)) + .error("setExposurePointFailed", "Could not set exposure point.", null); + } + + @Test + public void setFlashMode_shouldUpdateFlashFeature() { + FlashFeature mockFlashFeature = + mockCameraFeatureFactory.createFlashFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + FlashMode flashMode = FlashMode.always; + + camera.setFlashMode(mockResult, flashMode); + + verify(mockFlashFeature, times(1)).setValue(flashMode); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(null); + } + + @Test + public void setFlashMode_shouldUpdateBuilder() { + FlashFeature mockFlashFeature = + mockCameraFeatureFactory.createFlashFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + FlashMode flashMode = FlashMode.always; + + camera.setFlashMode(mockResult, flashMode); + + verify(mockFlashFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setFlashMode_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + FlashMode flashMode = FlashMode.always; + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setFlashMode(mockResult, flashMode); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)).error("setFlashModeFailed", "Could not set flash mode.", null); + } + + @Test + public void setFocusPoint_shouldUpdateFocusPointFeature() { + SensorOrientationFeature mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + FocusPointFeature mockFocusPointFeature = + mockCameraFeatureFactory.createFocusPointFeature( + mockCameraProperties, mockSensorOrientationFeature); + AutoFocusFeature mockAutoFocusFeature = + mockCameraFeatureFactory.createAutoFocusFeature(mockCameraProperties, false); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Point point = new Point(42d, 42d); + when(mockAutoFocusFeature.getValue()).thenReturn(FocusMode.auto); + + camera.setFocusPoint(mockResult, point); + + verify(mockFocusPointFeature, times(1)).setValue(point); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(null); + } + + @Test + public void setFocusPoint_shouldUpdateBuilder() { + SensorOrientationFeature mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + FocusPointFeature mockFocusPointFeature = + mockCameraFeatureFactory.createFocusPointFeature( + mockCameraProperties, mockSensorOrientationFeature); + AutoFocusFeature mockAutoFocusFeature = + mockCameraFeatureFactory.createAutoFocusFeature(mockCameraProperties, false); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Point point = new Point(42d, 42d); + when(mockAutoFocusFeature.getValue()).thenReturn(FocusMode.auto); + + camera.setFocusPoint(mockResult, point); + + verify(mockFocusPointFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setFocusPoint_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + AutoFocusFeature mockAutoFocusFeature = + mockCameraFeatureFactory.createAutoFocusFeature(mockCameraProperties, false); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Point point = new Point(42d, 42d); + when(mockAutoFocusFeature.getValue()).thenReturn(FocusMode.auto); + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setFocusPoint(mockResult, point); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)).error("setFocusPointFailed", "Could not set focus point.", null); + } + + @Test + public void setZoomLevel_shouldUpdateZoomLevelFeature() throws CameraAccessException { + ZoomLevelFeature mockZoomLevelFeature = + mockCameraFeatureFactory.createZoomLevelFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + float zoomLevel = 1.0f; + + when(mockZoomLevelFeature.getValue()).thenReturn(zoomLevel); + when(mockZoomLevelFeature.getMinimumZoomLevel()).thenReturn(0f); + when(mockZoomLevelFeature.getMaximumZoomLevel()).thenReturn(2f); + + camera.setZoomLevel(mockResult, zoomLevel); + + verify(mockZoomLevelFeature, times(1)).setValue(zoomLevel); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(null); + } + + @Test + public void setZoomLevel_shouldUpdateBuilder() throws CameraAccessException { + ZoomLevelFeature mockZoomLevelFeature = + mockCameraFeatureFactory.createZoomLevelFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + float zoomLevel = 1.0f; + + when(mockZoomLevelFeature.getValue()).thenReturn(zoomLevel); + when(mockZoomLevelFeature.getMinimumZoomLevel()).thenReturn(0f); + when(mockZoomLevelFeature.getMaximumZoomLevel()).thenReturn(2f); + + camera.setZoomLevel(mockResult, zoomLevel); + + verify(mockZoomLevelFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setZoomLevel_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + ZoomLevelFeature mockZoomLevelFeature = + mockCameraFeatureFactory.createZoomLevelFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + float zoomLevel = 1.0f; + + when(mockZoomLevelFeature.getValue()).thenReturn(zoomLevel); + when(mockZoomLevelFeature.getMinimumZoomLevel()).thenReturn(0f); + when(mockZoomLevelFeature.getMaximumZoomLevel()).thenReturn(2f); + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setZoomLevel(mockResult, zoomLevel); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)).error("setZoomLevelFailed", "Could not set zoom level.", null); + } + + @Test + public void pauseVideoRecording_shouldSendNullResultWhenNotRecording() { + TestUtils.setPrivateField(camera, "recordingVideo", false); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.pauseVideoRecording(mockResult); + + verify(mockResult, times(1)).success(null); + verify(mockResult, never()).error(any(), any(), any()); + } + + @Test + public void pauseVideoRecording_shouldCallPauseWhenRecordingAndOnAPIN() { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + TestUtils.setPrivateField(camera, "mediaRecorder", mockMediaRecorder); + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 24); + + camera.pauseVideoRecording(mockResult); + + verify(mockMediaRecorder, times(1)).pause(); + verify(mockResult, times(1)).success(null); + verify(mockResult, never()).error(any(), any(), any()); + } + + @Test + public void pauseVideoRecording_shouldSendVideoRecordingFailedErrorWhenVersionCodeSmallerThenN() { + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 23); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.pauseVideoRecording(mockResult); + + verify(mockResult, times(1)) + .error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); + verify(mockResult, never()).success(any()); + } + + @Test + public void + pauseVideoRecording_shouldSendVideoRecordingFailedErrorWhenMediaRecorderPauseThrowsIllegalStateException() { + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + TestUtils.setPrivateField(camera, "mediaRecorder", mockMediaRecorder); + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 24); + + IllegalStateException expectedException = new IllegalStateException("Test error message"); + + doThrow(expectedException).when(mockMediaRecorder).pause(); + + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.pauseVideoRecording(mockResult); + + verify(mockResult, times(1)).error("videoRecordingFailed", "Test error message", null); + verify(mockResult, never()).success(any()); + } + + @Test + public void resumeVideoRecording_shouldSendNullResultWhenNotRecording() { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + TestUtils.setPrivateField(camera, "recordingVideo", false); + + camera.resumeVideoRecording(mockResult); + + verify(mockResult, times(1)).success(null); + verify(mockResult, never()).error(any(), any(), any()); + } + + @Test + public void resumeVideoRecording_shouldCallPauseWhenRecordingAndOnAPIN() { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + TestUtils.setPrivateField(camera, "mediaRecorder", mockMediaRecorder); + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 24); + + camera.resumeVideoRecording(mockResult); + + verify(mockMediaRecorder, times(1)).resume(); + verify(mockResult, times(1)).success(null); + verify(mockResult, never()).error(any(), any(), any()); + } + + @Test + public void + resumeVideoRecording_shouldSendVideoRecordingFailedErrorWhenVersionCodeSmallerThanN() { + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 23); + + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.resumeVideoRecording(mockResult); + + verify(mockResult, times(1)) + .error("videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); + verify(mockResult, never()).success(any()); + } + + @Test + public void + resumeVideoRecording_shouldSendVideoRecordingFailedErrorWhenMediaRecorderPauseThrowsIllegalStateException() { + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + TestUtils.setPrivateField(camera, "mediaRecorder", mockMediaRecorder); + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 24); + + IllegalStateException expectedException = new IllegalStateException("Test error message"); + + doThrow(expectedException).when(mockMediaRecorder).resume(); + + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.resumeVideoRecording(mockResult); + + verify(mockResult, times(1)).error("videoRecordingFailed", "Test error message", null); + verify(mockResult, never()).success(any()); + } + + @Test + public void setFocusMode_shouldUpdateAutoFocusFeature() { + AutoFocusFeature mockAutoFocusFeature = + mockCameraFeatureFactory.createAutoFocusFeature(mockCameraProperties, false); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.setFocusMode(mockResult, FocusMode.auto); + + verify(mockAutoFocusFeature, times(1)).setValue(FocusMode.auto); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(null); + } + + @Test + public void setFocusMode_shouldUpdateBuilder() { + AutoFocusFeature mockAutoFocusFeature = + mockCameraFeatureFactory.createAutoFocusFeature(mockCameraProperties, false); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.setFocusMode(mockResult, FocusMode.auto); + + verify(mockAutoFocusFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setFocusMode_shouldUnlockAutoFocusForAutoMode() { + camera.setFocusMode(mock(MethodChannel.Result.class), FocusMode.auto); + verify(mockPreviewRequestBuilder, times(1)) + .set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + verify(mockPreviewRequestBuilder, times(1)) + .set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + } + + @Test + public void setFocusMode_shouldSkipUnlockAutoFocusWhenNullCaptureSession() { + TestUtils.setPrivateField(camera, "captureSession", null); + camera.setFocusMode(mock(MethodChannel.Result.class), FocusMode.auto); + verify(mockPreviewRequestBuilder, never()) + .set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + verify(mockPreviewRequestBuilder, never()) + .set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + } + + @Test + public void setFocusMode_shouldSendErrorEventOnUnlockAutoFocusCameraAccessException() + throws CameraAccessException { + when(mockCaptureSession.capture(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + camera.setFocusMode(mock(MethodChannel.Result.class), FocusMode.auto); + verify(mockDartMessenger, times(1)).sendCameraErrorEvent(any()); + } + + @Test + public void setFocusMode_shouldLockAutoFocusForLockedMode() throws CameraAccessException { + camera.setFocusMode(mock(MethodChannel.Result.class), FocusMode.locked); + verify(mockPreviewRequestBuilder, times(1)) + .set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START); + verify(mockCaptureSession, times(1)).capture(any(), any(), any()); + verify(mockCaptureSession, times(1)).setRepeatingRequest(any(), any(), any()); + } + + @Test + public void setFocusMode_shouldSkipLockAutoFocusWhenNullCaptureSession() { + TestUtils.setPrivateField(camera, "captureSession", null); + camera.setFocusMode(mock(MethodChannel.Result.class), FocusMode.locked); + verify(mockPreviewRequestBuilder, never()) + .set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + } + + @Test + public void setFocusMode_shouldSendErrorEventOnLockAutoFocusCameraAccessException() + throws CameraAccessException { + when(mockCaptureSession.capture(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + camera.setFocusMode(mock(MethodChannel.Result.class), FocusMode.locked); + verify(mockDartMessenger, times(1)).sendCameraErrorEvent(any()); + } + + @Test + public void setFocusMode_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setFocusMode(mockResult, FocusMode.locked); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)) + .error("setFocusModeFailed", "Error setting focus mode: null", null); + } + + @Test + public void setExposureOffset_shouldUpdateExposureOffsetFeature() { + ExposureOffsetFeature mockExposureOffsetFeature = + mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.setExposureOffset(mockResult, 1.0); + + verify(mockExposureOffsetFeature, times(1)).setValue(1.0); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(null); + } + + @Test + public void setExposureOffset_shouldAndUpdateBuilder() { + ExposureOffsetFeature mockExposureOffsetFeature = + mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.setExposureOffset(mockResult, 1.0); + + verify(mockExposureOffsetFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setExposureOffset_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setExposureOffset(mockResult, 1.0); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)) + .error("setExposureOffsetFailed", "Could not set exposure offset.", null); + } + + @Test + public void lockCaptureOrientation_shouldLockCaptureOrientation() { + final Activity mockActivity = mock(Activity.class); + SensorOrientationFeature mockSensorOrientationFeature = + mockCameraFeatureFactory.createSensorOrientationFeature( + mockCameraProperties, mockActivity, mockDartMessenger); + + camera.lockCaptureOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP); + + verify(mockSensorOrientationFeature, times(1)) + .lockCaptureOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } + + @Test + public void unlockCaptureOrientation_shouldUnlockCaptureOrientation() { + final Activity mockActivity = mock(Activity.class); + SensorOrientationFeature mockSensorOrientationFeature = + mockCameraFeatureFactory.createSensorOrientationFeature( + mockCameraProperties, mockActivity, mockDartMessenger); + + camera.unlockCaptureOrientation(); + + verify(mockSensorOrientationFeature, times(1)).unlockCaptureOrientation(); + } + + private static class TestCameraFeatureFactory implements CameraFeatureFactory { + private final AutoFocusFeature mockAutoFocusFeature; + private final ExposureLockFeature mockExposureLockFeature; + private final ExposureOffsetFeature mockExposureOffsetFeature; + private final ExposurePointFeature mockExposurePointFeature; + private final FlashFeature mockFlashFeature; + private final FocusPointFeature mockFocusPointFeature; + private final FpsRangeFeature mockFpsRangeFeature; + private final NoiseReductionFeature mockNoiseReductionFeature; + private final ResolutionFeature mockResolutionFeature; + private final SensorOrientationFeature mockSensorOrientationFeature; + private final ZoomLevelFeature mockZoomLevelFeature; + + public TestCameraFeatureFactory() { + this.mockAutoFocusFeature = mock(AutoFocusFeature.class); + this.mockExposureLockFeature = mock(ExposureLockFeature.class); + this.mockExposureOffsetFeature = mock(ExposureOffsetFeature.class); + this.mockExposurePointFeature = mock(ExposurePointFeature.class); + this.mockFlashFeature = mock(FlashFeature.class); + this.mockFocusPointFeature = mock(FocusPointFeature.class); + this.mockFpsRangeFeature = mock(FpsRangeFeature.class); + this.mockNoiseReductionFeature = mock(NoiseReductionFeature.class); + this.mockResolutionFeature = mock(ResolutionFeature.class); + this.mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + this.mockZoomLevelFeature = mock(ZoomLevelFeature.class); + } + + @Override + public AutoFocusFeature createAutoFocusFeature( + @NonNull CameraProperties cameraProperties, boolean recordingVideo) { + return mockAutoFocusFeature; + } + + @Override + public ExposureLockFeature createExposureLockFeature( + @NonNull CameraProperties cameraProperties) { + return mockExposureLockFeature; + } + + @Override + public ExposureOffsetFeature createExposureOffsetFeature( + @NonNull CameraProperties cameraProperties) { + return mockExposureOffsetFeature; + } + + @Override + public FlashFeature createFlashFeature(@NonNull CameraProperties cameraProperties) { + return mockFlashFeature; + } + + @Override + public ResolutionFeature createResolutionFeature( + @NonNull CameraProperties cameraProperties, + ResolutionPreset initialSetting, + String cameraName) { + return mockResolutionFeature; + } + + @Override + public FocusPointFeature createFocusPointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrienttionFeature) { + return mockFocusPointFeature; + } + + @Override + public FpsRangeFeature createFpsRangeFeature(@NonNull CameraProperties cameraProperties) { + return mockFpsRangeFeature; + } + + @Override + public SensorOrientationFeature createSensorOrientationFeature( + @NonNull CameraProperties cameraProperties, + @NonNull Activity activity, + @NonNull DartMessenger dartMessenger) { + return mockSensorOrientationFeature; + } + + @Override + public ZoomLevelFeature createZoomLevelFeature(@NonNull CameraProperties cameraProperties) { + return mockZoomLevelFeature; + } + + @Override + public ExposurePointFeature createExposurePointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature) { + return mockExposurePointFeature; + } + + @Override + public NoiseReductionFeature createNoiseReductionFeature( + @NonNull CameraProperties cameraProperties) { + return mockNoiseReductionFeature; + } + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java index b97192b889cf..6b714ce41e34 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java @@ -12,7 +12,7 @@ public class CameraUtilsTest { @Test - public void serializeDeviceOrientation_serializes_correctly() { + public void serializeDeviceOrientation_serializesCorrectly() { assertEquals( "portraitUp", CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP)); @@ -33,7 +33,7 @@ public void serializeDeviceOrientation_throws_for_null() { } @Test - public void deserializeDeviceOrientation_deserializes_correctly() { + public void deserializeDeviceOrientation_deserializesCorrectly() { assertEquals( PlatformChannel.DeviceOrientation.PORTRAIT_UP, CameraUtils.deserializeDeviceOrientation("portraitUp")); @@ -49,54 +49,7 @@ public void deserializeDeviceOrientation_deserializes_correctly() { } @Test(expected = UnsupportedOperationException.class) - public void deserializeDeviceOrientation_throws_for_null() { + public void deserializeDeviceOrientation_throwsForNull() { CameraUtils.deserializeDeviceOrientation(null); } - - @Test - public void getDeviceOrientationFromDegrees_converts_correctly() { - // Portrait UP - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(0)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(315)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(44)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - CameraUtils.getDeviceOrientationFromDegrees(-45)); - // Portrait DOWN - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - CameraUtils.getDeviceOrientationFromDegrees(180)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - CameraUtils.getDeviceOrientationFromDegrees(135)); - assertEquals( - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - CameraUtils.getDeviceOrientationFromDegrees(224)); - // Landscape LEFT - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - CameraUtils.getDeviceOrientationFromDegrees(90)); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - CameraUtils.getDeviceOrientationFromDegrees(45)); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - CameraUtils.getDeviceOrientationFromDegrees(134)); - // Landscape RIGHT - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - CameraUtils.getDeviceOrientationFromDegrees(270)); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - CameraUtils.getDeviceOrientationFromDegrees(225)); - assertEquals( - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - CameraUtils.getDeviceOrientationFromDegrees(314)); - } } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java index 1385c2e36949..d3e495551608 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java @@ -19,7 +19,7 @@ public class CameraZoomTest { @Test - public void ctor_when_parameters_are_valid() { + public void ctor_whenParametersAreValid() { final Rect sensorSize = new Rect(0, 0, 0, 0); final Float maxZoom = 4.0f; final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); @@ -31,7 +31,7 @@ public void ctor_when_parameters_are_valid() { } @Test - public void ctor_when_sensor_size_is_null() { + public void ctor_whenSensorSizeIsNull() { final Rect sensorSize = null; final Float maxZoom = 4.0f; final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); @@ -42,7 +42,7 @@ public void ctor_when_sensor_size_is_null() { } @Test - public void ctor_when_max_zoom_is_null() { + public void ctor_whenMaxZoomIsNull() { final Rect sensorSize = new Rect(0, 0, 0, 0); final Float maxZoom = null; final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); @@ -53,7 +53,7 @@ public void ctor_when_max_zoom_is_null() { } @Test - public void ctor_when_max_zoom_is_smaller_then_default_zoom_factor() { + public void ctor_whenMaxZoomIsSmallerThenDefaultZoomFactor() { final Rect sensorSize = new Rect(0, 0, 0, 0); final Float maxZoom = 0.5f; final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); @@ -64,7 +64,7 @@ public void ctor_when_max_zoom_is_smaller_then_default_zoom_factor() { } @Test - public void setZoom_when_no_support_should_not_set_scaler_crop_region() { + public void setZoom_whenNoSupportShouldNotSetScalerCropRegion() { final CameraZoom cameraZoom = new CameraZoom(null, null); final Rect computedZoom = cameraZoom.computeZoom(2f); @@ -72,7 +72,7 @@ public void setZoom_when_no_support_should_not_set_scaler_crop_region() { } @Test - public void setZoom_when_sensor_size_equals_zero_should_return_crop_region_of_zero() { + public void setZoom_whenSensorSizeEqualsZeroShouldReturnCropRegionOfZero() { final Rect sensorSize = new Rect(0, 0, 0, 0); final CameraZoom cameraZoom = new CameraZoom(sensorSize, 20f); final Rect computedZoom = cameraZoom.computeZoom(18f); @@ -85,7 +85,7 @@ public void setZoom_when_sensor_size_equals_zero_should_return_crop_region_of_ze } @Test - public void setZoom_when_sensor_size_is_valid_should_return_crop_region() { + public void setZoom_whenSensorSizeIsValidShouldReturnCropRegion() { final Rect sensorSize = new Rect(0, 0, 100, 100); final CameraZoom cameraZoom = new CameraZoom(sensorSize, 20f); final Rect computedZoom = cameraZoom.computeZoom(18f); @@ -98,7 +98,7 @@ public void setZoom_when_sensor_size_is_valid_should_return_crop_region() { } @Test - public void setZoom_when_zoom_is_greater_then_max_zoom_clamp_to_max_zoom() { + public void setZoom_whenZoomIsGreaterThenMaxZoomClampToMaxZoom() { final Rect sensorSize = new Rect(0, 0, 100, 100); final CameraZoom cameraZoom = new CameraZoom(sensorSize, 10f); final Rect computedZoom = cameraZoom.computeZoom(25f); @@ -111,7 +111,7 @@ public void setZoom_when_zoom_is_greater_then_max_zoom_clamp_to_max_zoom() { } @Test - public void setZoom_when_zoom_is_smaller_then_min_zoom_clamp_to_min_zoom() { + public void setZoom_whenZoomIsSmallerThenMinZoomClampToMinZoom() { final Rect sensorSize = new Rect(0, 0, 100, 100); final CameraZoom cameraZoom = new CameraZoom(sensorSize, 10f); final Rect computedZoom = cameraZoom.computeZoom(0.5f); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java index 25f5df9e9db9..0a2fc43d03cb 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java @@ -16,8 +16,8 @@ import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.StandardMethodCodec; -import io.flutter.plugins.camera.types.ExposureMode; -import io.flutter.plugins.camera.types.FocusMode; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java index d2c9f4498332..0358ce6cb785 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java @@ -80,7 +80,7 @@ public void teardown() { } @Test - public void run_writes_bytes_to_file_and_finishes_with_path() throws IOException { + public void runWritesBytesToFileAndFinishesWithPath() throws IOException { imageSaver.run(); verify(mockFileOutputStream, times(1)).write(new byte[] {0x42, 0x00, 0x13}); @@ -89,7 +89,7 @@ public void run_writes_bytes_to_file_and_finishes_with_path() throws IOException } @Test - public void run_calls_error_on_write_ioexception() throws IOException { + public void runCallsErrorOnWriteIoexception() throws IOException { doThrow(new IOException()).when(mockFileOutputStream).write(any()); imageSaver.run(); verify(mockCallback, times(1)).onError("IOError", "Failed saving image"); @@ -97,7 +97,7 @@ public void run_calls_error_on_write_ioexception() throws IOException { } @Test - public void run_calls_error_on_close_ioexception() throws IOException { + public void runCallsErrorOnCloseIoexception() throws IOException { doThrow(new IOException("message")).when(mockFileOutputStream).close(); imageSaver.run(); verify(mockCallback, times(1)).onError("cameraAccess", "message"); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java deleted file mode 100644 index f257a7f7fd4b..000000000000 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.camera; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import io.flutter.plugin.common.MethodChannel; -import org.junit.Test; - -public class PictureCaptureRequestTest { - - @Test - public void state_is_idle_by_default() { - PictureCaptureRequest req = new PictureCaptureRequest(null); - assertEquals("Default state is idle", req.getState(), PictureCaptureRequest.State.idle); - } - - @Test - public void setState_sets_state() { - PictureCaptureRequest req = new PictureCaptureRequest(null); - req.setState(PictureCaptureRequest.State.focusing); - assertEquals("State is focusing", req.getState(), PictureCaptureRequest.State.focusing); - req.setState(PictureCaptureRequest.State.preCapture); - assertEquals("State is preCapture", req.getState(), PictureCaptureRequest.State.preCapture); - req.setState(PictureCaptureRequest.State.waitingPreCaptureReady); - assertEquals( - "State is waitingPreCaptureReady", - req.getState(), - PictureCaptureRequest.State.waitingPreCaptureReady); - req.setState(PictureCaptureRequest.State.capturing); - assertEquals( - "State is awaitingPreCapture", req.getState(), PictureCaptureRequest.State.capturing); - } - - @Test - public void setState_resets_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - PictureCaptureRequest req = new PictureCaptureRequest(null, mockTimeoutHandler); - req.setState(PictureCaptureRequest.State.focusing); - req.setState(PictureCaptureRequest.State.preCapture); - req.setState(PictureCaptureRequest.State.waitingPreCaptureReady); - req.setState(PictureCaptureRequest.State.capturing); - verify(mockTimeoutHandler, times(4)).resetTimeout(any()); - verify(mockTimeoutHandler, never()).clearTimeout(any()); - } - - @Test - public void setState_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - PictureCaptureRequest req = new PictureCaptureRequest(null, mockTimeoutHandler); - req.setState(PictureCaptureRequest.State.idle); - req.setState(PictureCaptureRequest.State.finished); - req = new PictureCaptureRequest(null, mockTimeoutHandler); - req.setState(PictureCaptureRequest.State.error); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler, times(3)).clearTimeout(any()); - } - - @Test - public void finish_sets_result_and_state() { - // Setup - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult); - // Act - req.finish("/test/path"); - // Test - verify(mockResult).success("/test/path"); - assertEquals("State is finished", req.getState(), PictureCaptureRequest.State.finished); - } - - @Test - public void finish_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult, mockTimeoutHandler); - req.finish("/test/path"); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler).clearTimeout(any()); - } - - @Test - public void isFinished_is_true_When_state_is_finished_or_error() { - // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null); - // Test false states - req.setState(PictureCaptureRequest.State.idle); - assertFalse(req.isFinished()); - req.setState(PictureCaptureRequest.State.preCapture); - assertFalse(req.isFinished()); - req.setState(PictureCaptureRequest.State.capturing); - assertFalse(req.isFinished()); - // Test true states - req.setState(PictureCaptureRequest.State.finished); - assertTrue(req.isFinished()); - req = new PictureCaptureRequest(null); // Refresh - req.setState(PictureCaptureRequest.State.error); - assertTrue(req.isFinished()); - } - - @Test(expected = IllegalStateException.class) - public void finish_throws_When_already_finished() { - // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null); - req.setState(PictureCaptureRequest.State.finished); - // Act - req.finish("/test/path"); - } - - @Test - public void error_sets_result_and_state() { - // Setup - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult); - // Act - req.error("ERROR_CODE", "Error Message", null); - // Test - verify(mockResult).error("ERROR_CODE", "Error Message", null); - assertEquals("State is error", req.getState(), PictureCaptureRequest.State.error); - } - - @Test - public void error_clears_timeout() { - PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = - mock(PictureCaptureRequest.TimeoutHandler.class); - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - PictureCaptureRequest req = new PictureCaptureRequest(mockResult, mockTimeoutHandler); - req.error("ERROR_CODE", "Error Message", null); - verify(mockTimeoutHandler, never()).resetTimeout(any()); - verify(mockTimeoutHandler).clearTimeout(any()); - } - - @Test(expected = IllegalStateException.class) - public void error_throws_When_already_finished() { - // Setup - PictureCaptureRequest req = new PictureCaptureRequest(null); - req.setState(PictureCaptureRequest.State.finished); - // Act - req.error(null, null, null); - } -} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java index 84e4ad0d0e91..fd8ef7c766a2 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java @@ -28,7 +28,7 @@ public class AutoFocusFeatureTest { }; @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); @@ -36,7 +36,7 @@ public void getDebugName_should_return_the_name_of_the_feature() { } @Test - public void getValue_should_return_auto_if_not_set() { + public void getValue_shouldReturnAutoIfNotSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); @@ -44,7 +44,7 @@ public void getValue_should_return_auto_if_not_set() { } @Test - public void getValue_should_echo_the_set_value() { + public void getValue_shouldEchoTheSetValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); FocusMode expectedValue = FocusMode.locked; @@ -56,7 +56,7 @@ public void getValue_should_echo_the_set_value() { } @Test - public void checkIsSupported_should_return_false_when_minimum_focus_distance_is_zero() { + public void checkIsSupported_shouldReturnFalseWhenMinimumFocusDistanceIsZero() { CameraProperties mockCameraProperties = mock(CameraProperties.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); @@ -67,7 +67,7 @@ public void checkIsSupported_should_return_false_when_minimum_focus_distance_is_ } @Test - public void checkIsSupported_should_return_false_when_minimum_focus_distance_is_null() { + public void checkIsSupported_shouldReturnFalseWhenMinimumFocusDistanceIsNull() { CameraProperties mockCameraProperties = mock(CameraProperties.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); @@ -78,7 +78,7 @@ public void checkIsSupported_should_return_false_when_minimum_focus_distance_is_ } @Test - public void checkIsSupport_should_return_false_when_no_focus_modes_are_available() { + public void checkIsSupport_shouldReturnFalseWhenNoFocusModesAreAvailable() { CameraProperties mockCameraProperties = mock(CameraProperties.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); @@ -89,7 +89,7 @@ public void checkIsSupport_should_return_false_when_no_focus_modes_are_available } @Test - public void checkIsSupport_should_return_false_when_only_focus_off_is_available() { + public void checkIsSupport_shouldReturnFalseWhenOnlyFocusOffIsAvailable() { CameraProperties mockCameraProperties = mock(CameraProperties.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); @@ -100,7 +100,7 @@ public void checkIsSupport_should_return_false_when_only_focus_off_is_available( } @Test - public void checkIsSupport_should_return_true_when_only_multiple_focus_modes_are_available() { + public void checkIsSupport_shouldReturnTrueWhenOnlyMultipleFocusModesAreAvailable() { CameraProperties mockCameraProperties = mock(CameraProperties.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); @@ -111,7 +111,7 @@ public void checkIsSupport_should_return_true_when_only_multiple_focus_modes_are } @Test - public void updateBuilder_should_return_when_checkIsSupported_is_false() { + public void updateBuilderShouldReturnWhenCheckIsSupportedIsFalse() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); @@ -125,7 +125,7 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { } @Test - public void updateBuilder_should_set_control_mode_to_auto_when_focus_is_locked() { + public void updateBuilder_shouldSetControlModeToAutoWhenFocusIsLocked() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); @@ -142,7 +142,7 @@ public void updateBuilder_should_set_control_mode_to_auto_when_focus_is_locked() @Test public void - updateBuilder_should_set_control_mode_to_continuous_video_when_focus_is_auto_and_recording_video() { + updateBuilder_shouldSetControlModeToContinuousVideoWhenFocusIsAutoAndRecordingVideo() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, true); @@ -159,7 +159,7 @@ public void updateBuilder_should_set_control_mode_to_auto_when_focus_is_locked() @Test public void - updateBuilder_should_set_control_mode_to_continuous_video_when_focus_is_auto_and_not_recording_video() { + updateBuilder_shouldSetControlModeToContinuousVideoWhenFocusIsAutoAndNotRecordingVideo() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/FocusModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/FocusModeTest.java index 70d52d458d4d..f68ae7140601 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/FocusModeTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/FocusModeTest.java @@ -11,7 +11,7 @@ public class FocusModeTest { @Test - public void getValueForString_returns_correct_values() { + public void getValueForString_returnsCorrectValues() { assertEquals( "Returns FocusMode.auto for 'auto'", FocusMode.getValueForString("auto"), FocusMode.auto); assertEquals( @@ -21,13 +21,13 @@ public void getValueForString_returns_correct_values() { } @Test - public void getValueForString_returns_null_for_nonexistant_value() { + public void getValueForString_returnsNullForNonexistantValue() { assertEquals( "Returns null for 'nonexistant'", FocusMode.getValueForString("nonexistant"), null); } @Test - public void toString_returns_correct_value() { + public void toString_returnsCorrectValue() { assertEquals("Returns 'auto' for FocusMode.auto", FocusMode.auto.toString(), "auto"); assertEquals("Returns 'locked' for FocusMode.locked", FocusMode.locked.toString(), "locked"); } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java index d9e0a8d69c96..1cda0a86d575 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java @@ -16,7 +16,7 @@ public class ExposureLockFeatureTest { @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); @@ -24,7 +24,7 @@ public void getDebugName_should_return_the_name_of_the_feature() { } @Test - public void getValue_should_return_auto_if_not_set() { + public void getValue_shouldReturnAutoIfNotSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); @@ -32,7 +32,7 @@ public void getValue_should_return_auto_if_not_set() { } @Test - public void getValue_should_echo_the_set_value() { + public void getValue_shouldEchoTheSetValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); ExposureMode expectedValue = ExposureMode.locked; @@ -44,7 +44,7 @@ public void getValue_should_echo_the_set_value() { } @Test - public void checkIsSupported_should_return_true() { + public void checkIsSupported_shouldReturnTrue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); @@ -52,8 +52,7 @@ public void checkIsSupported_should_return_true() { } @Test - public void - updateBuilder_should_set_control_ae_lock_to_false_when_auto_exposure_is_set_to_auto() { + public void updateBuilder_shouldSetControlAeLockToFalseWhenAutoExposureIsSetToAuto() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); @@ -65,8 +64,7 @@ public void checkIsSupported_should_return_true() { } @Test - public void - updateBuilder_should_set_control_ae_lock_to_false_when_auto_exposure_is_set_to_locked() { + public void updateBuilder_shouldSetControlAeLockToFalseWhenAutoExposureIsSetToLocked() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureModeTest.java index ad1d3d98f295..d5d47697776c 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureModeTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureModeTest.java @@ -11,7 +11,7 @@ public class ExposureModeTest { @Test - public void getValueForString_returns_correct_values() { + public void getValueForString_returnsCorrectValues() { assertEquals( "Returns ExposureMode.auto for 'auto'", ExposureMode.getValueForString("auto"), @@ -23,13 +23,13 @@ public void getValueForString_returns_correct_values() { } @Test - public void getValueForString_returns_null_for_nonexistant_value() { + public void getValueForString_returnsNullForNonexistantValue() { assertEquals( "Returns null for 'nonexistant'", ExposureMode.getValueForString("nonexistant"), null); } @Test - public void toString_returns_correct_value() { + public void toString_returnsCorrectValue() { assertEquals("Returns 'auto' for ExposureMode.auto", ExposureMode.auto.toString(), "auto"); assertEquals( "Returns 'locked' for ExposureMode.locked", ExposureMode.locked.toString(), "locked"); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java index 40d17fdc496e..ee428f3d5e02 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java @@ -17,7 +17,7 @@ public class ExposureOffsetFeatureTest { @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); @@ -25,7 +25,7 @@ public void getDebugName_should_return_the_name_of_the_feature() { } @Test - public void getValue_should_return_zero_if_not_set() { + public void getValue_shouldReturnZeroIfNotSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); @@ -35,7 +35,7 @@ public void getValue_should_return_zero_if_not_set() { } @Test - public void getValue_should_echo_the_set_value() { + public void getValue_shouldEchoTheSetValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); double expectedValue = 4.0; @@ -49,8 +49,7 @@ public void getValue_should_echo_the_set_value() { } @Test - public void - getExposureOffsetStepSize_should_return_the_control_exposure_compensation_step_value() { + public void getExposureOffsetStepSize_shouldReturnTheControlExposureCompensationStepValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); @@ -60,7 +59,7 @@ public void getValue_should_echo_the_set_value() { } @Test - public void checkIsSupported_should_return_true() { + public void checkIsSupported_shouldReturnTrue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); @@ -68,7 +67,7 @@ public void checkIsSupported_should_return_true() { } @Test - public void updateBuilder_should_set_control_ae_exposure_compensation_to_offset() { + public void updateBuilder_shouldSetControlAeExposureCompensationToOffset() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java index 4a515c6fd0ec..b34a04fe26b7 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java @@ -19,9 +19,12 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.MeteringRectangle; import android.util.Size; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegionUtils; import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; import org.junit.Before; import org.junit.Test; import org.mockito.MockedStatic; @@ -30,35 +33,44 @@ public class ExposurePointFeatureTest { Size mockCameraBoundaries; + SensorOrientationFeature mockSensorOrientationFeature; + DeviceOrientationManager mockDeviceOrientationManager; @Before public void setUp() { this.mockCameraBoundaries = mock(Size.class); when(this.mockCameraBoundaries.getWidth()).thenReturn(100); when(this.mockCameraBoundaries.getHeight()).thenReturn(100); + mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + mockDeviceOrientationManager = mock(DeviceOrientationManager.class); + when(mockSensorOrientationFeature.getDeviceOrientationManager()) + .thenReturn(mockDeviceOrientationManager); + when(mockDeviceOrientationManager.getLastUIOrientation()) + .thenReturn(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); } @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - CameraRegionUtils mockCameraRegions = mock(CameraRegionUtils.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); assertEquals("ExposurePointFeature", exposurePointFeature.getDebugName()); } @Test - public void getValue_should_return_null_if_not_set() { + public void getValue_shouldReturnNullIfNotSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); - Point actualPoint = exposurePointFeature.getValue(); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); assertNull(exposurePointFeature.getValue()); } @Test - public void getValue_should_echo_the_set_value() { + public void getValue_shouldEchoTheSetValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); Point expectedPoint = new Point(0.0, 0.0); @@ -69,9 +81,10 @@ public void getValue_should_echo_the_set_value() { } @Test - public void setValue_should_reset_point_when_x_coord_is_null() { + public void setValue_shouldResetPointWhenXCoordIsNull() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); exposurePointFeature.setValue(new Point(null, 0.0)); @@ -80,9 +93,10 @@ public void setValue_should_reset_point_when_x_coord_is_null() { } @Test - public void setValue_should_reset_point_when_y_coord_is_null() { + public void setValue_shouldResetPointWhenYCoordIsNull() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); exposurePointFeature.setValue(new Point(0.0, null)); @@ -91,9 +105,10 @@ public void setValue_should_reset_point_when_y_coord_is_null() { } @Test - public void setValue_should_set_point_when_valid_coords_are_supplied() { + public void setValue_shouldSetPointWhenValidCoordsAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); Point point = new Point(0.0, 0.0); @@ -103,11 +118,11 @@ public void setValue_should_set_point_when_valid_coords_are_supplied() { } @Test - public void - setValue_should_determine_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { + public void setValue_shouldDetermineMeteringRectangleWhenValidBoundariesAndCoordsAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); Size mockedCameraBoundaries = mock(Size.class); exposurePointFeature.setCameraBoundaries(mockedCameraBoundaries); @@ -117,16 +132,22 @@ public void setValue_should_set_point_when_valid_coords_are_supplied() { exposurePointFeature.setValue(new Point(0.5, 0.5)); mockedCameraRegionUtils.verify( - () -> CameraRegionUtils.convertPointToMeteringRectangle(mockedCameraBoundaries, 0.5, 0.5), + () -> + CameraRegionUtils.convertPointToMeteringRectangle( + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT), times(1)); } } @Test(expected = AssertionError.class) - public void setValue_should_throw_assertion_error_when_no_valid_boundaries_are_set() { + public void setValue_shouldThrowAssertionErrorWhenNoValidBoundariesAreSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); try (MockedStatic mockedCameraRegionUtils = Mockito.mockStatic(CameraRegionUtils.class)) { @@ -135,10 +156,11 @@ public void setValue_should_throw_assertion_error_when_no_valid_boundaries_are_s } @Test - public void setValue_should_not_determine_metering_rectangle_when_null_coords_are_set() { + public void setValue_shouldNotDetermineMeteringRectangleWhenNullCoordsAreSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); Size mockedCameraBoundaries = mock(Size.class); exposurePointFeature.setCameraBoundaries(mockedCameraBoundaries); @@ -155,10 +177,11 @@ public void setValue_should_not_determine_metering_rectangle_when_null_coords_ar @Test public void - setCameraBoundaries_should_determine_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { + setCameraBoundaries_shouldDetermineMeteringRectangleWhenValidBoundariesAndCoordsAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); exposurePointFeature.setValue(new Point(0.5, 0.5)); Size mockedCameraBoundaries = mock(Size.class); @@ -169,15 +192,21 @@ public void setValue_should_not_determine_metering_rectangle_when_null_coords_ar exposurePointFeature.setCameraBoundaries(mockedCameraBoundaries); mockedCameraRegionUtils.verify( - () -> CameraRegionUtils.convertPointToMeteringRectangle(mockedCameraBoundaries, 0.5, 0.5), + () -> + CameraRegionUtils.convertPointToMeteringRectangle( + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT), times(1)); } } @Test - public void checkIsSupported_should_return_false_when_max_regions_is_null() { + public void checkIsSupported_shouldReturnFalseWhenMaxRegionsIsNull() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(null); @@ -186,9 +215,10 @@ public void checkIsSupported_should_return_false_when_max_regions_is_null() { } @Test - public void checkIsSupported_should_return_false_when_max_regions_is_zero() { + public void checkIsSupported_shouldReturnFalseWhenMaxRegionsIsZero() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(0); @@ -197,9 +227,10 @@ public void checkIsSupported_should_return_false_when_max_regions_is_zero() { } @Test - public void checkIsSupported_should_return_true_when_max_regions_is_bigger_then_zero() { + public void checkIsSupported_shouldReturnTrueWhenMaxRegionsIsBiggerThenZero() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); @@ -208,10 +239,11 @@ public void checkIsSupported_should_return_true_when_max_regions_is_bigger_then_ } @Test - public void updateBuilder_should_return_when_checkIsSupported_is_false() { + public void updateBuilder_shouldReturnWhenCheckIsSupportedIsFalse() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(0); @@ -221,12 +253,12 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { } @Test - public void - updateBuilder_should_set_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { + public void updateBuilder_shouldSetMeteringRectangleWhenValidBoundariesAndCoordsAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); Size mockedCameraBoundaries = mock(Size.class); MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); @@ -236,7 +268,10 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { .when( () -> CameraRegionUtils.convertPointToMeteringRectangle( - mockedCameraBoundaries, 0.5, 0.5)) + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)) .thenReturn(mockedMeteringRectangle); exposurePointFeature.setCameraBoundaries(mockedCameraBoundaries); exposurePointFeature.setValue(new Point(0.5, 0.5)); @@ -249,13 +284,12 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { } @Test - public void - updateBuilder_should_not_set_metering_rectangle_when_no_valid_boundaries_are_supplied() { + public void updateBuilder_shouldNotSetMeteringRectangleWhenNoValidBoundariesAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); - MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.updateBuilder(mockCaptureRequestBuilder); @@ -263,11 +297,12 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { } @Test - public void updateBuilder_should_not_set_metering_rectangle_when_no_valid_coords_are_supplied() { + public void updateBuilder_shouldNotSetMeteringRectangleWhenNoValidCoordsAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - ExposurePointFeature exposurePointFeature = new ExposurePointFeature(mockCameraProperties); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); exposurePointFeature.setValue(null); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java index eccfb07993c1..f2b4ffc8197c 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java @@ -20,7 +20,7 @@ public class FlashFeatureTest { @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); @@ -28,7 +28,7 @@ public void getDebugName_should_return_the_name_of_the_feature() { } @Test - public void getValue_should_return_auto_if_not_set() { + public void getValue_shouldReturnAutoIfNotSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); @@ -36,7 +36,7 @@ public void getValue_should_return_auto_if_not_set() { } @Test - public void getValue_should_echo_the_set_value() { + public void getValue_shouldEchoTheSetValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); FlashMode expectedValue = FlashMode.torch; @@ -48,7 +48,7 @@ public void getValue_should_echo_the_set_value() { } @Test - public void checkIsSupported_should_return_false_when_flash_info_available_is_null() { + public void checkIsSupported_shouldReturnFalseWhenFlashInfoAvailableIsNull() { CameraProperties mockCameraProperties = mock(CameraProperties.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); @@ -58,7 +58,7 @@ public void checkIsSupported_should_return_false_when_flash_info_available_is_nu } @Test - public void checkIsSupported_should_return_false_when_flash_info_available_is_false() { + public void checkIsSupported_shouldReturnFalseWhenFlashInfoAvailableIsFalse() { CameraProperties mockCameraProperties = mock(CameraProperties.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); @@ -68,7 +68,7 @@ public void checkIsSupported_should_return_false_when_flash_info_available_is_fa } @Test - public void checkIsSupported_should_return_true_when_flash_info_available_is_true() { + public void checkIsSupported_shouldReturnTrueWhenFlashInfoAvailableIsTrue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); @@ -78,7 +78,7 @@ public void checkIsSupported_should_return_true_when_flash_info_available_is_tru } @Test - public void updateBuilder_should_return_when_checkIsSupported_is_false() { + public void updateBuilder_shouldReturnWhenCheckIsSupportedIsFalse() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); @@ -91,7 +91,7 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { } @Test - public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_off() { + public void updateBuilder_shouldSetAeModeAndFlashModeWhenFlashModeIsOff() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); @@ -107,7 +107,7 @@ public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_o } @Test - public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_always() { + public void updateBuilder_shouldSetAeModeAndFlashModeWhenFlashModeIsAlways() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); @@ -123,7 +123,7 @@ public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_a } @Test - public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_torch() { + public void updateBuilder_shouldSetAeModeAndFlashModeWhenFlashModeIsTorch() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); @@ -139,7 +139,7 @@ public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_t } @Test - public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_auto() { + public void updateBuilder_shouldSetAeModeAndFlashModeWhenFlashModeIsAuto() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); FlashFeature flashFeature = new FlashFeature(mockCameraProperties); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java index d158336ef235..f03dc9f62e87 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java @@ -19,9 +19,12 @@ import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.MeteringRectangle; import android.util.Size; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugins.camera.CameraProperties; import io.flutter.plugins.camera.CameraRegionUtils; import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; import org.junit.Before; import org.junit.Test; import org.mockito.MockedStatic; @@ -30,35 +33,45 @@ public class FocusPointFeatureTest { Size mockCameraBoundaries; + SensorOrientationFeature mockSensorOrientationFeature; + DeviceOrientationManager mockDeviceOrientationManager; @Before public void setUp() { this.mockCameraBoundaries = mock(Size.class); when(this.mockCameraBoundaries.getWidth()).thenReturn(100); when(this.mockCameraBoundaries.getHeight()).thenReturn(100); + mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + mockDeviceOrientationManager = mock(DeviceOrientationManager.class); + when(mockSensorOrientationFeature.getDeviceOrientationManager()) + .thenReturn(mockDeviceOrientationManager); + when(mockDeviceOrientationManager.getLastUIOrientation()) + .thenReturn(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); } @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - CameraRegionUtils mockCameraRegions = mock(CameraRegionUtils.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); assertEquals("FocusPointFeature", focusPointFeature.getDebugName()); } @Test - public void getValue_should_return_null_if_not_set() { + public void getValue_shouldReturnNullIfNotSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); Point actualPoint = focusPointFeature.getValue(); assertNull(focusPointFeature.getValue()); } @Test - public void getValue_should_echo_the_set_value() { + public void getValue_shouldEchoTheSetValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); Point expectedPoint = new Point(0.0, 0.0); @@ -69,9 +82,10 @@ public void getValue_should_echo_the_set_value() { } @Test - public void setValue_should_reset_point_when_x_coord_is_null() { + public void setValue_shouldResetPointWhenXCoordIsNull() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); focusPointFeature.setValue(new Point(null, 0.0)); @@ -80,9 +94,10 @@ public void setValue_should_reset_point_when_x_coord_is_null() { } @Test - public void setValue_should_reset_point_when_y_coord_is_null() { + public void setValue_shouldResetPointWhenYCoordIsNull() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); focusPointFeature.setValue(new Point(0.0, null)); @@ -91,9 +106,10 @@ public void setValue_should_reset_point_when_y_coord_is_null() { } @Test - public void setValue_should_set_point_when_valid_coords_are_supplied() { + public void setValue_shouldSetPointWhenValidCoordsAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); Point point = new Point(0.0, 0.0); @@ -103,11 +119,11 @@ public void setValue_should_set_point_when_valid_coords_are_supplied() { } @Test - public void - setValue_should_determine_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { + public void setValue_shouldDetermineMeteringRectangleWhenValidBoundariesAndCoordsAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); Size mockedCameraBoundaries = mock(Size.class); focusPointFeature.setCameraBoundaries(mockedCameraBoundaries); @@ -117,16 +133,22 @@ public void setValue_should_set_point_when_valid_coords_are_supplied() { focusPointFeature.setValue(new Point(0.5, 0.5)); mockedCameraRegionUtils.verify( - () -> CameraRegionUtils.convertPointToMeteringRectangle(mockedCameraBoundaries, 0.5, 0.5), + () -> + CameraRegionUtils.convertPointToMeteringRectangle( + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT), times(1)); } } @Test(expected = AssertionError.class) - public void setValue_should_throw_assertion_error_when_no_valid_boundaries_are_set() { + public void setValue_shouldThrowAssertionErrorWhenNoValidBoundariesAreSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); try (MockedStatic mockedCameraRegionUtils = Mockito.mockStatic(CameraRegionUtils.class)) { @@ -135,10 +157,11 @@ public void setValue_should_throw_assertion_error_when_no_valid_boundaries_are_s } @Test - public void setValue_should_not_determine_metering_rectangle_when_null_coords_are_set() { + public void setValue_shouldNotDetermineMeteringRectangleWhenNullCoordsAreSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); Size mockedCameraBoundaries = mock(Size.class); focusPointFeature.setCameraBoundaries(mockedCameraBoundaries); @@ -155,10 +178,11 @@ public void setValue_should_not_determine_metering_rectangle_when_null_coords_ar @Test public void - setCameraBoundaries_should_determine_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { + setCameraBoundaries_shouldDetermineMeteringRectangleWhenValidBoundariesAndCoordsAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); focusPointFeature.setValue(new Point(0.5, 0.5)); Size mockedCameraBoundaries = mock(Size.class); @@ -169,15 +193,21 @@ public void setValue_should_not_determine_metering_rectangle_when_null_coords_ar focusPointFeature.setCameraBoundaries(mockedCameraBoundaries); mockedCameraRegionUtils.verify( - () -> CameraRegionUtils.convertPointToMeteringRectangle(mockedCameraBoundaries, 0.5, 0.5), + () -> + CameraRegionUtils.convertPointToMeteringRectangle( + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT), times(1)); } } @Test - public void checkIsSupported_should_return_false_when_max_regions_is_null() { + public void checkIsSupported_shouldReturnFalseWhenMaxRegionsIsNull() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(null); @@ -186,9 +216,10 @@ public void checkIsSupported_should_return_false_when_max_regions_is_null() { } @Test - public void checkIsSupported_should_return_false_when_max_regions_is_zero() { + public void checkIsSupported_shouldReturnFalseWhenMaxRegionsIsZero() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(0); @@ -197,9 +228,10 @@ public void checkIsSupported_should_return_false_when_max_regions_is_zero() { } @Test - public void checkIsSupported_should_return_true_when_max_regions_is_bigger_then_zero() { + public void checkIsSupported_shouldReturnTrueWhenMaxRegionsIsBiggerThenZero() { CameraProperties mockCameraProperties = mock(CameraProperties.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(new Size(100, 100)); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); @@ -208,10 +240,11 @@ public void checkIsSupported_should_return_true_when_max_regions_is_bigger_then_ } @Test - public void updateBuilder_should_return_when_checkIsSupported_is_false() { + public void updateBuilder_shouldReturnWhenCheckIsSupportedIsFalse() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(0); @@ -221,12 +254,12 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { } @Test - public void - updateBuilder_should_set_metering_rectangle_when_valid_boundaries_and_coords_are_supplied() { + public void updateBuilder_shouldSetMeteringRectangleWhenValidBoundariesAndCoordsAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); Size mockedCameraBoundaries = mock(Size.class); MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); @@ -236,7 +269,10 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { .when( () -> CameraRegionUtils.convertPointToMeteringRectangle( - mockedCameraBoundaries, 0.5, 0.5)) + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)) .thenReturn(mockedMeteringRectangle); focusPointFeature.setCameraBoundaries(mockedCameraBoundaries); focusPointFeature.setValue(new Point(0.5, 0.5)); @@ -249,12 +285,12 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { } @Test - public void - updateBuilder_should_not_set_metering_rectangle_when_no_valid_boundaries_are_supplied() { + public void updateBuilder_shouldNotSetMeteringRectangleWhenNoValidBoundariesAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); focusPointFeature.updateBuilder(mockCaptureRequestBuilder); @@ -263,11 +299,12 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { } @Test - public void updateBuilder_should_not_set_metering_rectangle_when_no_valid_coords_are_supplied() { + public void updateBuilder_shouldNotSetMeteringRectangleWhenNoValidCoordsAreSupplied() { CameraProperties mockCameraProperties = mock(CameraProperties.class); when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); - FocusPointFeature focusPointFeature = new FocusPointFeature(mockCameraProperties); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); focusPointFeature.setValue(null); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java index 7b6e70fff5b2..93cfe5523df3 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java @@ -18,7 +18,7 @@ @RunWith(RobolectricTestRunner.class) public class FpsRangeFeaturePixel4aTest { @Test - public void ctor_should_initialize_fps_range_with_30_when_device_is_pixel_4a() { + public void ctor_shouldInitializeFpsRangeWith30WhenDeviceIsPixel4a() { TestUtils.setFinalStatic(Build.class, "BRAND", "google"); TestUtils.setFinalStatic(Build.class, "MODEL", "Pixel 4a"); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java index 77937b5e87c6..2bb4d849a277 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java @@ -35,19 +35,19 @@ public void after() { } @Test - public void ctor_should_initialize_fps_range_with_highest_upper_value_from_range_array() { + public void ctor_shouldInitializeFpsRangeWithHighestUpperValueFromRangeArray() { FpsRangeFeature fpsRangeFeature = createTestInstance(); assertEquals(13, (int) fpsRangeFeature.getValue().getUpper()); } @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { FpsRangeFeature fpsRangeFeature = createTestInstance(); assertEquals("FpsRangeFeature", fpsRangeFeature.getDebugName()); } @Test - public void getValue_should_return_highest_upper_range_if_not_set() { + public void getValue_shouldReturnHighestUpperRangeIfNotSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); FpsRangeFeature fpsRangeFeature = createTestInstance(); @@ -55,7 +55,7 @@ public void getValue_should_return_highest_upper_range_if_not_set() { } @Test - public void getValue_should_echo_the_set_value() { + public void getValue_shouldEchoTheSetValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); FpsRangeFeature fpsRangeFeature = new FpsRangeFeature(mockCameraProperties); @SuppressWarnings("unchecked") @@ -68,14 +68,14 @@ public void getValue_should_echo_the_set_value() { } @Test - public void checkIsSupported_should_return_true() { + public void checkIsSupported_shouldReturnTrue() { FpsRangeFeature fpsRangeFeature = createTestInstance(); assertTrue(fpsRangeFeature.checkIsSupported()); } @Test @SuppressWarnings("unchecked") - public void updateBuilder_should_set_ae_target_fps_range() { + public void updateBuilder_shouldSetAeTargetFpsRange() { CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); FpsRangeFeature fpsRangeFeature = createTestInstance(); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java index eb1a639a2ac3..b89aad0f6773 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java @@ -37,7 +37,7 @@ public void after() { } @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); @@ -45,7 +45,7 @@ public void getDebugName_should_return_the_name_of_the_feature() { } @Test - public void getValue_should_return_fast_if_not_set() { + public void getValue_shouldReturnFastIfNotSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); @@ -53,7 +53,7 @@ public void getValue_should_return_fast_if_not_set() { } @Test - public void getValue_should_echo_the_set_value() { + public void getValue_shouldEchoTheSetValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); NoiseReductionMode expectedValue = NoiseReductionMode.fast; @@ -65,7 +65,7 @@ public void getValue_should_echo_the_set_value() { } @Test - public void checkIsSupported_should_return_false_when_available_noise_reduction_modes_is_null() { + public void checkIsSupported_shouldReturnFalseWhenAvailableNoiseReductionModesIsNull() { CameraProperties mockCameraProperties = mock(CameraProperties.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); @@ -76,7 +76,7 @@ public void checkIsSupported_should_return_false_when_available_noise_reduction_ @Test public void - checkIsSupported_should_return_false_when_available_noise_reduction_modes_returns_an_empty_array() { + checkIsSupported_shouldReturnFalseWhenAvailableNoiseReductionModesReturnsAnEmptyArray() { CameraProperties mockCameraProperties = mock(CameraProperties.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); @@ -87,7 +87,7 @@ public void checkIsSupported_should_return_false_when_available_noise_reduction_ @Test public void - checkIsSupported_should_return_true_when_available_noise_reduction_modes_returns_at_least_one_item() { + checkIsSupported_shouldReturnTrueWhenAvailableNoiseReductionModesReturnsAtLeastOneItem() { CameraProperties mockCameraProperties = mock(CameraProperties.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); @@ -97,7 +97,7 @@ public void checkIsSupported_should_return_false_when_available_noise_reduction_ } @Test - public void updateBuilder_should_return_when_checkIsSupported_is_false() { + public void updateBuilder_shouldReturnWhenCheckIsSupportedIsFalse() { CameraProperties mockCameraProperties = mock(CameraProperties.class); CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); @@ -110,29 +110,28 @@ public void updateBuilder_should_return_when_checkIsSupported_is_false() { } @Test - public void updateBuilder_should_set_noise_reduction_mode_off_when_off() { + public void updateBuilder_shouldSetNoiseReductionModeOffWhenOff() { testUpdateBuilderWith(NoiseReductionMode.off, CaptureRequest.NOISE_REDUCTION_MODE_OFF); } @Test - public void updateBuilder_should_set_noise_reduction_mode_fast_when_fast() { + public void updateBuilder_shouldSetNoiseReductionModeFastWhenFast() { testUpdateBuilderWith(NoiseReductionMode.fast, CaptureRequest.NOISE_REDUCTION_MODE_FAST); } @Test - public void updateBuilder_should_set_noise_reduction_mode_high_quality_when_high_quality() { + public void updateBuilder_shouldSetNoiseReductionModeHighQualityWhenHighQuality() { testUpdateBuilderWith( NoiseReductionMode.highQuality, CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY); } @Test - public void updateBuilder_should_set_noise_reduction_mode_minimal_when_minimal() { + public void updateBuilder_shouldSetNoiseReductionModeMinimalWhenMinimal() { testUpdateBuilderWith(NoiseReductionMode.minimal, CaptureRequest.NOISE_REDUCTION_MODE_MINIMAL); } @Test - public void - updateBuilder_should_set_noise_reduction_mode_zero_shutter_lag_when_zero_shutter_lag() { + public void updateBuilder_shouldSetNoiseReductionModeZeroShutterLagWhenZeroShutterLag() { testUpdateBuilderWith( NoiseReductionMode.zeroShutterLag, CaptureRequest.NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG); } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java index bb9cb61e1508..e09223dfabe9 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java @@ -79,7 +79,7 @@ public void after() { } @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ResolutionFeature resolutionFeature = new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); @@ -88,7 +88,7 @@ public void getDebugName_should_return_the_name_of_the_feature() { } @Test - public void getValue_should_return_initial_value_when_not_set() { + public void getValue_shouldReturnInitialValueWhenNotSet() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ResolutionFeature resolutionFeature = new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); @@ -97,7 +97,7 @@ public void getValue_should_return_initial_value_when_not_set() { } @Test - public void getValue_should_echo_setValue() { + public void getValue_shouldEchoSetValue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ResolutionFeature resolutionFeature = new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); @@ -108,7 +108,7 @@ public void getValue_should_echo_setValue() { } @Test - public void checkIsSupport_returns_true() { + public void checkIsSupport_returnsTrue() { CameraProperties mockCameraProperties = mock(CameraProperties.class); ResolutionFeature resolutionFeature = new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); @@ -117,7 +117,7 @@ public void checkIsSupport_returns_true() { } @Test - public void getBestAvailableCamcorderProfileForResolutionPreset_should_fall_through() { + public void getBestAvailableCamcorderProfileForResolutionPreset_shouldFallThrough() { mockedStaticProfile .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_HIGH)) .thenReturn(false); @@ -147,42 +147,42 @@ public void getBestAvailableCamcorderProfileForResolutionPreset_should_fall_thro } @Test - public void computeBestPreviewSize_should_use_720P_when_resolution_preset_max() { + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetMax() { ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.max); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); } @Test - public void computeBestPreviewSize_should_use_720P_when_resolution_preset_ultraHigh() { + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetUltraHigh() { ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.ultraHigh); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); } @Test - public void computeBestPreviewSize_should_use_720P_when_resolution_preset_veryHigh() { + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetVeryHigh() { ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.veryHigh); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); } @Test - public void computeBestPreviewSize_should_use_720P_when_resolution_preset_high() { + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetHigh() { ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.high); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); } @Test - public void computeBestPreviewSize_should_use_480P_when_resolution_preset_medium() { + public void computeBestPreviewSize_shouldUse480PWhenResolutionPresetMedium() { ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.medium); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_480P)); } @Test - public void computeBestPreviewSize_should_use_QVGA_when_resolution_preset_low() { + public void computeBestPreviewSize_shouldUseQVGAWhenResolutionPresetLow() { ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.low); mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA)); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java index 6e8d04d20e99..58f17cb758bf 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java @@ -50,15 +50,15 @@ public void before() { } @Test - public void getMediaOrientation_when_natural_screen_orientation_equals_portrait_up() { + public void getVideoOrientation_whenNaturalScreenOrientationEqualsPortraitUp() { int degreesPortraitUp = - deviceOrientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_UP); + deviceOrientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_UP); int degreesPortraitDown = - deviceOrientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_DOWN); + deviceOrientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_DOWN); int degreesLandscapeLeft = - deviceOrientationManager.getMediaOrientation(DeviceOrientation.LANDSCAPE_LEFT); + deviceOrientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_LEFT); int degreesLandscapeRight = - deviceOrientationManager.getMediaOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + deviceOrientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); assertEquals(0, degreesPortraitUp); assertEquals(90, degreesLandscapeLeft); @@ -67,17 +67,17 @@ public void getMediaOrientation_when_natural_screen_orientation_equals_portrait_ } @Test - public void getMediaOrientation_when_natural_screen_orientation_equals_landscape_left() { + public void getVideoOrientation_whenNaturalScreenOrientationEqualsLandscapeLeft() { DeviceOrientationManager orientationManager = DeviceOrientationManager.create(mockActivity, mockDartMessenger, false, 90); - int degreesPortraitUp = orientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitUp = orientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_UP); int degreesPortraitDown = - orientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_DOWN); + orientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_DOWN); int degreesLandscapeLeft = - orientationManager.getMediaOrientation(DeviceOrientation.LANDSCAPE_LEFT); + orientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_LEFT); int degreesLandscapeRight = - orientationManager.getMediaOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + orientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); assertEquals(90, degreesPortraitUp); assertEquals(180, degreesLandscapeLeft); @@ -86,105 +86,96 @@ public void getMediaOrientation_when_natural_screen_orientation_equals_landscape } @Test - public void getMediaOrientation_should_fallback_to_sensor_orientation_when_orientation_is_null() { + public void getVideoOrientation_shouldFallbackToSensorOrientationWhenOrientationIsNull() { setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); - int degrees = deviceOrientationManager.getMediaOrientation(null); + int degrees = deviceOrientationManager.getVideoOrientation(null); assertEquals(90, degrees); } @Test - public void handleSensorOrientationChange_should_send_message_when_sensor_access_is_allowed() { - try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { - mockedSystem - .when( - () -> - Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) - .thenReturn(1); - setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); - - deviceOrientationManager.handleSensorOrientationChange(90); - } + public void getPhotoOrientation_whenNaturalScreenOrientationEqualsPortraitUp() { + int degreesPortraitUp = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); - verify(mockDartMessenger, times(1)) - .sendDeviceOrientationChangeEvent(DeviceOrientation.LANDSCAPE_LEFT); + assertEquals(0, degreesPortraitUp); + assertEquals(90, degreesLandscapeRight); + assertEquals(180, degreesPortraitDown); + assertEquals(270, degreesLandscapeLeft); } @Test - public void - handleSensorOrientationChange_should_send_message_when_sensor_access_is_not_allowed() { - try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { - mockedSystem - .when( - () -> - Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) - .thenReturn(0); - setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + public void getPhotoOrientation_whenNaturalScreenOrientationEqualsLandscapeLeft() { + DeviceOrientationManager orientationManager = + DeviceOrientationManager.create(mockActivity, mockDartMessenger, false, 90); - deviceOrientationManager.handleSensorOrientationChange(90); - } + int degreesPortraitUp = orientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + orientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + orientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + orientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); - verify(mockDartMessenger, never()).sendDeviceOrientationChangeEvent(any()); + assertEquals(90, degreesPortraitUp); + assertEquals(180, degreesLandscapeRight); + assertEquals(270, degreesPortraitDown); + assertEquals(0, degreesLandscapeLeft); } @Test - public void handleUIOrientationChange_should_send_message_when_sensor_access_is_allowed() { - try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { - mockedSystem - .when( - () -> - Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) - .thenReturn(0); - setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + public void getPhotoOrientation_shouldFallbackToCurrentOrientationWhenOrientationIsNull() { + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); - deviceOrientationManager.handleUIOrientationChange(); - } + int degrees = deviceOrientationManager.getPhotoOrientation(null); - verify(mockDartMessenger, times(1)) - .sendDeviceOrientationChangeEvent(DeviceOrientation.LANDSCAPE_LEFT); + assertEquals(270, degrees); } @Test - public void handleUIOrientationChange_should_send_message_when_sensor_access_is_not_allowed() { + public void handleUIOrientationChange_shouldSendMessageWhenSensorAccessIsAllowed() { try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { mockedSystem .when( () -> Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) - .thenReturn(1); + .thenReturn(0); setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); deviceOrientationManager.handleUIOrientationChange(); } - verify(mockDartMessenger, never()).sendDeviceOrientationChangeEvent(any()); + verify(mockDartMessenger, times(1)) + .sendDeviceOrientationChangeEvent(DeviceOrientation.LANDSCAPE_LEFT); } @Test - public void handleOrientationChange_should_send_message_when_orientation_is_updated() { + public void handleOrientationChange_shouldSendMessageWhenOrientationIsUpdated() { DeviceOrientation previousOrientation = DeviceOrientation.PORTRAIT_UP; DeviceOrientation newOrientation = DeviceOrientation.LANDSCAPE_LEFT; - DeviceOrientation orientation = - DeviceOrientationManager.handleOrientationChange( - newOrientation, previousOrientation, mockDartMessenger); + DeviceOrientationManager.handleOrientationChange( + newOrientation, previousOrientation, mockDartMessenger); verify(mockDartMessenger, times(1)).sendDeviceOrientationChangeEvent(newOrientation); - assertEquals(newOrientation, orientation); } @Test - public void handleOrientationChange_should_not_send_message_when_orientation_is_not_updated() { + public void handleOrientationChange_shouldNotSendMessageWhenOrientationIsNotUpdated() { DeviceOrientation previousOrientation = DeviceOrientation.PORTRAIT_UP; DeviceOrientation newOrientation = DeviceOrientation.PORTRAIT_UP; - DeviceOrientation orientation = - DeviceOrientationManager.handleOrientationChange( - newOrientation, previousOrientation, mockDartMessenger); + DeviceOrientationManager.handleOrientationChange( + newOrientation, previousOrientation, mockDartMessenger); verify(mockDartMessenger, never()).sendDeviceOrientationChangeEvent(any()); - assertEquals(newOrientation, orientation); } @Test diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java index ce2bb7bb2670..2c3a5ab46634 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java @@ -52,7 +52,7 @@ public void after() { } @Test - public void ctor_should_start_device_orientation_manager() { + public void ctor_shouldStartDeviceOrientationManager() { SensorOrientationFeature sensorOrientationFeature = new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); @@ -60,7 +60,7 @@ public void ctor_should_start_device_orientation_manager() { } @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { SensorOrientationFeature sensorOrientationFeature = new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); @@ -68,7 +68,7 @@ public void getDebugName_should_return_the_name_of_the_feature() { } @Test - public void getValue_should_return_null_if_not_set() { + public void getValue_shouldReturnNullIfNotSet() { SensorOrientationFeature sensorOrientationFeature = new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); @@ -76,7 +76,7 @@ public void getValue_should_return_null_if_not_set() { } @Test - public void getValue_should_echo_setValue() { + public void getValue_shouldEchoSetValue() { SensorOrientationFeature sensorOrientationFeature = new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); @@ -86,7 +86,7 @@ public void getValue_should_echo_setValue() { } @Test - public void checkIsSupport_returns_true() { + public void checkIsSupport_returnsTrue() { SensorOrientationFeature sensorOrientationFeature = new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); @@ -94,8 +94,7 @@ public void checkIsSupport_returns_true() { } @Test - public void - getDeviceOrientationManager_should_return_initialized_DartOrientationManager_instance() { + public void getDeviceOrientationManager_shouldReturnInitializedDartOrientationManagerInstance() { SensorOrientationFeature sensorOrientationFeature = new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); @@ -104,7 +103,7 @@ public void checkIsSupport_returns_true() { } @Test - public void lockCaptureOrientation_should_lock_to_specified_orientation() { + public void lockCaptureOrientation_shouldLockToSpecifiedOrientation() { SensorOrientationFeature sensorOrientationFeature = new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); @@ -115,7 +114,7 @@ public void lockCaptureOrientation_should_lock_to_specified_orientation() { } @Test - public void unlockCaptureOrientation_should_set_lock_to_null() { + public void unlockCaptureOrientation_shouldSetLockToNull() { SensorOrientationFeature sensorOrientationFeature = new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java index c76708a3769e..9f05cc255a8b 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java @@ -50,7 +50,7 @@ public void after() { } @Test - public void ctor_when_parameters_are_valid() { + public void ctor_whenParametersAreValid() { when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(mockSensorArray); when(mockCameraProperties.getScalerAvailableMaxDigitalZoom()).thenReturn(42f); @@ -63,7 +63,7 @@ public void ctor_when_parameters_are_valid() { } @Test - public void ctor_when_sensor_size_is_null() { + public void ctor_whenSensorSizeIsNull() { when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(null); when(mockCameraProperties.getScalerAvailableMaxDigitalZoom()).thenReturn(42f); @@ -77,7 +77,7 @@ public void ctor_when_sensor_size_is_null() { } @Test - public void ctor_when_max_zoom_is_null() { + public void ctor_whenMaxZoomIsNull() { when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(mockSensorArray); when(mockCameraProperties.getScalerAvailableMaxDigitalZoom()).thenReturn(null); @@ -91,7 +91,7 @@ public void ctor_when_max_zoom_is_null() { } @Test - public void ctor_when_max_zoom_is_smaller_then_default_zoom_factor() { + public void ctor_whenMaxZoomIsSmallerThenDefaultZoomFactor() { when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(mockSensorArray); when(mockCameraProperties.getScalerAvailableMaxDigitalZoom()).thenReturn(0.5f); @@ -105,21 +105,21 @@ public void ctor_when_max_zoom_is_smaller_then_default_zoom_factor() { } @Test - public void getDebugName_should_return_the_name_of_the_feature() { + public void getDebugName_shouldReturnTheNameOfTheFeature() { ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); assertEquals("ZoomLevelFeature", zoomLevelFeature.getDebugName()); } @Test - public void getValue_should_return_null_if_not_set() { + public void getValue_shouldReturnNullIfNotSet() { ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); assertEquals(1.0, (float) zoomLevelFeature.getValue(), 0); } @Test - public void getValue_should_echo_setValue() { + public void getValue_shouldEchoSetValue() { ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); zoomLevelFeature.setValue(2.3f); @@ -128,14 +128,14 @@ public void getValue_should_echo_setValue() { } @Test - public void checkIsSupport_returns_false_by_default() { + public void checkIsSupport_returnsFalseByDefault() { ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); assertFalse(zoomLevelFeature.checkIsSupported()); } @Test - public void updateBuilder_should_set_scalar_crop_region_when_checkIsSupport_is_true() { + public void updateBuilder_shouldSetScalarCropRegionWhenCheckIsSupportIsTrue() { when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(mockSensorArray); when(mockCameraProperties.getScalerAvailableMaxDigitalZoom()).thenReturn(42f); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtilsTest.java index f83e5fb11e08..28160ff30714 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtilsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtilsTest.java @@ -15,7 +15,7 @@ @RunWith(RobolectricTestRunner.class) public class ZoomUtilsTest { @Test - public void setZoom_when_sensor_size_equals_zero_should_return_crop_region_of_zero() { + public void setZoom_whenSensorSizeEqualsZeroShouldReturnCropRegionOfZero() { final Rect sensorSize = new Rect(0, 0, 0, 0); final Rect computedZoom = ZoomUtils.computeZoom(18f, sensorSize, 1f, 20f); @@ -27,7 +27,7 @@ public void setZoom_when_sensor_size_equals_zero_should_return_crop_region_of_ze } @Test - public void setZoom_when_sensor_size_is_valid_should_return_crop_region() { + public void setZoom_whenSensorSizeIsValidShouldReturnCropRegion() { final Rect sensorSize = new Rect(0, 0, 100, 100); final Rect computedZoom = ZoomUtils.computeZoom(18f, sensorSize, 1f, 20f); @@ -39,7 +39,7 @@ public void setZoom_when_sensor_size_is_valid_should_return_crop_region() { } @Test - public void setZoom_when_zoom_is_greater_then_max_zoom_clamp_to_max_zoom() { + public void setZoom_whenZoomIsGreaterThenMaxZoomClampToMaxZoom() { final Rect sensorSize = new Rect(0, 0, 100, 100); final Rect computedZoom = ZoomUtils.computeZoom(25f, sensorSize, 1f, 10f); @@ -51,7 +51,7 @@ public void setZoom_when_zoom_is_greater_then_max_zoom_clamp_to_max_zoom() { } @Test - public void setZoom_when_zoom_is_smaller_then_min_zoom_clamp_to_min_zoom() { + public void setZoom_whenZoomIsSmallerThenMinZoomClampToMinZoom() { final Rect sensorSize = new Rect(0, 0, 100, 100); final Rect computedZoom = ZoomUtils.computeZoom(0.5f, sensorSize, 1f, 10f); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java index 9b8b54cc959c..5425409c2f3a 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java @@ -24,7 +24,7 @@ public void ctor_test() { } @Test - public void build_Should_set_values_in_correct_order_When_audio_is_disabled() throws IOException { + public void build_shouldSetValuesInCorrectOrderWhenAudioIsDisabled() throws IOException { CamcorderProfile recorderProfile = getEmptyCamcorderProfile(); MediaRecorderBuilder.MediaRecorderFactory mockFactory = mock(MediaRecorderBuilder.MediaRecorderFactory.class); @@ -55,7 +55,7 @@ public void build_Should_set_values_in_correct_order_When_audio_is_disabled() th } @Test - public void build_Should_set_values_in_correct_order_When_audio_is_enabled() throws IOException { + public void build_shouldSetValuesInCorrectOrderWhenAudioIsEnabled() throws IOException { CamcorderProfile recorderProfile = getEmptyCamcorderProfile(); MediaRecorderBuilder.MediaRecorderFactory mockFactory = mock(MediaRecorderBuilder.MediaRecorderFactory.class); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java index 5f4bd9f89ec7..dbef8510e021 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java @@ -11,7 +11,7 @@ public class ExposureModeTest { @Test - public void getValueForString_returns_correct_values() { + public void getValueForString_returnsCorrectValues() { assertEquals( "Returns ExposureMode.auto for 'auto'", ExposureMode.getValueForString("auto"), @@ -23,13 +23,13 @@ public void getValueForString_returns_correct_values() { } @Test - public void getValueForString_returns_null_for_nonexistant_value() { + public void getValueForString_returnsNullForNonexistantValue() { assertEquals( "Returns null for 'nonexistant'", ExposureMode.getValueForString("nonexistant"), null); } @Test - public void toString_returns_correct_value() { + public void toString_returnsCorrectValue() { assertEquals("Returns 'auto' for ExposureMode.auto", ExposureMode.auto.toString(), "auto"); assertEquals( "Returns 'locked' for ExposureMode.locked", ExposureMode.locked.toString(), "locked"); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java index 5a53648bc51e..7ae175ee4649 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java @@ -11,7 +11,7 @@ public class FlashModeTest { @Test - public void getValueForString_returns_correct_values() { + public void getValueForString_returnsCorrectValues() { assertEquals( "Returns FlashMode.off for 'off'", FlashMode.getValueForString("off"), FlashMode.off); assertEquals( @@ -27,13 +27,13 @@ public void getValueForString_returns_correct_values() { } @Test - public void getValueForString_returns_null_for_nonexistant_value() { + public void getValueForString_returnsNullForNonexistantValue() { assertEquals( "Returns null for 'nonexistant'", FlashMode.getValueForString("nonexistant"), null); } @Test - public void toString_returns_correct_value() { + public void toString_returnsCorrectValue() { assertEquals("Returns 'off' for FlashMode.off", FlashMode.off.toString(), "off"); assertEquals("Returns 'auto' for FlashMode.auto", FlashMode.auto.toString(), "auto"); assertEquals("Returns 'always' for FlashMode.always", FlashMode.always.toString(), "always"); diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java index 58e6d7ce3306..1d7b95c1b548 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java @@ -11,7 +11,7 @@ public class FocusModeTest { @Test - public void getValueForString_returns_correct_values() { + public void getValueForString_returnsCorrectValues() { assertEquals( "Returns FocusMode.auto for 'auto'", FocusMode.getValueForString("auto"), FocusMode.auto); assertEquals( @@ -21,13 +21,13 @@ public void getValueForString_returns_correct_values() { } @Test - public void getValueForString_returns_null_for_nonexistant_value() { + public void getValueForString_returnsNullForNonexistantValue() { assertEquals( "Returns null for 'nonexistant'", FocusMode.getValueForString("nonexistant"), null); } @Test - public void toString_returns_correct_value() { + public void toString_returnsCorrectValue() { assertEquals("Returns 'auto' for FocusMode.auto", FocusMode.auto.toString(), "auto"); assertEquals("Returns 'locked' for FocusMode.locked", FocusMode.locked.toString(), "locked"); } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java index 9fc669527bfa..dbf9d11be8b6 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java @@ -23,4 +23,14 @@ public static void setFinalStatic(Class classToModify, String fieldName, Assert.fail("Unable to mock static field: " + fieldName); } } + + public static void setPrivateField(T instance, String fieldName, Object newValue) { + try { + Field field = instance.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(instance, newValue); + } catch (Exception e) { + Assert.fail("Unable to mock private field: " + fieldName); + } + } } diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 3284a9b01fa2..37869fe78528 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -118,7 +118,7 @@ class CameraValue { /// Whether setting the focus point is supported. final bool focusPointSupported; - /// The current device orientation. + /// The current device UI orientation. final DeviceOrientation deviceOrientation; /// The currently locked capture orientation. diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart index ad3175a320a9..1df9f8e2e393 100644 --- a/packages/camera/camera/lib/src/camera_preview.dart +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -61,9 +61,9 @@ class CameraPreview extends StatelessWidget { int _getQuarterTurns() { Map turns = { DeviceOrientation.portraitUp: 0, - DeviceOrientation.landscapeLeft: 1, + DeviceOrientation.landscapeRight: 1, DeviceOrientation.portraitDown: 2, - DeviceOrientation.landscapeRight: 3, + DeviceOrientation.landscapeLeft: 3, }; return turns[_getApplicableOrientation()]!; } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 57161656fc03..a7c6a61a4ef2 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.8.1+7 +version: 0.9.0 environment: sdk: ">=2.12.0 <3.0.0" @@ -25,6 +25,7 @@ dependencies: sdk: flutter pedantic: ^1.10.0 quiver: ^3.0.0 + flutter_plugin_android_lifecycle: ^2.0.2 dev_dependencies: flutter_test: diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index d579341c0e58..8275461192b4 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -146,7 +146,7 @@ void main() { RotatedBox rotatedBox = tester.widget(find.byType(RotatedBox)); - expect(rotatedBox.quarterTurns, 1); + expect(rotatedBox.quarterTurns, 3); debugDefaultTargetPlatformOverride = null; }); @@ -179,7 +179,7 @@ void main() { RotatedBox rotatedBox = tester.widget(find.byType(RotatedBox)); - expect(rotatedBox.quarterTurns, 3); + expect(rotatedBox.quarterTurns, 1); debugDefaultTargetPlatformOverride = null; }); diff --git a/packages/camera/camera_platform_interface/lib/src/events/device_event.dart b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart index c6cedd135fed..ac1c66e4df82 100644 --- a/packages/camera/camera_platform_interface/lib/src/events/device_event.dart +++ b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart @@ -20,8 +20,7 @@ import 'package:flutter/services.dart'; /// They can be (and in fact, are) filtered by the `instanceof`-operator. abstract class DeviceEvent {} -/// The [DeviceOrientationChangedEvent] is fired every time the user changes the -/// physical orientation of the device. +/// The [DeviceOrientationChangedEvent] is fired every time the orientation of the device UI changes. class DeviceOrientationChangedEvent extends DeviceEvent { /// The new orientation of the device final DeviceOrientation orientation; diff --git a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart index 9e84e8fdf47c..7a7bbf3da592 100644 --- a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -96,11 +96,10 @@ abstract class CameraPlatform extends PlatformInterface { throw UnimplementedError('onCameraTimeLimitReached() is not implemented.'); } - /// The device orientation changed. + /// The ui orientation changed. /// /// Implementations for this: /// - Should support all 4 orientations. - /// - Should not emit new values when the screen orientation is locked. Stream onDeviceOrientationChanged() { throw UnimplementedError( 'onDeviceOrientationChanged() is not implemented.'); From 97f61147c983f7ff4613d9dfecfb0a15d6ff67ed Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 24 Aug 2021 14:42:21 -0400 Subject: [PATCH 35/57] [flutter_plugin_tools] Improve process mocking (#4254) The mock process runner used in most of the tests had poor handling of stdout/stderr: - By default it would return the `List` output from the mock process, which was never correct since the parent process runner interface always sets encodings, thus the `dynamic` should always be of type `String` - The process for setting output on a `MockProcess` was awkward, since it was based on a `Stream>`, even though in every case what we actually want to do is just set the full output to a string. - A hack was at some point added (presumably due to the above issues) to bypass that flow at the process runner level, and instead return a `String` set there. That meant there were two ways of setting output (one of which that only worked for one of the ways of running processes) - That hack wasn't updated when the ability to return multiple mock processes instead of a single global mock process was added, so the API was even more confusing, and there was no way to set different output for different processes. This changes the test APIs so that: - `MockProcess` takes stdout and stderr as strings, and internally manages converting them to a `Stream>`. - `RecordingProcessRunner` correctly decodes and returns the output streams when constructing a process result. It also removes the resultStdout and resultStderr hacks, as well as the legacy `processToReturn` API, and converts all uses to the new structure, which is both simpler to use, and clearly associates output with specific processes. --- script/tool/test/analyze_command_test.dart | 4 +- .../test/build_examples_command_test.dart | 2 +- script/tool/test/common/gradle_test.dart | 2 +- script/tool/test/common/xcode_test.dart | 40 +++++--- .../test/drive_examples_command_test.dart | 10 +- .../test/firebase_test_lab_command_test.dart | 22 ++--- script/tool/test/format_command_test.dart | 30 +++--- .../tool/test/lint_android_command_test.dart | 2 +- .../tool/test/lint_podspecs_command_test.dart | 14 ++- script/tool/test/mocks.dart | 43 +++++--- .../tool/test/native_test_command_test.dart | 60 ++++++++---- .../tool/test/publish_check_command_test.dart | 98 ++++++++----------- .../test/publish_plugin_command_test.dart | 19 ++-- script/tool/test/test_command_test.dart | 10 +- script/tool/test/util.dart | 28 +++--- .../tool/test/xcode_analyze_command_test.dart | 4 +- 16 files changed, 197 insertions(+), 191 deletions(-) diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index da2f0aba86c8..502fa9a0634c 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -211,7 +211,7 @@ void main() { createFakePlugin('foo', packagesDir); processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess.failing() // flutter packages get + MockProcess(exitCode: 1) // flutter packages get ]; Error? commandError; @@ -233,7 +233,7 @@ void main() { createFakePlugin('foo', packagesDir); processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess.failing() // dart analyze + MockProcess(exitCode: 1) // dart analyze ]; Error? commandError; diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index 27489a50228a..9c7291c31ddb 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -63,7 +63,7 @@ void main() { processRunner .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [ - MockProcess.failing() // flutter packages get + MockProcess(exitCode: 1) // flutter packages get ]; Error? commandError; diff --git a/script/tool/test/common/gradle_test.dart b/script/tool/test/common/gradle_test.dart index c24887d3d469..3eac60baf3c3 100644 --- a/script/tool/test/common/gradle_test.dart +++ b/script/tool/test/common/gradle_test.dart @@ -168,7 +168,7 @@ void main() { processRunner.mockProcessesForExecutable[project.gradleWrapper.path] = [ - MockProcess.failing(), + MockProcess(exitCode: 1), ]; final int exitCode = await project.runCommand('foo'); diff --git a/script/tool/test/common/xcode_test.dart b/script/tool/test/common/xcode_test.dart index 7e046a2446c2..259d8ea36cd2 100644 --- a/script/tool/test/common/xcode_test.dart +++ b/script/tool/test/common/xcode_test.dart @@ -94,8 +94,9 @@ void main() { } }; - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = jsonEncode(devices); + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(devices)), + ]; expect(await xcode.findBestAvailableIphoneSimulator(), expectedDeviceId); }); @@ -137,15 +138,16 @@ void main() { } }; - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = jsonEncode(devices); + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(devices)), + ]; expect(await xcode.findBestAvailableIphoneSimulator(), null); }); test('returns null if simctl fails', () async { processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing(), + MockProcess(exitCode: 1), ]; expect(await xcode.findBestAvailableIphoneSimulator(), null); @@ -216,7 +218,7 @@ void main() { test('returns error codes', () async { processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing(), + MockProcess(exitCode: 1), ]; final Directory directory = const LocalFileSystem().currentDirectory; @@ -247,8 +249,7 @@ void main() { group('projectHasTarget', () { test('returns true when present', () async { - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = ''' + const String stdout = ''' { "project" : { "configurations" : [ @@ -266,6 +267,9 @@ void main() { ] } }'''; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: stdout), + ]; final Directory project = const LocalFileSystem().directory('/foo.xcodeproj'); @@ -287,8 +291,7 @@ void main() { }); test('returns false when not present', () async { - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = ''' + const String stdout = ''' { "project" : { "configurations" : [ @@ -305,6 +308,9 @@ void main() { ] } }'''; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: stdout), + ]; final Directory project = const LocalFileSystem().directory('/foo.xcodeproj'); @@ -326,8 +332,9 @@ void main() { }); test('returns null for unexpected output', () async { - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = '{}'; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: '{}'), + ]; final Directory project = const LocalFileSystem().directory('/foo.xcodeproj'); @@ -349,8 +356,9 @@ void main() { }); test('returns null for invalid output', () async { - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = ':)'; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: ':)'), + ]; final Directory project = const LocalFileSystem().directory('/foo.xcodeproj'); @@ -372,7 +380,9 @@ void main() { }); test('returns null for failure', () async { - processRunner.processToReturn = MockProcess.failing(); + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(exitCode: 1), // xcodebuild -list + ]; final Directory project = const LocalFileSystem().directory('/foo.xcodeproj'); diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index c6893181e286..bbf865d3edf2 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -60,12 +60,10 @@ void main() { final String output = '''${includeBanner ? updateBanner : ''}[${devices.join(',')}]'''; - final MockProcess mockDevicesProcess = MockProcess.succeeding(); - mockDevicesProcess.stdoutController.close(); // ignore: unawaited_futures + final MockProcess mockDevicesProcess = MockProcess(stdout: output); processRunner .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [mockDevicesProcess]; - processRunner.resultStdout = output; } test('fails if no platforms are provided', () async { @@ -151,7 +149,7 @@ void main() { // Simulate failure from `flutter devices`. processRunner .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = - [MockProcess.failing()]; + [MockProcess(exitCode: 1)]; Error? commandError; final List output = await runCapturingPrint( @@ -954,8 +952,8 @@ void main() { .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [ // No mock for 'devices', since it's running for macOS. - MockProcess.failing(), // 'drive' #1 - MockProcess.failing(), // 'drive' #2 + MockProcess(exitCode: 1), // 'drive' #1 + MockProcess(exitCode: 1), // 'drive' #2 ]; Error? commandError; diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index 35697af3f5fd..7716990b323c 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -40,7 +40,7 @@ void main() { test('fails if gcloud auth fails', () async { processRunner.mockProcessesForExecutable['gcloud'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', @@ -64,8 +64,8 @@ void main() { test('retries gcloud set', () async { processRunner.mockProcessesForExecutable['gcloud'] = [ - MockProcess.succeeding(), // auth - MockProcess.failing(), // config + MockProcess(), // auth + MockProcess(exitCode: 1), // config ]; createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart', @@ -245,10 +245,10 @@ void main() { ]); processRunner.mockProcessesForExecutable['gcloud'] = [ - MockProcess.succeeding(), // auth - MockProcess.succeeding(), // config - MockProcess.failing(), // integration test #1 - MockProcess.succeeding(), // integration test #2 + MockProcess(), // auth + MockProcess(), // config + MockProcess(exitCode: 1), // integration test #1 + MockProcess(), // integration test #2 ]; Error? commandError; @@ -459,7 +459,7 @@ void main() { ]); processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess.failing() // flutter build + MockProcess(exitCode: 1) // flutter build ]; Error? commandError; @@ -496,7 +496,7 @@ void main() { .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; @@ -533,8 +533,8 @@ void main() { .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess.succeeding(), // assembleAndroidTest - MockProcess.failing(), // assembleDebug + MockProcess(), // assembleAndroidTest + MockProcess(exitCode: 1), // assembleDebug ]; Error? commandError; diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index b072e5d30aaf..cf57a9d0dcf7 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -115,7 +115,7 @@ void main() { createFakePlugin('a_plugin', packagesDir, extraFiles: files); processRunner.mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = - [MockProcess.failing()]; + [MockProcess(exitCode: 1)]; Error? commandError; final List output = await runCapturingPrint( runner, ['format'], errorHandler: (Error e) { @@ -167,7 +167,7 @@ void main() { createFakePlugin('a_plugin', packagesDir, extraFiles: files); processRunner.mockProcessesForExecutable['java'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; final List output = await runCapturingPrint( @@ -193,8 +193,8 @@ void main() { createFakePlugin('a_plugin', packagesDir, extraFiles: files); processRunner.mockProcessesForExecutable['java'] = [ - MockProcess.succeeding(), // check for working java - MockProcess.failing(), // format + MockProcess(), // check for working java + MockProcess(exitCode: 1), // format ]; Error? commandError; final List output = await runCapturingPrint( @@ -280,7 +280,7 @@ void main() { createFakePlugin('a_plugin', packagesDir, extraFiles: files); processRunner.mockProcessesForExecutable['clang-format'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; final List output = await runCapturingPrint( @@ -335,8 +335,8 @@ void main() { createFakePlugin('a_plugin', packagesDir, extraFiles: files); processRunner.mockProcessesForExecutable['clang-format'] = [ - MockProcess.succeeding(), // check for working clang-format - MockProcess.failing(), // format + MockProcess(), // check for working clang-format + MockProcess(exitCode: 1), // format ]; Error? commandError; final List output = await runCapturingPrint( @@ -418,11 +418,11 @@ void main() { ]; createFakePlugin('a_plugin', packagesDir, extraFiles: files); + const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; processRunner.mockProcessesForExecutable['git'] = [ - MockProcess.succeeding(), + MockProcess(stdout: changedFilePath), ]; - const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; - processRunner.resultStdout = changedFilePath; + Error? commandError; final List output = await runCapturingPrint(runner, ['format', '--fail-on-change'], @@ -448,7 +448,7 @@ void main() { createFakePlugin('a_plugin', packagesDir, extraFiles: files); processRunner.mockProcessesForExecutable['git'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; final List output = @@ -472,12 +472,12 @@ void main() { ]; createFakePlugin('a_plugin', packagesDir, extraFiles: files); + const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; processRunner.mockProcessesForExecutable['git'] = [ - MockProcess.succeeding(), // ls-files - MockProcess.failing(), // diff + MockProcess(stdout: changedFilePath), // ls-files + MockProcess(exitCode: 1), // diff ]; - const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; - processRunner.resultStdout = changedFilePath; + Error? commandError; final List output = await runCapturingPrint(runner, ['format', '--fail-on-change'], diff --git a/script/tool/test/lint_android_command_test.dart b/script/tool/test/lint_android_command_test.dart index 05ead220c15b..d08058468636 100644 --- a/script/tool/test/lint_android_command_test.dart +++ b/script/tool/test/lint_android_command_test.dart @@ -101,7 +101,7 @@ void main() { }); processRunner.mockProcessesForExecutable['gradlew'] = [ - MockProcess.failing(), + MockProcess(exitCode: 1), ]; Error? commandError; diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart index 51a4e6267770..44247274028f 100644 --- a/script/tool/test/lint_podspecs_command_test.dart +++ b/script/tool/test/lint_podspecs_command_test.dart @@ -75,11 +75,9 @@ void main() { ); processRunner.mockProcessesForExecutable['pod'] = [ - MockProcess.succeeding(), - MockProcess.succeeding(), + MockProcess(stdout: 'Foo', stderr: 'Bar'), + MockProcess(), ]; - processRunner.resultStdout = 'Foo'; - processRunner.resultStderr = 'Bar'; final List output = await runCapturingPrint(runner, ['podspecs']); @@ -173,7 +171,7 @@ void main() { // Simulate failure from `which pod`. processRunner.mockProcessesForExecutable['which'] = [ - MockProcess.failing(), + MockProcess(exitCode: 1), ]; Error? commandError; @@ -199,7 +197,7 @@ void main() { // Simulate failure from `pod`. processRunner.mockProcessesForExecutable['pod'] = [ - MockProcess.failing(), + MockProcess(exitCode: 1), ]; Error? commandError; @@ -227,8 +225,8 @@ void main() { // Simulate failure from the second call to `pod`. processRunner.mockProcessesForExecutable['pod'] = [ - MockProcess.succeeding(), - MockProcess.failing(), + MockProcess(), + MockProcess(exitCode: 1), ]; Error? commandError; diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart index 0dcdedd3db03..3d0aef1b3971 100644 --- a/script/tool/test/mocks.dart +++ b/script/tool/test/mocks.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:io' as io; import 'package:file/file.dart'; @@ -32,22 +33,32 @@ class MockPlatform extends Mock implements Platform { } class MockProcess extends Mock implements io.Process { - MockProcess(); - - /// A mock process that terminates with exitCode 0. - MockProcess.succeeding() { - exitCodeCompleter.complete(0); - } - - /// A mock process that terminates with exitCode 1. - MockProcess.failing() { - exitCodeCompleter.complete(1); + /// Creates a mock process with the given results. + /// + /// The default encodings match the ProcessRunner defaults; mocks for + /// processes run with a different encoding will need to be created with + /// the matching encoding. + MockProcess({ + int exitCode = 0, + String? stdout, + String? stderr, + Encoding stdoutEncoding = io.systemEncoding, + Encoding stderrEncoding = io.systemEncoding, + }) : _exitCode = exitCode { + if (stdout != null) { + _stdoutController.add(stdoutEncoding.encoder.convert(stdout)); + } + if (stderr != null) { + _stderrController.add(stderrEncoding.encoder.convert(stderr)); + } + _stdoutController.close(); + _stderrController.close(); } - final Completer exitCodeCompleter = Completer(); - final StreamController> stdoutController = + final int _exitCode; + final StreamController> _stdoutController = StreamController>(); - final StreamController> stderrController = + final StreamController> _stderrController = StreamController>(); final MockIOSink stdinMock = MockIOSink(); @@ -55,13 +66,13 @@ class MockProcess extends Mock implements io.Process { int get pid => 99; @override - Future get exitCode => exitCodeCompleter.future; + Future get exitCode async => _exitCode; @override - Stream> get stdout => stdoutController.stream; + Stream> get stdout => _stdoutController.stream; @override - Stream> get stderr => stderrController.stream; + Stream> get stderr => _stderrController.stream; @override IOSink get stdin => stdinMock; diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index 59ca17b25c0b..f367dc80182f 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -122,11 +122,9 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - // Exit code 66 from testing indicates no tests. - final MockProcess noTestsProcessResult = MockProcess(); - noTestsProcessResult.exitCodeCompleter.complete(66); processRunner.mockProcessesForExecutable['xcrun'] = [ - noTestsProcessResult, + // Exit code 66 from testing indicates no tests. + MockProcess(exitCode: 66), ]; final List output = await runCapturingPrint(runner, ['native-test', '--macos']); @@ -239,12 +237,13 @@ void main() { 'plugin', packagesDir, platformSupport: { kPlatformIos: PlatformSupport.inline }); - final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = jsonEncode(_kDeviceListMap); + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(_kDeviceListMap)), // simctl + ]; + await runCapturingPrint(runner, ['native-test', '--ios']); expect( @@ -673,7 +672,7 @@ void main() { .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; @@ -745,7 +744,7 @@ void main() { }); processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; @@ -775,9 +774,14 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["RunnerTests", "RunnerUITests"]}}'; + const Map projects = { + 'project': { + 'targets': ['RunnerTests', 'RunnerUITests'] + } + }; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(projects)), // xcodebuild -list + ]; final List output = await runCapturingPrint(runner, [ 'native-test', @@ -835,9 +839,14 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["RunnerTests", "RunnerUITests"]}}'; + const Map projects = { + 'project': { + 'targets': ['RunnerTests', 'RunnerUITests'] + } + }; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(projects)), // xcodebuild -list + ]; final List output = await runCapturingPrint(runner, [ 'native-test', @@ -895,9 +904,16 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); // Simulate a project with unit tests but no integration tests... - processRunner.resultStdout = '{"project":{"targets":["RunnerTests"]}}'; + const Map projects = { + 'project': { + 'targets': ['RunnerTests'] + } + }; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(projects)), // xcodebuild -list + ]; + // ... then try to run only integration tests. final List output = await runCapturingPrint(runner, [ 'native-test', @@ -941,7 +957,9 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.failing(); + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(exitCode: 1), // xcodebuild -list + ]; Error? commandError; final List output = await runCapturingPrint(runner, [ @@ -1192,7 +1210,7 @@ void main() { .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; @@ -1243,11 +1261,11 @@ void main() { .childFile('gradlew') .path; processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; // Simulate failing Android. processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index 11de9f095481..65b0cb54547c 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:collection'; import 'dart:convert'; import 'dart:io' as io; @@ -19,18 +18,18 @@ import 'mocks.dart'; import 'util.dart'; void main() { - group('$PublishCheckProcessRunner tests', () { + group('$PublishCheckCommand tests', () { FileSystem fileSystem; late MockPlatform mockPlatform; late Directory packagesDir; - late PublishCheckProcessRunner processRunner; + late RecordingProcessRunner processRunner; late CommandRunner runner; setUp(() { fileSystem = MemoryFileSystem(); mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = PublishCheckProcessRunner(); + processRunner = RecordingProcessRunner(); final PublishCheckCommand publishCheckCommand = PublishCheckCommand( packagesDir, processRunner: processRunner, @@ -50,12 +49,11 @@ void main() { final Directory plugin2Dir = createFakePlugin('plugin_tools_test_package_b', packagesDir); - processRunner.processesToReturn.add( - MockProcess.succeeding(), - ); - processRunner.processesToReturn.add( - MockProcess.succeeding(), - ); + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(), + MockProcess(), + ]; + await runCapturingPrint(runner, ['publish-check']); expect( @@ -75,11 +73,9 @@ void main() { test('fail on negative test', () async { createFakePlugin('plugin_tools_test_package_a', packagesDir); - final MockProcess process = MockProcess.failing(); - process.stdoutController.close(); // ignore: unawaited_futures - process.stderrController.close(); // ignore: unawaited_futures - - processRunner.processesToReturn.add(process); + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(exitCode: 1) + ]; expect( () => runCapturingPrint(runner, ['publish-check']), @@ -91,8 +87,9 @@ void main() { final Directory dir = createFakePlugin('c', packagesDir); await dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); - final MockProcess process = MockProcess(); - processRunner.processesToReturn.add(process); + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(), + ]; expect(() => runCapturingPrint(runner, ['publish-check']), throwsA(isA())); @@ -101,15 +98,14 @@ void main() { test('pass on prerelease if --allow-pre-release flag is on', () async { createFakePlugin('d', packagesDir); - const String preReleaseOutput = 'Package has 1 warning.' - 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'; - - final MockProcess process = MockProcess.failing(); - process.stdoutController.add(preReleaseOutput.codeUnits); - process.stdoutController.close(); // ignore: unawaited_futures - process.stderrController.close(); // ignore: unawaited_futures - - processRunner.processesToReturn.add(process); + final MockProcess process = MockProcess( + exitCode: 1, + stdout: 'Package has 1 warning.\n' + 'Packages with an SDK constraint on a pre-release of the Dart ' + 'SDK should themselves be published as a pre-release version.'); + processRunner.mockProcessesForExecutable['flutter'] = [ + process, + ]; expect( runCapturingPrint( @@ -120,15 +116,14 @@ void main() { test('fail on prerelease if --allow-pre-release flag is off', () async { createFakePlugin('d', packagesDir); - const String preReleaseOutput = 'Package has 1 warning.' - 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'; - - final MockProcess process = MockProcess.failing(); - process.stdoutController.add(preReleaseOutput.codeUnits); - process.stdoutController.close(); // ignore: unawaited_futures - process.stderrController.close(); // ignore: unawaited_futures - - processRunner.processesToReturn.add(process); + final MockProcess process = MockProcess( + exitCode: 1, + stdout: 'Package has 1 warning.\n' + 'Packages with an SDK constraint on a pre-release of the Dart ' + 'SDK should themselves be published as a pre-release version.'); + processRunner.mockProcessesForExecutable['flutter'] = [ + process, + ]; expect(runCapturingPrint(runner, ['publish-check']), throwsA(isA())); @@ -137,14 +132,9 @@ void main() { test('Success message on stderr is not printed as an error', () async { createFakePlugin('d', packagesDir); - const String publishOutput = 'Package has 0 warnings.'; - - final MockProcess process = MockProcess.succeeding(); - process.stderrController.add(publishOutput.codeUnits); - process.stdoutController.close(); // ignore: unawaited_futures - process.stderrController.close(); // ignore: unawaited_futures - - processRunner.processesToReturn.add(process); + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(stdout: 'Package has 0 warnings.'), + ]; final List output = await runCapturingPrint(runner, ['publish-check']); @@ -192,9 +182,6 @@ void main() { createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); - processRunner.processesToReturn.add( - MockProcess.succeeding(), - ); final List output = await runCapturingPrint( runner, ['publish-check', '--machine']); @@ -258,9 +245,9 @@ void main() { createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); - processRunner.processesToReturn.add( - MockProcess.succeeding(), - ); + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(), + ]; final List output = await runCapturingPrint( runner, ['publish-check', '--machine']); @@ -331,9 +318,9 @@ void main() { await plugin1Dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); - processRunner.processesToReturn.add( - MockProcess.succeeding(), - ); + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(), + ]; bool hasError = false; final List output = await runCapturingPrint( @@ -369,10 +356,3 @@ void main() { }); }); } - -class PublishCheckProcessRunner extends RecordingProcessRunner { - final Queue processesToReturn = Queue(); - - @override - io.Process get processToReturn => processesToReturn.removeFirst(); -} diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index c7df81952641..9a937daa2384 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -1060,7 +1060,7 @@ class TestProcessRunner extends ProcessRunner { String? mockPublishStdout; String? mockPublishStderr; - int? mockPublishCompleteCode; + int mockPublishCompleteCode = 0; @override Future run( @@ -1097,17 +1097,14 @@ class TestProcessRunner extends ProcessRunner { args[0] == 'pub' && args[1] == 'publish'); mockPublishArgs.addAll(args); - mockPublishProcess = MockProcess(); - if (mockPublishStdout != null) { - mockPublishProcess.stdoutController.add(utf8.encode(mockPublishStdout!)); - } - if (mockPublishStderr != null) { - mockPublishProcess.stderrController.add(utf8.encode(mockPublishStderr!)); - } - if (mockPublishCompleteCode != null) { - mockPublishProcess.exitCodeCompleter.complete(mockPublishCompleteCode); - } + mockPublishProcess = MockProcess( + exitCode: mockPublishCompleteCode, + stdout: mockPublishStdout, + stderr: mockPublishStderr, + stdoutEncoding: utf8, + stderrEncoding: utf8, + ); return mockPublishProcess; } } diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index 503e24d03056..3b350f7d88ae 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -67,8 +67,8 @@ void main() { processRunner .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [ - MockProcess.failing(), // plugin 1 test - MockProcess.succeeding(), // plugin 2 test + MockProcess(exitCode: 1), // plugin 1 test + MockProcess(), // plugin 2 test ]; Error? commandError; @@ -132,7 +132,7 @@ void main() { extraFiles: ['test/empty_test.dart']); processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess.failing(), // dart pub get + MockProcess(exitCode: 1), // dart pub get ]; Error? commandError; @@ -156,8 +156,8 @@ void main() { extraFiles: ['test/empty_test.dart']); processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess.succeeding(), // dart pub get - MockProcess.failing(), // dart pub run test + MockProcess(), // dart pub get + MockProcess(exitCode: 1), // dart pub run test ]; Error? commandError; diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 1984a25cc430..10a85f49e815 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -265,15 +265,6 @@ class RecordingProcessRunner extends ProcessRunner { final Map> mockProcessesForExecutable = >{}; - /// Populate for [io.ProcessResult] to use a String [stdout] instead of a [List] of [int]. - String? resultStdout; - - /// Populate for [io.ProcessResult] to use a String [stderr] instead of a [List] of [int]. - String? resultStderr; - - // Deprecated--do not add new uses. Use mockProcessesForExecutable instead. - io.Process? processToReturn; - @override Future runAndStream( String executable, @@ -291,8 +282,7 @@ class RecordingProcessRunner extends ProcessRunner { return Future.value(exitCode); } - /// Returns [io.ProcessResult] created from [mockProcessesForExecutable], - /// [resultStdout], and [resultStderr]. + /// Returns [io.ProcessResult] created from [mockProcessesForExecutable]. @override Future run( String executable, @@ -306,10 +296,16 @@ class RecordingProcessRunner extends ProcessRunner { recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); final io.Process? process = _getProcessToReturn(executable); + final List? processStdout = + await process?.stdout.transform(stdoutEncoding.decoder).toList(); + final String stdout = processStdout?.join('') ?? ''; + final List? processStderr = + await process?.stderr.transform(stderrEncoding.decoder).toList(); + final String stderr = processStderr?.join('') ?? ''; + final io.ProcessResult result = process == null ? io.ProcessResult(1, 0, '', '') - : io.ProcessResult(process.pid, await process.exitCode, - resultStdout ?? process.stdout, resultStderr ?? process.stderr); + : io.ProcessResult(process.pid, await process.exitCode, stdout, stderr); if (exitOnError && (result.exitCode != 0)) { throw io.ProcessException(executable, args); @@ -326,13 +322,11 @@ class RecordingProcessRunner extends ProcessRunner { } io.Process? _getProcessToReturn(String executable) { - io.Process? process; final List? processes = mockProcessesForExecutable[executable]; if (processes != null && processes.isNotEmpty) { - process = mockProcessesForExecutable[executable]!.removeAt(0); + return processes.removeAt(0); } - // Fall back to `processToReturn` for backwards compatibility. - return process ?? processToReturn; + return null; } } diff --git a/script/tool/test/xcode_analyze_command_test.dart b/script/tool/test/xcode_analyze_command_test.dart index b715ac531f50..790a526a8ae0 100644 --- a/script/tool/test/xcode_analyze_command_test.dart +++ b/script/tool/test/xcode_analyze_command_test.dart @@ -131,7 +131,7 @@ void main() { }); processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; @@ -228,7 +228,7 @@ void main() { }); processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess.failing() + MockProcess(exitCode: 1) ]; Error? commandError; From fb6622092bb8f1a56b701f6c6ede551b6c986d06 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 24 Aug 2021 16:29:56 -0400 Subject: [PATCH 36/57] [flutter_plugin_tools] Introduce a class for packages (#4252) Packages are the primary conceptual object in the tool, but currently they are represented simply as Directory (or occasionally a path string). This introduces an object for packages and: - moves a number of existing utility methods into it - sweeps the code for the obvious cases of using `Directory` to represent a package, especially in method signatures and migrates them - notes a few places where we should migrate later, to avoid ballooning the size of the PR There are no doubt other cases not caught in the sweep, but this gives us a foundation both for new code, and to migrate incrementally toward as we find existing code that was missed. --- script/tool/lib/src/analyze_command.dart | 14 +- .../tool/lib/src/build_examples_command.dart | 11 +- .../src/common/package_looping_command.dart | 86 +++++------- .../tool/lib/src/common/plugin_command.dart | 54 +++----- script/tool/lib/src/common/plugin_utils.dart | 42 +----- .../lib/src/common/pub_version_finder.dart | 6 +- .../lib/src/common/repository_package.dart | 78 +++++++++++ .../src/create_all_plugins_app_command.dart | 6 +- .../tool/lib/src/drive_examples_command.dart | 34 ++--- .../lib/src/firebase_test_lab_command.dart | 26 ++-- script/tool/lib/src/lint_android_command.dart | 9 +- .../tool/lib/src/lint_podspecs_command.dart | 5 +- script/tool/lib/src/list_command.dart | 18 +-- script/tool/lib/src/native_test_command.dart | 50 +++---- .../tool/lib/src/publish_check_command.dart | 18 +-- .../tool/lib/src/publish_plugin_command.dart | 14 +- .../tool/lib/src/pubspec_check_command.dart | 19 +-- script/tool/lib/src/test_command.dart | 19 +-- .../tool/lib/src/version_check_command.dart | 19 +-- .../tool/lib/src/xcode_analyze_command.dart | 13 +- .../common/package_looping_command_test.dart | 67 +--------- .../tool/test/common/plugin_command_test.dart | 10 +- .../tool/test/common/plugin_utils_test.dart | 21 +-- .../test/common/pub_version_finder_test.dart | 6 +- .../test/common/repository_package_test.dart | 123 ++++++++++++++++++ script/tool/test/format_command_test.dart | 4 +- script/tool/test/util.dart | 2 + 27 files changed, 440 insertions(+), 334 deletions(-) create mode 100644 script/tool/lib/src/common/repository_package.dart create mode 100644 script/tool/test/common/repository_package_test.dart diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 2b728e2b9073..faad7f4736eb 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -5,13 +5,14 @@ import 'dart:async'; import 'package:file/file.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_command.dart'; import 'package:platform/platform.dart'; import 'package:yaml/yaml.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; +import 'common/plugin_command.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; const int _exitPackagesGetFailed = 3; @@ -55,8 +56,9 @@ class AnalyzeCommand extends PackageLoopingCommand { final bool hasLongOutput = false; /// Checks that there are no unexpected analysis_options.yaml files. - bool _hasUnexpecetdAnalysisOptions(Directory package) { - final List files = package.listSync(recursive: true); + bool _hasUnexpecetdAnalysisOptions(RepositoryPackage package) { + final List files = + package.directory.listSync(recursive: true); for (final FileSystemEntity file in files) { if (file.basename != 'analysis_options.yaml' && file.basename != '.analysis_options') { @@ -87,7 +89,7 @@ class AnalyzeCommand extends PackageLoopingCommand { Future _runPackagesGetOnTargetPackages() async { final List packageDirectories = await getTargetPackagesAndSubpackages() - .map((PackageEnumerationEntry package) => package.directory) + .map((PackageEnumerationEntry entry) => entry.package.directory) .toList(); final Set packagePaths = packageDirectories.map((Directory dir) => dir.path).toSet(); @@ -135,13 +137,13 @@ class AnalyzeCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { if (_hasUnexpecetdAnalysisOptions(package)) { return PackageResult.fail(['Unexpected local analysis options']); } final int exitCode = await processRunner.runAndStream( _dartBinaryPath, ['analyze', '--fatal-infos'], - workingDir: package); + workingDir: package.directory); if (exitCode != 0) { return PackageResult.fail(); } diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index 0cac09980c94..ac5e84b7c3c7 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -11,6 +11,7 @@ import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; /// Key for APK. const String _platformFlagApk = 'apk'; @@ -96,7 +97,7 @@ class BuildExamplesCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { final List errors = []; final Iterable<_PlatformDetails> requestedPlatforms = _platforms.entries @@ -126,9 +127,9 @@ class BuildExamplesCommand extends PackageLoopingCommand { } print(''); - for (final Directory example in getExamplesForPlugin(package)) { + for (final RepositoryPackage example in package.getExamples()) { final String packageName = - getRelativePosixPath(example, from: packagesDir); + getRelativePosixPath(example.directory, from: packagesDir); for (final _PlatformDetails platform in buildPlatforms) { String buildPlatform = platform.label; @@ -149,7 +150,7 @@ class BuildExamplesCommand extends PackageLoopingCommand { } Future _buildExample( - Directory example, + RepositoryPackage example, String flutterBuildType, { List extraBuildFlags = const [], }) async { @@ -164,7 +165,7 @@ class BuildExamplesCommand extends PackageLoopingCommand { if (enableExperiment.isNotEmpty) '--enable-experiment=$enableExperiment', ], - workingDir: example, + workingDir: example.directory, ); return exitCode == 0; } diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index 0e0976ecc6a7..00caeb30ef42 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -13,6 +13,7 @@ import 'package:platform/platform.dart'; import 'core.dart'; import 'plugin_command.dart'; import 'process_runner.dart'; +import 'repository_package.dart'; /// Possible outcomes of a command run for a package. enum RunState { @@ -84,7 +85,7 @@ abstract class PackageLoopingCommand extends PluginCommand { int _otherWarningCount = 0; /// The package currently being run by [runForPackage]. - PackageEnumerationEntry? _currentPackage; + PackageEnumerationEntry? _currentPackageEntry; /// Called during [run] before any calls to [runForPackage]. This provides an /// opportunity to fail early if the command can't be run (e.g., because the @@ -97,7 +98,7 @@ abstract class PackageLoopingCommand extends PluginCommand { /// be included in the final error summary (e.g., a command that only has a /// single failure mode), or strings that should be listed for that package /// in the final summary. An empty list indicates success. - Future runForPackage(Directory package); + Future runForPackage(RepositoryPackage package); /// Called during [run] after all calls to [runForPackage]. This provides an /// opportunity to do any cleanup of run-level state. @@ -155,31 +156,13 @@ abstract class PackageLoopingCommand extends PluginCommand { /// things that might be useful to someone debugging an unexpected result. void logWarning(String warningMessage) { print(Colorize(warningMessage)..yellow()); - if (_currentPackage != null) { - _packagesWithWarnings.add(_currentPackage!); + if (_currentPackageEntry != null) { + _packagesWithWarnings.add(_currentPackageEntry!); } else { ++_otherWarningCount; } } - /// Returns the identifying name to use for [package]. - /// - /// Implementations should not expect a specific format for this string, since - /// it uses heuristics to try to be precise without being overly verbose. If - /// an exact format (e.g., published name, or basename) is required, that - /// should be used instead. - String getPackageDescription(Directory package) { - String packageName = getRelativePosixPath(package, from: packagesDir); - final List components = p.posix.split(packageName); - // For the common federated plugin pattern of `foo/foo_subpackage`, drop - // the first part since it's not useful. - if (components.length >= 2 && - components[1].startsWith('${components[0]}_')) { - packageName = p.posix.joinAll(components.sublist(1)); - } - return packageName; - } - /// Returns the relative path from [from] to [entity] in Posix style. /// /// This should be used when, for example, printing package-relative paths in @@ -219,36 +202,36 @@ abstract class PackageLoopingCommand extends PluginCommand { Future _runInternal() async { _packagesWithWarnings.clear(); _otherWarningCount = 0; - _currentPackage = null; + _currentPackageEntry = null; await initializeRun(); - final List packages = includeSubpackages + final List targetPackages = includeSubpackages ? await getTargetPackagesAndSubpackages(filterExcluded: false).toList() : await getTargetPackages(filterExcluded: false).toList(); final Map results = {}; - for (final PackageEnumerationEntry package in packages) { - _currentPackage = package; - _printPackageHeading(package); + for (final PackageEnumerationEntry entry in targetPackages) { + _currentPackageEntry = entry; + _printPackageHeading(entry); // Command implementations should never see excluded packages; they are // included at this level only for logging. - if (package.excluded) { - results[package] = PackageResult.exclude(); + if (entry.excluded) { + results[entry] = PackageResult.exclude(); continue; } - final PackageResult result = await runForPackage(package.directory); + final PackageResult result = await runForPackage(entry.package); if (result.state == RunState.skipped) { final String message = '${indentation}SKIPPING: ${result.details.first}'; captureOutput ? print(message) : print(Colorize(message)..darkGray()); } - results[package] = result; + results[entry] = result; } - _currentPackage = null; + _currentPackageEntry = null; completeRun(); @@ -256,13 +239,13 @@ abstract class PackageLoopingCommand extends PluginCommand { // If there were any errors reported, summarize them and exit. if (results.values .any((PackageResult result) => result.state == RunState.failed)) { - _printFailureSummary(packages, results); + _printFailureSummary(targetPackages, results); return false; } // Otherwise, print a summary of what ran for ease of auditing that all the // expected tests ran. - _printRunSummary(packages, results); + _printRunSummary(targetPackages, results); print('\n'); _printSuccess('No issues found!'); @@ -283,9 +266,9 @@ abstract class PackageLoopingCommand extends PluginCommand { /// Something is always printed to make it easier to distinguish between /// a command running for a package and producing no output, and a command /// not having been run for a package. - void _printPackageHeading(PackageEnumerationEntry package) { - final String packageDisplayName = getPackageDescription(package.directory); - String heading = package.excluded + void _printPackageHeading(PackageEnumerationEntry entry) { + final String packageDisplayName = entry.package.displayName; + String heading = entry.excluded ? 'Not running for $packageDisplayName; excluded' : 'Running for $packageDisplayName'; if (hasLongOutput) { @@ -295,16 +278,15 @@ abstract class PackageLoopingCommand extends PluginCommand { || $heading ============================================================ '''; - } else if (!package.excluded) { + } else if (!entry.excluded) { heading = '$heading...'; } if (captureOutput) { print(heading); } else { final Colorize colorizeHeading = Colorize(heading); - print(package.excluded - ? colorizeHeading.darkGray() - : colorizeHeading.cyan()); + print( + entry.excluded ? colorizeHeading.darkGray() : colorizeHeading.cyan()); } } @@ -349,17 +331,18 @@ abstract class PackageLoopingCommand extends PluginCommand { /// Prints a one-line-per-package overview of the run results for each /// package. - void _printPerPackageRunOverview(List packages, + void _printPerPackageRunOverview( + List packageEnumeration, {required Set skipped}) { print('Run overview:'); - for (final PackageEnumerationEntry package in packages) { - final bool hadWarning = _packagesWithWarnings.contains(package); + for (final PackageEnumerationEntry entry in packageEnumeration) { + final bool hadWarning = _packagesWithWarnings.contains(entry); Styles style; String summary; - if (package.excluded) { + if (entry.excluded) { summary = 'excluded'; style = Styles.DARK_GRAY; - } else if (skipped.contains(package)) { + } else if (skipped.contains(entry)) { summary = 'skipped'; style = hadWarning ? Styles.LIGHT_YELLOW : Styles.DARK_GRAY; } else { @@ -373,18 +356,18 @@ abstract class PackageLoopingCommand extends PluginCommand { if (!captureOutput) { summary = (Colorize(summary)..apply(style)).toString(); } - print(' ${getPackageDescription(package.directory)} - $summary'); + print(' ${entry.package.displayName} - $summary'); } print(''); } /// Prints a summary of all of the failures from [results]. - void _printFailureSummary(List packages, + void _printFailureSummary(List packageEnumeration, Map results) { const String indentation = ' '; _printError(failureListHeader); - for (final PackageEnumerationEntry package in packages) { - final PackageResult result = results[package]!; + for (final PackageEnumerationEntry entry in packageEnumeration) { + final PackageResult result = results[entry]!; if (result.state == RunState.failed) { final String errorIndentation = indentation * 2; String errorDetails = ''; @@ -392,8 +375,7 @@ abstract class PackageLoopingCommand extends PluginCommand { errorDetails = ':\n$errorIndentation${result.details.join('\n$errorIndentation')}'; } - _printError( - '$indentation${getPackageDescription(package.directory)}$errorDetails'); + _printError('$indentation${entry.package.displayName}$errorDetails'); } } _printError(failureListFooter); diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart index 10f423360878..ec51261ab617 100644 --- a/script/tool/lib/src/common/plugin_command.dart +++ b/script/tool/lib/src/common/plugin_command.dart @@ -14,15 +14,18 @@ import 'package:yaml/yaml.dart'; import 'core.dart'; import 'git_version_finder.dart'; import 'process_runner.dart'; +import 'repository_package.dart'; /// An entry in package enumeration for APIs that need to include extra /// data about the entry. class PackageEnumerationEntry { - /// Creates a new entry for the given package directory. - PackageEnumerationEntry(this.directory, {required this.excluded}); + /// Creates a new entry for the given package. + PackageEnumerationEntry(this.package, {required this.excluded}); - /// The package's location. - final Directory directory; + /// The package this entry corresponds to. Be sure to check `excluded` before + /// using this, as having an entry does not necessarily mean that the package + /// should be included in the processing of the enumeration. + final RepositoryPackage package; /// Whether or not this package was excluded by the command invocation. final bool excluded; @@ -225,7 +228,7 @@ abstract class PluginCommand extends Command { final List allPlugins = await _getAllPackages().toList(); allPlugins.sort((PackageEnumerationEntry p1, PackageEnumerationEntry p2) => - p1.directory.path.compareTo(p2.directory.path)); + p1.package.path.compareTo(p2.package.path)); final int shardSize = allPlugins.length ~/ shardCount + (allPlugins.length % shardCount == 0 ? 0 : 1); final int start = min(shardIndex * shardSize, allPlugins.length); @@ -287,7 +290,8 @@ abstract class PluginCommand extends Command { // A top-level Dart package is a plugin package. if (_isDartPackage(entity)) { if (plugins.isEmpty || plugins.contains(p.basename(entity.path))) { - yield PackageEnumerationEntry(entity as Directory, + yield PackageEnumerationEntry( + RepositoryPackage(entity as Directory), excluded: excludedPluginNames.contains(entity.basename)); } } else if (entity is Directory) { @@ -305,7 +309,8 @@ abstract class PluginCommand extends Command { if (plugins.isEmpty || plugins.contains(relativePath) || plugins.contains(basenamePath)) { - yield PackageEnumerationEntry(subdir as Directory, + yield PackageEnumerationEntry( + RepositoryPackage(subdir as Directory), excluded: excludedPluginNames.contains(basenamePath) || excludedPluginNames.contains(packageName) || excludedPluginNames.contains(relativePath)); @@ -327,26 +332,26 @@ abstract class PluginCommand extends Command { await for (final PackageEnumerationEntry plugin in getTargetPackages(filterExcluded: filterExcluded)) { yield plugin; - yield* plugin.directory + yield* plugin.package.directory .list(recursive: true, followLinks: false) .where(_isDartPackage) .map((FileSystemEntity directory) => PackageEnumerationEntry( - directory as Directory, // _isDartPackage guarantees this works. + // _isDartPackage guarantees that this cast is valid. + RepositoryPackage(directory as Directory), excluded: plugin.excluded)); } } - /// Returns the files contained, recursively, within the plugins + /// Returns the files contained, recursively, within the packages /// involved in this command execution. Stream getFiles() { - return getTargetPackages() - .map((PackageEnumerationEntry entry) => entry.directory) - .asyncExpand((Directory folder) => getFilesForPackage(folder)); + return getTargetPackages().asyncExpand( + (PackageEnumerationEntry entry) => getFilesForPackage(entry.package)); } /// Returns the files contained, recursively, within [package]. - Stream getFilesForPackage(Directory package) { - return package + Stream getFilesForPackage(RepositoryPackage package) { + return package.directory .list(recursive: true, followLinks: false) .where((FileSystemEntity entity) => entity is File) .cast(); @@ -358,25 +363,6 @@ abstract class PluginCommand extends Command { return entity is Directory && entity.childFile('pubspec.yaml').existsSync(); } - /// Returns the example Dart packages contained in the specified plugin, or - /// an empty List, if the plugin has no examples. - Iterable getExamplesForPlugin(Directory plugin) { - final Directory exampleFolder = plugin.childDirectory('example'); - if (!exampleFolder.existsSync()) { - return []; - } - if (isFlutterPackage(exampleFolder)) { - return [exampleFolder]; - } - // Only look at the subdirectories of the example directory if the example - // directory itself is not a Dart package, and only look one level below the - // example directory for other dart packages. - return exampleFolder - .listSync() - .where((FileSystemEntity entity) => isFlutterPackage(entity)) - .cast(); - } - /// Retrieve an instance of [GitVersionFinder] based on `_kBaseSha` and [gitDir]. /// /// Throws tool exit if [gitDir] nor root directory is a git directory. diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart index 0277b78d566a..d9c42e220c0b 100644 --- a/script/tool/lib/src/common/plugin_utils.dart +++ b/script/tool/lib/src/common/plugin_utils.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:yaml/yaml.dart'; import 'core.dart'; @@ -16,7 +17,7 @@ enum PlatformSupport { federated, } -/// Returns whether the given directory contains a Flutter [platform] plugin. +/// Returns whether the given [package] is a Flutter [platform] plugin. /// /// It checks this by looking for the following pattern in the pubspec: /// @@ -27,7 +28,7 @@ enum PlatformSupport { /// /// If [requiredMode] is provided, the plugin must have the given type of /// implementation in order to return true. -bool pluginSupportsPlatform(String platform, FileSystemEntity entity, +bool pluginSupportsPlatform(String platform, RepositoryPackage package, {PlatformSupport? requiredMode}) { assert(platform == kPlatformIos || platform == kPlatformAndroid || @@ -35,14 +36,9 @@ bool pluginSupportsPlatform(String platform, FileSystemEntity entity, platform == kPlatformMacos || platform == kPlatformWindows || platform == kPlatformLinux); - if (entity is! Directory) { - return false; - } - try { - final File pubspecFile = entity.childFile('pubspec.yaml'); final YamlMap pubspecYaml = - loadYaml(pubspecFile.readAsStringSync()) as YamlMap; + loadYaml(package.pubspecFile.readAsStringSync()) as YamlMap; final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; if (flutterSection == null) { return false; @@ -78,33 +74,3 @@ bool pluginSupportsPlatform(String platform, FileSystemEntity entity, return false; } } - -/// Returns whether the given directory contains a Flutter Android plugin. -bool isAndroidPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformAndroid, entity); -} - -/// Returns whether the given directory contains a Flutter iOS plugin. -bool isIosPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformIos, entity); -} - -/// Returns whether the given directory contains a Flutter web plugin. -bool isWebPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformWeb, entity); -} - -/// Returns whether the given directory contains a Flutter Windows plugin. -bool isWindowsPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformWindows, entity); -} - -/// Returns whether the given directory contains a Flutter macOS plugin. -bool isMacOsPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformMacos, entity); -} - -/// Returns whether the given directory contains a Flutter linux plugin. -bool isLinuxPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformLinux, entity); -} diff --git a/script/tool/lib/src/common/pub_version_finder.dart b/script/tool/lib/src/common/pub_version_finder.dart index ebac473de7ac..572cb913aa7d 100644 --- a/script/tool/lib/src/common/pub_version_finder.dart +++ b/script/tool/lib/src/common/pub_version_finder.dart @@ -27,10 +27,10 @@ class PubVersionFinder { /// Get the package version on pub. Future getPackageVersion( - {required String package}) async { - assert(package.isNotEmpty); + {required String packageName}) async { + assert(packageName.isNotEmpty); final Uri pubHostUri = Uri.parse(pubHost); - final Uri url = pubHostUri.replace(path: '/packages/$package.json'); + final Uri url = pubHostUri.replace(path: '/packages/$packageName.json'); final http.Response response = await httpClient.get(url); if (response.statusCode == 404) { diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart new file mode 100644 index 000000000000..f6601d39b79e --- /dev/null +++ b/script/tool/lib/src/common/repository_package.dart @@ -0,0 +1,78 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'core.dart'; + +/// A package in the repository. +// +// TODO(stuartmorgan): Add more package-related info here, such as an on-demand +// cache of the parsed pubspec. +class RepositoryPackage { + /// Creates a representation of the package at [directory]. + RepositoryPackage(this.directory); + + /// The location of the package. + final Directory directory; + + /// The path to the package. + String get path => directory.path; + + /// Returns the string to use when referring to the package in user-targeted + /// messages. + /// + /// Callers should not expect a specific format for this string, since + /// it uses heuristics to try to be precise without being overly verbose. If + /// an exact format (e.g., published name, or basename) is required, that + /// should be used instead. + String get displayName { + List components = directory.fileSystem.path.split(directory.path); + // Remove everything up to the packages directory. + final int packagesIndex = components.indexOf('packages'); + if (packagesIndex != -1) { + components = components.sublist(packagesIndex + 1); + } + // For the common federated plugin pattern of `foo/foo_subpackage`, drop + // the first part since it's not useful. + if (components.length >= 2 && + components[1].startsWith('${components[0]}_')) { + components = components.sublist(1); + } + return p.posix.joinAll(components); + } + + /// The package's top-level pubspec.yaml. + File get pubspecFile => directory.childFile('pubspec.yaml'); + + /// Returns the Flutter example packages contained in the package, if any. + Iterable getExamples() { + final Directory exampleDirectory = directory.childDirectory('example'); + if (!exampleDirectory.existsSync()) { + return []; + } + if (isFlutterPackage(exampleDirectory)) { + return [RepositoryPackage(exampleDirectory)]; + } + // Only look at the subdirectories of the example directory if the example + // directory itself is not a Dart package, and only look one level below the + // example directory for other Dart packages. + return exampleDirectory + .listSync() + .where((FileSystemEntity entity) => isFlutterPackage(entity)) + // isFlutterPackage guarantees that the cast to Directory is safe. + .map((FileSystemEntity entity) => + RepositoryPackage(entity as Directory)); + } + + /// Returns the example directory, assuming there is only one. + /// + /// DO NOT USE THIS METHOD. It exists only to easily find code that was + /// written to use a single example and needs to be restructured to handle + /// multiple examples. New code should always use [getExamples]. + // TODO(stuartmorgan): Eliminate all uses of this. + RepositoryPackage getSingleExampleDeprecated() => + RepositoryPackage(directory.childDirectory('example')); +} diff --git a/script/tool/lib/src/create_all_plugins_app_command.dart b/script/tool/lib/src/create_all_plugins_app_command.dart index e1cee6f3fe7d..6dbebf2f5c74 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -11,6 +11,7 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/plugin_command.dart'; +import 'common/repository_package.dart'; const String _outputDirectoryFlag = 'output-dir'; @@ -170,10 +171,11 @@ class CreateAllPluginsAppCommand extends PluginCommand { final Map pathDependencies = {}; - await for (final PackageEnumerationEntry package in getTargetPackages()) { + await for (final PackageEnumerationEntry entry in getTargetPackages()) { + final RepositoryPackage package = entry.package; final Directory pluginDirectory = package.directory; final String pluginName = pluginDirectory.basename; - final File pubspecFile = pluginDirectory.childFile('pubspec.yaml'); + final File pubspecFile = package.pubspecFile; final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); if (pubspec.publishTo != 'none') { diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 7e800ed54866..3605dcce1f22 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -12,6 +12,7 @@ import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; const int _exitNoPlatformFlags = 2; const int _exitNoAvailableDevice = 3; @@ -119,9 +120,9 @@ class DriveExamplesCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { - if (package.basename.endsWith('_platform_interface') && - !package.childDirectory('example').existsSync()) { + Future runForPackage(RepositoryPackage package) async { + if (package.directory.basename.endsWith('_platform_interface') && + !package.getSingleExampleDeprecated().directory.existsSync()) { // Platform interface packages generally aren't intended to have // examples, and don't need integration tests, so skip rather than fail. return PackageResult.skip( @@ -140,16 +141,16 @@ class DriveExamplesCommand extends PackageLoopingCommand { // If there is no supported target platform, skip the plugin. if (deviceFlags.isEmpty) { return PackageResult.skip( - '${getPackageDescription(package)} does not support any requested platform.'); + '${package.displayName} does not support any requested platform.'); } int examplesFound = 0; bool testsRan = false; final List errors = []; - for (final Directory example in getExamplesForPlugin(package)) { + for (final RepositoryPackage example in package.getExamples()) { ++examplesFound; final String exampleName = - getRelativePosixPath(example, from: packagesDir); + getRelativePosixPath(example.directory, from: packagesDir); final List drivers = await _getDrivers(example); if (drivers.isEmpty) { @@ -173,7 +174,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { if (testTargets.isEmpty) { final String driverRelativePath = - getRelativePosixPath(driver, from: package); + getRelativePosixPath(driver, from: package.directory); printError( 'Found $driverRelativePath, but no integration_test/*_test.dart files.'); errors.add('No test files for $driverRelativePath'); @@ -185,7 +186,8 @@ class DriveExamplesCommand extends PackageLoopingCommand { example, driver, testTargets, deviceFlags: deviceFlags); for (final File failingTarget in failingTargets) { - errors.add(getRelativePosixPath(failingTarget, from: package)); + errors.add( + getRelativePosixPath(failingTarget, from: package.directory)); } } } @@ -229,10 +231,10 @@ class DriveExamplesCommand extends PackageLoopingCommand { return deviceIds; } - Future> _getDrivers(Directory example) async { + Future> _getDrivers(RepositoryPackage example) async { final List drivers = []; - final Directory driverDir = example.childDirectory('test_driver'); + final Directory driverDir = example.directory.childDirectory('test_driver'); if (driverDir.existsSync()) { await for (final FileSystemEntity driver in driverDir.list()) { if (driver is File && driver.basename.endsWith('_test.dart')) { @@ -253,10 +255,10 @@ class DriveExamplesCommand extends PackageLoopingCommand { return testFile.existsSync() ? testFile : null; } - Future> _getIntegrationTests(Directory example) async { + Future> _getIntegrationTests(RepositoryPackage example) async { final List tests = []; final Directory integrationTestDir = - example.childDirectory('integration_test'); + example.directory.childDirectory('integration_test'); if (integrationTestDir.existsSync()) { await for (final FileSystemEntity file in integrationTestDir.list()) { @@ -278,7 +280,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { /// - `['-d', 'web-server', '--web-port=', '--browser-name=]` /// for web Future> _driveTests( - Directory example, + RepositoryPackage example, File driver, List targets, { required List deviceFlags, @@ -296,11 +298,11 @@ class DriveExamplesCommand extends PackageLoopingCommand { if (enableExperiment.isNotEmpty) '--enable-experiment=$enableExperiment', '--driver', - getRelativePosixPath(driver, from: example), + getRelativePosixPath(driver, from: example.directory), '--target', - getRelativePosixPath(target, from: example), + getRelativePosixPath(target, from: example.directory), ], - workingDir: example); + workingDir: example.directory); if (exitCode != 0) { failures.add(target); } diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index fd2de97be4b3..4fc47c0da70c 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -13,6 +13,7 @@ import 'common/core.dart'; import 'common/gradle.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; const int _exitGcloudAuthFailed = 2; @@ -117,13 +118,13 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { - final Directory exampleDirectory = package.childDirectory('example'); + Future runForPackage(RepositoryPackage package) async { + final RepositoryPackage example = package.getSingleExampleDeprecated(); final Directory androidDirectory = - exampleDirectory.childDirectory('android'); + example.directory.childDirectory('android'); if (!androidDirectory.existsSync()) { return PackageResult.skip( - '${getPackageDescription(exampleDirectory)} does not support Android.'); + '${example.displayName} does not support Android.'); } if (!androidDirectory @@ -137,7 +138,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { } // Ensures that gradle wrapper exists - final GradleProject project = GradleProject(exampleDirectory, + final GradleProject project = GradleProject(example.directory, processRunner: processRunner, platform: platform); if (!await _ensureGradleWrapperExists(project)) { return PackageResult.fail(['Unable to build example apk']); @@ -155,7 +156,8 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { // test file's run. int resultsCounter = 0; for (final File test in _findIntegrationTestFiles(package)) { - final String testName = getRelativePosixPath(test, from: package); + final String testName = + getRelativePosixPath(test, from: package.directory); print('Testing $testName...'); if (!await _runGradle(project, 'app:assembleDebug', testFile: test)) { printError('Could not build $testName'); @@ -165,7 +167,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { final String buildId = getStringArg('build-id'); final String testRunId = getStringArg('test-run-id'); final String resultsDir = - 'plugins_android_test/${getPackageDescription(package)}/$buildId/$testRunId/${resultsCounter++}/'; + 'plugins_android_test/${package.displayName}/$buildId/$testRunId/${resultsCounter++}/'; final List args = [ 'firebase', 'test', @@ -186,7 +188,7 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { args.addAll(['--device', device]); } final int exitCode = await processRunner.runAndStream('gcloud', args, - workingDir: exampleDirectory); + workingDir: example.directory); if (exitCode != 0) { printError('Test failure for $testName'); @@ -262,9 +264,11 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { } /// Finds and returns all integration test files for [package]. - Iterable _findIntegrationTestFiles(Directory package) sync* { - final Directory integrationTestDir = - package.childDirectory('example').childDirectory('integration_test'); + Iterable _findIntegrationTestFiles(RepositoryPackage package) sync* { + final Directory integrationTestDir = package + .getSingleExampleDeprecated() + .directory + .childDirectory('integration_test'); if (!integrationTestDir.existsSync()) { return; diff --git a/script/tool/lib/src/lint_android_command.dart b/script/tool/lib/src/lint_android_command.dart index be6c6ed32415..a7b5c4f2e8bf 100644 --- a/script/tool/lib/src/lint_android_command.dart +++ b/script/tool/lib/src/lint_android_command.dart @@ -10,6 +10,7 @@ import 'common/core.dart'; import 'common/gradle.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; /// Lint the CocoaPod podspecs and run unit tests. /// @@ -30,22 +31,22 @@ class LintAndroidCommand extends PackageLoopingCommand { 'Requires the example to have been build at least once before running.'; @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { if (!pluginSupportsPlatform(kPlatformAndroid, package, requiredMode: PlatformSupport.inline)) { return PackageResult.skip( 'Plugin does not have an Android implemenatation.'); } - final Directory exampleDirectory = package.childDirectory('example'); - final GradleProject project = GradleProject(exampleDirectory, + final RepositoryPackage example = package.getSingleExampleDeprecated(); + final GradleProject project = GradleProject(example.directory, processRunner: processRunner, platform: platform); if (!project.isConfigured()) { return PackageResult.fail(['Build example before linting']); } - final String packageName = package.basename; + final String packageName = package.directory.basename; // Only lint one build mode to avoid extra work. // Only lint the plugin project itself, to avoid failing due to errors in diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index d0d93fcb79b1..ee44a82da5b9 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -12,6 +12,7 @@ import 'package:platform/platform.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; const int _exitUnsupportedPlatform = 2; const int _exitPodNotInstalled = 3; @@ -64,7 +65,7 @@ class LintPodspecsCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { final List errors = []; final List podspecs = await _podspecsToLint(package); @@ -82,7 +83,7 @@ class LintPodspecsCommand extends PackageLoopingCommand { : PackageResult.fail(errors); } - Future> _podspecsToLint(Directory package) async { + Future> _podspecsToLint(RepositoryPackage package) async { final List podspecs = await getFilesForPackage(package).where((File entity) { final String filePath = entity.path; diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart index 29a8ceb12782..e45c09bfd2ef 100644 --- a/script/tool/lib/src/list_command.dart +++ b/script/tool/lib/src/list_command.dart @@ -6,6 +6,7 @@ import 'package:file/file.dart'; import 'package:platform/platform.dart'; import 'common/plugin_command.dart'; +import 'common/repository_package.dart'; /// A command to list different types of repository content. class ListCommand extends PluginCommand { @@ -39,23 +40,22 @@ class ListCommand extends PluginCommand { Future run() async { switch (getStringArg(_type)) { case _plugin: - await for (final PackageEnumerationEntry package - in getTargetPackages()) { - print(package.directory.path); + await for (final PackageEnumerationEntry entry in getTargetPackages()) { + print(entry.package.path); } break; case _example: - final Stream examples = getTargetPackages() - .map((PackageEnumerationEntry entry) => entry.directory) - .expand(getExamplesForPlugin); - await for (final Directory package in examples) { + final Stream examples = getTargetPackages() + .expand( + (PackageEnumerationEntry entry) => entry.package.getExamples()); + await for (final RepositoryPackage package in examples) { print(package.path); } break; case _package: - await for (final PackageEnumerationEntry package + await for (final PackageEnumerationEntry entry in getTargetPackagesAndSubpackages()) { - print(package.directory.path); + print(entry.package.path); } break; case _file: diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 0bd2ab45f634..725cf23a2e9a 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -10,6 +10,7 @@ import 'common/gradle.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; import 'common/xcode.dart'; const String _unitTestFlag = 'unit'; @@ -115,7 +116,7 @@ this command. } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { final List testPlatforms = []; for (final String platform in _requestedPlatforms) { if (pluginSupportsPlatform(platform, package, @@ -171,23 +172,24 @@ this command. : PackageResult.success(); } - Future<_PlatformResult> _testAndroid(Directory plugin, _TestMode mode) async { - bool exampleHasUnitTests(Directory example) { - return example + Future<_PlatformResult> _testAndroid( + RepositoryPackage plugin, _TestMode mode) async { + bool exampleHasUnitTests(RepositoryPackage example) { + return example.directory .childDirectory('android') .childDirectory('app') .childDirectory('src') .childDirectory('test') .existsSync() || - example.parent + example.directory.parent .childDirectory('android') .childDirectory('src') .childDirectory('test') .existsSync(); } - bool exampleHasNativeIntegrationTests(Directory example) { - final Directory integrationTestDirectory = example + bool exampleHasNativeIntegrationTests(RepositoryPackage example) { + final Directory integrationTestDirectory = example.directory .childDirectory('android') .childDirectory('app') .childDirectory('src') @@ -216,12 +218,12 @@ this command. }); } - final Iterable examples = getExamplesForPlugin(plugin); + final Iterable examples = plugin.getExamples(); bool ranTests = false; bool failed = false; bool hasMissingBuild = false; - for (final Directory example in examples) { + for (final RepositoryPackage example in examples) { final bool hasUnitTests = exampleHasUnitTests(example); final bool hasIntegrationTests = exampleHasNativeIntegrationTests(example); @@ -239,11 +241,11 @@ this command. continue; } - final String exampleName = getPackageDescription(example); + final String exampleName = example.displayName; _printRunningExampleTestsMessage(example, 'Android'); final GradleProject project = GradleProject( - example, + example.directory, processRunner: processRunner, platform: platform, ); @@ -301,12 +303,12 @@ this command. return _PlatformResult(RunState.succeeded); } - Future<_PlatformResult> _testIos(Directory plugin, _TestMode mode) { + Future<_PlatformResult> _testIos(RepositoryPackage plugin, _TestMode mode) { return _runXcodeTests(plugin, 'iOS', mode, extraFlags: _iosDestinationFlags); } - Future<_PlatformResult> _testMacOS(Directory plugin, _TestMode mode) { + Future<_PlatformResult> _testMacOS(RepositoryPackage plugin, _TestMode mode) { return _runXcodeTests(plugin, 'macOS', mode); } @@ -316,7 +318,7 @@ this command. /// The tests targets must be added to the Xcode project of the example app, /// usually at "example/{ios,macos}/Runner.xcworkspace". Future<_PlatformResult> _runXcodeTests( - Directory plugin, + RepositoryPackage plugin, String platform, _TestMode mode, { List extraFlags = const [], @@ -330,11 +332,11 @@ this command. // Assume skipped until at least one test has run. RunState overallResult = RunState.skipped; - for (final Directory example in getExamplesForPlugin(plugin)) { - final String exampleName = getPackageDescription(example); + for (final RepositoryPackage example in plugin.getExamples()) { + final String exampleName = example.displayName; if (testTarget != null) { - final Directory project = example + final Directory project = example.directory .childDirectory(platform.toLowerCase()) .childDirectory('Runner.xcodeproj'); final bool? hasTarget = @@ -351,7 +353,7 @@ this command. _printRunningExampleTestsMessage(example, platform); final int exitCode = await _xcode.runXcodeBuild( - example, + example.directory, actions: ['test'], workspace: '${platform.toLowerCase()}/Runner.xcworkspace', scheme: 'Runner', @@ -387,20 +389,22 @@ this command. /// Prints a standard format message indicating that [platform] tests for /// [plugin]'s [example] are about to be run. - void _printRunningExampleTestsMessage(Directory example, String platform) { - print('Running $platform tests for ${getPackageDescription(example)}...'); + void _printRunningExampleTestsMessage( + RepositoryPackage example, String platform) { + print('Running $platform tests for ${example.displayName}...'); } /// Prints a standard format message indicating that no tests were found for /// [plugin]'s [example] for [platform]. - void _printNoExampleTestsMessage(Directory example, String platform) { - print('No $platform tests found for ${getPackageDescription(example)}'); + void _printNoExampleTestsMessage(RepositoryPackage example, String platform) { + print('No $platform tests found for ${example.displayName}'); } } // The type for a function that takes a plugin directory and runs its native // tests for a specific platform. -typedef _TestFunction = Future<_PlatformResult> Function(Directory, _TestMode); +typedef _TestFunction = Future<_PlatformResult> Function( + RepositoryPackage, _TestMode); /// A collection of information related to a specific platform. class _PlatformDetails { diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index fda68a6a74a4..ab9f5f147495 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -16,6 +16,7 @@ import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; import 'common/pub_version_finder.dart'; +import 'common/repository_package.dart'; /// A command to check that packages are publishable via 'dart publish'. class PublishCheckCommand extends PackageLoopingCommand { @@ -75,7 +76,7 @@ class PublishCheckCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { final _PublishCheckResult? result = await _passesPublishCheck(package); if (result == null) { return PackageResult.skip('Package is marked as unpublishable.'); @@ -114,8 +115,8 @@ class PublishCheckCommand extends PackageLoopingCommand { } } - Pubspec? _tryParsePubspec(Directory package) { - final File pubspecFile = package.childFile('pubspec.yaml'); + Pubspec? _tryParsePubspec(RepositoryPackage package) { + final File pubspecFile = package.pubspecFile; try { return Pubspec.parse(pubspecFile.readAsStringSync()); @@ -127,12 +128,12 @@ class PublishCheckCommand extends PackageLoopingCommand { } } - Future _hasValidPublishCheckRun(Directory package) async { + Future _hasValidPublishCheckRun(RepositoryPackage package) async { print('Running pub publish --dry-run:'); final io.Process process = await processRunner.start( flutterCommand, ['pub', 'publish', '--', '--dry-run'], - workingDirectory: package, + workingDirectory: package.directory, ); final StringBuffer outputBuffer = StringBuffer(); @@ -183,8 +184,9 @@ class PublishCheckCommand extends PackageLoopingCommand { /// Returns the result of the publish check, or null if the package is marked /// as unpublishable. - Future<_PublishCheckResult?> _passesPublishCheck(Directory package) async { - final String packageName = package.basename; + Future<_PublishCheckResult?> _passesPublishCheck( + RepositoryPackage package) async { + final String packageName = package.directory.basename; final Pubspec? pubspec = _tryParsePubspec(package); if (pubspec == null) { print('no pubspec'); @@ -219,7 +221,7 @@ class PublishCheckCommand extends PackageLoopingCommand { Future<_PublishCheckResult> _checkPublishingStatus( {required String packageName, required Version? version}) async { final PubVersionFinderResponse pubVersionFinderResponse = - await _pubVersionFinder.getPackageVersion(package: packageName); + await _pubVersionFinder.getPackageVersion(packageName: packageName); switch (pubVersionFinderResponse.result) { case PubVersionFinderResult.success: return pubVersionFinderResponse.versions.contains(version) diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 8bcb9e37e8ef..6e1658f6f6e2 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -140,9 +140,9 @@ class PublishPluginCommand extends PluginCommand { @override Future run() async { - final String package = getStringArg(_packageOption); + final String packageName = getStringArg(_packageOption); final bool publishAllChanged = getBoolArg(_allChangedFlag); - if (package.isEmpty && !publishAllChanged) { + if (packageName.isEmpty && !publishAllChanged) { _print( 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); throw ToolExit(1); @@ -176,7 +176,7 @@ class PublishPluginCommand extends PluginCommand { ); } else { successful = await _publishAndTagPackage( - packageDir: _getPackageDir(package), + packageDir: _getPackageDir(packageName), remoteForTagPush: remote, ); } @@ -202,7 +202,7 @@ class PublishPluginCommand extends PluginCommand { await baseGitDir.runCommand(['tag', '--sort=-committerdate']); final List existingTags = (existingTagsResult.stdout as String) .split('\n') - ..removeWhere((String element) => element.isEmpty); + ..removeWhere((String element) => element.isEmpty); final List packagesReleased = []; final List packagesFailed = []; @@ -307,7 +307,7 @@ Safe to ignore if the package is deleted in this commit. // Check if the package named `packageName` with `version` has already published. final Version version = pubspec.version!; final PubVersionFinderResponse pubVersionFinderResponse = - await _pubVersionFinder.getPackageVersion(package: pubspec.name); + await _pubVersionFinder.getPackageVersion(packageName: pubspec.name); if (pubVersionFinderResponse.versions.contains(version)) { final String tagsForPackageWithSameVersion = existingTags.firstWhere( (String tag) => @@ -390,8 +390,8 @@ Safe to ignore if the package is deleted in this commit. // Returns the packageDirectory based on the package name. // Throws ToolExit if the `package` doesn't exist. - Directory _getPackageDir(String package) { - final Directory packageDir = packagesDir.childDirectory(package); + Directory _getPackageDir(String packageName) { + final Directory packageDir = packagesDir.childDirectory(packageName); if (!packageDir.existsSync()) { _print('${packageDir.absolute.path} does not exist.'); throw ToolExit(1); diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 0a066ab72baf..def2adaf2788 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -10,6 +10,7 @@ import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; /// A command to enforce pubspec conventions across the repository. /// @@ -64,8 +65,8 @@ class PubspecCheckCommand extends PackageLoopingCommand { bool get includeSubpackages => true; @override - Future runForPackage(Directory package) async { - final File pubspec = package.childFile('pubspec.yaml'); + Future runForPackage(RepositoryPackage package) async { + final File pubspec = package.pubspecFile; final bool passesCheck = !pubspec.existsSync() || await _checkPubspec(pubspec, package: package); if (!passesCheck) { @@ -76,7 +77,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { Future _checkPubspec( File pubspecFile, { - required Directory package, + required RepositoryPackage package, }) async { final String contents = pubspecFile.readAsStringSync(); final Pubspec? pubspec = _tryParsePubspec(contents); @@ -154,7 +155,7 @@ class PubspecCheckCommand extends PackageLoopingCommand { List _checkForRepositoryLinkErrors( Pubspec pubspec, { - required Directory package, + required RepositoryPackage package, }) { final List errorMessages = []; if (pubspec.repository == null) { @@ -189,12 +190,12 @@ class PubspecCheckCommand extends PackageLoopingCommand { // Should only be called on plugin packages. String? _checkForImplementsError( Pubspec pubspec, { - required Directory package, + required RepositoryPackage package, }) { if (_isImplementationPackage(package)) { final String? implements = pubspec.flutter!['plugin']!['implements'] as String?; - final String expectedImplements = package.parent.basename; + final String expectedImplements = package.directory.parent.basename; if (implements == null) { return 'Missing "implements: $expectedImplements" in "plugin" section.'; } else if (implements != expectedImplements) { @@ -207,13 +208,13 @@ class PubspecCheckCommand extends PackageLoopingCommand { // Returns true if [packageName] appears to be an implementation package // according to repository conventions. - bool _isImplementationPackage(Directory package) { + bool _isImplementationPackage(RepositoryPackage package) { // An implementation package should be in a group folder... - final Directory parentDir = package.parent; + final Directory parentDir = package.directory.parent; if (parentDir.path == packagesDir.path) { return false; } - final String packageName = package.basename; + final String packageName = package.directory.basename; final String parentName = parentDir.basename; // ... whose name is a prefix of the package name. if (!packageName.startsWith(parentName)) { diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index 9dfe66b7926a..5a0b43d3b223 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -9,6 +9,7 @@ import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; /// A command to run Dart unit tests for packages. class TestCommand extends PackageLoopingCommand { @@ -36,13 +37,13 @@ class TestCommand extends PackageLoopingCommand { 'This command requires "flutter" to be in your path.'; @override - Future runForPackage(Directory package) async { - if (!package.childDirectory('test').existsSync()) { + Future runForPackage(RepositoryPackage package) async { + if (!package.directory.childDirectory('test').existsSync()) { return PackageResult.skip('No test/ directory.'); } bool passed; - if (isFlutterPackage(package)) { + if (isFlutterPackage(package.directory)) { passed = await _runFlutterTests(package); } else { passed = await _runDartTests(package); @@ -51,7 +52,7 @@ class TestCommand extends PackageLoopingCommand { } /// Runs the Dart tests for a Flutter package, returning true on success. - Future _runFlutterTests(Directory package) async { + Future _runFlutterTests(RepositoryPackage package) async { final String experiment = getStringArg(kEnableExperiment); final int exitCode = await processRunner.runAndStream( @@ -61,21 +62,21 @@ class TestCommand extends PackageLoopingCommand { '--color', if (experiment.isNotEmpty) '--enable-experiment=$experiment', // TODO(ditman): Remove this once all plugins are migrated to 'drive'. - if (isWebPlugin(package)) '--platform=chrome', + if (pluginSupportsPlatform(kPlatformWeb, package)) '--platform=chrome', ], - workingDir: package, + workingDir: package.directory, ); return exitCode == 0; } /// Runs the Dart tests for a non-Flutter package, returning true on success. - Future _runDartTests(Directory package) async { + Future _runDartTests(RepositoryPackage package) async { // Unlike `flutter test`, `pub run test` does not automatically get // packages int exitCode = await processRunner.runAndStream( 'dart', ['pub', 'get'], - workingDir: package, + workingDir: package.directory, ); if (exitCode != 0) { printError('Unable to fetch dependencies.'); @@ -92,7 +93,7 @@ class TestCommand extends PackageLoopingCommand { if (experiment.isNotEmpty) '--enable-experiment=$experiment', 'test', ], - workingDir: package, + workingDir: package.directory, ); return exitCode == 0; diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 67c563782888..67a81b967a8e 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -16,6 +16,7 @@ import 'common/git_version_finder.dart'; import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; import 'common/pub_version_finder.dart'; +import 'common/repository_package.dart'; /// Categories of version change types. enum NextVersionType { @@ -133,7 +134,7 @@ class VersionCheckCommand extends PackageLoopingCommand { Future initializeRun() async {} @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { final Pubspec? pubspec = _tryParsePubspec(package); if (pubspec == null) { // No remaining checks make sense, so fail immediately. @@ -196,7 +197,7 @@ class VersionCheckCommand extends PackageLoopingCommand { /// the name from pubspec.yaml, not the on disk name if different.) Future _fetchPreviousVersionFromPub(String packageName) async { final PubVersionFinderResponse pubVersionFinderResponse = - await _pubVersionFinder.getPackageVersion(package: packageName); + await _pubVersionFinder.getPackageVersion(packageName: packageName); switch (pubVersionFinderResponse.result) { case PubVersionFinderResult.success: return pubVersionFinderResponse.versions.first; @@ -214,10 +215,10 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} /// Returns the version of [package] from git at the base comparison hash. Future _getPreviousVersionFromGit( - Directory package, { + RepositoryPackage package, { required GitVersionFinder gitVersionFinder, }) async { - final File pubspecFile = package.childFile('pubspec.yaml'); + final File pubspecFile = package.pubspecFile; final String relativePath = path.relative(pubspecFile.absolute.path, from: (await gitDir).path); // Use Posix-style paths for git. @@ -230,7 +231,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} /// Returns the state of the verison of [package] relative to the comparison /// base (git or pub, depending on flags). Future<_CurrentVersionState> _getVersionState( - Directory package, { + RepositoryPackage package, { required Pubspec pubspec, }) async { // This method isn't called unless `version` is non-null. @@ -310,7 +311,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} /// /// Returns false if the CHANGELOG fails validation. Future _validateChangelogVersion( - Directory package, { + RepositoryPackage package, { required Pubspec pubspec, required bool pubspecVersionChanged, }) async { @@ -318,7 +319,7 @@ ${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} final Version fromPubspec = pubspec.version!; // get first version from CHANGELOG - final File changelog = package.childFile('CHANGELOG.md'); + final File changelog = package.directory.childFile('CHANGELOG.md'); final List lines = changelog.readAsLinesSync(); String? firstLineWithText; final Iterator iterator = lines.iterator; @@ -386,8 +387,8 @@ ${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. return true; } - Pubspec? _tryParsePubspec(Directory package) { - final File pubspecFile = package.childFile('pubspec.yaml'); + Pubspec? _tryParsePubspec(RepositoryPackage package) { + final File pubspecFile = package.pubspecFile; try { final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); diff --git a/script/tool/lib/src/xcode_analyze_command.dart b/script/tool/lib/src/xcode_analyze_command.dart index 27cd8c435142..3d34dab9f087 100644 --- a/script/tool/lib/src/xcode_analyze_command.dart +++ b/script/tool/lib/src/xcode_analyze_command.dart @@ -9,6 +9,7 @@ import 'common/core.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; import 'common/process_runner.dart'; +import 'common/repository_package.dart'; import 'common/xcode.dart'; /// The command to run Xcode's static analyzer on plugins. @@ -42,7 +43,7 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { final bool testIos = getBoolArg(kPlatformIos) && pluginSupportsPlatform(kPlatformIos, package, requiredMode: PlatformSupport.inline); @@ -78,18 +79,18 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand { /// Analyzes [plugin] for [platform], returning true if it passed analysis. Future _analyzePlugin( - Directory plugin, + RepositoryPackage plugin, String platform, { List extraFlags = const [], }) async { bool passing = true; - for (final Directory example in getExamplesForPlugin(plugin)) { + for (final RepositoryPackage example in plugin.getExamples()) { // Running tests and static analyzer. - final String examplePath = - getRelativePosixPath(example, from: plugin.parent); + final String examplePath = getRelativePosixPath(example.directory, + from: plugin.directory.parent); print('Running $platform tests and analyzer for $examplePath...'); final int exitCode = await _xcode.runXcodeBuild( - example, + example.directory, actions: ['analyze'], workspace: '${platform.toLowerCase()}/Runner.xcworkspace', scheme: 'Runner', diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index 00e64ddc21fe..721923ae9c6e 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -11,6 +11,7 @@ import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/package_looping_command.dart'; import 'package:flutter_plugin_tools/src/common/process_runner.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:git/git.dart'; import 'package:mockito/mockito.dart'; import 'package:platform/platform.dart'; @@ -578,64 +579,6 @@ void main() { ])); }); }); - - group('utility', () { - test('getPackageDescription prints packageDir-relative paths by default', - () async { - final TestPackageLoopingCommand command = - TestPackageLoopingCommand(packagesDir, platform: mockPlatform); - - expect( - command.getPackageDescription(packagesDir.childDirectory('foo')), - 'foo', - ); - expect( - command.getPackageDescription(packagesDir - .childDirectory('foo') - .childDirectory('bar') - .childDirectory('baz')), - 'foo/bar/baz', - ); - }); - - test('getPackageDescription always uses Posix-style paths', () async { - mockPlatform.isWindows = true; - final TestPackageLoopingCommand command = - TestPackageLoopingCommand(packagesDir, platform: mockPlatform); - - expect( - command.getPackageDescription(packagesDir.childDirectory('foo')), - 'foo', - ); - expect( - command.getPackageDescription(packagesDir - .childDirectory('foo') - .childDirectory('bar') - .childDirectory('baz')), - 'foo/bar/baz', - ); - }); - - test( - 'getPackageDescription elides group name in grouped federated plugin structure', - () async { - final TestPackageLoopingCommand command = - TestPackageLoopingCommand(packagesDir, platform: mockPlatform); - - expect( - command.getPackageDescription(packagesDir - .childDirectory('a_plugin') - .childDirectory('a_plugin_platform_interface')), - 'a_plugin_platform_interface', - ); - expect( - command.getPackageDescription(packagesDir - .childDirectory('a_plugin') - .childDirectory('a_plugin_web')), - 'a_plugin_web', - ); - }); - }); } class TestPackageLoopingCommand extends PackageLoopingCommand { @@ -699,18 +642,18 @@ class TestPackageLoopingCommand extends PackageLoopingCommand { } @override - Future runForPackage(Directory package) async { + Future runForPackage(RepositoryPackage package) async { checkedPackages.add(package.path); - final File warningFile = package.childFile(_warningFile); + final File warningFile = package.directory.childFile(_warningFile); if (warningFile.existsSync()) { final List warnings = warningFile.readAsLinesSync(); warnings.forEach(logWarning); } - final File skipFile = package.childFile(_skipFile); + final File skipFile = package.directory.childFile(_skipFile); if (skipFile.existsSync()) { return PackageResult.skip(skipFile.readAsStringSync()); } - final File errorFile = package.childFile(_errorFile); + final File errorFile = package.directory.childFile(_errorFile); if (errorFile.existsSync()) { return PackageResult.fail(errorFile.readAsLinesSync()); } diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart index 2f332aa8eb55..10bdff4e9c56 100644 --- a/script/tool/test/common/plugin_command_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -498,7 +498,7 @@ packages/plugin3/plugin3.dart expect( localCommand.plugins, unorderedEquals(expectedShards[i] - .map((Directory package) => package.path) + .map((Directory packageDir) => packageDir.path) .toList())); } }); @@ -541,7 +541,7 @@ packages/plugin3/plugin3.dart expect( localCommand.plugins, unorderedEquals(expectedShards[i] - .map((Directory package) => package.path) + .map((Directory packageDir) => packageDir.path) .toList())); } }); @@ -594,7 +594,7 @@ packages/plugin3/plugin3.dart expect( localCommand.plugins, unorderedEquals(expectedShards[i] - .map((Directory package) => package.path) + .map((Directory packageDir) => packageDir.path) .toList())); } }); @@ -620,8 +620,8 @@ class SamplePluginCommand extends PluginCommand { @override Future run() async { - await for (final PackageEnumerationEntry package in getTargetPackages()) { - plugins.add(package.directory.path); + await for (final PackageEnumerationEntry entry in getTargetPackages()) { + plugins.add(entry.package.path); } } } diff --git a/script/tool/test/common/plugin_utils_test.dart b/script/tool/test/common/plugin_utils_test.dart index c32c3f8e02bf..7f1ba2add00a 100644 --- a/script/tool/test/common/plugin_utils_test.dart +++ b/script/tool/test/common/plugin_utils_test.dart @@ -6,6 +6,7 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; import 'package:test/test.dart'; import '../util.dart'; @@ -21,7 +22,8 @@ void main() { group('pluginSupportsPlatform', () { test('no platforms', () async { - final Directory plugin = createFakePlugin('plugin', packagesDir); + final RepositoryPackage plugin = + RepositoryPackage(createFakePlugin('plugin', packagesDir)); expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isFalse); expect(pluginSupportsPlatform(kPlatformIos, plugin), isFalse); @@ -32,7 +34,8 @@ void main() { }); test('all platforms', () async { - final Directory plugin = createFakePlugin('plugin', packagesDir, + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', packagesDir, platformSupport: { kPlatformAndroid: PlatformSupport.inline, kPlatformIos: PlatformSupport.inline, @@ -40,7 +43,7 @@ void main() { kPlatformMacos: PlatformSupport.inline, kPlatformWeb: PlatformSupport.inline, kPlatformWindows: PlatformSupport.inline, - }); + })); expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isTrue); expect(pluginSupportsPlatform(kPlatformIos, plugin), isTrue); @@ -51,7 +54,7 @@ void main() { }); test('some platforms', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -59,7 +62,7 @@ void main() { kPlatformLinux: PlatformSupport.inline, kPlatformWeb: PlatformSupport.inline, }, - ); + )); expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isTrue); expect(pluginSupportsPlatform(kPlatformIos, plugin), isFalse); @@ -70,7 +73,7 @@ void main() { }); test('inline plugins are only detected as inline', () async { - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( 'plugin', packagesDir, platformSupport: { @@ -81,7 +84,7 @@ void main() { kPlatformWeb: PlatformSupport.inline, kPlatformWindows: PlatformSupport.inline, }, - ); + )); expect( pluginSupportsPlatform(kPlatformAndroid, plugin, @@ -135,7 +138,7 @@ void main() { test('federated plugins are only detected as federated', () async { const String pluginName = 'plugin'; - final Directory plugin = createFakePlugin( + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( pluginName, packagesDir, platformSupport: { @@ -146,7 +149,7 @@ void main() { kPlatformWeb: PlatformSupport.federated, kPlatformWindows: PlatformSupport.federated, }, - ); + )); expect( pluginSupportsPlatform(kPlatformAndroid, plugin, diff --git a/script/tool/test/common/pub_version_finder_test.dart b/script/tool/test/common/pub_version_finder_test.dart index 7d8658a907ee..1692cf214abe 100644 --- a/script/tool/test/common/pub_version_finder_test.dart +++ b/script/tool/test/common/pub_version_finder_test.dart @@ -19,7 +19,7 @@ void main() { }); final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); final PubVersionFinderResponse response = - await finder.getPackageVersion(package: 'some_package'); + await finder.getPackageVersion(packageName: 'some_package'); expect(response.versions, isEmpty); expect(response.result, PubVersionFinderResult.noPackageFound); @@ -33,7 +33,7 @@ void main() { }); final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); final PubVersionFinderResponse response = - await finder.getPackageVersion(package: 'some_package'); + await finder.getPackageVersion(packageName: 'some_package'); expect(response.versions, isEmpty); expect(response.result, PubVersionFinderResult.fail); @@ -64,7 +64,7 @@ void main() { }); final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); final PubVersionFinderResponse response = - await finder.getPackageVersion(package: 'some_package'); + await finder.getPackageVersion(packageName: 'some_package'); expect(response.versions, [ Version.parse('2.0.0'), diff --git a/script/tool/test/common/repository_package_test.dart b/script/tool/test/common/repository_package_test.dart new file mode 100644 index 000000000000..5c5624312f51 --- /dev/null +++ b/script/tool/test/common/repository_package_test.dart @@ -0,0 +1,123 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; +import 'package:test/test.dart'; + +import '../util.dart'; + +void main() { + late FileSystem fileSystem; + late Directory packagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + }); + + group('displayName', () { + test('prints packageDir-relative paths by default', () async { + expect( + RepositoryPackage(packagesDir.childDirectory('foo')).displayName, + 'foo', + ); + expect( + RepositoryPackage(packagesDir + .childDirectory('foo') + .childDirectory('bar') + .childDirectory('baz')) + .displayName, + 'foo/bar/baz', + ); + }); + + test('handles third_party/packages/', () async { + expect( + RepositoryPackage(packagesDir.parent + .childDirectory('third_party') + .childDirectory('packages') + .childDirectory('foo') + .childDirectory('bar') + .childDirectory('baz')) + .displayName, + 'foo/bar/baz', + ); + }); + + test('always uses Posix-style paths', () async { + final Directory windowsPackagesDir = createPackagesDirectory( + fileSystem: MemoryFileSystem(style: FileSystemStyle.windows)); + + expect( + RepositoryPackage(windowsPackagesDir.childDirectory('foo')).displayName, + 'foo', + ); + expect( + RepositoryPackage(windowsPackagesDir + .childDirectory('foo') + .childDirectory('bar') + .childDirectory('baz')) + .displayName, + 'foo/bar/baz', + ); + }); + + test('elides group name in grouped federated plugin structure', () async { + expect( + RepositoryPackage(packagesDir + .childDirectory('a_plugin') + .childDirectory('a_plugin_platform_interface')) + .displayName, + 'a_plugin_platform_interface', + ); + expect( + RepositoryPackage(packagesDir + .childDirectory('a_plugin') + .childDirectory('a_plugin_platform_web')) + .displayName, + 'a_plugin_platform_web', + ); + }); + + // The app-facing package doesn't get elided to avoid potential confusion + // with the group folder itself. + test('does not elide group name for app-facing packages', () async { + expect( + RepositoryPackage(packagesDir + .childDirectory('a_plugin') + .childDirectory('a_plugin')) + .displayName, + 'a_plugin/a_plugin', + ); + }); + }); + + group('getExamples', () { + test('handles a single example', () async { + final Directory plugin = createFakePlugin('a_plugin', packagesDir); + + final List examples = + RepositoryPackage(plugin).getExamples().toList(); + + expect(examples.length, 1); + expect(examples[0].path, plugin.childDirectory('example').path); + }); + + test('handles multiple examples', () async { + final Directory plugin = createFakePlugin('a_plugin', packagesDir, + examples: ['example1', 'example2']); + + final List examples = + RepositoryPackage(plugin).getExamples().toList(); + + expect(examples.length, 2); + expect(examples[0].path, + plugin.childDirectory('example').childDirectory('example1').path); + expect(examples[1].path, + plugin.childDirectory('example').childDirectory('example2').path); + }); + }); +} diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart index cf57a9d0dcf7..e2bf1e3e6e8e 100644 --- a/script/tool/test/format_command_test.dart +++ b/script/tool/test/format_command_test.dart @@ -49,10 +49,10 @@ void main() { /// Returns a modified version of a list of [relativePaths] that are relative /// to [package] to instead be relative to [packagesDir]. List _getPackagesDirRelativePaths( - Directory package, List relativePaths) { + Directory packageDir, List relativePaths) { final p.Context path = analyzeCommand.path; final String relativeBase = - path.relative(package.path, from: packagesDir.path); + path.relative(packageDir.path, from: packagesDir.path); return relativePaths .map((String relativePath) => path.join(relativeBase, relativePath)) .toList(); diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 10a85f49e815..05aebe82fd79 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -46,6 +46,7 @@ Directory createPackagesDirectory( /// /// [extraFiles] is an optional list of plugin-relative paths, using Posix /// separators, of extra files to create in the plugin. +// TODO(stuartmorgan): Convert the return to a RepositoryPackage. Directory createFakePlugin( String name, Directory parentDirectory, { @@ -77,6 +78,7 @@ Directory createFakePlugin( /// /// [extraFiles] is an optional list of package-relative paths, using unix-style /// separators, of extra files to create in the package. +// TODO(stuartmorgan): Convert the return to a RepositoryPackage. Directory createFakePackage( String name, Directory parentDirectory, { From 729c3e4117e6d1a026b50f363b2a202352231fdd Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 24 Aug 2021 21:22:36 -0400 Subject: [PATCH 37/57] [flutter_plugin_tool] Migrate publish_plugin_command_test to runCapturingPrint (#4260) Finishes the migration of tool tests to `runCapturingPrint`. This makes the tests much less verbose, and makes it match the rest of the tool tests. Also adds the use of `printError` for error output, now that it's trivial to do so. --- .../tool/lib/src/publish_plugin_command.dart | 70 ++-- .../test/publish_plugin_command_test.dart | 355 ++++++++++-------- 2 files changed, 239 insertions(+), 186 deletions(-) diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 6e1658f6f6e2..5a75ce6af89f 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -11,6 +11,7 @@ import 'package:git/git.dart'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:yaml/yaml.dart'; @@ -48,15 +49,15 @@ class PublishPluginCommand extends PluginCommand { PublishPluginCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), - Print print = print, + Platform platform = const LocalPlatform(), io.Stdin? stdinput, GitDir? gitDir, http.Client? httpClient, }) : _pubVersionFinder = PubVersionFinder(httpClient: httpClient ?? http.Client()), - _print = print, _stdin = stdinput ?? io.stdin, - super(packagesDir, processRunner: processRunner, gitDir: gitDir) { + super(packagesDir, + platform: platform, processRunner: processRunner, gitDir: gitDir) { argParser.addOption( _packageOption, help: 'The package to publish.' @@ -133,7 +134,6 @@ class PublishPluginCommand extends PluginCommand { 'If running this on CI, an environment variable named $_pubCredentialName must be set to a String that represents the pub credential JSON.\n' 'WARNING: Do not check in the content of pub credential JSON, it should only come from secure sources.'; - final Print _print; final io.Stdin _stdin; StreamSubscription? _stdinSubscription; final PubVersionFinder _pubVersionFinder; @@ -143,12 +143,12 @@ class PublishPluginCommand extends PluginCommand { final String packageName = getStringArg(_packageOption); final bool publishAllChanged = getBoolArg(_allChangedFlag); if (packageName.isEmpty && !publishAllChanged) { - _print( + printError( 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); throw ToolExit(1); } - _print('Checking local repo...'); + print('Checking local repo...'); final GitDir repository = await gitDir; final bool shouldPushTag = getBoolArg(_pushTagsOption); @@ -163,9 +163,9 @@ class PublishPluginCommand extends PluginCommand { } remote = _RemoteInfo(name: remoteName, url: remoteUrl); } - _print('Local repo is ready!'); + print('Local repo is ready!'); if (getBoolArg(_dryRunFlag)) { - _print('=============== DRY RUN ==============='); + print('=============== DRY RUN ==============='); } bool successful; @@ -193,11 +193,11 @@ class PublishPluginCommand extends PluginCommand { final List changedPubspecs = await gitVersionFinder.getChangedPubSpecs(); if (changedPubspecs.isEmpty) { - _print('No version updates in this commit.'); + print('No version updates in this commit.'); return true; } - _print('Getting existing tags...'); + print('Getting existing tags...'); final io.ProcessResult existingTagsResult = await baseGitDir.runCommand(['tag', '--sort=-committerdate']); final List existingTags = (existingTagsResult.stdout as String) @@ -228,7 +228,7 @@ class PublishPluginCommand extends PluginCommand { packagesFailed.add(pubspecFile.parent.basename); continue; } - _print('\n'); + print('\n'); if (await _publishAndTagPackage( packageDir: pubspecFile.parent, remoteForTagPush: remoteForTagPush, @@ -237,13 +237,13 @@ class PublishPluginCommand extends PluginCommand { } else { packagesFailed.add(pubspecFile.parent.basename); } - _print('\n'); + print('\n'); } if (packagesReleased.isNotEmpty) { - _print('Packages released: ${packagesReleased.join(', ')}'); + print('Packages released: ${packagesReleased.join(', ')}'); } if (packagesFailed.isNotEmpty) { - _print( + printError( 'Failed to release the following packages: ${packagesFailed.join(', ')}, see above for details.'); } return packagesFailed.isEmpty; @@ -268,7 +268,7 @@ class PublishPluginCommand extends PluginCommand { return false; } } - _print('Released [${packageDir.basename}] successfully.'); + print('Released [${packageDir.basename}] successfully.'); return true; } @@ -278,7 +278,7 @@ class PublishPluginCommand extends PluginCommand { required List existingTags, }) async { if (!pubspecFile.existsSync()) { - _print(''' + print(''' The file at The pubspec file at ${pubspecFile.path} does not exist. Publishing will not happen for ${pubspecFile.parent.basename}. Safe to ignore if the package is deleted in this commit. '''); @@ -299,7 +299,7 @@ Safe to ignore if the package is deleted in this commit. } if (pubspec.version == null) { - _print( + printError( 'No version found. A package that intentionally has no version should be marked "publish_to: none"'); return _CheckNeedsReleaseResult.failure; } @@ -314,14 +314,14 @@ Safe to ignore if the package is deleted in this commit. tag.split('-v').first == pubspec.name && tag.split('-v').last == version.toString(), orElse: () => ''); - _print( + print( 'The version $version of ${pubspec.name} has already been published'); if (tagsForPackageWithSameVersion.isEmpty) { - _print( + printError( 'However, the git release tag for this version (${pubspec.name}-v$version) is not found. Please manually fix the tag then run the command again.'); return _CheckNeedsReleaseResult.failure; } else { - _print('skip.'); + print('skip.'); return _CheckNeedsReleaseResult.noRelease; } } @@ -340,7 +340,7 @@ Safe to ignore if the package is deleted in this commit. if (!publishOK) { return false; } - _print('Package published!'); + print('Package published!'); return true; } @@ -353,7 +353,7 @@ Safe to ignore if the package is deleted in this commit. _RemoteInfo? remoteForPush, }) async { final String tag = _getTag(packageDir); - _print('Tagging release $tag...'); + print('Tagging release $tag...'); if (!getBoolArg(_dryRunFlag)) { final io.ProcessResult result = await processRunner.run( 'git', @@ -370,7 +370,7 @@ Safe to ignore if the package is deleted in this commit. return true; } - _print('Pushing tag to ${remoteForPush.name}...'); + print('Pushing tag to ${remoteForPush.name}...'); return await _pushTagToRemote( tag: tag, remote: remoteForPush, @@ -381,9 +381,9 @@ Safe to ignore if the package is deleted in this commit. await _stdinSubscription?.cancel(); _stdinSubscription = null; if (successful) { - _print('Done!'); + print('Done!'); } else { - _print('Failed, see above for details.'); + printError('Failed, see above for details.'); throw ToolExit(1); } } @@ -393,7 +393,7 @@ Safe to ignore if the package is deleted in this commit. Directory _getPackageDir(String packageName) { final Directory packageDir = packagesDir.childDirectory(packageName); if (!packageDir.existsSync()) { - _print('${packageDir.absolute.path} does not exist.'); + printError('${packageDir.absolute.path} does not exist.'); throw ToolExit(1); } return packageDir; @@ -412,7 +412,7 @@ Safe to ignore if the package is deleted in this commit. final String statusOutput = statusResult.stdout as String; if (statusOutput.isNotEmpty) { - _print( + printError( "There are files in the package directory that haven't been saved in git. Refusing to publish these files:\n\n" '$statusOutput\n' 'If the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.'); @@ -435,7 +435,7 @@ Safe to ignore if the package is deleted in this commit. Future _publish(Directory packageDir) async { final List publishFlags = getStringListArg(_pubFlagsOption); - _print( + print( 'Running `pub publish ${publishFlags.join(' ')}` in ${packageDir.absolute.path}...\n'); if (getBoolArg(_dryRunFlag)) { return true; @@ -451,18 +451,14 @@ Safe to ignore if the package is deleted in this commit. final io.Process publish = await processRunner.start( flutterCommand, ['pub', 'publish'] + publishFlags, workingDirectory: packageDir); - publish.stdout - .transform(utf8.decoder) - .listen((String data) => _print(data)); - publish.stderr - .transform(utf8.decoder) - .listen((String data) => _print(data)); + publish.stdout.transform(utf8.decoder).listen((String data) => print(data)); + publish.stderr.transform(utf8.decoder).listen((String data) => print(data)); _stdinSubscription ??= _stdin .transform(utf8.decoder) .listen((String data) => publish.stdin.writeln(data)); final int result = await publish.exitCode; if (result != 0) { - _print('Publish ${packageDir.basename} failed.'); + printError('Publish ${packageDir.basename} failed.'); return false; } return true; @@ -490,10 +486,10 @@ Safe to ignore if the package is deleted in this commit. }) async { assert(remote != null && tag != null); if (!getBoolArg(_skipConfirmationFlag)) { - _print('Ready to push $tag to ${remote.url} (y/n)?'); + print('Ready to push $tag to ${remote.url} (y/n)?'); final String? input = _stdin.readLineSync(); if (input?.toLowerCase() != 'y') { - _print('Tag push canceled.'); + print('Tag push canceled.'); return false; } } diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 9a937daa2384..576d3a4c88c8 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -24,7 +24,6 @@ import 'util.dart'; void main() { const String testPluginName = 'foo'; - late List printedMessages; late Directory testRoot; late Directory packagesDir; @@ -62,13 +61,9 @@ void main() { await gitDir.runCommand(['commit', '-m', 'Initial commit']); processRunner = TestProcessRunner(); mockStdin = MockStdin(); - printedMessages = []; commandRunner = CommandRunner('tester', '') ..addCommand(PublishPluginCommand(packagesDir, - processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), - stdinput: mockStdin, - gitDir: gitDir)); + processRunner: processRunner, stdinput: mockStdin, gitDir: gitDir)); }); tearDown(() { @@ -77,50 +72,66 @@ void main() { group('Initial validation', () { test('requires a package flag', () async { - await expectLater(() => commandRunner.run(['publish-plugin']), - throwsA(isA())); - expect( - printedMessages.last, contains('Must specify a package to publish.')); + Error? commandError; + final List output = await runCapturingPrint( + commandRunner, ['publish-plugin'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect(output.last, contains('Must specify a package to publish.')); }); test('requires an existing flag', () async { - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - 'iamerror', - '--no-push-tags' - ]), - throwsA(isA())); - - expect(printedMessages.last, contains('iamerror does not exist')); + Error? commandError; + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--package', 'iamerror', '--no-push-tags'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect(output.last, contains('iamerror does not exist')); }); test('refuses to proceed with dirty files', () async { pluginDir.childFile('tmp').createSync(); - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags' - ]), - throwsA(isA())); + Error? commandError; + final List output = await runCapturingPrint( + commandRunner, [ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags' + ], errorHandler: (Error e) { + commandError = e; + }); + expect(commandError, isA()); expect( - printedMessages, - containsAllInOrder([ - 'There are files in the package directory that haven\'t been saved in git. Refusing to publish these files:\n\n?? packages/foo/tmp\n\nIf the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.', - 'Failed, see above for details.', + output, + containsAllInOrder([ + contains('There are files in the package directory that haven\'t ' + 'been saved in git. Refusing to publish these files:\n\n' + '?? packages/foo/tmp\n\n' + 'If the directory should be clean, you can run `git clean -xdf && ' + 'git reset --hard HEAD` to wipe all local changes.'), + contains('Failed, see above for details.'), ])); }); test('fails immediately if the remote doesn\'t exist', () async { - await expectLater( - () => commandRunner - .run(['publish-plugin', '--package', testPluginName]), - throwsA(isA())); + Error? commandError; + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--package', + testPluginName + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); expect(processRunner.results.last.stderr, contains('No such remote')); }); @@ -128,7 +139,8 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; - await commandRunner.run([ + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -136,7 +148,7 @@ void main() { '--no-tag-release' ]); - expect(printedMessages.last, 'Done!'); + expect(output.last, 'Done!'); }); test('can publish non-flutter package', () async { @@ -149,20 +161,28 @@ void main() { await gitDir.runCommand(['commit', '-m', 'Initial commit']); // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; - await commandRunner.run([ + + final List output = await runCapturingPrint( + commandRunner, [ 'publish-plugin', '--package', packageName, '--no-push-tags', '--no-tag-release' ]); - expect(printedMessages.last, 'Done!'); + + expect(output.last, 'Done!'); }); }); group('Publishes package', () { test('while showing all output from pub publish to the user', () async { - final Future publishCommand = commandRunner.run([ + processRunner.mockPublishStdout = 'Foo'; + processRunner.mockPublishStderr = 'Bar'; + processRunner.mockPublishCompleteCode = 0; + + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -170,28 +190,21 @@ void main() { '--no-tag-release' ]); - processRunner.mockPublishStdout = 'Foo'; - processRunner.mockPublishStderr = 'Bar'; - processRunner.mockPublishCompleteCode = 0; - - await publishCommand; - - expect(printedMessages, contains('Foo')); - expect(printedMessages, contains('Bar')); + expect(output, contains('Foo')); + expect(output, contains('Bar')); }); test('forwards input from the user to `pub publish`', () async { - final Future publishCommand = commandRunner.run([ + mockStdin.mockUserInputs.add(utf8.encode('user input')); + processRunner.mockPublishCompleteCode = 0; + + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, '--no-push-tags', '--no-tag-release' ]); - mockStdin.mockUserInputs.add(utf8.encode('user input')); - processRunner.mockPublishCompleteCode = 0; - - await publishCommand; expect(processRunner.mockPublishProcess.stdinMock.lines, contains('user input')); @@ -199,7 +212,8 @@ void main() { test('forwards --pub-publish-flags to pub publish', () async { processRunner.mockPublishCompleteCode = 0; - await commandRunner.run([ + + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -221,7 +235,8 @@ void main() { () async { processRunner.mockPublishCompleteCode = 0; _createMockCredentialFile(); - await commandRunner.run([ + + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -241,23 +256,30 @@ void main() { test('throws if pub publish fails', () async { processRunner.mockPublishCompleteCode = 128; - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - '--no-tag-release', - ]), - throwsA(isA())); - - expect(printedMessages, contains('Publish foo failed.')); + + Error? commandError; + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Publish foo failed.'), + ])); }); test('publish, dry run', () async { - // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. - processRunner.mockPublishCompleteCode = 1; - await commandRunner.run([ + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -268,7 +290,7 @@ void main() { expect(processRunner.pushTagsArgs, isEmpty); expect( - printedMessages, + output, containsAllInOrder([ '=============== DRY RUN ===============', 'Running `pub publish ` in ${pluginDir.path}...\n', @@ -280,7 +302,8 @@ void main() { group('Tags release', () { test('with the version and name from the pubspec.yaml', () async { processRunner.mockPublishCompleteCode = 0; - await commandRunner.run([ + + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -295,16 +318,24 @@ void main() { test('only if publishing succeeded', () async { processRunner.mockPublishCompleteCode = 128; - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags', - ]), - throwsA(isA())); - - expect(printedMessages, contains('Publish foo failed.')); + + Error? commandError; + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Publish foo failed.'), + ])); final String? tag = (await gitDir.runCommand( ['show-ref', '$testPluginName-v0.0.1'], throwOnError: false)) @@ -322,22 +353,28 @@ void main() { test('requires user confirmation', () async { processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'help'; - await expectLater( - () => commandRunner.run([ - 'publish-plugin', - '--package', - testPluginName, - ]), - throwsA(isA())); - - expect(printedMessages, contains('Tag push canceled.')); + + Error? commandError; + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--package', + testPluginName, + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect(output, contains('Tag push canceled.')); }); test('to upstream by default', () async { await gitDir.runCommand(['tag', 'garbage']); processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner.run([ + + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -346,7 +383,7 @@ void main() { expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); expect(processRunner.pushTagsArgs[1], 'upstream'); expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); - expect(printedMessages.last, 'Done!'); + expect(output.last, 'Done!'); }); test('does not ask for user input if the --skip-confirmation flag is on', @@ -354,7 +391,9 @@ void main() { await gitDir.runCommand(['tag', 'garbage']); processRunner.mockPublishCompleteCode = 0; _createMockCredentialFile(); - await commandRunner.run([ + + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--skip-confirmation', '--package', @@ -364,7 +403,7 @@ void main() { expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); expect(processRunner.pushTagsArgs[1], 'upstream'); expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); - expect(printedMessages.last, 'Done!'); + expect(output.last, 'Done!'); }); test('to upstream by default, dry run', () async { @@ -372,12 +411,13 @@ void main() { // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. processRunner.mockPublishCompleteCode = 1; mockStdin.readLineOutput = 'y'; - await commandRunner.run( + + final List output = await runCapturingPrint(commandRunner, ['publish-plugin', '--package', testPluginName, '--dry-run']); expect(processRunner.pushTagsArgs, isEmpty); expect( - printedMessages, + output, containsAllInOrder([ '=============== DRY RUN ===============', 'Running `pub publish ` in ${pluginDir.path}...\n', @@ -392,7 +432,9 @@ void main() { ['remote', 'add', 'origin', 'http://localhost:8001']); processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner.run([ + + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -403,12 +445,14 @@ void main() { expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); expect(processRunner.pushTagsArgs[1], 'origin'); expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); - expect(printedMessages.last, 'Done!'); + expect(output.last, 'Done!'); }); test('only if tagging and pushing to remotes are both enabled', () async { processRunner.mockPublishCompleteCode = 0; - await commandRunner.run([ + + final List output = + await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', testPluginName, @@ -416,7 +460,7 @@ void main() { ]); expect(processRunner.pushTagsArgs.isEmpty, isTrue); - expect(printedMessages.last, 'Done!'); + expect(output.last, 'Done!'); }); }); @@ -450,7 +494,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -473,10 +516,12 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -523,7 +568,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -543,8 +587,10 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + processRunner.pushTagsArgs.clear(); // Non-federated @@ -554,11 +600,12 @@ void main() { createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); await gitDir.runCommand(['add', '-A']); await gitDir.runCommand(['commit', '-m', 'Add plugins']); - // Immediately return 0 when running `pub publish`. - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + output.addAll(await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~'])); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -597,7 +644,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -617,14 +663,17 @@ void main() { // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. processRunner.mockPublishCompleteCode = 1; mockStdin.readLineOutput = 'y'; - await commandRunner.run([ + + final List output = await runCapturingPrint( + commandRunner, [ 'publish-plugin', '--all-changed', '--base-sha=HEAD~', '--dry-run' ]); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -662,7 +711,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -683,10 +731,12 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -704,7 +754,6 @@ void main() { expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); processRunner.pushTagsArgs.clear(); - printedMessages.clear(); final List plugin1Pubspec = pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); @@ -724,10 +773,10 @@ void main() { await gitDir .runCommand(['commit', '-m', 'Update versions to 0.0.2']); - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + final List output2 = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( - printedMessages, + output2, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -769,7 +818,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -790,10 +838,11 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -811,7 +860,6 @@ void main() { expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); processRunner.pushTagsArgs.clear(); - printedMessages.clear(); final List plugin1Pubspec = pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); @@ -830,10 +878,10 @@ void main() { 'Update plugin1 versions to 0.0.2, delete plugin2' ]); - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + final List output2 = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( - printedMessages, + output2, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -872,7 +920,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -895,10 +942,12 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -935,7 +984,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -956,10 +1004,17 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await expectLater( - () => commandRunner.run( - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']), - throwsA(isA())); + + Error? commandError; + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--all-changed', + '--base-sha=HEAD~' + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); expect(processRunner.pushTagsArgs, isEmpty); }); @@ -984,10 +1039,12 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', @@ -1011,7 +1068,6 @@ void main() { }); final PublishPluginCommand command = PublishPluginCommand(packagesDir, processRunner: processRunner, - print: (Object? message) => printedMessages.add(message.toString()), stdinput: mockStdin, httpClient: mockClient, gitDir: gitDir); @@ -1029,23 +1085,24 @@ void main() { // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - await commandRunner - .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( - printedMessages, + output, containsAllInOrder([ 'Checking local repo...', 'Local repo is ready!', 'Done!' ])); expect( - printedMessages.contains( + output.contains( 'Running `pub publish ` in ${flutterPluginTools.path}...\n', ), isFalse); expect(processRunner.pushTagsArgs, isEmpty); processRunner.pushTagsArgs.clear(); - printedMessages.clear(); }); }); } From 5b5f8016d31a172470ae5da49f2c0b57e2fe2481 Mon Sep 17 00:00:00 2001 From: BeMacized Date: Wed, 25 Aug 2021 16:41:09 +0200 Subject: [PATCH 38/57] [camera] Expand CameraImage DTO with properties for lens aperture, exposure time and ISO. (#4256) --- packages/camera/camera/CHANGELOG.md | 4 ++ .../io/flutter/plugins/camera/Camera.java | 11 ++- .../plugins/camera/CameraCaptureCallback.java | 21 +++++- .../camera/types/CameraCaptureProperties.java | 67 +++++++++++++++++ .../CameraCaptureCallbackStatesTest.java | 6 +- .../camera/CameraCaptureCallbackTest.java | 72 +++++++++++++++++++ .../camera/camera/ios/Classes/CameraPlugin.m | 5 ++ .../camera/camera/lib/src/camera_image.dart | 14 ++++ packages/camera/camera/pubspec.yaml | 2 +- .../camera/camera/test/camera_image_test.dart | 15 ++++ 10 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 68188d6510ff..73cce2c539c1 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.1 + +* Added `lensAperture`, `sensorExposureTime` and `sensorSensitivity` properties to the `CameraImage` dto. + ## 0.9.0 * Complete rewrite of Android plugin to fix many capture, focus, flash, orientation and exposure issues. diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 4724d22a1bcd..43479aca616c 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -61,6 +61,7 @@ import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; import io.flutter.plugins.camera.media.MediaRecorderBuilder; +import io.flutter.plugins.camera.types.CameraCaptureProperties; import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; import io.flutter.view.TextureRegistry.SurfaceTextureEntry; import java.io.File; @@ -130,6 +131,8 @@ class Camera /** Holds the current capture timeouts */ private CaptureTimeoutsWrapper captureTimeouts; + /** Holds the last known capture properties */ + private CameraCaptureProperties captureProps; private MethodChannel.Result flutterResult; @@ -158,7 +161,8 @@ public Camera( // Create capture callback. captureTimeouts = new CaptureTimeoutsWrapper(3000, 3000); - cameraCaptureCallback = CameraCaptureCallback.create(this, captureTimeouts); + captureProps = new CameraCaptureProperties(); + cameraCaptureCallback = CameraCaptureCallback.create(this, captureTimeouts, captureProps); startBackgroundThread(); } @@ -1042,6 +1046,11 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i imageBuffer.put("height", img.getHeight()); imageBuffer.put("format", img.getFormat()); imageBuffer.put("planes", planes); + imageBuffer.put("lensAperture", this.captureProps.getLastLensAperture()); + imageBuffer.put("sensorExposureTime", this.captureProps.getLastSensorExposureTime()); + Integer sensorSensitivity = this.captureProps.getLastSensorSensitivity(); + imageBuffer.put( + "sensorSensitivity", sensorSensitivity == null ? null : (double) sensorSensitivity); final Handler handler = new Handler(Looper.getMainLooper()); handler.post(() -> imageStreamSink.success(imageBuffer)); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java index 21dcb602655d..805f18298958 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java @@ -11,6 +11,7 @@ import android.hardware.camera2.TotalCaptureResult; import android.util.Log; import androidx.annotation.NonNull; +import io.flutter.plugins.camera.types.CameraCaptureProperties; import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; /** @@ -22,13 +23,16 @@ class CameraCaptureCallback extends CaptureCallback { private final CameraCaptureStateListener cameraStateListener; private CameraState cameraState; private final CaptureTimeoutsWrapper captureTimeouts; + private final CameraCaptureProperties captureProps; private CameraCaptureCallback( @NonNull CameraCaptureStateListener cameraStateListener, - @NonNull CaptureTimeoutsWrapper captureTimeouts) { + @NonNull CaptureTimeoutsWrapper captureTimeouts, + @NonNull CameraCaptureProperties captureProps) { cameraState = CameraState.STATE_PREVIEW; this.cameraStateListener = cameraStateListener; this.captureTimeouts = captureTimeouts; + this.captureProps = captureProps; } /** @@ -41,8 +45,9 @@ private CameraCaptureCallback( */ public static CameraCaptureCallback create( @NonNull CameraCaptureStateListener cameraStateListener, - @NonNull CaptureTimeoutsWrapper captureTimeouts) { - return new CameraCaptureCallback(cameraStateListener, captureTimeouts); + @NonNull CaptureTimeoutsWrapper captureTimeouts, + @NonNull CameraCaptureProperties captureProps) { + return new CameraCaptureCallback(cameraStateListener, captureTimeouts, captureProps); } /** @@ -67,6 +72,16 @@ private void process(CaptureResult result) { Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); + // Update capture properties + if (result instanceof TotalCaptureResult) { + Float lensAperture = result.get(CaptureResult.LENS_APERTURE); + Long sensorExposureTime = result.get(CaptureResult.SENSOR_EXPOSURE_TIME); + Integer sensorSensitivity = result.get(CaptureResult.SENSOR_SENSITIVITY); + this.captureProps.setLastLensAperture(lensAperture); + this.captureProps.setLastSensorExposureTime(sensorExposureTime); + this.captureProps.setLastSensorSensitivity(sensorSensitivity); + } + if (cameraState != CameraState.STATE_PREVIEW) { Log.d( TAG, diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java new file mode 100644 index 000000000000..68177f4ecfd6 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java @@ -0,0 +1,67 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.types; + +public class CameraCaptureProperties { + + private Float lastLensAperture; + private Long lastSensorExposureTime; + private Integer lastSensorSensitivity; + + /** + * Gets the last known lens aperture. (As f-stop value) + * + * @return the last known lens aperture. (As f-stop value) + */ + public Float getLastLensAperture() { + return lastLensAperture; + } + + /** + * Sets the last known lens aperture. (As f-stop value) + * + * @param lastLensAperture - The last known lens aperture to set. (As f-stop value) + */ + public void setLastLensAperture(Float lastLensAperture) { + this.lastLensAperture = lastLensAperture; + } + + /** + * Gets the last known sensor exposure time in nanoseconds. + * + * @return the last known sensor exposure time in nanoseconds. + */ + public Long getLastSensorExposureTime() { + return lastSensorExposureTime; + } + + /** + * Sets the last known sensor exposure time in nanoseconds. + * + * @param lastSensorExposureTime - The last known sensor exposure time to set, in nanoseconds. + */ + public void setLastSensorExposureTime(Long lastSensorExposureTime) { + this.lastSensorExposureTime = lastSensorExposureTime; + } + + /** + * Gets the last known sensor sensitivity in ISO arithmetic units. + * + * @return the last known sensor sensitivity in ISO arithmetic units. + */ + public Integer getLastSensorSensitivity() { + return lastSensorSensitivity; + } + + /** + * Sets the last known sensor sensitivity in ISO arithmetic units. + * + * @param lastSensorSensitivity - The last known sensor sensitivity to set, in ISO arithmetic + * units. + */ + public void setLastSensorSensitivity(Integer lastSensorSensitivity) { + this.lastSensorSensitivity = lastSensorSensitivity; + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java index 4964aef8b8c9..934aff857ec7 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java @@ -17,6 +17,7 @@ import android.hardware.camera2.CaptureResult.Key; import android.hardware.camera2.TotalCaptureResult; import io.flutter.plugins.camera.CameraCaptureCallback.CameraCaptureStateListener; +import io.flutter.plugins.camera.types.CameraCaptureProperties; import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; import io.flutter.plugins.camera.types.Timeout; import io.flutter.plugins.camera.utils.TestUtils; @@ -40,6 +41,7 @@ public class CameraCaptureCallbackStatesTest extends TestCase { private CaptureRequest mockCaptureRequest; private CaptureResult mockPartialCaptureResult; private CaptureTimeoutsWrapper mockCaptureTimeouts; + private CameraCaptureProperties mockCaptureProps; private TotalCaptureResult mockTotalCaptureResult; private MockedStatic mockedStaticTimeout; private Timeout mockTimeout; @@ -83,6 +85,7 @@ protected void setUp() throws Exception { mockTotalCaptureResult = mock(TotalCaptureResult.class); mockTimeout = mock(Timeout.class); mockCaptureTimeouts = mock(CaptureTimeoutsWrapper.class); + mockCaptureProps = mock(CameraCaptureProperties.class); when(mockCaptureTimeouts.getPreCaptureFocusing()).thenReturn(mockTimeout); when(mockCaptureTimeouts.getPreCaptureMetering()).thenReturn(mockTimeout); @@ -95,7 +98,8 @@ protected void setUp() throws Exception { mockedStaticTimeout.when(() -> Timeout.create(1000)).thenReturn(mockTimeout); cameraCaptureCallback = - CameraCaptureCallback.create(mockCaptureStateListener, mockCaptureTimeouts); + CameraCaptureCallback.create( + mockCaptureStateListener, mockCaptureTimeouts, mockCaptureProps); } @Override diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java new file mode 100644 index 000000000000..75a5b25995e2 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java @@ -0,0 +1,72 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; +import io.flutter.plugins.camera.types.CameraCaptureProperties; +import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class CameraCaptureCallbackTest { + + private CameraCaptureCallback cameraCaptureCallback; + private CameraCaptureProperties mockCaptureProps; + + @Before + public void setUp() { + CameraCaptureCallback.CameraCaptureStateListener mockCaptureStateListener = + mock(CameraCaptureCallback.CameraCaptureStateListener.class); + CaptureTimeoutsWrapper mockCaptureTimeouts = mock(CaptureTimeoutsWrapper.class); + mockCaptureProps = mock(CameraCaptureProperties.class); + cameraCaptureCallback = + CameraCaptureCallback.create( + mockCaptureStateListener, mockCaptureTimeouts, mockCaptureProps); + } + + @Test + public void onCaptureProgressed_doesNotUpdateCameraCaptureProperties() { + CameraCaptureSession mockSession = mock(CameraCaptureSession.class); + CaptureRequest mockRequest = mock(CaptureRequest.class); + CaptureResult mockResult = mock(CaptureResult.class); + + cameraCaptureCallback.onCaptureProgressed(mockSession, mockRequest, mockResult); + + verify(mockCaptureProps, never()).setLastLensAperture(anyFloat()); + verify(mockCaptureProps, never()).setLastSensorExposureTime(anyLong()); + verify(mockCaptureProps, never()).setLastSensorSensitivity(anyInt()); + } + + @Test + public void onCaptureCompleted_updatesCameraCaptureProperties() { + CameraCaptureSession mockSession = mock(CameraCaptureSession.class); + CaptureRequest mockRequest = mock(CaptureRequest.class); + TotalCaptureResult mockResult = mock(TotalCaptureResult.class); + when(mockResult.get(CaptureResult.LENS_APERTURE)).thenReturn(1.0f); + when(mockResult.get(CaptureResult.SENSOR_EXPOSURE_TIME)).thenReturn(2L); + when(mockResult.get(CaptureResult.SENSOR_SENSITIVITY)).thenReturn(3); + + cameraCaptureCallback.onCaptureCompleted(mockSession, mockRequest, mockResult); + + verify(mockCaptureProps, times(1)).setLastLensAperture(1.0f); + verify(mockCaptureProps, times(1)).setLastSensorExposureTime(2L); + verify(mockCaptureProps, times(1)).setLastSensorSensitivity(3); + } +} diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index d88eb45945fe..ea03ce57649c 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -661,6 +661,11 @@ - (void)captureOutput:(AVCaptureOutput *)output imageBuffer[@"height"] = [NSNumber numberWithUnsignedLong:imageHeight]; imageBuffer[@"format"] = @(videoFormat); imageBuffer[@"planes"] = planes; + imageBuffer[@"lensAperture"] = [NSNumber numberWithFloat:[_captureDevice lensAperture]]; + Float64 exposureDuration = CMTimeGetSeconds([_captureDevice exposureDuration]); + Float64 nsExposureDuration = 1000000000 * exposureDuration; + imageBuffer[@"sensorExposureTime"] = [NSNumber numberWithInt:nsExposureDuration]; + imageBuffer[@"sensorSensitivity"] = [NSNumber numberWithFloat:[_captureDevice ISO]]; _imageStreamHandler.eventSink(imageBuffer); diff --git a/packages/camera/camera/lib/src/camera_image.dart b/packages/camera/camera/lib/src/camera_image.dart index 411c7e86db41..43fa763bed48 100644 --- a/packages/camera/camera/lib/src/camera_image.dart +++ b/packages/camera/camera/lib/src/camera_image.dart @@ -100,6 +100,9 @@ class CameraImage { : format = ImageFormat._fromPlatformData(data['format']), height = data['height'], width = data['width'], + lensAperture = data['lensAperture'], + sensorExposureTime = data['sensorExposureTime'], + sensorSensitivity = data['sensorSensitivity'], planes = List.unmodifiable(data['planes'] .map((dynamic planeData) => Plane._fromPlatformData(planeData))); @@ -125,4 +128,15 @@ class CameraImage { /// /// The number of planes is determined by the format of the image. final List planes; + + /// The aperture settings for this image. + /// + /// Represented as an f-stop value. + final double? lensAperture; + + /// The sensor exposure time for this image in nanoseconds. + final int? sensorExposureTime; + + /// The sensor sensitivity in standard ISO arithmetic units. + final double? sensorSensitivity; } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index a7c6a61a4ef2..08d1e3eead4f 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/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.0 +version: 0.9.1 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/camera/camera/test/camera_image_test.dart b/packages/camera/camera/test/camera_image_test.dart index 2d827d983f3a..85d613f41485 100644 --- a/packages/camera/camera/test/camera_image_test.dart +++ b/packages/camera/camera/test/camera_image_test.dart @@ -18,6 +18,9 @@ void main() { 'format': 35, 'height': 1, 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, 'planes': [ { 'bytes': Uint8List.fromList([1, 2, 3, 4]), @@ -41,6 +44,9 @@ void main() { 'format': 875704438, 'height': 1, 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, 'planes': [ { 'bytes': Uint8List.fromList([1, 2, 3, 4]), @@ -61,6 +67,9 @@ void main() { 'format': 35, 'height': 1, 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, 'planes': [ { 'bytes': Uint8List.fromList([1, 2, 3, 4]), @@ -81,6 +90,9 @@ void main() { 'format': 1111970369, 'height': 1, 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, 'planes': [ { 'bytes': Uint8List.fromList([1, 2, 3, 4]), @@ -98,6 +110,9 @@ void main() { 'format': null, 'height': 1, 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, 'planes': [ { 'bytes': Uint8List.fromList([1, 2, 3, 4]), From 88f84104f8df94938fe67716a38d5adc6e1fd81a Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 25 Aug 2021 16:39:58 -0400 Subject: [PATCH 39/57] [flutter_plugin_tools] Convert publish tests to mock git (#4263) Replaces the use of an actual git repository on the filesystem with mock git output and an in-memory filesystem. This: - makes the tests more hermetic. - simplifies the setup of some tests considerably, avoiding the need to run the command once to set up the expected state before running a second time for the intended test. - eliminates some of the special handling in the test's custom process runner (making it easier to eliminate in a PR that will follow after). Also adds some output checking in a couple of tests that didn't have enough to ensure that they were necessarily testing the right thing (e.g., testing that a specific thing didn't happen, but not checking that the publish step that could have caused that thing to happen even ran at all). --- .../tool/lib/src/publish_plugin_command.dart | 24 +- .../test/publish_plugin_command_test.dart | 563 +++++++++--------- 2 files changed, 285 insertions(+), 302 deletions(-) diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 5a75ce6af89f..be9e6d300125 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -355,11 +355,9 @@ Safe to ignore if the package is deleted in this commit. final String tag = _getTag(packageDir); print('Tagging release $tag...'); if (!getBoolArg(_dryRunFlag)) { - final io.ProcessResult result = await processRunner.run( - 'git', + final io.ProcessResult result = await (await gitDir).runCommand( ['tag', tag], - workingDir: packageDir, - logOnError: true, + throwOnError: false, ); if (result.exitCode != 0) { return false; @@ -400,11 +398,9 @@ Safe to ignore if the package is deleted in this commit. } Future _checkGitStatus(Directory packageDir) async { - final io.ProcessResult statusResult = await processRunner.run( - 'git', + final io.ProcessResult statusResult = await (await gitDir).runCommand( ['status', '--porcelain', '--ignored', packageDir.absolute.path], - workingDir: packageDir, - logOnError: true, + throwOnError: false, ); if (statusResult.exitCode != 0) { return false; @@ -421,11 +417,9 @@ Safe to ignore if the package is deleted in this commit. } Future _verifyRemote(String remote) async { - final io.ProcessResult getRemoteUrlResult = await processRunner.run( - 'git', + final io.ProcessResult getRemoteUrlResult = await (await gitDir).runCommand( ['remote', 'get-url', remote], - workingDir: packagesDir, - logOnError: true, + throwOnError: false, ); if (getRemoteUrlResult.exitCode != 0) { return null; @@ -494,11 +488,9 @@ Safe to ignore if the package is deleted in this commit. } } if (!getBoolArg(_dryRunFlag)) { - final io.ProcessResult result = await processRunner.run( - 'git', + final io.ProcessResult result = await (await gitDir).runCommand( ['push', remote.name, tag], - workingDir: packagesDir, - logOnError: true, + throwOnError: false, ); if (result.exitCode != 0) { return false; diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 576d3a4c88c8..40018b6edb61 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -8,34 +8,31 @@ import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; -import 'package:file/local.dart'; +import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/process_runner.dart'; import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; -import 'package:git/git.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:mockito/mockito.dart'; import 'package:platform/platform.dart'; import 'package:test/test.dart'; +import 'common/plugin_command_test.mocks.dart'; import 'mocks.dart'; import 'util.dart'; void main() { const String testPluginName = 'foo'; - late Directory testRoot; late Directory packagesDir; late Directory pluginDir; - late GitDir gitDir; + late MockGitDir gitDir; late TestProcessRunner processRunner; + late RecordingProcessRunner gitProcessRunner; late CommandRunner commandRunner; late MockStdin mockStdin; - // This test uses a local file system instead of an in memory one throughout - // so that git actually works. In setup we initialize a mono repo of plugins - // with one package and commit everything to Git. - const FileSystem fileSystem = LocalFileSystem(); + late FileSystem fileSystem; void _createMockCredentialFile() { final String credentialPath = PublishPluginCommand.getCredentialPath(); @@ -45,20 +42,26 @@ void main() { } setUp(() async { - testRoot = fileSystem.systemTempDirectory - .createTempSync('publish_plugin_command_test-'); - // The temp directory can have symbolic links, which won't match git output; - // use a fully resolved version to avoid potential path comparison issues. - testRoot = fileSystem.directory(testRoot.resolveSymbolicLinksSync()); - packagesDir = createPackagesDirectory(parentDir: testRoot); + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + // TODO(stuartmorgan): Move this from setup to individual tests. pluginDir = createFakePlugin(testPluginName, packagesDir, examples: []); assert(pluginDir != null && pluginDir.existsSync()); - io.Process.runSync('git', ['init'], - workingDirectory: testRoot.path); - gitDir = await GitDir.fromExisting(testRoot.path); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Initial commit']); + + gitProcessRunner = RecordingProcessRunner(); + gitDir = MockGitDir(); + when(gitDir.path).thenReturn(packagesDir.parent.path); + when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) + .thenAnswer((Invocation invocation) { + final List arguments = + invocation.positionalArguments[0]! as List; + // Attach the first argument to the command to make targeting the mock + // results easier. + final String gitCommand = arguments.removeAt(0); + return gitProcessRunner.run('git-$gitCommand', arguments); + }); + processRunner = TestProcessRunner(); mockStdin = MockStdin(); commandRunner = CommandRunner('tester', '') @@ -66,10 +69,6 @@ void main() { processRunner: processRunner, stdinput: mockStdin, gitDir: gitDir)); }); - tearDown(() { - testRoot.deleteSync(recursive: true); - }); - group('Initial validation', () { test('requires a package flag', () async { Error? commandError; @@ -79,7 +78,11 @@ void main() { }); expect(commandError, isA()); - expect(output.last, contains('Must specify a package to publish.')); + expect( + output, + containsAllInOrder([ + contains('Must specify a package to publish.'), + ])); }); test('requires an existing flag', () async { @@ -91,11 +94,14 @@ void main() { }); expect(commandError, isA()); - expect(output.last, contains('iamerror does not exist')); + expect(output, + containsAllInOrder([contains('iamerror does not exist')])); }); test('refuses to proceed with dirty files', () async { - pluginDir.childFile('tmp').createSync(); + gitProcessRunner.mockProcessesForExecutable['git-status'] = [ + MockProcess(stdout: '?? ${pluginDir.childFile('tmp').path}\n') + ]; Error? commandError; final List output = await runCapturingPrint( @@ -114,7 +120,7 @@ void main() { containsAllInOrder([ contains('There are files in the package directory that haven\'t ' 'been saved in git. Refusing to publish these files:\n\n' - '?? packages/foo/tmp\n\n' + '?? /packages/foo/tmp\n\n' 'If the directory should be clean, you can run `git clean -xdf && ' 'git reset --hard HEAD` to wipe all local changes.'), contains('Failed, see above for details.'), @@ -122,20 +128,32 @@ void main() { }); test('fails immediately if the remote doesn\'t exist', () async { + gitProcessRunner.mockProcessesForExecutable['git-remote'] = [ + MockProcess(exitCode: 1), + ]; + Error? commandError; - await runCapturingPrint(commandRunner, [ - 'publish-plugin', - '--package', - testPluginName - ], errorHandler: (Error e) { + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--package', testPluginName], + errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); - expect(processRunner.results.last.stderr, contains('No such remote')); + expect( + output, + containsAllInOrder([ + contains( + 'Unable to find URL for remote upstream; cannot push tags'), + ])); }); test("doesn't validate the remote if it's not pushing tags", () async { + // Checking the remote should fail. + gitProcessRunner.mockProcessesForExecutable['git-remote'] = [ + MockProcess(exitCode: 1), + ]; + // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; @@ -148,17 +166,18 @@ void main() { '--no-tag-release' ]); - expect(output.last, 'Done!'); + expect( + output, + containsAllInOrder([ + contains('Running `pub publish ` in /packages/$testPluginName...'), + contains('Package published!'), + contains('Released [$testPluginName] successfully.'), + ])); }); test('can publish non-flutter package', () async { const String packageName = 'a_package'; createFakePackage(packageName, packagesDir); - io.Process.runSync('git', ['init'], - workingDirectory: testRoot.path); - gitDir = await GitDir.fromExisting(testRoot.path); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Initial commit']); // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; @@ -171,7 +190,15 @@ void main() { '--no-tag-release' ]); - expect(output.last, 'Done!'); + expect( + output, + containsAllInOrder( + [ + contains('Running `pub publish ` in /packages/a_package...'), + contains('Package published!'), + ], + ), + ); }); }); @@ -190,8 +217,12 @@ void main() { '--no-tag-release' ]); - expect(output, contains('Foo')); - expect(output, contains('Bar')); + expect( + output, + containsAllInOrder([ + contains('Foo'), + contains('Bar'), + ])); }); test('forwards input from the user to `pub publish`', () async { @@ -288,7 +319,10 @@ void main() { '--no-tag-release', ]); - expect(processRunner.pushTagsArgs, isEmpty); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); expect( output, containsAllInOrder([ @@ -310,10 +344,10 @@ void main() { '--no-push-tags', ]); - final String? tag = (await gitDir - .runCommand(['show-ref', '$testPluginName-v0.0.1'])) - .stdout as String?; - expect(tag, isNotEmpty); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-tag', ['$testPluginName-v0.0.1'], null))); }); test('only if publishing succeeded', () async { @@ -336,20 +370,14 @@ void main() { containsAllInOrder([ contains('Publish foo failed.'), ])); - final String? tag = (await gitDir.runCommand( - ['show-ref', '$testPluginName-v0.0.1'], - throwOnError: false)) - .stdout as String?; - expect(tag, isEmpty); + expect( + gitProcessRunner.recordedCalls, + isNot(contains( + const ProcessCall('git-tag', ['foo-v0.0.1'], null)))); }); }); group('Pushes tags', () { - setUp(() async { - await gitDir.runCommand( - ['remote', 'add', 'upstream', 'http://localhost:8000']); - }); - test('requires user confirmation', () async { processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'help'; @@ -369,7 +397,6 @@ void main() { }); test('to upstream by default', () async { - await gitDir.runCommand(['tag', 'garbage']); processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; @@ -380,15 +407,19 @@ void main() { testPluginName, ]); - expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); - expect(output.last, 'Done!'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall('git-push', + ['upstream', '$testPluginName-v0.0.1'], null))); + expect( + output, + containsAllInOrder([ + contains('Released [$testPluginName] successfully.'), + ])); }); test('does not ask for user input if the --skip-confirmation flag is on', () async { - await gitDir.runCommand(['tag', 'garbage']); processRunner.mockPublishCompleteCode = 0; _createMockCredentialFile(); @@ -400,14 +431,18 @@ void main() { testPluginName, ]); - expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); - expect(output.last, 'Done!'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall('git-push', + ['upstream', '$testPluginName-v0.0.1'], null))); + expect( + output, + containsAllInOrder([ + contains('Released [$testPluginName] successfully.'), + ])); }); test('to upstream by default, dry run', () async { - await gitDir.runCommand(['tag', 'garbage']); // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. processRunner.mockPublishCompleteCode = 1; mockStdin.readLineOutput = 'y'; @@ -415,7 +450,10 @@ void main() { final List output = await runCapturingPrint(commandRunner, ['publish-plugin', '--package', testPluginName, '--dry-run']); - expect(processRunner.pushTagsArgs, isEmpty); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); expect( output, containsAllInOrder([ @@ -428,8 +466,6 @@ void main() { }); test('to different remotes based on a flag', () async { - await gitDir.runCommand( - ['remote', 'add', 'origin', 'http://localhost:8001']); processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; @@ -442,10 +478,15 @@ void main() { 'origin', ]); - expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); - expect(processRunner.pushTagsArgs[1], 'origin'); - expect(processRunner.pushTagsArgs[2], '$testPluginName-v0.0.1'); - expect(output.last, 'Done!'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['origin', '$testPluginName-v0.0.1'], null))); + expect( + output, + containsAllInOrder([ + contains('Released [$testPluginName] successfully.'), + ])); }); test('only if tagging and pushing to remotes are both enabled', () async { @@ -459,20 +500,21 @@ void main() { '--no-tag-release', ]); - expect(processRunner.pushTagsArgs.isEmpty, isTrue); - expect(output.last, 'Done!'); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); + expect( + output, + containsAllInOrder([ + contains('Running `pub publish ` in /packages/$testPluginName...'), + contains('Package published!'), + contains('Released [$testPluginName] successfully.'), + ])); }); }); group('Auto release (all-changed flag)', () { - setUp(() async { - io.Process.runSync('git', ['init'], - workingDirectory: testRoot.path); - gitDir = await GitDir.fromExisting(testRoot.path); - await gitDir.runCommand( - ['remote', 'add', 'upstream', 'http://localhost:8000']); - }); - test('can release newly created plugins', () async { const Map httpResponsePlugin1 = { 'name': 'plugin1', @@ -511,8 +553,11 @@ void main() { 'plugin2', packagesDir.childDirectory('plugin2'), ); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; @@ -530,13 +575,14 @@ void main() { 'Packages released: plugin1, plugin2', 'Done!' ])); - expect(processRunner.pushTagsArgs, isNotEmpty); - expect(processRunner.pushTagsArgs[0], 'push'); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); - expect(processRunner.pushTagsArgs[3], 'push'); - expect(processRunner.pushTagsArgs[4], 'upstream'); - expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin1-v0.0.1'], null))); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin2-v0.0.1'], null))); }); test('can release newly created plugins, while there are existing plugins', @@ -578,11 +624,24 @@ void main() { ); commandRunner.addCommand(command); - // Prepare an exiting plugin and tag it + // The existing plugin. createFakePlugin('plugin0', packagesDir); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - await gitDir.runCommand(['tag', 'plugin0-v0.0.1']); + // Non-federated + final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + // federated + final Directory pluginDir2 = + createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); + + // Git results for plugin0 having been released already, and plugin1 and + // plugin2 being new. + gitProcessRunner.mockProcessesForExecutable['git-tag'] = [ + MockProcess(stdout: 'plugin0-v0.0.1\n') + ]; + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; @@ -591,19 +650,6 @@ void main() { final List output = await runCapturingPrint(commandRunner, ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); - processRunner.pushTagsArgs.clear(); - - // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); - // federated - final Directory pluginDir2 = - createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - - output.addAll(await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~'])); - expect( output, containsAllInOrder([ @@ -614,13 +660,14 @@ void main() { 'Packages released: plugin1, plugin2', 'Done!' ])); - expect(processRunner.pushTagsArgs, isNotEmpty); - expect(processRunner.pushTagsArgs[0], 'push'); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); - expect(processRunner.pushTagsArgs[3], 'push'); - expect(processRunner.pushTagsArgs[4], 'upstream'); - expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin1-v0.0.1'], null))); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin2-v0.0.1'], null))); }); test('can release newly created plugins, dry run', () async { @@ -658,10 +705,12 @@ void main() { // federated final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. - processRunner.mockPublishCompleteCode = 1; + + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint( @@ -687,18 +736,21 @@ void main() { 'Packages released: plugin1, plugin2', 'Done!' ])); - expect(processRunner.pushTagsArgs, isEmpty); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); }); test('version change triggers releases.', () async { const Map httpResponsePlugin1 = { 'name': 'plugin1', - 'versions': [], + 'versions': ['0.0.1'], }; const Map httpResponsePlugin2 = { 'name': 'plugin2', - 'versions': [], + 'versions': ['0.0.1'], }; final MockClient mockClient = MockClient((http.Request request) async { @@ -722,57 +774,23 @@ void main() { commandRunner.addCommand(command); // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated - final Directory pluginDir2 = - createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); + final Directory pluginDir2 = createFakePlugin( + 'plugin2', packagesDir.childDirectory('plugin2'), + version: '0.0.2'); + + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; + // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); - - expect( - output, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - 'Running `pub publish ` in ${pluginDir1.path}...\n', - 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Packages released: plugin1, plugin2', - 'Done!' - ])); - expect(processRunner.pushTagsArgs, isNotEmpty); - expect(processRunner.pushTagsArgs[0], 'push'); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); - expect(processRunner.pushTagsArgs[3], 'push'); - expect(processRunner.pushTagsArgs[4], 'upstream'); - expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); - - processRunner.pushTagsArgs.clear(); - - final List plugin1Pubspec = - pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); - plugin1Pubspec[plugin1Pubspec.indexWhere( - (String element) => element.contains('version:'))] = 'version: 0.0.2'; - pluginDir1 - .childFile('pubspec.yaml') - .writeAsStringSync(plugin1Pubspec.join('\n')); - final List plugin2Pubspec = - pluginDir2.childFile('pubspec.yaml').readAsLinesSync(); - plugin2Pubspec[plugin2Pubspec.indexWhere( - (String element) => element.contains('version:'))] = 'version: 0.0.2'; - pluginDir2 - .childFile('pubspec.yaml') - .writeAsStringSync(plugin2Pubspec.join('\n')); - await gitDir.runCommand(['add', '-A']); - await gitDir - .runCommand(['commit', '-m', 'Update versions to 0.0.2']); - final List output2 = await runCapturingPrint(commandRunner, ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( @@ -785,14 +803,14 @@ void main() { 'Packages released: plugin1, plugin2', 'Done!' ])); - - expect(processRunner.pushTagsArgs, isNotEmpty); - expect(processRunner.pushTagsArgs[0], 'push'); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2'); - expect(processRunner.pushTagsArgs[3], 'push'); - expect(processRunner.pushTagsArgs[4], 'upstream'); - expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.2'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin1-v0.0.2'], null))); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin2-v0.0.2'], null))); }); test( @@ -800,12 +818,12 @@ void main() { () async { const Map httpResponsePlugin1 = { 'name': 'plugin1', - 'versions': [], + 'versions': ['0.0.1'], }; const Map httpResponsePlugin2 = { 'name': 'plugin2', - 'versions': [], + 'versions': ['0.0.1'], }; final MockClient mockClient = MockClient((http.Request request) async { @@ -829,55 +847,23 @@ void main() { commandRunner.addCommand(command); // Non-federated - final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); + pluginDir2.deleteSync(recursive: true); + + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; + // Immediately return 0 when running `pub publish`. processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; - final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); - expect( - output, - containsAllInOrder([ - 'Checking local repo...', - 'Local repo is ready!', - 'Running `pub publish ` in ${pluginDir1.path}...\n', - 'Running `pub publish ` in ${pluginDir2.path}...\n', - 'Packages released: plugin1, plugin2', - 'Done!' - ])); - expect(processRunner.pushTagsArgs, isNotEmpty); - expect(processRunner.pushTagsArgs[0], 'push'); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); - expect(processRunner.pushTagsArgs[3], 'push'); - expect(processRunner.pushTagsArgs[4], 'upstream'); - expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); - - processRunner.pushTagsArgs.clear(); - - final List plugin1Pubspec = - pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); - plugin1Pubspec[plugin1Pubspec.indexWhere( - (String element) => element.contains('version:'))] = 'version: 0.0.2'; - pluginDir1 - .childFile('pubspec.yaml') - .writeAsStringSync(plugin1Pubspec.join('\n')); - - pluginDir2.deleteSync(recursive: true); - - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand([ - 'commit', - '-m', - 'Update plugin1 versions to 0.0.2, delete plugin2' - ]); - final List output2 = await runCapturingPrint(commandRunner, ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); expect( @@ -890,15 +876,13 @@ void main() { 'Packages released: plugin1', 'Done!' ])); - - expect(processRunner.pushTagsArgs, isNotEmpty); - expect(processRunner.pushTagsArgs.length, 3); - expect(processRunner.pushTagsArgs[0], 'push'); - expect(processRunner.pushTagsArgs[1], 'upstream'); - expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2'); + expect( + gitProcessRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin1-v0.0.2'], null))); }); - test('Exiting versions do not trigger release, also prints out message.', + test('Existing versions do not trigger release, also prints out message.', () async { const Map httpResponsePlugin1 = { 'name': 'plugin1', @@ -931,17 +915,23 @@ void main() { commandRunner.addCommand(command); // Non-federated - createFakePlugin('plugin1', packagesDir, version: '0.0.2'); + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated - createFakePlugin('plugin2', packagesDir.childDirectory('plugin2'), + final Directory pluginDir2 = createFakePlugin( + 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - await gitDir.runCommand(['tag', 'plugin1-v0.0.2']); - await gitDir.runCommand(['tag', 'plugin2-v0.0.2']); - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; - mockStdin.readLineOutput = 'y'; + + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; + gitProcessRunner.mockProcessesForExecutable['git-tag'] = [ + MockProcess( + stdout: 'plugin1-v0.0.2\n' + 'plugin2-v0.0.2\n') + ]; final List output = await runCapturingPrint(commandRunner, ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); @@ -958,11 +948,14 @@ void main() { 'Done!' ])); - expect(processRunner.pushTagsArgs, isEmpty); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); }); test( - 'Exiting versions do not trigger release, but fail if the tags do not exist.', + 'Existing versions do not trigger release, but fail if the tags do not exist.', () async { const Map httpResponsePlugin1 = { 'name': 'plugin1', @@ -995,27 +988,41 @@ void main() { commandRunner.addCommand(command); // Non-federated - createFakePlugin('plugin1', packagesDir, version: '0.0.2'); + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, version: '0.0.2'); // federated - createFakePlugin('plugin2', packagesDir.childDirectory('plugin2'), + final Directory pluginDir2 = createFakePlugin( + 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; - mockStdin.readLineOutput = 'y'; + + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; Error? commandError; - await runCapturingPrint(commandRunner, [ - 'publish-plugin', - '--all-changed', - '--base-sha=HEAD~' - ], errorHandler: (Error e) { + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~'], + errorHandler: (Error e) { commandError = e; }); expect(commandError, isA()); - expect(processRunner.pushTagsArgs, isEmpty); + expect( + output, + containsAllInOrder([ + contains('The version 0.0.2 of plugin1 has already been published'), + contains( + 'However, the git release tag for this version (plugin1-v0.0.2) is not found.'), + contains('The version 0.0.2 of plugin2 has already been published'), + contains( + 'However, the git release tag for this version (plugin2-v0.0.2) is not found.'), + ])); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); }); test('No version change does not release any plugins', () async { @@ -1025,20 +1032,11 @@ void main() { final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - io.Process.runSync('git', ['init'], - workingDirectory: testRoot.path); - gitDir = await GitDir.fromExisting(testRoot.path); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - - pluginDir1.childFile('plugin1.dart').createSync(); - pluginDir2.childFile('plugin2.dart').createSync(); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add dart files']); - - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; - mockStdin.readLineOutput = 'y'; + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('plugin1.dart').path}\n' + '${pluginDir2.childFile('plugin2.dart').path}\n') + ]; final List output = await runCapturingPrint(commandRunner, ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); @@ -1051,7 +1049,10 @@ void main() { 'No version updates in this commit.', 'Done!' ])); - expect(processRunner.pushTagsArgs, isEmpty); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); }); test('Do not release flutter_plugin_tools', () async { @@ -1080,11 +1081,9 @@ void main() { final Directory flutterPluginTools = createFakePlugin('flutter_plugin_tools', packagesDir); - await gitDir.runCommand(['add', '-A']); - await gitDir.runCommand(['commit', '-m', 'Add plugins']); - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; - mockStdin.readLineOutput = 'y'; + gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: flutterPluginTools.childFile('pubspec.yaml').path) + ]; final List output = await runCapturingPrint(commandRunner, ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); @@ -1101,19 +1100,18 @@ void main() { 'Running `pub publish ` in ${flutterPluginTools.path}...\n', ), isFalse); - expect(processRunner.pushTagsArgs, isEmpty); - processRunner.pushTagsArgs.clear(); + expect( + gitProcessRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); }); }); } class TestProcessRunner extends ProcessRunner { - final List results = []; // Most recent returned publish process. late MockProcess mockPublishProcess; final List mockPublishArgs = []; - final MockProcessResult mockPushTagsResult = MockProcessResult(); - final List pushTagsArgs = []; String? mockPublishStdout; String? mockPublishStderr; @@ -1129,15 +1127,8 @@ class TestProcessRunner extends ProcessRunner { Encoding stdoutEncoding = io.systemEncoding, Encoding stderrEncoding = io.systemEncoding, }) async { - // Don't ever really push tags. - if (executable == 'git' && args.isNotEmpty && args[0] == 'push') { - pushTagsArgs.addAll(args); - return mockPushTagsResult; - } - final io.ProcessResult result = io.Process.runSync(executable, args, workingDirectory: workingDir?.path); - results.add(result); if (result.exitCode != 0) { throw ToolExit(result.exitCode); } From 56f092a8105d2a21d26844edb1bf8458f79f195e Mon Sep 17 00:00:00 2001 From: Bartosz Selwesiuk Date: Thu, 26 Aug 2021 02:36:07 +0200 Subject: [PATCH 40/57] [camera_web] Add `onCameraClosing` implementation (#4259) --- .../example/integration_test/camera_test.dart | 74 ++++++- .../integration_test/camera_web_test.dart | 195 ++++++++++++------ .../camera/camera_web/lib/src/camera.dart | 43 +++- .../camera/camera_web/lib/src/camera_web.dart | 18 +- 4 files changed, 267 insertions(+), 63 deletions(-) diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart index 1d1659352f26..f331cc1485ab 100644 --- a/packages/camera/camera_web/example/integration_test/camera_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -5,6 +5,7 @@ import 'dart:html'; import 'dart:ui'; +import 'package:async/async.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:camera_web/src/camera.dart'; import 'package:camera_web/src/camera_service.dart'; @@ -843,10 +844,81 @@ void main() { await camera.initialize(); - camera.dispose(); + await camera.dispose(); expect(camera.videoElement.srcObject, isNull); }); }); + + group('events', () { + group('onEnded', () { + testWidgets( + 'emits the default video track ' + 'when it emits an ended event', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + final streamQueue = StreamQueue(camera.onEnded); + + await camera.initialize(); + + final videoTracks = camera.stream!.getVideoTracks(); + final defaultVideoTrack = videoTracks.first; + + defaultVideoTrack.dispatchEvent(Event('ended')); + + expect( + await streamQueue.next, + equals(defaultVideoTrack), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits the default video track ' + 'when the camera is stopped', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + final streamQueue = StreamQueue(camera.onEnded); + + await camera.initialize(); + + final videoTracks = camera.stream!.getVideoTracks(); + final defaultVideoTrack = videoTracks.first; + + camera.stop(); + + expect( + await streamQueue.next, + equals(defaultVideoTrack), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'no longer emits the default video track ' + 'when the camera is disposed', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + await camera.dispose(); + + expect( + camera.onEndedStreamController.isClosed, + isTrue, + ); + }); + }); + }); }); } diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart index d48df122277f..9ab8c511f753 100644 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -563,24 +563,34 @@ void main() { late Camera camera; late VideoElement videoElement; + late StreamController errorStreamController, abortStreamController; + late StreamController endedStreamController; + setUp(() { camera = MockCamera(); videoElement = MockVideoElement(); - when(() => camera.videoElement).thenReturn(videoElement); - when(() => videoElement.onError) - .thenAnswer((_) => FakeElementStream(Stream.empty())); - when(() => videoElement.onAbort) - .thenAnswer((_) => FakeElementStream(Stream.empty())); - }); + errorStreamController = StreamController(); + abortStreamController = StreamController(); + endedStreamController = StreamController(); - testWidgets('initializes and plays the camera', (tester) async { when(camera.getVideoSize).thenAnswer( (_) => Future.value(Size(10, 10)), ); when(camera.initialize).thenAnswer((_) => Future.value()); when(camera.play).thenAnswer((_) => Future.value()); + when(() => camera.videoElement).thenReturn(videoElement); + when(() => videoElement.onError) + .thenAnswer((_) => FakeElementStream(errorStreamController.stream)); + when(() => videoElement.onAbort) + .thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + + when(() => camera.onEnded) + .thenAnswer((_) => endedStreamController.stream); + }); + + testWidgets('initializes and plays the camera', (tester) async { // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; @@ -590,6 +600,32 @@ void main() { verify(camera.play).called(1); }); + testWidgets('starts listening to the camera video error and abort events', + (tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect(errorStreamController.hasListener, isFalse); + expect(abortStreamController.hasListener, isFalse); + + await CameraPlatform.instance.initializeCamera(cameraId); + + expect(errorStreamController.hasListener, isTrue); + expect(abortStreamController.hasListener, isTrue); + }); + + testWidgets('starts listening to the camera ended events', + (tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect(endedStreamController.hasListener, isFalse); + + await CameraPlatform.instance.initializeCamera(cameraId); + + expect(endedStreamController.hasListener, isTrue); + }); + group('throws PlatformException', () { testWidgets( 'with notFound error ' @@ -1610,6 +1646,37 @@ void main() { }); group('dispose', () { + late Camera camera; + late VideoElement videoElement; + + late StreamController errorStreamController, abortStreamController; + late StreamController endedStreamController; + + setUp(() { + camera = MockCamera(); + videoElement = MockVideoElement(); + + errorStreamController = StreamController(); + abortStreamController = StreamController(); + endedStreamController = StreamController(); + + when(camera.getVideoSize).thenAnswer( + (_) => Future.value(Size(10, 10)), + ); + when(camera.initialize).thenAnswer((_) => Future.value()); + when(camera.play).thenAnswer((_) => Future.value()); + when(camera.dispose).thenAnswer((_) => Future.value()); + + when(() => camera.videoElement).thenReturn(videoElement); + when(() => videoElement.onError) + .thenAnswer((_) => FakeElementStream(errorStreamController.stream)); + when(() => videoElement.onAbort) + .thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + + when(() => camera.onEnded) + .thenAnswer((_) => endedStreamController.stream); + }); + testWidgets('disposes the correct camera', (tester) async { const firstCameraId = 0; const secondCameraId = 1; @@ -1642,38 +1709,26 @@ void main() { ); }); - testWidgets('cancels camera video and abort error subscriptions', + testWidgets('cancels the camera video error and abort subscriptions', (tester) async { - final camera = MockCamera(); - final videoElement = MockVideoElement(); - - final errorStreamController = StreamController(); - final abortStreamController = StreamController(); + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - when(camera.getVideoSize).thenAnswer( - (_) => Future.value(Size(10, 10)), - ); - when(camera.initialize).thenAnswer((_) => Future.value()); - when(camera.play).thenAnswer((_) => Future.value()); + await CameraPlatform.instance.initializeCamera(cameraId); + await CameraPlatform.instance.dispose(cameraId); - when(() => camera.videoElement).thenReturn(videoElement); - when(() => videoElement.onError) - .thenAnswer((_) => FakeElementStream(errorStreamController.stream)); - when(() => videoElement.onAbort) - .thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + expect(errorStreamController.hasListener, isFalse); + expect(abortStreamController.hasListener, isFalse); + }); + testWidgets('cancels the camera ended subscriptions', (tester) async { // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; await CameraPlatform.instance.initializeCamera(cameraId); - - expect(errorStreamController.hasListener, isTrue); - expect(abortStreamController.hasListener, isTrue); - await CameraPlatform.instance.dispose(cameraId); - expect(errorStreamController.hasListener, isFalse); - expect(abortStreamController.hasListener, isFalse); + expect(endedStreamController.hasListener, isFalse); }); group('throws PlatformException', () { @@ -1749,6 +1804,36 @@ void main() { }); group('events', () { + late Camera camera; + late VideoElement videoElement; + + late StreamController errorStreamController, abortStreamController; + late StreamController endedStreamController; + + setUp(() { + camera = MockCamera(); + videoElement = MockVideoElement(); + + errorStreamController = StreamController(); + abortStreamController = StreamController(); + endedStreamController = StreamController(); + + when(camera.getVideoSize).thenAnswer( + (_) => Future.value(Size(10, 10)), + ); + when(camera.initialize).thenAnswer((_) => Future.value()); + when(camera.play).thenAnswer((_) => Future.value()); + + when(() => camera.videoElement).thenReturn(videoElement); + when(() => videoElement.onError) + .thenAnswer((_) => FakeElementStream(errorStreamController.stream)); + when(() => videoElement.onAbort) + .thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + + when(() => camera.onEnded) + .thenAnswer((_) => endedStreamController.stream); + }); + testWidgets( 'onCameraInitialized emits a CameraInitializedEvent ' 'on initializeCamera', (tester) async { @@ -1805,46 +1890,40 @@ void main() { ); }); - testWidgets('onCameraClosing throws UnimplementedError', (tester) async { - expect( - () => CameraPlatform.instance.onCameraClosing(cameraId), - throwsUnimplementedError, - ); - }); + testWidgets( + 'onCameraClosing emits a CameraClosingEvent ' + 'on the camera ended event', (tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - group('onCameraError', () { - late Camera camera; - late VideoElement videoElement; + final Stream eventStream = + CameraPlatform.instance.onCameraClosing(cameraId); - late StreamController errorStreamController, - abortStreamController; + final streamQueue = StreamQueue(eventStream); - setUp(() { - camera = MockCamera(); - videoElement = MockVideoElement(); + await CameraPlatform.instance.initializeCamera(cameraId); - errorStreamController = StreamController(); - abortStreamController = StreamController(); + endedStreamController.add(MockMediaStreamTrack()); - when(camera.getVideoSize).thenAnswer( - (_) => Future.value(Size(10, 10)), - ); - when(camera.initialize).thenAnswer((_) => Future.value()); - when(camera.play).thenAnswer((_) => Future.value()); + expect( + await streamQueue.next, + equals( + CameraClosingEvent(cameraId), + ), + ); - when(() => camera.videoElement).thenReturn(videoElement); - when(() => videoElement.onError).thenAnswer( - (_) => FakeElementStream(errorStreamController.stream)); - when(() => videoElement.onAbort).thenAnswer( - (_) => FakeElementStream(abortStreamController.stream)); + await streamQueue.cancel(); + }); + group('onCameraError', () { + setUp(() { // Save the camera in the camera plugin. (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; }); testWidgets( 'emits a CameraErrorEvent ' - 'on initialize video error ' + 'on the camera video error event ' 'with a message', (tester) async { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); @@ -1879,7 +1958,7 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on initialize video error ' + 'on the camera video error event ' 'with no message', (tester) async { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); @@ -1910,7 +1989,7 @@ void main() { testWidgets( 'emits a CameraErrorEvent ' - 'on initialize abort error', (tester) async { + 'on the camera video abort event', (tester) async { final Stream eventStream = CameraPlatform.instance.onCameraError(cameraId); diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart index c1343ceccf49..74d8546fbb12 100644 --- a/packages/camera/camera_web/lib/src/camera.dart +++ b/packages/camera/camera_web/lib/src/camera.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; import 'dart:html' as html; import 'dart:ui'; @@ -67,6 +68,23 @@ class Camera { /// Initialized in [initialize] and [play], reset in [stop]. html.MediaStream? stream; + /// The stream of the camera video tracks that have ended playing. + /// + /// This occurs when there is no more camera stream data, e.g. + /// the user has stopped the stream by changing the camera device, + /// revoked the camera permissions or ejected the camera device. + /// + /// MediaStreamTrack.onended: + /// https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/onended + Stream get onEnded => onEndedStreamController.stream; + + /// The stream controller for the [onEnded] stream. + @visibleForTesting + final onEndedStreamController = + StreamController.broadcast(); + + StreamSubscription? _onEndedSubscription; + /// The camera flash mode. @visibleForTesting FlashMode? flashMode; @@ -80,6 +98,7 @@ class Camera { /// Initializes the camera stream displayed in the [videoElement]. /// Registers the camera view with [textureId] under [_getViewType] type. + /// Emits the camera default video track on the [onEnded] stream when it ends. Future initialize() async { stream = await _cameraService.getMediaStreamForOptions( options, @@ -103,6 +122,16 @@ class Camera { ..muted = !options.audio.enabled ..srcObject = stream ..setAttribute('playsinline', ''); + + final videoTracks = stream!.getVideoTracks(); + + if (videoTracks.isNotEmpty) { + final defaultVideoTrack = videoTracks.first; + + _onEndedSubscription = defaultVideoTrack.onEnded.listen((html.Event _) { + onEndedStreamController.add(defaultVideoTrack); + }); + } } /// Starts the camera stream. @@ -126,7 +155,12 @@ class Camera { /// Stops the camera stream and resets the camera source. void stop() { - final tracks = videoElement.srcObject?.getTracks(); + final videoTracks = stream!.getVideoTracks(); + if (videoTracks.isNotEmpty) { + onEndedStreamController.add(videoTracks.first); + } + + final tracks = stream?.getTracks(); if (tracks != null) { for (final track in tracks) { track.stop(); @@ -303,7 +337,7 @@ class Camera { /// Disposes the camera by stopping the camera stream /// and reloading the camera source. - void dispose() { + Future dispose() async { /// Stop the camera stream. stop(); @@ -311,6 +345,11 @@ class Camera { videoElement ..srcObject = null ..load(); + + await _onEndedSubscription?.cancel(); + _onEndedSubscription = null; + + await onEndedStreamController.close(); } /// Applies default styles to the video [element]. diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart index 8b131f5d4f6e..19ee43f36660 100644 --- a/packages/camera/camera_web/lib/src/camera_web.dart +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -61,6 +61,9 @@ class CameraPlugin extends CameraPlatform { final _cameraVideoAbortSubscriptions = >{}; + final _cameraEndedSubscriptions = + >{}; + /// Returns a stream of camera events for the given [cameraId]. Stream _cameraEvents(int cameraId) => cameraEventStreamController.stream @@ -273,6 +276,15 @@ class CameraPlugin extends CameraPlatform { await camera.play(); + // Add camera's closing events to the camera events stream. + // The onEnded stream fires when there is no more camera stream data. + _cameraEndedSubscriptions[cameraId] = + camera.onEnded.listen((html.MediaStreamTrack _) { + cameraEventStreamController.add( + CameraClosingEvent(cameraId), + ); + }); + final cameraSize = await camera.getVideoSize(); cameraEventStreamController.add( @@ -313,7 +325,7 @@ class CameraPlugin extends CameraPlatform { @override Stream onCameraClosing(int cameraId) { - throw UnimplementedError('onCameraClosing() is not implemented.'); + return _cameraEvents(cameraId).whereType(); } @override @@ -548,13 +560,15 @@ class CameraPlugin extends CameraPlatform { @override Future dispose(int cameraId) async { try { - getCamera(cameraId).dispose(); + await getCamera(cameraId).dispose(); await _cameraVideoErrorSubscriptions[cameraId]?.cancel(); await _cameraVideoAbortSubscriptions[cameraId]?.cancel(); + await _cameraEndedSubscriptions[cameraId]?.cancel(); cameras.remove(cameraId); _cameraVideoErrorSubscriptions.remove(cameraId); _cameraVideoAbortSubscriptions.remove(cameraId); + _cameraEndedSubscriptions.remove(cameraId); } on html.DomException catch (e) { throw PlatformException(code: e.name, message: e.message); } From 79595de6752d75e226d601e275776e709e343f69 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 26 Aug 2021 09:10:15 -0400 Subject: [PATCH 41/57] [flutter_plugin_tool] Fix CHANGELOG validation failure summary (#4266) The error summary for a CHANGELOG validation failure was written when the only thing being checked was that the versions matched, but now there are other ways to fail as well (i.e., leaving NEXT). This fixes the summary message to be more generic so that it doesn't mislead people who hit validation failures. While adding the test for this message, I discovered that almost all of the tests were actually talking to pub.dev, causing their behavior to in some cases depend on whether a package with that name happened to have been published, and if so what its version was. In order to make the tests hermetic and predictable, this fixes that by making all tests use a mock HTTP client. --- .../tool/lib/src/version_check_command.dart | 2 +- .../tool/test/version_check_command_test.dart | 66 +++++++------------ 2 files changed, 26 insertions(+), 42 deletions(-) diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 67a81b967a8e..6b49c40d66bb 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -178,7 +178,7 @@ class VersionCheckCommand extends PackageLoopingCommand { if (!(await _validateChangelogVersion(package, pubspec: pubspec, pubspecVersionChanged: versionChanged))) { - errors.add('pubspec.yaml and CHANGELOG.md have different versions'); + errors.add('CHANGELOG.md failed validation.'); } return errors.isEmpty diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 7765073feb08..9ab7c57089a3 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -54,11 +54,15 @@ void main() { late List> gitDirCommands; Map gitShowResponses; late MockGitDir gitDir; + // Ignored if mockHttpResponse is set. + int mockHttpStatus; + Map? mockHttpResponse; setUp(() { fileSystem = MemoryFileSystem(); mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); + gitDirCommands = >[]; gitShowResponses = {}; gitDir = MockGitDir(); @@ -81,9 +85,21 @@ void main() { } return Future.value(mockProcessResult); }); + + // Default to simulating the plugin never having been published. + mockHttpStatus = 404; + mockHttpResponse = null; + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response(json.encode(mockHttpResponse), + mockHttpResponse == null ? mockHttpStatus : 200); + }); + processRunner = RecordingProcessRunner(); final VersionCheckCommand command = VersionCheckCommand(packagesDir, - processRunner: processRunner, platform: mockPlatform, gitDir: gitDir); + processRunner: processRunner, + platform: mockPlatform, + gitDir: gitDir, + httpClient: mockClient); runner = CommandRunner( 'version_check_command', 'Test for $VersionCheckCommand'); @@ -456,7 +472,9 @@ void main() { output, containsAllInOrder([ contains('When bumping the version for release, the NEXT section ' - 'should be incorporated into the new version\'s release notes.') + 'should be incorporated into the new version\'s release notes.'), + contains('plugin:\n' + ' CHANGELOG.md failed validation.'), ]), ); }); @@ -497,7 +515,7 @@ void main() { }); test('allows valid against pub', () async { - const Map httpResponse = { + mockHttpResponse = { 'name': 'some_package', 'versions': [ '0.0.1', @@ -505,15 +523,6 @@ void main() { '1.0.0', ], }; - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response(json.encode(httpResponse), 200); - }); - final VersionCheckCommand command = VersionCheckCommand(packagesDir, - processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); - - runner = CommandRunner( - 'version_check_command', 'Test for $VersionCheckCommand'); - runner.addCommand(command); createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { @@ -531,22 +540,13 @@ void main() { }); test('denies invalid against pub', () async { - const Map httpResponse = { + mockHttpResponse = { 'name': 'some_package', 'versions': [ '0.0.1', '0.0.2', ], }; - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response(json.encode(httpResponse), 200); - }); - final VersionCheckCommand command = VersionCheckCommand(packagesDir, - processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); - - runner = CommandRunner( - 'version_check_command', 'Test for $VersionCheckCommand'); - runner.addCommand(command); createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { @@ -578,15 +578,7 @@ ${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: N test( 'throw and print error message if http request failed when checking against pub', () async { - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response('xx', 400); - }); - final VersionCheckCommand command = VersionCheckCommand(packagesDir, - processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); - - runner = CommandRunner( - 'version_check_command', 'Test for $VersionCheckCommand'); - runner.addCommand(command); + mockHttpStatus = 400; createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { @@ -609,7 +601,7 @@ ${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: N contains(''' ${indentation}Error fetching version on pub for plugin. ${indentation}HTTP Status 400 -${indentation}HTTP response: xx +${indentation}HTTP response: null ''') ]), ); @@ -617,15 +609,7 @@ ${indentation}HTTP response: xx test('when checking against pub, allow any version if http status is 404.', () async { - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response('xx', 404); - }); - final VersionCheckCommand command = VersionCheckCommand(packagesDir, - processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); - - runner = CommandRunner( - 'version_check_command', 'Test for $VersionCheckCommand'); - runner.addCommand(command); + mockHttpStatus = 404; createFakePlugin('plugin', packagesDir, version: '2.0.0'); gitShowResponses = { From 9cea8db971ac0d0240736d2358a6ade0d9950652 Mon Sep 17 00:00:00 2001 From: Casey Hillers Date: Thu, 26 Aug 2021 10:26:06 -0700 Subject: [PATCH 42/57] [ci.yaml] Add auto-roller (#4270) --- .ci.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.ci.yaml b/.ci.yaml index c2b7deebab14..86bc72c7aebf 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -44,3 +44,11 @@ targets: {"dependency": "vs_build"} ] scheduler: luci + + - name: Linux ci_yaml plugins roller + recipe: infra/ci_yaml + bringup: true + timeout: 30 + scheduler: luci + runIf: + - .ci.yaml From ee8355bdcff82cd2ab887141a0eb56fb5a71bc64 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 26 Aug 2021 15:01:06 -0400 Subject: [PATCH 43/57] [flutter_plugin_tools] Check 'implements' for unpublished plugins (#4273) --- packages/camera/camera_web/pubspec.yaml | 1 + .../tool/lib/src/pubspec_check_command.dart | 19 +++--- .../tool/test/pubspec_check_command_test.dart | 64 +++++++++++++++++++ 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index 822af60a979b..70194d9037d4 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -15,6 +15,7 @@ environment: flutter: plugin: + implements: camera platforms: web: pluginClass: CameraPlugin diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index def2adaf2788..29f9ea733a03 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -97,6 +97,16 @@ class PubspecCheckCommand extends PackageLoopingCommand { printError('$listIndentation${sectionOrder.join('\n$listIndentation')}'); } + if (isPlugin) { + final String? error = _checkForImplementsError(pubspec, package: package); + if (error != null) { + printError('$indentation$error'); + passing = false; + } + } + + // Ignore metadata that's only relevant for published packages if the + // packages is not intended for publishing. if (pubspec.publishTo != 'none') { final List repositoryErrors = _checkForRepositoryLinkErrors(pubspec, package: package); @@ -114,15 +124,6 @@ class PubspecCheckCommand extends PackageLoopingCommand { '${indentation * 2}$_expectedIssueLinkFormat'); passing = false; } - - if (isPlugin) { - final String? error = - _checkForImplementsError(pubspec, package: package); - if (error != null) { - printError('$indentation$error'); - passing = false; - } - } } return passing; diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 833f7b601e50..c5d36013c40b 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -55,6 +55,7 @@ void main() { String? repositoryPackagesDirRelativePath, bool includeHomepage = false, bool includeIssueTracker = true, + bool publishable = true, }) { final String repositoryPath = repositoryPackagesDirRelativePath ?? name; final String repoLink = 'https://github.com/flutter/' @@ -69,6 +70,7 @@ ${includeRepository ? 'repository: $repoLink' : ''} ${includeHomepage ? 'homepage: $repoLink' : ''} ${includeIssueTracker ? 'issue_tracker: $issueTrackerLink' : ''} version: 1.0.0 +${publishable ? '' : 'publish_to: \'none\''} '''; } @@ -567,5 +569,67 @@ ${devDependenciesSection()} ]), ); }); + + test('validates some properties even for unpublished packages', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + + // Environment section is in the wrong location. + // Missing 'implements'. + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a_foo', isPlugin: true, publishable: false)} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +${environmentSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + contains('Missing "implements: plugin_a" in "plugin" section.'), + ]), + ); + }); + + test('ignores some checks for unpublished packages', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + // Missing metadata that is only useful for published packages, such as + // repository and issue tracker. + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection( + 'plugin', + isPlugin: true, + publishable: false, + includeRepository: false, + includeIssueTracker: false, + )} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final List output = + await runCapturingPrint(runner, ['pubspec-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin...'), + contains('No issues found!'), + ]), + ); + }); }); } From 00ace648e5b6116f2b7dcc7a2f14c3a299ab1c69 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 26 Aug 2021 15:05:28 -0400 Subject: [PATCH 44/57] [flutter_plugin_tools] Move publish tests to RecordingProcessRunner (#4269) Replaces almost all of the `TestProcessRunner`, which was specific to the `publish` tests, with the repo-standard `RecordingProcessRunner` (which now has most of the capabilities these tests need). This finishes aligning these tests with the rest of the repository tests, so they will be easier to maintain as part of the overall repository. To support this, `RecordingProcessRunner` was modified slightly to return a succeeding, no-output process by default for `start`. That makes it consistent with its existing `run` behavior, so is a good change in general. --- .../tool/test/publish_check_command_test.dart | 17 - .../test/publish_plugin_command_test.dart | 333 ++++++++---------- script/tool/test/util.dart | 5 +- 3 files changed, 152 insertions(+), 203 deletions(-) diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index 65b0cb54547c..e1ab0e224e44 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -49,11 +49,6 @@ void main() { final Directory plugin2Dir = createFakePlugin('plugin_tools_test_package_b', packagesDir); - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(), - MockProcess(), - ]; - await runCapturingPrint(runner, ['publish-check']); expect( @@ -87,10 +82,6 @@ void main() { final Directory dir = createFakePlugin('c', packagesDir); await dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(), - ]; - expect(() => runCapturingPrint(runner, ['publish-check']), throwsA(isA())); }); @@ -245,10 +236,6 @@ void main() { createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(), - ]; - final List output = await runCapturingPrint( runner, ['publish-check', '--machine']); @@ -318,10 +305,6 @@ void main() { await plugin1Dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(), - ]; - bool hasError = false; final List output = await runCapturingPrint( runner, ['publish-check', '--machine'], diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 40018b6edb61..663c2633a9db 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -10,7 +10,6 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/process_runner.dart'; import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; @@ -23,13 +22,11 @@ import 'mocks.dart'; import 'util.dart'; void main() { - const String testPluginName = 'foo'; + final String flutterCommand = getFlutterCommand(const LocalPlatform()); late Directory packagesDir; - late Directory pluginDir; late MockGitDir gitDir; late TestProcessRunner processRunner; - late RecordingProcessRunner gitProcessRunner; late CommandRunner commandRunner; late MockStdin mockStdin; late FileSystem fileSystem; @@ -44,25 +41,21 @@ void main() { setUp(() async { fileSystem = MemoryFileSystem(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); - // TODO(stuartmorgan): Move this from setup to individual tests. - pluginDir = - createFakePlugin(testPluginName, packagesDir, examples: []); - assert(pluginDir != null && pluginDir.existsSync()); - gitProcessRunner = RecordingProcessRunner(); + processRunner = TestProcessRunner(); gitDir = MockGitDir(); when(gitDir.path).thenReturn(packagesDir.parent.path); when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) .thenAnswer((Invocation invocation) { final List arguments = invocation.positionalArguments[0]! as List; - // Attach the first argument to the command to make targeting the mock - // results easier. + // Route git calls through the process runner, to make mock output + // consistent with outer processes. Attach the first argument to the + // command to make targeting the mock results easier. final String gitCommand = arguments.removeAt(0); - return gitProcessRunner.run('git-$gitCommand', arguments); + return processRunner.run('git-$gitCommand', arguments); }); - processRunner = TestProcessRunner(); mockStdin = MockStdin(); commandRunner = CommandRunner('tester', '') ..addCommand(PublishPluginCommand(packagesDir, @@ -99,18 +92,17 @@ void main() { }); test('refuses to proceed with dirty files', () async { - gitProcessRunner.mockProcessesForExecutable['git-status'] = [ + final Directory pluginDir = + createFakePlugin('foo', packagesDir, examples: []); + + processRunner.mockProcessesForExecutable['git-status'] = [ MockProcess(stdout: '?? ${pluginDir.childFile('tmp').path}\n') ]; Error? commandError; - final List output = await runCapturingPrint( - commandRunner, [ - 'publish-plugin', - '--package', - testPluginName, - '--no-push-tags' - ], errorHandler: (Error e) { + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--package', 'foo', '--no-push-tags'], + errorHandler: (Error e) { commandError = e; }); @@ -128,13 +120,15 @@ void main() { }); test('fails immediately if the remote doesn\'t exist', () async { - gitProcessRunner.mockProcessesForExecutable['git-remote'] = [ + createFakePlugin('foo', packagesDir, examples: []); + + processRunner.mockProcessesForExecutable['git-remote'] = [ MockProcess(exitCode: 1), ]; Error? commandError; - final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--package', testPluginName], + final List output = await runCapturingPrint( + commandRunner, ['publish-plugin', '--package', 'foo'], errorHandler: (Error e) { commandError = e; }); @@ -149,19 +143,18 @@ void main() { }); test("doesn't validate the remote if it's not pushing tags", () async { + createFakePlugin('foo', packagesDir, examples: []); + // Checking the remote should fail. - gitProcessRunner.mockProcessesForExecutable['git-remote'] = [ + processRunner.mockProcessesForExecutable['git-remote'] = [ MockProcess(exitCode: 1), ]; - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; - - final List output = - await runCapturingPrint(commandRunner, [ + final List output = await runCapturingPrint( + commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', '--no-tag-release' ]); @@ -169,17 +162,15 @@ void main() { expect( output, containsAllInOrder([ - contains('Running `pub publish ` in /packages/$testPluginName...'), + contains('Running `pub publish ` in /packages/foo...'), contains('Package published!'), - contains('Released [$testPluginName] successfully.'), + contains('Released [foo] successfully.'), ])); }); test('can publish non-flutter package', () async { const String packageName = 'a_package'; createFakePackage(packageName, packagesDir); - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; final List output = await runCapturingPrint( commandRunner, [ @@ -204,15 +195,21 @@ void main() { group('Publishes package', () { test('while showing all output from pub publish to the user', () async { - processRunner.mockPublishStdout = 'Foo'; - processRunner.mockPublishStderr = 'Bar'; - processRunner.mockPublishCompleteCode = 0; + createFakePlugin('foo', packagesDir, examples: []); - final List output = - await runCapturingPrint(commandRunner, [ + processRunner.mockProcessesForExecutable[flutterCommand] = [ + MockProcess( + stdout: 'Foo', + stderr: 'Bar', + stdoutEncoding: utf8, + stderrEncoding: utf8) // pub publish + ]; + + final List output = await runCapturingPrint( + commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', '--no-tag-release' ]); @@ -226,13 +223,14 @@ void main() { }); test('forwards input from the user to `pub publish`', () async { + createFakePlugin('foo', packagesDir, examples: []); + mockStdin.mockUserInputs.add(utf8.encode('user input')); - processRunner.mockPublishCompleteCode = 0; await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', '--no-tag-release' ]); @@ -242,35 +240,38 @@ void main() { }); test('forwards --pub-publish-flags to pub publish', () async { - processRunner.mockPublishCompleteCode = 0; + final Directory pluginDir = + createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', '--no-tag-release', '--pub-publish-flags', '--dry-run,--server=foo' ]); - expect(processRunner.mockPublishArgs.length, 4); - expect(processRunner.mockPublishArgs[0], 'pub'); - expect(processRunner.mockPublishArgs[1], 'publish'); - expect(processRunner.mockPublishArgs[2], '--dry-run'); - expect(processRunner.mockPublishArgs[3], '--server=foo'); + expect( + processRunner.recordedCalls, + contains(ProcessCall( + flutterCommand, + const ['pub', 'publish', '--dry-run', '--server=foo'], + pluginDir.path))); }); test( '--skip-confirmation flag automatically adds --force to --pub-publish-flags', () async { - processRunner.mockPublishCompleteCode = 0; _createMockCredentialFile(); + final Directory pluginDir = + createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', '--no-tag-release', '--skip-confirmation', @@ -278,22 +279,27 @@ void main() { '--server=foo' ]); - expect(processRunner.mockPublishArgs.length, 4); - expect(processRunner.mockPublishArgs[0], 'pub'); - expect(processRunner.mockPublishArgs[1], 'publish'); - expect(processRunner.mockPublishArgs[2], '--server=foo'); - expect(processRunner.mockPublishArgs[3], '--force'); + expect( + processRunner.recordedCalls, + contains(ProcessCall( + flutterCommand, + const ['pub', 'publish', '--server=foo', '--force'], + pluginDir.path))); }); test('throws if pub publish fails', () async { - processRunner.mockPublishCompleteCode = 128; + createFakePlugin('foo', packagesDir, examples: []); + + processRunner.mockProcessesForExecutable[flutterCommand] = [ + MockProcess(exitCode: 128) // pub publish + ]; Error? commandError; final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', '--no-tag-release', ], errorHandler: (Error e) { @@ -309,18 +315,21 @@ void main() { }); test('publish, dry run', () async { + final Directory pluginDir = + createFakePlugin('foo', packagesDir, examples: []); + final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--dry-run', '--no-push-tags', '--no-tag-release', ]); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); expect( @@ -335,30 +344,31 @@ void main() { group('Tags release', () { test('with the version and name from the pubspec.yaml', () async { - processRunner.mockPublishCompleteCode = 0; - + createFakePlugin('foo', packagesDir, examples: []); await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', ]); - expect( - gitProcessRunner.recordedCalls, - contains(const ProcessCall( - 'git-tag', ['$testPluginName-v0.0.1'], null))); + expect(processRunner.recordedCalls, + contains(const ProcessCall('git-tag', ['foo-v0.0.1'], null))); }); test('only if publishing succeeded', () async { - processRunner.mockPublishCompleteCode = 128; + createFakePlugin('foo', packagesDir, examples: []); + + processRunner.mockProcessesForExecutable[flutterCommand] = [ + MockProcess(exitCode: 128) // pub publish + ]; Error? commandError; final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-push-tags', ], errorHandler: (Error e) { commandError = e; @@ -371,7 +381,7 @@ void main() { contains('Publish foo failed.'), ])); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, isNot(contains( const ProcessCall('git-tag', ['foo-v0.0.1'], null)))); }); @@ -379,7 +389,8 @@ void main() { group('Pushes tags', () { test('requires user confirmation', () async { - processRunner.mockPublishCompleteCode = 0; + createFakePlugin('foo', packagesDir, examples: []); + mockStdin.readLineOutput = 'help'; Error? commandError; @@ -387,7 +398,7 @@ void main() { await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', ], errorHandler: (Error e) { commandError = e; }); @@ -397,61 +408,63 @@ void main() { }); test('to upstream by default', () async { - processRunner.mockPublishCompleteCode = 0; + createFakePlugin('foo', packagesDir, examples: []); + mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', ]); expect( - gitProcessRunner.recordedCalls, - contains(const ProcessCall('git-push', - ['upstream', '$testPluginName-v0.0.1'], null))); + processRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'foo-v0.0.1'], null))); expect( output, containsAllInOrder([ - contains('Released [$testPluginName] successfully.'), + contains('Released [foo] successfully.'), ])); }); test('does not ask for user input if the --skip-confirmation flag is on', () async { - processRunner.mockPublishCompleteCode = 0; _createMockCredentialFile(); + createFakePlugin('foo', packagesDir, examples: []); final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--skip-confirmation', '--package', - testPluginName, + 'foo', ]); expect( - gitProcessRunner.recordedCalls, - contains(const ProcessCall('git-push', - ['upstream', '$testPluginName-v0.0.1'], null))); + processRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'foo-v0.0.1'], null))); expect( output, containsAllInOrder([ - contains('Released [$testPluginName] successfully.'), + contains('Released [foo] successfully.'), ])); }); test('to upstream by default, dry run', () async { - // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. - processRunner.mockPublishCompleteCode = 1; + final Directory pluginDir = + createFakePlugin('foo', packagesDir, examples: []); + mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--package', testPluginName, '--dry-run']); + ['publish-plugin', '--package', 'foo', '--dry-run']); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); expect( @@ -459,57 +472,58 @@ void main() { containsAllInOrder([ '=============== DRY RUN ===============', 'Running `pub publish ` in ${pluginDir.path}...\n', - 'Tagging release $testPluginName-v0.0.1...', + 'Tagging release foo-v0.0.1...', 'Pushing tag to upstream...', 'Done!' ])); }); test('to different remotes based on a flag', () async { - processRunner.mockPublishCompleteCode = 0; + createFakePlugin('foo', packagesDir, examples: []); + mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--remote', 'origin', ]); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( - 'git-push', ['origin', '$testPluginName-v0.0.1'], null))); + 'git-push', ['origin', 'foo-v0.0.1'], null))); expect( output, containsAllInOrder([ - contains('Released [$testPluginName] successfully.'), + contains('Released [foo] successfully.'), ])); }); test('only if tagging and pushing to remotes are both enabled', () async { - processRunner.mockPublishCompleteCode = 0; + createFakePlugin('foo', packagesDir, examples: []); final List output = await runCapturingPrint(commandRunner, [ 'publish-plugin', '--package', - testPluginName, + 'foo', '--no-tag-release', ]); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); expect( output, containsAllInOrder([ - contains('Running `pub publish ` in /packages/$testPluginName...'), + contains('Running `pub publish ` in /packages/foo...'), contains('Package published!'), - contains('Released [$testPluginName] successfully.'), + contains('Released [foo] successfully.'), ])); }); }); @@ -553,13 +567,11 @@ void main() { 'plugin2', packagesDir.childDirectory('plugin2'), ); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') ]; - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, @@ -576,11 +588,11 @@ void main() { 'Done!' ])); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin1-v0.0.1'], null))); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin2-v0.0.1'], null))); }); @@ -634,17 +646,15 @@ void main() { // Git results for plugin0 having been released already, and plugin1 and // plugin2 being new. - gitProcessRunner.mockProcessesForExecutable['git-tag'] = [ + processRunner.mockProcessesForExecutable['git-tag'] = [ MockProcess(stdout: 'plugin0-v0.0.1\n') ]; - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') ]; - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; final List output = await runCapturingPrint(commandRunner, @@ -661,11 +671,11 @@ void main() { 'Done!' ])); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin1-v0.0.1'], null))); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin2-v0.0.1'], null))); }); @@ -706,7 +716,7 @@ void main() { final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') @@ -737,7 +747,7 @@ void main() { 'Done!' ])); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); }); @@ -781,14 +791,12 @@ void main() { 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') ]; - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; final List output2 = await runCapturingPrint(commandRunner, @@ -804,11 +812,11 @@ void main() { 'Done!' ])); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin1-v0.0.2'], null))); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin2-v0.0.2'], null))); }); @@ -854,14 +862,12 @@ void main() { createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); pluginDir2.deleteSync(recursive: true); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') ]; - // Immediately return 0 when running `pub publish`. - processRunner.mockPublishCompleteCode = 0; mockStdin.readLineOutput = 'y'; final List output2 = await runCapturingPrint(commandRunner, @@ -877,7 +883,7 @@ void main() { 'Done!' ])); expect( - gitProcessRunner.recordedCalls, + processRunner.recordedCalls, contains(const ProcessCall( 'git-push', ['upstream', 'plugin1-v0.0.2'], null))); }); @@ -922,12 +928,12 @@ void main() { 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') ]; - gitProcessRunner.mockProcessesForExecutable['git-tag'] = [ + processRunner.mockProcessesForExecutable['git-tag'] = [ MockProcess( stdout: 'plugin1-v0.0.2\n' 'plugin2-v0.0.2\n') @@ -949,7 +955,7 @@ void main() { ])); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); }); @@ -995,7 +1001,7 @@ void main() { 'plugin2', packagesDir.childDirectory('plugin2'), version: '0.0.2'); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' '${pluginDir2.childFile('pubspec.yaml').path}\n') @@ -1020,7 +1026,7 @@ void main() { 'However, the git release tag for this version (plugin2-v0.0.2) is not found.'), ])); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); }); @@ -1032,7 +1038,7 @@ void main() { final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess( stdout: '${pluginDir1.childFile('plugin1.dart').path}\n' '${pluginDir2.childFile('plugin2.dart').path}\n') @@ -1050,7 +1056,7 @@ void main() { 'Done!' ])); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); }); @@ -1081,7 +1087,7 @@ void main() { final Directory flutterPluginTools = createFakePlugin('flutter_plugin_tools', packagesDir); - gitProcessRunner.mockProcessesForExecutable['git-diff'] = [ + processRunner.mockProcessesForExecutable['git-diff'] = [ MockProcess(stdout: flutterPluginTools.childFile('pubspec.yaml').path) ]; @@ -1101,75 +1107,41 @@ void main() { ), isFalse); expect( - gitProcessRunner.recordedCalls + processRunner.recordedCalls .map((ProcessCall call) => call.executable), isNot(contains('git-push'))); }); }); } -class TestProcessRunner extends ProcessRunner { +/// An extension of [RecordingProcessRunner] that stores 'flutter pub publish' +/// calls so that their input streams can be checked in tests. +class TestProcessRunner extends RecordingProcessRunner { // Most recent returned publish process. late MockProcess mockPublishProcess; - final List mockPublishArgs = []; - - String? mockPublishStdout; - String? mockPublishStderr; - int mockPublishCompleteCode = 0; - - @override - Future run( - String executable, - List args, { - Directory? workingDir, - bool exitOnError = false, - bool logOnError = false, - Encoding stdoutEncoding = io.systemEncoding, - Encoding stderrEncoding = io.systemEncoding, - }) async { - final io.ProcessResult result = io.Process.runSync(executable, args, - workingDirectory: workingDir?.path); - if (result.exitCode != 0) { - throw ToolExit(result.exitCode); - } - return result; - } @override Future start(String executable, List args, {Directory? workingDirectory}) async { - /// Never actually publish anything. Start is always and only used for this - /// since it returns something we can route stdin through. - assert(executable == getFlutterCommand(const LocalPlatform()) && + final io.Process process = + await super.start(executable, args, workingDirectory: workingDirectory); + if (executable == getFlutterCommand(const LocalPlatform()) && args.isNotEmpty && args[0] == 'pub' && - args[1] == 'publish'); - mockPublishArgs.addAll(args); - - mockPublishProcess = MockProcess( - exitCode: mockPublishCompleteCode, - stdout: mockPublishStdout, - stderr: mockPublishStderr, - stdoutEncoding: utf8, - stderrEncoding: utf8, - ); - return mockPublishProcess; + args[1] == 'publish') { + mockPublishProcess = process as MockProcess; + } + return process; } } class MockStdin extends Mock implements io.Stdin { List> mockUserInputs = >[]; - late StreamController> _controller; + final StreamController> _controller = StreamController>(); String? readLineOutput; @override Stream transform(StreamTransformer, S> streamTransformer) { - // In the test context, only one `PublishPluginCommand` object is created for a single test case. - // However, sometimes, we need to run multiple commands in a single test case. - // In such situation, this `MockStdin`'s StreamController might be listened to more than once, which is not allowed. - // - // Create a new controller every time so this Stdin could be listened to multiple times. - _controller = StreamController>(); mockUserInputs.forEach(_addUserInputsToSteam); return _controller.stream.transform(streamTransformer); } @@ -1189,12 +1161,3 @@ class MockStdin extends Mock implements io.Stdin { void _addUserInputsToSteam(List input) => _controller.add(input); } - -class MockProcessResult extends Mock implements io.ProcessResult { - MockProcessResult({int exitCode = 0}) : _exitCode = exitCode; - - final int _exitCode; - - @override - int get exitCode => _exitCode; -} diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 05aebe82fd79..7bd94fb66e22 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -17,6 +17,8 @@ import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; import 'package:quiver/collection.dart'; +import 'mocks.dart'; + /// Returns the exe name that command will use when running Flutter on /// [platform]. String getFlutterCommand(Platform platform) => @@ -320,7 +322,8 @@ class RecordingProcessRunner extends ProcessRunner { Future start(String executable, List args, {Directory? workingDirectory}) async { recordedCalls.add(ProcessCall(executable, args, workingDirectory?.path)); - return Future.value(_getProcessToReturn(executable)); + return Future.value( + _getProcessToReturn(executable) ?? MockProcess()); } io.Process? _getProcessToReturn(String executable) { From a011b309b77c6b13c844c1dc6ed081327214573a Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 26 Aug 2021 15:07:33 -0400 Subject: [PATCH 45/57] [flutter_plugin_tool] Add support for building UWP plugins (#4047) This allows building UWP plugin examples with `build-examples --winuwp`. As with previous pre-stable-template desktop support, this avoids the issue of unstable app templates by running `flutter create` on the fly before trying to build, so a template that will bitrot doesn't need to be checked in. Also adds no-op "support" for `drive-examples --winuwp`, with warnings about it not doing anything. This is to handle the fact that the LUCI recipe is shared between Win32 and UWP, and didn't conditionalize `drive`. Rather than change that, then change it back later, this just adds the no-op support now (since changing the tooling is much easier than changing LUCI recipes currently). This required some supporting tool changes: - Adds the ability to check for the new platform variants in a pubspec - Adds the ability to write test pubspecs that include variants, for testing Part of https://github.com/flutter/flutter/issues/82817 --- script/tool/CHANGELOG.md | 1 + .../tool/lib/src/build_examples_command.dart | 64 ++++++-- script/tool/lib/src/common/core.dart | 37 +++-- script/tool/lib/src/common/plugin_utils.dart | 39 ++++- .../tool/lib/src/drive_examples_command.dart | 26 ++- .../test/build_examples_command_test.dart | 129 ++++++++++++--- .../tool/test/common/plugin_utils_test.dart | 148 +++++++++++++----- .../create_all_plugins_app_command_test.dart | 2 +- .../test/drive_examples_command_test.dart | 112 ++++++++----- .../tool/test/lint_android_command_test.dart | 16 +- .../tool/test/native_test_command_test.dart | 124 +++++++-------- script/tool/test/test_command_test.dart | 4 +- script/tool/test/util.dart | 89 ++++++++--- .../tool/test/xcode_analyze_command_test.dart | 42 ++--- 14 files changed, 589 insertions(+), 244 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 1881d1bb6689..a32fb0016cb3 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -4,6 +4,7 @@ - Added a new `android-lint` command to lint Android plugin native code. - Pubspec validation now checks for `implements` in implementation packages. - Pubspec valitation now checks the full relative path of `repository` entries. +- `build-examples` now supports UWP plugins via a `--winuwp` flag. ## 0.5.0 diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index ac5e84b7c3c7..e441f61d5644 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -16,7 +16,16 @@ import 'common/repository_package.dart'; /// Key for APK. const String _platformFlagApk = 'apk'; -const int _exitNoPlatformFlags = 2; +const int _exitNoPlatformFlags = 3; + +// Flutter build types. These are the values passed to `flutter build `. +const String _flutterBuildTypeAndroid = 'apk'; +const String _flutterBuildTypeIos = 'ios'; +const String _flutterBuildTypeLinux = 'linux'; +const String _flutterBuildTypeMacOS = 'macos'; +const String _flutterBuildTypeWeb = 'web'; +const String _flutterBuildTypeWin32 = 'windows'; +const String _flutterBuildTypeWinUwp = 'winuwp'; /// A command to build the example applications for packages. class BuildExamplesCommand extends PackageLoopingCommand { @@ -30,6 +39,7 @@ class BuildExamplesCommand extends PackageLoopingCommand { argParser.addFlag(kPlatformMacos); argParser.addFlag(kPlatformWeb); argParser.addFlag(kPlatformWindows); + argParser.addFlag(kPlatformWinUwp); argParser.addFlag(kPlatformIos); argParser.addFlag(_platformFlagApk); argParser.addOption( @@ -46,33 +56,40 @@ class BuildExamplesCommand extends PackageLoopingCommand { _platformFlagApk: const _PlatformDetails( 'Android', pluginPlatform: kPlatformAndroid, - flutterBuildType: 'apk', + flutterBuildType: _flutterBuildTypeAndroid, ), kPlatformIos: const _PlatformDetails( 'iOS', pluginPlatform: kPlatformIos, - flutterBuildType: 'ios', + flutterBuildType: _flutterBuildTypeIos, extraBuildFlags: ['--no-codesign'], ), kPlatformLinux: const _PlatformDetails( 'Linux', pluginPlatform: kPlatformLinux, - flutterBuildType: 'linux', + flutterBuildType: _flutterBuildTypeLinux, ), kPlatformMacos: const _PlatformDetails( 'macOS', pluginPlatform: kPlatformMacos, - flutterBuildType: 'macos', + flutterBuildType: _flutterBuildTypeMacOS, ), kPlatformWeb: const _PlatformDetails( 'web', pluginPlatform: kPlatformWeb, - flutterBuildType: 'web', + flutterBuildType: _flutterBuildTypeWeb, ), kPlatformWindows: const _PlatformDetails( - 'Windows', + 'Win32', + pluginPlatform: kPlatformWindows, + pluginPlatformVariant: platformVariantWin32, + flutterBuildType: _flutterBuildTypeWin32, + ), + kPlatformWinUwp: const _PlatformDetails( + 'UWP', pluginPlatform: kPlatformWindows, - flutterBuildType: 'windows', + pluginPlatformVariant: platformVariantWinUwp, + flutterBuildType: _flutterBuildTypeWinUwp, ), }; @@ -107,7 +124,8 @@ class BuildExamplesCommand extends PackageLoopingCommand { final Set<_PlatformDetails> buildPlatforms = <_PlatformDetails>{}; final Set<_PlatformDetails> unsupportedPlatforms = <_PlatformDetails>{}; for (final _PlatformDetails platform in requestedPlatforms) { - if (pluginSupportsPlatform(platform.pluginPlatform, package)) { + if (pluginSupportsPlatform(platform.pluginPlatform, package, + variant: platform.pluginPlatformVariant)) { buildPlatforms.add(platform); } else { unsupportedPlatforms.add(platform); @@ -156,6 +174,22 @@ class BuildExamplesCommand extends PackageLoopingCommand { }) async { final String enableExperiment = getStringArg(kEnableExperiment); + // The UWP template is not yet stable, so the UWP directory + // needs to be created on the fly with 'flutter create .' + Directory? temporaryPlatformDirectory; + if (flutterBuildType == _flutterBuildTypeWinUwp) { + final Directory uwpDirectory = example.directory.childDirectory('winuwp'); + if (!uwpDirectory.existsSync()) { + print('Creating temporary winuwp folder'); + final int exitCode = await processRunner.runAndStream(flutterCommand, + ['create', '--platforms=$kPlatformWinUwp', '.'], + workingDir: example.directory); + if (exitCode == 0) { + temporaryPlatformDirectory = uwpDirectory; + } + } + } + final int exitCode = await processRunner.runAndStream( flutterCommand, [ @@ -167,6 +201,13 @@ class BuildExamplesCommand extends PackageLoopingCommand { ], workingDir: example.directory, ); + + if (temporaryPlatformDirectory != null && + temporaryPlatformDirectory.existsSync()) { + print('Cleaning up ${temporaryPlatformDirectory.path}'); + temporaryPlatformDirectory.deleteSync(recursive: true); + } + return exitCode == 0; } } @@ -176,6 +217,7 @@ class _PlatformDetails { const _PlatformDetails( this.label, { required this.pluginPlatform, + this.pluginPlatformVariant, required this.flutterBuildType, this.extraBuildFlags = const [], }); @@ -186,6 +228,10 @@ class _PlatformDetails { /// The key in a pubspec's platform: entry. final String pluginPlatform; + /// The supportedVariants key under a plugin's [pluginPlatform] entry, if + /// applicable. + final String? pluginPlatformVariant; + /// The `flutter build` build type. final String flutterBuildType; diff --git a/script/tool/lib/src/common/core.dart b/script/tool/lib/src/common/core.dart index b2be8f56d172..53778eccb87f 100644 --- a/script/tool/lib/src/common/core.dart +++ b/script/tool/lib/src/common/core.dart @@ -10,24 +10,43 @@ import 'package:yaml/yaml.dart'; /// print destination. typedef Print = void Function(Object? object); -/// Key for windows platform. -const String kPlatformWindows = 'windows'; +/// Key for APK (Android) platform. +const String kPlatformAndroid = 'android'; -/// Key for macos platform. -const String kPlatformMacos = 'macos'; +/// Key for IPA (iOS) platform. +const String kPlatformIos = 'ios'; /// Key for linux platform. const String kPlatformLinux = 'linux'; -/// Key for IPA (iOS) platform. -const String kPlatformIos = 'ios'; - -/// Key for APK (Android) platform. -const String kPlatformAndroid = 'android'; +/// Key for macos platform. +const String kPlatformMacos = 'macos'; /// Key for Web platform. const String kPlatformWeb = 'web'; +/// Key for windows platform. +/// +/// Note that this corresponds to the Win32 variant for flutter commands like +/// `build` and `run`, but is a general platform containing all Windows +/// variants for purposes of the `platform` section of a plugin pubspec). +const String kPlatformWindows = 'windows'; + +/// Key for WinUWP platform. +/// +/// Note that UWP is a platform for the purposes of flutter commands like +/// `build` and `run`, but a variant of the `windows` platform for the purposes +/// of plugin pubspecs). +const String kPlatformWinUwp = 'winuwp'; + +/// Key for Win32 variant of the Windows platform. +const String platformVariantWin32 = 'win32'; + +/// Key for UWP variant of the Windows platform. +/// +/// See the note on [kPlatformWinUwp]. +const String platformVariantWinUwp = 'uwp'; + /// Key for enable experiment. const String kEnableExperiment = 'enable-experiment'; diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart index d9c42e220c0b..49da67655e91 100644 --- a/script/tool/lib/src/common/plugin_utils.dart +++ b/script/tool/lib/src/common/plugin_utils.dart @@ -28,8 +28,12 @@ enum PlatformSupport { /// /// If [requiredMode] is provided, the plugin must have the given type of /// implementation in order to return true. -bool pluginSupportsPlatform(String platform, RepositoryPackage package, - {PlatformSupport? requiredMode}) { +bool pluginSupportsPlatform( + String platform, + RepositoryPackage package, { + PlatformSupport? requiredMode, + String? variant, +}) { assert(platform == kPlatformIos || platform == kPlatformAndroid || platform == kPlatformWeb || @@ -65,9 +69,34 @@ bool pluginSupportsPlatform(String platform, RepositoryPackage package, } // If the platform entry is present, then it supports the platform. Check // for required mode if specified. - final bool federated = platformEntry.containsKey('default_package'); - return requiredMode == null || - federated == (requiredMode == PlatformSupport.federated); + if (requiredMode != null) { + final bool federated = platformEntry.containsKey('default_package'); + if (federated != (requiredMode == PlatformSupport.federated)) { + return false; + } + } + + // If a variant is specified, check for that variant. + if (variant != null) { + const String variantsKey = 'supportedVariants'; + if (platformEntry.containsKey(variantsKey)) { + if (!(platformEntry['supportedVariants']! as YamlList) + .contains(variant)) { + return false; + } + } else { + // Platforms with variants have a default variant when unspecified for + // backward compatibility. Must match the flutter tool logic. + const Map defaultVariants = { + kPlatformWindows: platformVariantWin32, + }; + if (variant != defaultVariants[platform]) { + return false; + } + } + } + + return true; } on FileSystemException { return false; } on YamlException { diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 3605dcce1f22..b3434b0659f3 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -36,7 +36,10 @@ class DriveExamplesCommand extends PackageLoopingCommand { argParser.addFlag(kPlatformWeb, help: 'Runs the web implementation of the examples'); argParser.addFlag(kPlatformWindows, - help: 'Runs the Windows implementation of the examples'); + help: 'Runs the Windows (Win32) implementation of the examples'); + argParser.addFlag(kPlatformWinUwp, + help: + 'Runs the UWP implementation of the examples [currently a no-op]'); argParser.addOption( kEnableExperiment, defaultsTo: '', @@ -67,6 +70,7 @@ class DriveExamplesCommand extends PackageLoopingCommand { kPlatformMacos, kPlatformWeb, kPlatformWindows, + kPlatformWinUwp, ]; final int platformCount = platformSwitches .where((String platform) => getBoolArg(platform)) @@ -81,6 +85,10 @@ class DriveExamplesCommand extends PackageLoopingCommand { throw ToolExit(_exitNoPlatformFlags); } + if (getBoolArg(kPlatformWinUwp)) { + logWarning('Driving UWP applications is not yet supported'); + } + String? androidDevice; if (getBoolArg(kPlatformAndroid)) { final List devices = await _getDevicesForPlatform('android'); @@ -116,6 +124,10 @@ class DriveExamplesCommand extends PackageLoopingCommand { ], if (getBoolArg(kPlatformWindows)) kPlatformWindows: ['-d', 'windows'], + // TODO(stuartmorgan): Check these flags once drive supports UWP: + // https://github.com/flutter/flutter/issues/82821 + if (getBoolArg(kPlatformWinUwp)) + kPlatformWinUwp: ['-d', 'winuwp'], }; } @@ -132,7 +144,17 @@ class DriveExamplesCommand extends PackageLoopingCommand { final List deviceFlags = []; for (final MapEntry> entry in _targetDeviceFlags.entries) { - if (pluginSupportsPlatform(entry.key, package)) { + final String platform = entry.key; + String? variant; + if (platform == kPlatformWindows) { + variant = platformVariantWin32; + } else if (platform == kPlatformWinUwp) { + variant = platformVariantWinUwp; + // TODO(stuartmorgan): Remove this once drive supports UWP. + // https://github.com/flutter/flutter/issues/82821 + return PackageResult.skip('Drive does not yet support UWP'); + } + if (pluginSupportsPlatform(platform, package, variant: variant)) { deviceFlags.addAll(entry.value); } else { print('Skipping unsupported platform ${entry.key}...'); diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index 9c7291c31ddb..a17107c18e27 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -56,8 +56,8 @@ void main() { test('fails if building fails', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.inline + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }); processRunner @@ -106,8 +106,8 @@ void main() { test('building for iOS', () async { mockPlatform.isMacOS = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.inline + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -163,8 +163,8 @@ void main() { test('building for Linux', () async { mockPlatform.isLinux = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformLinux: PlatformSupport.inline, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -212,8 +212,8 @@ void main() { test('building for macOS', () async { mockPlatform.isMacOS = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -258,8 +258,8 @@ void main() { test('building for web', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformWeb: PlatformSupport.inline, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -284,7 +284,7 @@ void main() { }); test( - 'building for Windows when plugin is not set up for Windows results in no-op', + 'building for win32 when plugin is not set up for Windows results in no-op', () async { mockPlatform.isWindows = true; createFakePlugin('plugin', packagesDir); @@ -296,7 +296,7 @@ void main() { output, containsAllInOrder([ contains('Running for plugin'), - contains('Windows is not supported by this plugin'), + contains('Win32 is not supported by this plugin'), ]), ); @@ -305,11 +305,11 @@ void main() { expect(processRunner.recordedCalls, orderedEquals([])); }); - test('building for Windows', () async { + test('building for win32', () async { mockPlatform.isWindows = true; final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformWindows: PlatformSupport.inline + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -321,7 +321,7 @@ void main() { expect( output, containsAllInOrder([ - '\nBUILDING plugin/example for Windows', + '\nBUILDING plugin/example for Win32 (windows)', ]), ); @@ -335,6 +335,91 @@ void main() { ])); }); + test('building for UWP when plugin does not support UWP is a no-op', + () async { + createFakePlugin('plugin', packagesDir); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--winuwp']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('UWP is not supported by this plugin'), + ]), + ); + + print(processRunner.recordedCalls); + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('building for UWP', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', + ], platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.federated, + variants: [platformVariantWinUwp]), + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--winuwp']); + + expect( + output, + containsAllInOrder([ + contains('BUILDING plugin/example for UWP (winuwp)'), + ]), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['build', 'winuwp'], pluginExampleDirectory.path), + ])); + }); + + test('building for UWP creates a folder if necessary', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', + ], platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.federated, + variants: [platformVariantWinUwp]), + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--winuwp']); + + expect( + output, + contains('Creating temporary winuwp folder'), + ); + + print(processRunner.recordedCalls); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const ['create', '--platforms=winuwp', '.'], + pluginExampleDirectory.path), + ProcessCall(getFlutterCommand(mockPlatform), + const ['build', 'winuwp'], pluginExampleDirectory.path), + ])); + }); + test( 'building for Android when plugin is not set up for Android results in no-op', () async { @@ -358,8 +443,8 @@ void main() { test('building for Android', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -387,8 +472,8 @@ void main() { test('enable-experiment flag for Android', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -409,8 +494,8 @@ void main() { test('enable-experiment flag for ios', () async { final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.inline + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = diff --git a/script/tool/test/common/plugin_utils_test.dart b/script/tool/test/common/plugin_utils_test.dart index 7f1ba2add00a..2e08f725eb4b 100644 --- a/script/tool/test/common/plugin_utils_test.dart +++ b/script/tool/test/common/plugin_utils_test.dart @@ -36,13 +36,13 @@ void main() { test('all platforms', () async { final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, - kPlatformLinux: PlatformSupport.inline, - kPlatformMacos: PlatformSupport.inline, - kPlatformWeb: PlatformSupport.inline, - kPlatformWindows: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), })); expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isTrue); @@ -55,14 +55,12 @@ void main() { test('some platforms', () async { final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformLinux: PlatformSupport.inline, - kPlatformWeb: PlatformSupport.inline, - }, - )); + 'plugin', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + })); expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isTrue); expect(pluginSupportsPlatform(kPlatformIos, plugin), isFalse); @@ -74,17 +72,15 @@ void main() { test('inline plugins are only detected as inline', () async { final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, - kPlatformLinux: PlatformSupport.inline, - kPlatformMacos: PlatformSupport.inline, - kPlatformWeb: PlatformSupport.inline, - kPlatformWindows: PlatformSupport.inline, - }, - )); + 'plugin', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + })); expect( pluginSupportsPlatform(kPlatformAndroid, plugin, @@ -137,19 +133,16 @@ void main() { }); test('federated plugins are only detected as federated', () async { - const String pluginName = 'plugin'; final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( - pluginName, - packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.federated, - kPlatformIos: PlatformSupport.federated, - kPlatformLinux: PlatformSupport.federated, - kPlatformMacos: PlatformSupport.federated, - kPlatformWeb: PlatformSupport.federated, - kPlatformWindows: PlatformSupport.federated, - }, - )); + 'plugin', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.federated), + kPlatformIos: const PlatformDetails(PlatformSupport.federated), + kPlatformLinux: const PlatformDetails(PlatformSupport.federated), + kPlatformMacos: const PlatformDetails(PlatformSupport.federated), + kPlatformWeb: const PlatformDetails(PlatformSupport.federated), + kPlatformWindows: const PlatformDetails(PlatformSupport.federated), + })); expect( pluginSupportsPlatform(kPlatformAndroid, plugin, @@ -200,5 +193,84 @@ void main() { requiredMode: PlatformSupport.inline), isFalse); }); + + test('windows without variants is only win32', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }, + )); + + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWin32), + isTrue); + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWinUwp), + isFalse); + }); + + test('windows with both variants matches win32 and winuwp', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails( + PlatformSupport.federated, + variants: [platformVariantWin32, platformVariantWinUwp], + ), + })); + + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWin32), + isTrue); + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWinUwp), + isTrue); + }); + + test('win32 plugin is only win32', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails( + PlatformSupport.federated, + variants: [platformVariantWin32], + ), + })); + + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWin32), + isTrue); + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWinUwp), + isFalse); + }); + + test('winup plugin is only winuwp', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.federated, + variants: [platformVariantWinUwp]), + }, + )); + + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWin32), + isFalse); + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWinUwp), + isTrue); + }); }); } diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart index 4439d13c3625..0066cc53f61a 100644 --- a/script/tool/test/create_all_plugins_app_command_test.dart +++ b/script/tool/test/create_all_plugins_app_command_test.dart @@ -21,7 +21,7 @@ void main() { setUp(() { // Since the core of this command is a call to 'flutter create', the test // has to use the real filesystem. Put everything possible in a unique - // temporary to minimize affect on the host system. + // temporary to minimize effect on the host system. fileSystem = const LocalFileSystem(); testRoot = fileSystem.systemTempDirectory.createTempSync(); packagesDir = testRoot.childDirectory('packages'); diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index bbf865d3edf2..85d2326d0689 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -127,8 +127,8 @@ void main() { 'example/test_driver/integration_test.dart', 'example/integration_test/foo_test.dart', ], - platformSupport: { - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -192,9 +192,9 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -242,9 +242,9 @@ void main() { extraFiles: [ 'example/test_driver/plugin_test.dart', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -275,9 +275,9 @@ void main() { extraFiles: [ 'example/lib/main.dart', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -311,9 +311,9 @@ void main() { 'example/integration_test/foo_test.dart', 'example/integration_test/ignore_me.dart', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -397,8 +397,8 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformLinux: PlatformSupport.inline, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), }, ); @@ -470,8 +470,8 @@ void main() { 'example/test_driver/plugin.dart', 'example/macos/macos.swift', ], - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -541,8 +541,8 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformWeb: PlatformSupport.inline, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -615,8 +615,8 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformWindows: PlatformSupport.inline + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), }, ); @@ -654,6 +654,40 @@ void main() { ])); }); + test('driving UWP is a no-op', () async { + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline, + variants: [platformVariantWinUwp]), + }, + ); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--winuwp', + ]); + + expect( + output, + containsAllInOrder([ + contains('Driving UWP applications is not yet supported'), + contains('Running for plugin'), + contains('SKIPPING: Drive does not yet support UWP'), + contains('No issues found!'), + ]), + ); + + // Output should be empty since running drive-examples --windows on a + // non-Windows plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + test('driving on an Android plugin', () async { final Directory pluginDirectory = createFakePlugin( 'plugin', @@ -662,8 +696,8 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), }, ); @@ -712,8 +746,8 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -745,8 +779,8 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -800,9 +834,9 @@ void main() { 'example/test_driver/plugin_test.dart', 'example/test_driver/plugin.dart', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -842,8 +876,8 @@ void main() { 'plugin', packagesDir, examples: [], - platformSupport: { - kPlatformWeb: PlatformSupport.inline, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -874,8 +908,8 @@ void main() { 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', ], - platformSupport: { - kPlatformWeb: PlatformSupport.inline, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -906,8 +940,8 @@ void main() { extraFiles: [ 'example/test_driver/integration_test.dart', ], - platformSupport: { - kPlatformWeb: PlatformSupport.inline, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), }, ); @@ -942,8 +976,8 @@ void main() { 'example/integration_test/bar_test.dart', 'example/integration_test/foo_test.dart', ], - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }, ); diff --git a/script/tool/test/lint_android_command_test.dart b/script/tool/test/lint_android_command_test.dart index d08058468636..5670a64f30d8 100644 --- a/script/tool/test/lint_android_command_test.dart +++ b/script/tool/test/lint_android_command_test.dart @@ -43,8 +43,8 @@ void main() { final Directory pluginDir = createFakePlugin('plugin1', packagesDir, extraFiles: [ 'example/android/gradlew', - ], platformSupport: { - kPlatformAndroid: PlatformSupport.inline + ], platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }); final Directory androidDir = @@ -74,8 +74,8 @@ void main() { test('fails if gradlew is missing', () async { createFakePlugin('plugin1', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }); Error? commandError; @@ -96,8 +96,8 @@ void main() { test('fails if linting finds issues', () async { createFakePlugin('plugin1', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }); processRunner.mockProcessesForExecutable['gradlew'] = [ @@ -138,8 +138,8 @@ void main() { test('skips non-inline plugins', () async { createFakePlugin('plugin1', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.federated + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.federated) }); final List output = diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index f367dc80182f..7b2a3d3ba39c 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -115,8 +115,8 @@ void main() { test('reports skips with no tests', () async { final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -154,8 +154,8 @@ void main() { group('iOS', () { test('skip if iOS is not supported', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final List output = await runCapturingPrint(runner, @@ -171,8 +171,8 @@ void main() { test('skip if iOS is implemented in a federated package', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.federated + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.federated) }); final List output = await runCapturingPrint(runner, @@ -188,8 +188,8 @@ void main() { test('running with correct destination', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline + 'plugin', packagesDir, platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) }); final Directory pluginExampleDirectory = @@ -234,8 +234,8 @@ void main() { test('Not specifying --ios-destination assigns an available simulator', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline + 'plugin', packagesDir, platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); @@ -298,8 +298,8 @@ void main() { test('skip if macOS is implemented in a federated package', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.federated, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.federated), }); final List output = @@ -317,8 +317,8 @@ void main() { test('runs for macOS plugin', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -360,8 +360,8 @@ void main() { final Directory plugin = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -390,8 +390,8 @@ void main() { final Directory plugin = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -420,8 +420,8 @@ void main() { final Directory plugin = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -455,8 +455,8 @@ void main() { createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -480,8 +480,8 @@ void main() { final Directory plugin = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'android/src/test/example_test.java', @@ -519,8 +519,8 @@ void main() { final Directory plugin = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'android/src/test/example_test.java', @@ -554,8 +554,8 @@ void main() { final Directory plugin = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'android/src/test/example_test.java', @@ -586,8 +586,8 @@ void main() { createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/app/src/test/example_test.java', @@ -618,8 +618,8 @@ void main() { createFakePlugin( 'plugin1', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -630,8 +630,8 @@ void main() { createFakePlugin( 'plugin2', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'android/src/test/example_test.java', @@ -657,8 +657,8 @@ void main() { final Directory pluginDir = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, extraFiles: [ 'example/android/gradlew', @@ -716,8 +716,8 @@ void main() { createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) }, ); @@ -739,8 +739,8 @@ void main() { group('iOS/macOS', () { test('fails if xcrun fails', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); processRunner.mockProcessesForExecutable['xcrun'] = [ @@ -767,8 +767,8 @@ void main() { test('honors unit-only', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -832,8 +832,8 @@ void main() { test('honors integration-only', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -897,8 +897,8 @@ void main() { test('skips when the requested target is not present', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -950,8 +950,8 @@ void main() { test('fails if unable to check for requested target', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -1007,10 +1007,10 @@ void main() { 'example/android/gradlew', 'android/src/test/example_test.java', ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }, ); @@ -1077,8 +1077,8 @@ void main() { test('runs only macOS for a macOS plugin', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -1121,8 +1121,8 @@ void main() { test('runs only iOS for a iOS plugin', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline + 'plugin', packagesDir, platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) }); final Directory pluginExampleDirectory = @@ -1193,9 +1193,9 @@ void main() { final Directory pluginDir = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, extraFiles: [ 'example/android/gradlew', @@ -1244,9 +1244,9 @@ void main() { final Directory pluginDir = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), }, extraFiles: [ 'example/android/gradlew', diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index 3b350f7d88ae..f8aca38d3478 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -180,8 +180,8 @@ void main() { 'plugin', packagesDir, extraFiles: ['test/empty_test.dart'], - platformSupport: { - kPlatformWeb: PlatformSupport.inline, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), }, ); diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 7bd94fb66e22..9b92a5d94ac8 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -41,6 +41,21 @@ Directory createPackagesDirectory( return packagesDir; } +/// Details for platform support in a plugin. +@immutable +class PlatformDetails { + const PlatformDetails( + this.type, { + this.variants = const [], + }); + + /// The type of support for the platform. + final PlatformSupport type; + + /// Any 'supportVariants' to list in the pubspec. + final List variants; +} + /// Creates a plugin package with the given [name] in [packagesDirectory]. /// /// [platformSupport] is a map of platform string to the support details for @@ -54,8 +69,8 @@ Directory createFakePlugin( Directory parentDirectory, { List examples = const ['example'], List extraFiles = const [], - Map platformSupport = - const {}, + Map platformSupport = + const {}, String? version = '0.0.1', }) { final Directory pluginDirectory = createFakePackage(name, parentDirectory, @@ -143,8 +158,8 @@ void createFakePubspec( String name = 'fake_package', bool isFlutter = true, bool isPlugin = false, - Map platformSupport = - const {}, + Map platformSupport = + const {}, String publishTo = 'http://no_pub_server.com', String? version, }) { @@ -160,12 +175,11 @@ flutter: plugin: platforms: '''; - for (final MapEntry platform + for (final MapEntry platform in platformSupport.entries) { yaml += _pluginPlatformSection(platform.key, platform.value, name); } } - yaml += ''' dependencies: flutter: @@ -186,50 +200,73 @@ publish_to: $publishTo # Hardcoded safeguard to prevent this from somehow being } String _pluginPlatformSection( - String platform, PlatformSupport type, String packageName) { - if (type == PlatformSupport.federated) { - return ''' + String platform, PlatformDetails support, String packageName) { + String entry = ''; + // Build the main plugin entry. + if (support.type == PlatformSupport.federated) { + entry = ''' $platform: default_package: ${packageName}_$platform '''; - } - switch (platform) { - case kPlatformAndroid: - return ''' + } else { + switch (platform) { + case kPlatformAndroid: + entry = ''' android: package: io.flutter.plugins.fake pluginClass: FakePlugin '''; - case kPlatformIos: - return ''' + break; + case kPlatformIos: + entry = ''' ios: pluginClass: FLTFakePlugin '''; - case kPlatformLinux: - return ''' + break; + case kPlatformLinux: + entry = ''' linux: pluginClass: FakePlugin '''; - case kPlatformMacos: - return ''' + break; + case kPlatformMacos: + entry = ''' macos: pluginClass: FakePlugin '''; - case kPlatformWeb: - return ''' + break; + case kPlatformWeb: + entry = ''' web: pluginClass: FakePlugin fileName: ${packageName}_web.dart '''; - case kPlatformWindows: - return ''' + break; + case kPlatformWindows: + entry = ''' windows: pluginClass: FakePlugin '''; - default: - assert(false); - return ''; + break; + default: + assert(false, 'Unrecognized platform: $platform'); + break; + } } + + // Add any variants. + if (support.variants.isNotEmpty) { + entry += ''' + supportedVariants: +'''; + for (final String variant in support.variants) { + entry += ''' + - $variant +'''; + } + } + + return entry; } typedef _ErrorHandler = void Function(Error error); diff --git a/script/tool/test/xcode_analyze_command_test.dart b/script/tool/test/xcode_analyze_command_test.dart index 790a526a8ae0..10008ae33a11 100644 --- a/script/tool/test/xcode_analyze_command_test.dart +++ b/script/tool/test/xcode_analyze_command_test.dart @@ -57,8 +57,8 @@ void main() { group('iOS', () { test('skip if iOS is not supported', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final List output = @@ -70,8 +70,8 @@ void main() { test('skip if iOS is implemented in a federated package', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.federated + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.federated) }); final List output = @@ -83,8 +83,8 @@ void main() { test('runs for iOS plugin', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline + 'plugin', packagesDir, platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) }); final Directory pluginExampleDirectory = @@ -126,8 +126,8 @@ void main() { test('fails if xcrun fails', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.inline + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) }); processRunner.mockProcessesForExecutable['xcrun'] = [ @@ -172,8 +172,8 @@ void main() { test('skip if macOS is implemented in a federated package', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.federated, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.federated), }); final List output = await runCapturingPrint( @@ -186,8 +186,8 @@ void main() { test('runs for macOS plugin', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -223,8 +223,8 @@ void main() { test('fails if xcrun fails', () async { createFakePlugin('plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); processRunner.mockProcessesForExecutable['xcrun'] = [ @@ -253,9 +253,9 @@ void main() { test('runs both iOS and macOS when supported', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformIos: PlatformSupport.inline, - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -313,8 +313,8 @@ void main() { test('runs only macOS for a macOS plugin', () async { final Directory pluginDirectory1 = createFakePlugin( 'plugin', packagesDir, - platformSupport: { - kPlatformMacos: PlatformSupport.inline, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), }); final Directory pluginExampleDirectory = @@ -354,8 +354,8 @@ void main() { test('runs only iOS for a iOS plugin', () async { final Directory pluginDirectory = createFakePlugin( - 'plugin', packagesDir, platformSupport: { - kPlatformIos: PlatformSupport.inline + 'plugin', packagesDir, platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) }); final Directory pluginExampleDirectory = From 8e8954731570a8088c7398884908c67b38b35a8d Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 26 Aug 2021 15:56:59 -0400 Subject: [PATCH 46/57] Disable some flaky tests (#4274) These tests are failing frequently, and interfering with the tree staying open. Tracking issues: https://github.com/flutter/flutter/issues/88837 https://github.com/flutter/flutter/issues/86915 https://github.com/flutter/flutter/issues/86757 --- .../plugins/androidalarmmanager/BackgroundExecutionTest.java | 2 ++ .../example/integration_test/video_player_test.dart | 5 ++++- .../example/integration_test/webview_flutter_test.dart | 4 +++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/BackgroundExecutionTest.java b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/BackgroundExecutionTest.java index d6927232fb80..a841a239d3af 100644 --- a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/BackgroundExecutionTest.java +++ b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/BackgroundExecutionTest.java @@ -17,6 +17,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.rule.ActivityTestRule; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,6 +40,7 @@ public void setUp() throws Exception { ActivityScenario.launch(DriverExtensionActivity.class); } + @Ignore("Disabled due to flake: https://github.com/flutter/flutter/issues/88837") @Test public void startBackgroundIsolate() throws Exception { diff --git a/packages/video_player/video_player/example/integration_test/video_player_test.dart b/packages/video_player/video_player/example/integration_test/video_player_test.dart index 6821b26e0409..373538ad365e 100644 --- a/packages/video_player/video_player/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player/example/integration_test/video_player_test.dart @@ -220,6 +220,9 @@ void main() { await tester.pumpAndSettle(); expect(_controller.value.isPlaying, true); - }, skip: kIsWeb); // Web does not support local assets. + }, + skip: kIsWeb || // Web does not support local assets. + // Extremely flaky on iOS: https://github.com/flutter/flutter/issues/86915 + defaultTargetPlatform == TargetPlatform.iOS); }); } diff --git a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart index f3eeee156421..0e128caa8f32 100644 --- a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -1330,7 +1330,9 @@ void main() { await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, 'https://flutter.dev/'); - }); + }, + // Flaky on Android: https://github.com/flutter/flutter/issues/86757 + skip: Platform.isAndroid); // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets( From 46dd6093793aa19af45244cd7fc16cc5b0ee2082 Mon Sep 17 00:00:00 2001 From: Majid Hajian Date: Thu, 26 Aug 2021 23:21:07 +0200 Subject: [PATCH 47/57] [camera] Replace device info with new device_info_plus (#4265) --- packages/camera/camera/CHANGELOG.md | 4 ++++ packages/camera/camera/README.md | 2 +- packages/camera/camera/pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 73cce2c539c1..6d38fa204540 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.1+1 + +* Replace `device_info` reference with `device_info_plus` in the [README.md](README.md) + ## 0.9.1 * Added `lensAperture`, `sensorExposureTime` and `sensorSensitivity` properties to the `CameraImage` dto. diff --git a/packages/camera/camera/README.md b/packages/camera/camera/README.md index fb6144face9b..c66ed67af6cb 100644 --- a/packages/camera/camera/README.md +++ b/packages/camera/camera/README.md @@ -19,7 +19,7 @@ First, add `camera` as a [dependency in your pubspec.yaml file](https://flutter. ### iOS -iOS 10.0 of higher is needed to use the camera plugin. If compiling for any version lower than 10.0 make sure to check the iOS version before using the camera plugin. For example, using the [device_info](https://pub.dev/packages/device_info) plugin. +iOS 10.0 of higher is needed to use the camera plugin. If compiling for any version lower than 10.0 make sure to check the iOS version before using the camera plugin. For example, using the [device_info_plus](https://pub.dev/packages/device_info_plus) plugin. Add two rows to the `ios/Runner/Info.plist`: diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 08d1e3eead4f..1009191e771e 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/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.1 +version: 0.9.1+1 environment: sdk: ">=2.12.0 <3.0.0" From 1f502e8b6f0082a0f2e2e89e223b7f2de899177e Mon Sep 17 00:00:00 2001 From: BeMacized Date: Fri, 27 Aug 2021 10:26:04 +0200 Subject: [PATCH 48/57] [camera] Add Android & iOS implementations for pausing the camera preview (#4258) --- packages/camera/camera/CHANGELOG.md | 4 + .../io/flutter/plugins/camera/Camera.java | 78 ++++++---- .../plugins/camera/MethodCallHandlerImpl.java | 16 ++ .../io/flutter/plugins/camera/CameraTest.java | 27 ++++ .../camera/MethodCallHandlerImplTest.java | 69 +++++++++ .../plugins/camera/utils/TestUtils.java | 11 ++ .../ios/Runner.xcodeproj/project.pbxproj | 4 + .../ios/RunnerTests/CameraPreviewPauseTests.m | 50 +++++++ packages/camera/camera/example/lib/main.dart | 28 +++- .../camera/camera/ios/Classes/CameraPlugin.m | 19 ++- .../camera/lib/src/camera_controller.dart | 48 +++++- .../camera/camera/lib/src/camera_preview.dart | 3 +- packages/camera/camera/pubspec.yaml | 4 +- .../camera/test/camera_preview_test.dart | 6 + packages/camera/camera/test/camera_test.dart | 140 ++++++++++++++++++ .../camera/camera/test/camera_value_test.dart | 43 +++--- 16 files changed, 498 insertions(+), 52 deletions(-) create mode 100644 packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java create mode 100644 packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 6d38fa204540..bb0048036f58 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.2 + +* Added functions to pause and resume the camera preview. + ## 0.9.1+1 * Replace `device_info` reference with `device_info_plus` in the [README.md](README.md) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 43479aca616c..c036c1c7e9d3 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -126,6 +126,8 @@ class Camera private MediaRecorder mediaRecorder; /** True when recording video. */ private boolean recordingVideo; + /** True when the preview is paused. */ + private boolean pausedPreview; private File captureFile; @@ -428,8 +430,10 @@ private void refreshPreviewCaptureSession( } try { - captureSession.setRepeatingRequest( - previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); + if (!pausedPreview) { + captureSession.setRepeatingRequest( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); + } if (onSuccessCallback != null) { onSuccessCallback.run(); @@ -834,33 +838,36 @@ public void setFocusMode(final Result result, @NonNull FocusMode newMode) { * For focus mode an extra step of actually locking/unlocking the * focus has to be done, in order to ensure it goes into the correct state. */ - switch (newMode) { - case locked: - // Perform a single focus trigger. - lockAutoFocus(); - if (captureSession == null) { - Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); - return; - } - - // Set AF state to idle again. - previewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); - - try { - captureSession.setRepeatingRequest( - previewRequestBuilder.build(), null, backgroundHandler); - } catch (CameraAccessException e) { - if (result != null) { - result.error("setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); + if (!pausedPreview) { + switch (newMode) { + case locked: + // Perform a single focus trigger. + if (captureSession == null) { + Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); + return; + } + lockAutoFocus(); + + // Set AF state to idle again. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + + try { + captureSession.setRepeatingRequest( + previewRequestBuilder.build(), null, backgroundHandler); + } catch (CameraAccessException e) { + if (result != null) { + result.error( + "setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); + } + return; } - return; - } - break; - case auto: - // Cancel current AF trigger and set AF to idle again. - unlockAutoFocus(); - break; + break; + case auto: + // Cancel current AF trigger and set AF to idle again. + unlockAutoFocus(); + break; + } } if (result != null) { @@ -966,6 +973,19 @@ public void unlockCaptureOrientation() { cameraFeatures.getSensorOrientation().unlockCaptureOrientation(); } + /** Pause the preview from dart. */ + public void pausePreview() throws CameraAccessException { + this.pausedPreview = true; + this.captureSession.stopRepeating(); + } + + /** Resume the preview from dart. */ + public void resumePreview() { + this.pausedPreview = false; + this.refreshPreviewCaptureSession( + null, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); + } + public void startPreview() throws CameraAccessException { if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; Log.i(TAG, "startPreview"); @@ -1022,8 +1042,8 @@ public void onError(String errorCode, String errorMessage) { private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { imageStreamReader.setOnImageAvailableListener( reader -> { - // Use acquireNextImage since image reader is only for one image. Image img = reader.acquireNextImage(); + // Use acquireNextImage since image reader is only for one image. if (img == null) return; List> planes = new ArrayList<>(); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index 893785f1a58f..5e25353cbca9 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -339,6 +339,22 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) } break; } + case "pausePreview": + { + try { + camera.pausePreview(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "resumePreview": + { + camera.resumePreview(); + result.success(null); + break; + } case "dispose": { if (camera != null) { diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java index cab2ae8974a4..5431df0df636 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java @@ -744,6 +744,33 @@ public void unlockCaptureOrientation_shouldUnlockCaptureOrientation() { verify(mockSensorOrientationFeature, times(1)).unlockCaptureOrientation(); } + @Test + public void pausePreview_shouldPausePreview() throws CameraAccessException { + camera.pausePreview(); + + assertEquals(TestUtils.getPrivateField(camera, "pausedPreview"), true); + verify(mockCaptureSession, times(1)).stopRepeating(); + } + + @Test + public void resumePreview_shouldResumePreview() throws CameraAccessException { + camera.resumePreview(); + + assertEquals(TestUtils.getPrivateField(camera, "pausedPreview"), false); + verify(mockCaptureSession, times(1)).setRepeatingRequest(any(), any(), any()); + } + + @Test + public void resumePreview_shouldSendErrorEventOnCameraAccessException() + throws CameraAccessException { + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0)); + + camera.resumePreview(); + + verify(mockDartMessenger, times(1)).sendCameraErrorEvent(any()); + } + private static class TestCameraFeatureFactory implements CameraFeatureFactory { private final AutoFocusFeature mockAutoFocusFeature; private final ExposureLockFeature mockExposureLockFeature; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java new file mode 100644 index 000000000000..35eed7a66a1a --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java @@ -0,0 +1,69 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.Activity; +import android.hardware.camera2.CameraAccessException; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.camera.utils.TestUtils; +import io.flutter.view.TextureRegistry; +import org.junit.Before; +import org.junit.Test; + +public class MethodCallHandlerImplTest { + + MethodChannel.MethodCallHandler handler; + MethodChannel.Result mockResult; + Camera mockCamera; + + @Before + public void setUp() { + handler = + new MethodCallHandlerImpl( + mock(Activity.class), + mock(BinaryMessenger.class), + mock(CameraPermissions.class), + mock(CameraPermissions.PermissionsRegistry.class), + mock(TextureRegistry.class), + null); + mockResult = mock(MethodChannel.Result.class); + mockCamera = mock(Camera.class); + TestUtils.setPrivateField(handler, "camera", mockCamera); + } + + @Test + public void onMethodCall_pausePreview_shouldPausePreviewAndSendSuccessResult() + throws CameraAccessException { + handler.onMethodCall(new MethodCall("pausePreview", null), mockResult); + + verify(mockCamera, times(1)).pausePreview(); + verify(mockResult, times(1)).success(null); + } + + @Test + public void onMethodCall_pausePreview_shouldSendErrorResultOnCameraAccessException() + throws CameraAccessException { + doThrow(new CameraAccessException(0)).when(mockCamera).pausePreview(); + + handler.onMethodCall(new MethodCall("pausePreview", null), mockResult); + + verify(mockResult, times(1)).error("CameraAccess", null, null); + } + + @Test + public void onMethodCall_resumePreview_shouldResumePreviewAndSendSuccessResult() { + handler.onMethodCall(new MethodCall("resumePreview", null), mockResult); + + verify(mockCamera, times(1)).resumePreview(); + verify(mockResult, times(1)).success(null); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java index dbf9d11be8b6..fce99b54384b 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java @@ -33,4 +33,15 @@ public static void setPrivateField(T instance, String fieldName, Object newV Assert.fail("Unable to mock private field: " + fieldName); } } + + public static Object getPrivateField(T instance, String fieldName) { + try { + Field field = instance.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(instance); + } catch (Exception e) { + Assert.fail("Unable to mock private field: " + fieldName); + return null; + } + } } diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index aead167a5e99..5a622f17fc63 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; A513685080F868CF2695CE75 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5555DD51E06E67921CFA83DD /* libPods-RunnerTests.a */; }; D065CD815D405ECB22FB1BBA /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A4F2DE74AE0C572296A00BF /* libPods-Runner.a */; }; + E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -68,6 +69,7 @@ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A4725B4F24805CD3CA67828F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; D1FF8C34CA9E9BE702C5EC06 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPreviewPauseTests.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -96,6 +98,7 @@ 03BB766A2665316900CE5A93 /* CameraFocusTests.m */, 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */, 03BB766C2665316900CE5A93 /* Info.plist */, + E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */, ); path = RunnerTests; sourceTree = ""; @@ -359,6 +362,7 @@ buildActionMask = 2147483647; files = ( 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */, + E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */, 334733EA2668111C00DCC49E /* CameraOrientationTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m new file mode 100644 index 000000000000..549b40a52e46 --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m @@ -0,0 +1,50 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import camera; +@import XCTest; +@import AVFoundation; +#import + +@interface FLTCam : NSObject +@property(assign, nonatomic) BOOL isPreviewPaused; +- (void)pausePreviewWithResult:(FlutterResult)result; +- (void)resumePreviewWithResult:(FlutterResult)result; +@end + +@interface CameraPreviewPauseTests : XCTestCase +@property(readonly, nonatomic) FLTCam* camera; +@end + +@implementation CameraPreviewPauseTests + +- (void)setUp { + _camera = [[FLTCam alloc] init]; +} + +- (void)testPausePreviewWithResult_shouldPausePreview { + XCTestExpectation* resultExpectation = + [self expectationWithDescription:@"Succeeding result with nil value"]; + [_camera pausePreviewWithResult:^void(id _Nullable result) { + XCTAssertNil(result); + [resultExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:2.0 handler:nil]; + XCTAssertTrue(_camera.isPreviewPaused); +} + +- (void)testResumePreviewWithResult_shouldResumePreview { + XCTestExpectation* resultExpectation = + [self expectationWithDescription:@"Succeeding result with nil value"]; + [_camera resumePreviewWithResult:^void(id _Nullable result) { + XCTAssertNil(result); + [resultExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:2.0 handler:nil]; + XCTAssertFalse(_camera.isPreviewPaused); +} + +@end diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 2314aecbece3..364f59d81356 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -530,7 +530,16 @@ class _CameraExampleHomeState extends State cameraController.value.isRecordingVideo ? onStopButtonPressed : null, - ) + ), + IconButton( + icon: const Icon(Icons.pause_presentation), + color: + cameraController != null && cameraController.value.isPreviewPaused + ? Colors.red + : Colors.blue, + onPressed: + cameraController == null ? null : onPausePreviewButtonPressed, + ), ], ); } @@ -747,6 +756,23 @@ class _CameraExampleHomeState extends State }); } + Future onPausePreviewButtonPressed() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isInitialized) { + showInSnackBar('Error: select a camera first.'); + return; + } + + if (cameraController.value.isPreviewPaused) { + await cameraController.resumePreview(); + } else { + await cameraController.pausePreview(); + } + + if (mounted) setState(() {}); + } + void onPauseButtonPressed() { pauseVideoRecording().then((_) { if (mounted) setState(() {}); diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index ea03ce57649c..cb93e9f5349d 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -330,6 +330,7 @@ @interface FLTCam : NSObject isRecordingVideo && _isRecordingPaused; @@ -150,6 +159,8 @@ class CameraValue { DeviceOrientation? deviceOrientation, Optional? lockedCaptureOrientation, Optional? recordingOrientation, + bool? isPreviewPaused, + Optional? previewPauseOrientation, }) { return CameraValue( isInitialized: isInitialized ?? this.isInitialized, @@ -172,6 +183,10 @@ class CameraValue { recordingOrientation: recordingOrientation == null ? this.recordingOrientation : recordingOrientation.orNull, + isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused, + previewPauseOrientation: previewPauseOrientation == null + ? this.previewPauseOrientation + : previewPauseOrientation.orNull, ); } @@ -190,7 +205,9 @@ class CameraValue { 'focusPointSupported: $focusPointSupported, ' 'deviceOrientation: $deviceOrientation, ' 'lockedCaptureOrientation: $lockedCaptureOrientation, ' - 'recordingOrientation: $recordingOrientation)'; + 'recordingOrientation: $recordingOrientation, ' + 'isPreviewPaused: $isPreviewPaused, ' + 'previewPausedOrientation: $previewPauseOrientation)'; } } @@ -325,6 +342,35 @@ class CameraController extends ValueNotifier { await CameraPlatform.instance.prepareForVideoRecording(); } + /// Pauses the current camera preview + Future pausePreview() async { + if (value.isPreviewPaused) { + return; + } + try { + await CameraPlatform.instance.pausePreview(_cameraId); + value = value.copyWith( + isPreviewPaused: true, + previewPauseOrientation: Optional.of(this.value.deviceOrientation)); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Resumes the current camera preview + Future resumePreview() async { + if (!value.isPreviewPaused) { + return; + } + try { + await CameraPlatform.instance.resumePreview(_cameraId); + value = value.copyWith( + isPreviewPaused: false, previewPauseOrientation: Optional.absent()); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + /// Captures an image and returns the file where it was saved. /// /// Throws a [CameraException] if the capture fails. diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart index 1df9f8e2e393..6a15896bfa47 100644 --- a/packages/camera/camera/lib/src/camera_preview.dart +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -71,7 +71,8 @@ class CameraPreview extends StatelessWidget { DeviceOrientation _getApplicableOrientation() { return controller.value.isRecordingVideo ? controller.value.recordingOrientation! - : (controller.value.lockedCaptureOrientation ?? + : (controller.value.previewPauseOrientation ?? + controller.value.lockedCaptureOrientation ?? controller.value.deviceOrientation); } } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 1009191e771e..3e3fad15051b 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/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.1+1 +version: 0.9.2 environment: sdk: ">=2.12.0 <3.0.0" @@ -20,7 +20,7 @@ flutter: pluginClass: CameraPlugin dependencies: - camera_platform_interface: ^2.0.0 + camera_platform_interface: ^2.1.0 flutter: sdk: flutter pedantic: ^1.10.0 diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index 8275461192b4..14afddaea070 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -113,6 +113,12 @@ class FakeController extends ValueNotifier @override Future unlockCaptureOrientation() async {} + + @override + Future pausePreview() async {} + + @override + Future resumePreview() async {} } void main() { diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index 26382a9b7d60..6904e68ef89f 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -1137,6 +1137,138 @@ void main() { .called(4); }); + test('pausePreview() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + cameraController.value = cameraController.value + .copyWith(deviceOrientation: DeviceOrientation.portraitUp); + + await cameraController.pausePreview(); + + verify(CameraPlatform.instance.pausePreview(cameraController.cameraId)) + .called(1); + expect(cameraController.value.isPreviewPaused, equals(true)); + expect(cameraController.value.previewPauseOrientation, + DeviceOrientation.portraitUp); + }); + + test('pausePreview() does not call $CameraPlatform when already paused', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + cameraController.value = + cameraController.value.copyWith(isPreviewPaused: true); + + await cameraController.pausePreview(); + + verifyNever( + CameraPlatform.instance.pausePreview(cameraController.cameraId)); + expect(cameraController.value.isPreviewPaused, equals(true)); + }); + + test('pausePreview() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance.pausePreview(cameraController.cameraId)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.pausePreview(), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('resumePreview() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + cameraController.value = + cameraController.value.copyWith(isPreviewPaused: true); + + await cameraController.resumePreview(); + + verify(CameraPlatform.instance.resumePreview(cameraController.cameraId)) + .called(1); + expect(cameraController.value.isPreviewPaused, equals(false)); + }); + + test('resumePreview() does not call $CameraPlatform when not paused', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + cameraController.value = + cameraController.value.copyWith(isPreviewPaused: false); + + await cameraController.resumePreview(); + + verifyNever( + CameraPlatform.instance.resumePreview(cameraController.cameraId)); + expect(cameraController.value.isPreviewPaused, equals(false)); + }); + + test('resumePreview() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + cameraController.value = + cameraController.value.copyWith(isPreviewPaused: true); + when(CameraPlatform.instance.resumePreview(cameraController.cameraId)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.resumePreview(), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + test('lockCaptureOrientation() calls $CameraPlatform', () async { CameraController cameraController = CameraController( CameraDescription( @@ -1314,6 +1446,14 @@ class MockCameraPlatform extends Mock Future unlockCaptureOrientation(int? cameraId) async => super .noSuchMethod(Invocation.method(#unlockCaptureOrientation, [cameraId])); + @override + Future pausePreview(int? cameraId) async => + super.noSuchMethod(Invocation.method(#pausePreview, [cameraId])); + + @override + Future resumePreview(int? cameraId) async => + super.noSuchMethod(Invocation.method(#resumePreview, [cameraId])); + @override Future getMaxZoomLevel(int? cameraId) async => super.noSuchMethod( Invocation.method(#getMaxZoomLevel, [cameraId]), diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart index e0378cca2cb9..4718d8943c34 100644 --- a/packages/camera/camera/test/camera_value_test.dart +++ b/packages/camera/camera/test/camera_value_test.dart @@ -29,6 +29,8 @@ void main() { lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, focusPointSupported: true, + isPreviewPaused: false, + previewPauseOrientation: DeviceOrientation.portraitUp, ); expect(cameraValue, isA()); @@ -46,6 +48,8 @@ void main() { expect( cameraValue.lockedCaptureOrientation, DeviceOrientation.portraitUp); expect(cameraValue.recordingOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.isPreviewPaused, false); + expect(cameraValue.previewPauseOrientation, DeviceOrientation.portraitUp); }); test('Can be created as uninitialized', () { @@ -66,6 +70,8 @@ void main() { expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); expect(cameraValue.lockedCaptureOrientation, null); expect(cameraValue.recordingOrientation, null); + expect(cameraValue.isPreviewPaused, isFalse); + expect(cameraValue.previewPauseOrientation, null); }); test('Can be copied with isInitialized', () { @@ -87,6 +93,8 @@ void main() { expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); expect(cameraValue.lockedCaptureOrientation, null); expect(cameraValue.recordingOrientation, null); + expect(cameraValue.isPreviewPaused, isFalse); + expect(cameraValue.previewPauseOrientation, null); }); test('Has aspectRatio after setting size', () { @@ -117,25 +125,26 @@ void main() { test('toString() works as expected', () { var cameraValue = const CameraValue( - isInitialized: false, - errorDescription: null, - previewSize: Size(10, 10), - isRecordingPaused: false, - isRecordingVideo: false, - isTakingPicture: false, - isStreamingImages: false, - flashMode: FlashMode.auto, - exposureMode: ExposureMode.auto, - focusMode: FocusMode.auto, - exposurePointSupported: true, - focusPointSupported: true, - deviceOrientation: DeviceOrientation.portraitUp, - lockedCaptureOrientation: DeviceOrientation.portraitUp, - recordingOrientation: DeviceOrientation.portraitUp, - ); + isInitialized: false, + errorDescription: null, + previewSize: Size(10, 10), + isRecordingPaused: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + focusMode: FocusMode.auto, + exposurePointSupported: true, + focusPointSupported: true, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: DeviceOrientation.portraitUp, + recordingOrientation: DeviceOrientation.portraitUp, + isPreviewPaused: true, + previewPauseOrientation: DeviceOrientation.portraitUp); expect(cameraValue.toString(), - 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp)'); + 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp, isPreviewPaused: true, previewPausedOrientation: DeviceOrientation.portraitUp)'); }); }); } From 3f808c1dd1abd00a8256b16a4f8cef6e8010f37d Mon Sep 17 00:00:00 2001 From: Maurits van Beusekom Date: Fri, 27 Aug 2021 14:44:39 +0200 Subject: [PATCH 49/57] [in_app_purchase] Ensure purchases correctly report if they are acknowledged on Android (#4257) * Ensure purchases correctly show they are acknowledged * Update packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md Co-authored-by: Rene Floor * Update packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md Co-authored-by: Rene Floor * Modify so public API is not changed Co-authored-by: Rene Floor --- .../in_app_purchase_android/CHANGELOG.md | 6 ++++- .../types/google_play_purchase_details.dart | 25 ++++++------------- .../in_app_purchase_android/pubspec.yaml | 2 +- .../purchase_wrapper_test.dart | 22 +++++++++++++++- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md index 8e342a65422c..1a03ba27feb7 100644 --- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.4+6 + +* Ensure that purchases correctly indicate whether they are acknowledged or not. The `PurchaseDetails.pendingCompletePurchase` field now correctly indicates if the purchase still needs to be completed. + ## 0.1.4+5 * Add `implements` to pubspec. @@ -9,7 +13,7 @@ ## 0.1.4+3 -- Updated installation instructions in README. +* Updated installation instructions in README. ## 0.1.4+2 diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart index 66e3a8f5a590..53b58bd664fd 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart @@ -20,30 +20,19 @@ class GooglePlayPurchaseDetails extends PurchaseDetails { required this.billingClientPurchase, required PurchaseStatus status, }) : super( - productID: productID, - purchaseID: purchaseID, - transactionDate: transactionDate, - verificationData: verificationData, - status: status) { - this.status = status; + productID: productID, + purchaseID: purchaseID, + transactionDate: transactionDate, + verificationData: verificationData, + status: status, + ) { + this.pendingCompletePurchase = !billingClientPurchase.isAcknowledged; } /// Points back to the [PurchaseWrapper] which was used to generate this /// [GooglePlayPurchaseDetails] object. final PurchaseWrapper billingClientPurchase; - late PurchaseStatus _status; - - /// The status that this [PurchaseDetails] is currently on. - PurchaseStatus get status => _status; - set status(PurchaseStatus status) { - _pendingCompletePurchase = status == PurchaseStatus.purchased; - _status = status; - } - - bool _pendingCompletePurchase = false; - bool get pendingCompletePurchase => _pendingCompletePurchase; - /// Generate a [PurchaseDetails] object based on an Android [Purchase] object. factory GooglePlayPurchaseDetails.fromPurchase(PurchaseWrapper purchase) { final GooglePlayPurchaseDetails purchaseDetails = GooglePlayPurchaseDetails( diff --git a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml index 745b651e5828..d9b09827824b 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_android description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs. repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.1.4+5 +version: 0.1.4+6 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart index bb7ff8535c7a..70b9fcad4da7 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart @@ -71,9 +71,10 @@ void main() { expect(parsed, equals(expected)); }); - test('toPurchaseDetails() should return correct PurchaseDetail object', () { + test('fromPurchase() should return correct PurchaseDetail object', () { final GooglePlayPurchaseDetails details = GooglePlayPurchaseDetails.fromPurchase(dummyPurchase); + expect(details.purchaseID, dummyPurchase.orderId); expect(details.productID, dummyPurchase.sku); expect(details.transactionDate, dummyPurchase.purchaseTime.toString()); @@ -84,6 +85,25 @@ void main() { expect(details.verificationData.serverVerificationData, dummyPurchase.purchaseToken); expect(details.billingClientPurchase, dummyPurchase); + expect(details.pendingCompletePurchase, false); + }); + + test( + 'fromPurchase() should return set pendingCompletePurchase to true for unacknowledged purchase', + () { + final GooglePlayPurchaseDetails details = + GooglePlayPurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase); + + expect(details.purchaseID, dummyPurchase.orderId); + expect(details.productID, dummyPurchase.sku); + expect(details.transactionDate, dummyPurchase.purchaseTime.toString()); + expect(details.verificationData, isNotNull); + expect(details.verificationData.source, kIAPSource); + expect(details.verificationData.localVerificationData, + dummyPurchase.originalJson); + expect(details.verificationData.serverVerificationData, + dummyPurchase.purchaseToken); + expect(details.billingClientPurchase, dummyUnacknowledgedPurchase); expect(details.pendingCompletePurchase, true); }); }); From 0588bfea1d5ce41bf84adcd798d234758149850d Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 27 Aug 2021 11:36:03 -0400 Subject: [PATCH 50/57] Remove support for bypassing, or prompting for, git tagging (#4275) We never want a plugin to be published without tagging the release, so there's no reason to support the added complexity of these flags. Similarly, once someone has confirmed publishing, we don't want to give them an opt-out for doing the tag. --- script/tool/CHANGELOG.md | 2 + .../tool/lib/src/publish_plugin_command.dart | 69 +++----- .../test/publish_plugin_command_test.dart | 150 ++++-------------- script/tool/test/util.dart | 3 +- 4 files changed, 51 insertions(+), 173 deletions(-) diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index a32fb0016cb3..b10237b45913 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -5,6 +5,8 @@ - Pubspec validation now checks for `implements` in implementation packages. - Pubspec valitation now checks the full relative path of `repository` entries. - `build-examples` now supports UWP plugins via a `--winuwp` flag. +- **Breaking change**: `publish` no longer accepts `--no-tag-release` or + `--no-push-flags`. Releases now always tag and push. ## 0.5.0 diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index be9e6d300125..8432e342cda3 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -66,23 +66,9 @@ class PublishPluginCommand extends PluginCommand { argParser.addMultiOption(_pubFlagsOption, help: 'A list of options that will be forwarded on to pub. Separate multiple flags with commas.'); - argParser.addFlag( - _tagReleaseOption, - help: 'Whether or not to tag the release.', - defaultsTo: true, - negatable: true, - ); - argParser.addFlag( - _pushTagsOption, - help: - 'Whether or not tags should be pushed to a remote after creation. Ignored if tag-release is false.', - defaultsTo: true, - negatable: true, - ); argParser.addOption( _remoteOption, - help: - 'The name of the remote to push the tags to. Ignored if push-tags or tag-release is false.', + help: 'The name of the remote to push the tags to.', // Flutter convention is to use "upstream" for the single source of truth, and "origin" for personal forks. defaultsTo: 'upstream', ); @@ -104,15 +90,12 @@ class PublishPluginCommand extends PluginCommand { ); argParser.addFlag(_skipConfirmationFlag, help: 'Run the command without asking for Y/N inputs.\n' - 'This command will add a `--force` flag to the `pub publish` command if it is not added with $_pubFlagsOption\n' - 'It also skips the y/n inputs when pushing tags to remote.\n', + 'This command will add a `--force` flag to the `pub publish` command if it is not added with $_pubFlagsOption\n', defaultsTo: false, negatable: true); } static const String _packageOption = 'package'; - static const String _tagReleaseOption = 'tag-release'; - static const String _pushTagsOption = 'push-tags'; static const String _pubFlagsOption = 'pub-publish-flags'; static const String _remoteOption = 'remote'; static const String _allChangedFlag = 'all-changed'; @@ -150,19 +133,14 @@ class PublishPluginCommand extends PluginCommand { print('Checking local repo...'); final GitDir repository = await gitDir; - - final bool shouldPushTag = getBoolArg(_pushTagsOption); - _RemoteInfo? remote; - if (shouldPushTag) { - final String remoteName = getStringArg(_remoteOption); - final String? remoteUrl = await _verifyRemote(remoteName); - if (remoteUrl == null) { - printError( - 'Unable to find URL for remote $remoteName; cannot push tags'); - throw ToolExit(1); - } - remote = _RemoteInfo(name: remoteName, url: remoteUrl); + final String remoteName = getStringArg(_remoteOption); + final String? remoteUrl = await _verifyRemote(remoteName); + if (remoteUrl == null) { + printError('Unable to find URL for remote $remoteName; cannot push tags'); + throw ToolExit(1); } + final _RemoteInfo remote = _RemoteInfo(name: remoteName, url: remoteUrl); + print('Local repo is ready!'); if (getBoolArg(_dryRunFlag)) { print('=============== DRY RUN ==============='); @@ -187,7 +165,7 @@ class PublishPluginCommand extends PluginCommand { Future _publishAllChangedPackages({ required GitDir baseGitDir, - _RemoteInfo? remoteForTagPush, + required _RemoteInfo remoteForTagPush, }) async { final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); final List changedPubspecs = @@ -249,24 +227,21 @@ class PublishPluginCommand extends PluginCommand { return packagesFailed.isEmpty; } - // Publish the package to pub with `pub publish`. - // If `_tagReleaseOption` is on, git tag the release. - // If `remoteForTagPush` is non-null, the tag will be pushed to that remote. + // Publish the package to pub with `pub publish`, then git tag the release + // and push the tag to [remoteForTagPush]. // Returns `true` if publishing and tagging are successful. Future _publishAndTagPackage({ required Directory packageDir, - _RemoteInfo? remoteForTagPush, + required _RemoteInfo remoteForTagPush, }) async { if (!await _publishPlugin(packageDir: packageDir)) { return false; } - if (getBoolArg(_tagReleaseOption)) { - if (!await _tagRelease( - packageDir: packageDir, - remoteForPush: remoteForTagPush, - )) { - return false; - } + if (!await _tagRelease( + packageDir: packageDir, + remoteForPush: remoteForTagPush, + )) { + return false; } print('Released [${packageDir.basename}] successfully.'); return true; @@ -479,14 +454,6 @@ Safe to ignore if the package is deleted in this commit. required _RemoteInfo remote, }) async { assert(remote != null && tag != null); - if (!getBoolArg(_skipConfirmationFlag)) { - print('Ready to push $tag to ${remote.url} (y/n)?'); - final String? input = _stdin.readLineSync(); - if (input?.toLowerCase() != 'y') { - print('Tag push canceled.'); - return false; - } - } if (!getBoolArg(_dryRunFlag)) { final io.ProcessResult result = await (await gitDir).runCommand( ['push', remote.name, tag], diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 663c2633a9db..927c146a874d 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -80,8 +80,8 @@ void main() { test('requires an existing flag', () async { Error? commandError; - final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--package', 'iamerror', '--no-push-tags'], + final List output = await runCapturingPrint( + commandRunner, ['publish-plugin', '--package', 'iamerror'], errorHandler: (Error e) { commandError = e; }); @@ -100,8 +100,8 @@ void main() { ]; Error? commandError; - final List output = await runCapturingPrint(commandRunner, - ['publish-plugin', '--package', 'foo', '--no-push-tags'], + final List output = await runCapturingPrint( + commandRunner, ['publish-plugin', '--package', 'foo'], errorHandler: (Error e) { commandError = e; }); @@ -141,56 +141,6 @@ void main() { 'Unable to find URL for remote upstream; cannot push tags'), ])); }); - - test("doesn't validate the remote if it's not pushing tags", () async { - createFakePlugin('foo', packagesDir, examples: []); - - // Checking the remote should fail. - processRunner.mockProcessesForExecutable['git-remote'] = [ - MockProcess(exitCode: 1), - ]; - - final List output = await runCapturingPrint( - commandRunner, [ - 'publish-plugin', - '--package', - 'foo', - '--no-push-tags', - '--no-tag-release' - ]); - - expect( - output, - containsAllInOrder([ - contains('Running `pub publish ` in /packages/foo...'), - contains('Package published!'), - contains('Released [foo] successfully.'), - ])); - }); - - test('can publish non-flutter package', () async { - const String packageName = 'a_package'; - createFakePackage(packageName, packagesDir); - - final List output = await runCapturingPrint( - commandRunner, [ - 'publish-plugin', - '--package', - packageName, - '--no-push-tags', - '--no-tag-release' - ]); - - expect( - output, - containsAllInOrder( - [ - contains('Running `pub publish ` in /packages/a_package...'), - contains('Package published!'), - ], - ), - ); - }); }); group('Publishes package', () { @@ -206,13 +156,7 @@ void main() { ]; final List output = await runCapturingPrint( - commandRunner, [ - 'publish-plugin', - '--package', - 'foo', - '--no-push-tags', - '--no-tag-release' - ]); + commandRunner, ['publish-plugin', '--package', 'foo']); expect( output, @@ -227,13 +171,8 @@ void main() { mockStdin.mockUserInputs.add(utf8.encode('user input')); - await runCapturingPrint(commandRunner, [ - 'publish-plugin', - '--package', - 'foo', - '--no-push-tags', - '--no-tag-release' - ]); + await runCapturingPrint( + commandRunner, ['publish-plugin', '--package', 'foo']); expect(processRunner.mockPublishProcess.stdinMock.lines, contains('user input')); @@ -247,8 +186,6 @@ void main() { 'publish-plugin', '--package', 'foo', - '--no-push-tags', - '--no-tag-release', '--pub-publish-flags', '--dry-run,--server=foo' ]); @@ -272,8 +209,6 @@ void main() { 'publish-plugin', '--package', 'foo', - '--no-push-tags', - '--no-tag-release', '--skip-confirmation', '--pub-publish-flags', '--server=foo' @@ -300,8 +235,6 @@ void main() { 'publish-plugin', '--package', 'foo', - '--no-push-tags', - '--no-tag-release', ], errorHandler: (Error e) { commandError = e; }); @@ -324,8 +257,6 @@ void main() { '--package', 'foo', '--dry-run', - '--no-push-tags', - '--no-tag-release', ]); expect( @@ -340,6 +271,28 @@ void main() { 'Done!' ])); }); + + test('can publish non-flutter package', () async { + const String packageName = 'a_package'; + createFakePackage(packageName, packagesDir); + + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--package', + packageName, + ]); + + expect( + output, + containsAllInOrder( + [ + contains('Running `pub publish ` in /packages/a_package...'), + contains('Package published!'), + ], + ), + ); + }); }); group('Tags release', () { @@ -349,7 +302,6 @@ void main() { 'publish-plugin', '--package', 'foo', - '--no-push-tags', ]); expect(processRunner.recordedCalls, @@ -369,7 +321,6 @@ void main() { 'publish-plugin', '--package', 'foo', - '--no-push-tags', ], errorHandler: (Error e) { commandError = e; }); @@ -388,25 +339,6 @@ void main() { }); group('Pushes tags', () { - test('requires user confirmation', () async { - createFakePlugin('foo', packagesDir, examples: []); - - mockStdin.readLineOutput = 'help'; - - Error? commandError; - final List output = - await runCapturingPrint(commandRunner, [ - 'publish-plugin', - '--package', - 'foo', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect(output, contains('Tag push canceled.')); - }); - test('to upstream by default', () async { createFakePlugin('foo', packagesDir, examples: []); @@ -502,30 +434,6 @@ void main() { contains('Released [foo] successfully.'), ])); }); - - test('only if tagging and pushing to remotes are both enabled', () async { - createFakePlugin('foo', packagesDir, examples: []); - - final List output = - await runCapturingPrint(commandRunner, [ - 'publish-plugin', - '--package', - 'foo', - '--no-tag-release', - ]); - - expect( - processRunner.recordedCalls - .map((ProcessCall call) => call.executable), - isNot(contains('git-push'))); - expect( - output, - containsAllInOrder([ - contains('Running `pub publish ` in /packages/foo...'), - contains('Package published!'), - contains('Released [foo] successfully.'), - ])); - }); }); group('Auto release (all-changed flag)', () { diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index 9b92a5d94ac8..74c036489233 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -107,7 +107,8 @@ Directory createFakePackage( final Directory packageDirectory = parentDirectory.childDirectory(name); packageDirectory.createSync(recursive: true); - createFakePubspec(packageDirectory, name: name, isFlutter: isFlutter); + createFakePubspec(packageDirectory, + name: name, isFlutter: isFlutter, version: version); createFakeCHANGELOG(packageDirectory, ''' ## $version * Some changes. From 797c61d6b613068326997ac2456dff70af41a3ec Mon Sep 17 00:00:00 2001 From: BeMacized Date: Fri, 27 Aug 2021 17:41:03 +0200 Subject: [PATCH 51/57] [camera] Fix a disposed camera controller throwing an exception when being replaced in the preview widget. (#4272) --- packages/camera/camera/CHANGELOG.md | 4 ++++ packages/camera/camera/example/lib/main.dart | 10 +++------- packages/camera/camera/lib/src/camera_controller.dart | 10 ++++++++++ packages/camera/camera/pubspec.yaml | 2 +- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index bb0048036f58..5a3a1bf251d7 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.2+1 + +* Fixed camera controller throwing an exception when being replaced in the preview widget. + ## 0.9.2 * Added functions to pause and resume the camera preview. diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 364f59d81356..a8067001aae5 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -603,7 +603,9 @@ class _CameraExampleHomeState extends State } void onNewCameraSelected(CameraDescription cameraDescription) async { - final previousCameraController = controller; + if (controller != null) { + await controller!.dispose(); + } final CameraController cameraController = CameraController( cameraDescription, @@ -614,10 +616,6 @@ class _CameraExampleHomeState extends State controller = cameraController; - if (mounted) { - setState(() {}); - } - // If the controller is updated then update the UI. cameraController.addListener(() { if (mounted) setState(() {}); @@ -650,8 +648,6 @@ class _CameraExampleHomeState extends State if (mounted) { setState(() {}); } - - await previousCameraController?.dispose(); } void onTakePictureButtonPressed() { diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index 58193bd204c4..f21a3b12c81f 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -824,4 +824,14 @@ class CameraController extends ValueNotifier { ); } } + + @override + void removeListener(VoidCallback listener) { + // Prevent ValueListenableBuilder in CameraPreview widget from causing an + // exception to be thrown by attempting to remove its own listener after + // the controller has already been disposed. + if (!_isDisposed) { + super.removeListener(listener); + } + } } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 3e3fad15051b..400b8c03f44a 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for getting information about and controlling the and streaming image buffers to dart. repository: https://github.com/flutter/plugins/tree/master/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.2 +version: 0.9.2+1 environment: sdk: ">=2.12.0 <3.0.0" From 78b914d4556d839598ce196efaabb1e4a44d6384 Mon Sep 17 00:00:00 2001 From: Casey Hillers Date: Fri, 27 Aug 2021 10:26:03 -0700 Subject: [PATCH 52/57] [ci.yaml] Add linux platform properties (#4282) --- .ci.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.ci.yaml b/.ci.yaml index 86bc72c7aebf..1205c1ac104d 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -9,6 +9,17 @@ enabled_branches: - master platform_properties: + linux: + properties: + caches: >- + [ + ] + dependencies: > + [ + {"dependency": "curl"} + ] + device_type: none + os: Linux windows: properties: caches: >- From 83f8c4c6a435ab543537875afbb7d5c2e0f6a4dc Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 27 Aug 2021 11:22:10 -0700 Subject: [PATCH 53/57] [ci] update wait-on-check version and set verbose to false (#4262) --- .github/workflows/release.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f6753e5a2add..d3418683fde2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,15 +28,17 @@ jobs: run: dart pub get working-directory: ${{ github.workspace }}/script/tool - # # This workflow should be the last to run. So wait for all the other tests to succeed. + # This workflow should be the last to run. So wait for all the other tests to succeed. - name: Wait on all tests - uses: lewagon/wait-on-check-action@1b1630e169116b58a4b933d5ad7effc46d3d312d + uses: lewagon/wait-on-check-action@0179dfc359f90a703c41240506f998ee1603f9ea with: ref: ${{ github.sha }} running-workflow-name: 'release' repo-token: ${{ secrets.GITHUB_TOKEN }} wait-interval: 180 # seconds allowed-conclusions: success + # verbose:true will produce too many logs that hang github actions web UI. + verbose: false - name: run release run: | From 8d4be08b6f5f50444e8f0415736f9fb97f1adf97 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 27 Aug 2021 11:50:11 -0700 Subject: [PATCH 54/57] [ci] Fix wrong hash used in release.yml (#4286) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d3418683fde2..7f1a4a360949 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: # This workflow should be the last to run. So wait for all the other tests to succeed. - name: Wait on all tests - uses: lewagon/wait-on-check-action@0179dfc359f90a703c41240506f998ee1603f9ea + uses: lewagon/wait-on-check-action@a0f99ce1e713de216866868c3da4d4183a051cbe with: ref: ${{ github.sha }} running-workflow-name: 'release' From 4f4a88900b772a83e5f7111a05be9132ec17d65e Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 27 Aug 2021 12:18:06 -0700 Subject: [PATCH 55/57] [ci] Revert the wait-on-check hash change (#4287) --- .github/workflows/release.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f1a4a360949..00fa140b131a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,15 +30,13 @@ jobs: # This workflow should be the last to run. So wait for all the other tests to succeed. - name: Wait on all tests - uses: lewagon/wait-on-check-action@a0f99ce1e713de216866868c3da4d4183a051cbe + uses: lewagon/wait-on-check-action@1b1630e169116b58a4b933d5ad7effc46d3d312d with: ref: ${{ github.sha }} running-workflow-name: 'release' repo-token: ${{ secrets.GITHUB_TOKEN }} wait-interval: 180 # seconds allowed-conclusions: success - # verbose:true will produce too many logs that hang github actions web UI. - verbose: false - name: run release run: | From 9b614eaa8394de04bbd530bcba8a585b1ded4cab Mon Sep 17 00:00:00 2001 From: Casey Hillers Date: Fri, 27 Aug 2021 12:21:05 -0700 Subject: [PATCH 56/57] [ci.yaml] Add roller to presubmit (#4283) --- .ci.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.ci.yaml b/.ci.yaml index 1205c1ac104d..6b5c385aa98e 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -58,7 +58,6 @@ targets: - name: Linux ci_yaml plugins roller recipe: infra/ci_yaml - bringup: true timeout: 30 scheduler: luci runIf: From 0f20461340af7d4ab86e65c80ecccf8ba37c69f9 Mon Sep 17 00:00:00 2001 From: Christopher Boyd Date: Mon, 28 Sep 2020 22:07:36 -0400 Subject: [PATCH 57/57] [image_picker] add forceFullMetadata to example --- .../image_picker/image_picker/CHANGELOG.md | 8 ++ .../image_picker/example/lib/main.dart | 115 ++++++++++-------- .../ios/Classes/FLTImagePickerPlugin.m | 30 ++++- .../image_picker/lib/image_picker.dart | 14 +++ .../image_picker/image_picker/pubspec.yaml | 2 +- .../test/image_picker_deprecated_test.dart | 29 +++-- .../image_picker/test/image_picker_test.dart | 29 +++-- .../lib/image_picker_for_web.dart | 1 + .../CHANGELOG.md | 8 ++ .../method_channel_image_picker.dart | 6 + .../image_picker_platform.dart | 12 ++ .../pubspec.yaml | 2 +- .../new_method_channel_image_picker_test.dart | 58 ++++++--- 13 files changed, 221 insertions(+), 93 deletions(-) diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index a9255976c526..7be6722f9f57 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.8.4 + +* Add `forceFullMetadata` option to `pickImage`. + * To keep this non-breaking `forceFullMetadata` defaults to `true`, so the plugin tries + to get the full image metadata which may require extra permission requests on certain platforms. + * If `forceFullMetadata` is set to `false`, the plugin fetches the image in a way that reduces + permission requests from the platform (e.g on iOS the plugin won’t ask for the `NSPhotoLibraryUsageDescription` permission). + ## 0.8.3+3 * Fix pickImage not returning a value on iOS when dismissing PHPicker sheet by swiping. diff --git a/packages/image_picker/image_picker/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index 2d5fd9aee4a7..0d00c20fba71 100755 --- a/packages/image_picker/image_picker/example/lib/main.dart +++ b/packages/image_picker/image_picker/example/lib/main.dart @@ -88,8 +88,8 @@ class _MyHomePageState extends State { source: source, maxDuration: const Duration(seconds: 10)); await _playVideo(file); } else if (isMultiImage) { - await _displayPickImageDialog(context!, - (double? maxWidth, double? maxHeight, int? quality) async { + await _displayPickImageDialog(context!, (double? maxWidth, + double? maxHeight, int? quality, bool forceFullMetadata) async { try { final pickedFileList = await _picker.pickMultiImage( maxWidth: maxWidth, @@ -106,14 +106,15 @@ class _MyHomePageState extends State { } }); } else { - await _displayPickImageDialog(context!, - (double? maxWidth, double? maxHeight, int? quality) async { + await _displayPickImageDialog(context!, (double? maxWidth, + double? maxHeight, int? quality, bool forceFullMetadata) async { try { final pickedFile = await _picker.pickImage( source: source, maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: quality, + forceFullMetadata: forceFullMetadata, ); setState(() { _imageFile = pickedFile; @@ -358,60 +359,74 @@ class _MyHomePageState extends State { return showDialog( context: context, builder: (context) { - return AlertDialog( - title: Text('Add optional parameters'), - content: Column( - children: [ - TextField( - controller: maxWidthController, - keyboardType: TextInputType.numberWithOptions(decimal: true), - decoration: - InputDecoration(hintText: "Enter maxWidth if desired"), - ), - TextField( - controller: maxHeightController, - keyboardType: TextInputType.numberWithOptions(decimal: true), - decoration: - InputDecoration(hintText: "Enter maxHeight if desired"), - ), - TextField( - controller: qualityController, - keyboardType: TextInputType.number, - decoration: - InputDecoration(hintText: "Enter quality if desired"), - ), - ], - ), - actions: [ - TextButton( - child: const Text('CANCEL'), - onPressed: () { - Navigator.of(context).pop(); - }, + bool forceFullMetadata = true; + return StatefulBuilder(builder: (context, setState) { + return AlertDialog( + title: Text('Add optional parameters'), + content: Column( + children: [ + TextField( + controller: maxWidthController, + keyboardType: + TextInputType.numberWithOptions(decimal: true), + decoration: + InputDecoration(hintText: "Enter maxWidth if desired"), + ), + TextField( + controller: maxHeightController, + keyboardType: + TextInputType.numberWithOptions(decimal: true), + decoration: + InputDecoration(hintText: "Enter maxHeight if desired"), + ), + TextField( + controller: qualityController, + keyboardType: TextInputType.number, + decoration: + InputDecoration(hintText: "Enter quality if desired"), + ), + CheckboxListTile( + value: forceFullMetadata, + onChanged: (bool? value) { + setState(() { + forceFullMetadata = value ?? false; + }); + }, + title: Text("Force full metadata"), + ) + ], ), - TextButton( - child: const Text('PICK'), + actions: [ + TextButton( + child: const Text('CANCEL'), onPressed: () { - double? width = maxWidthController.text.isNotEmpty - ? double.parse(maxWidthController.text) - : null; - double? height = maxHeightController.text.isNotEmpty - ? double.parse(maxHeightController.text) - : null; - int? quality = qualityController.text.isNotEmpty - ? int.parse(qualityController.text) - : null; - onPick(width, height, quality); Navigator.of(context).pop(); - }), - ], - ); + }, + ), + TextButton( + child: const Text('PICK'), + onPressed: () { + double? width = maxWidthController.text.isNotEmpty + ? double.parse(maxWidthController.text) + : null; + double? height = maxHeightController.text.isNotEmpty + ? double.parse(maxHeightController.text) + : null; + int? quality = qualityController.text.isNotEmpty + ? int.parse(qualityController.text) + : null; + onPick(width, height, quality, forceFullMetadata); + Navigator.of(context).pop(); + }), + ], + ); + }); }); } } typedef void OnPickImageCallback( - double? maxWidth, double? maxHeight, int? quality); + double? maxWidth, double? maxHeight, int? quality, bool forceFullMetadata); class AspectRatioVideo extends StatefulWidget { AspectRatioVideo(this.controller); diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m index cf3103195482..207178d01bf7 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m @@ -97,7 +97,12 @@ - (void)pickImageWithPHPicker:(int)maxImagesAllowed API_AVAILABLE(ios(14)) { self.maxImagesAllowed = maxImagesAllowed; - [self checkPhotoAuthorizationForAccessLevel]; + BOOL usePhaAsset = [[_arguments objectForKey:@"forceFullMetadata"] boolValue]; + if (usePhaAsset) { + [self checkPhotoAuthorizationForAccessLevel]; + return; + } + [self showPhotoLibrary:PHPickerClassType]; } - (void)pickImageWithUIImagePicker { @@ -107,6 +112,7 @@ - (void)pickImageWithUIImagePicker { _imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ]; int imageSource = [[_arguments objectForKey:@"source"] intValue]; + BOOL usePhaAsset = [[_arguments objectForKey:@"forceFullMetadata"] boolValue]; self.maxImagesAllowed = 1; @@ -115,7 +121,11 @@ - (void)pickImageWithUIImagePicker { [self checkCameraAuthorization]; break; case SOURCE_GALLERY: - [self checkPhotoAuthorization]; + if (usePhaAsset) { + [self checkPhotoAuthorization]; + break; + } + [self showPhotoLibrary:UIImagePickerClassType]; break; default: self.result([FlutterError errorWithCode:@"invalid_source" @@ -132,13 +142,14 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result details:nil]); self.result = nil; } + BOOL usePhaAsset = [[_arguments objectForKey:@"forceFullMetadata"] boolValue]; if ([@"pickImage" isEqualToString:call.method]) { self.result = result; _arguments = call.arguments; int imageSource = [[_arguments objectForKey:@"source"] intValue]; - if (imageSource == SOURCE_GALLERY) { // Capture is not possible with PHPicker + if (usePhaAsset && imageSource == SOURCE_GALLERY) { // Capture is not possible with PHPicker if (@available(iOS 14, *)) { // PHPicker is used [self pickImageWithPHPicker:1]; @@ -171,6 +182,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result _arguments = call.arguments; int imageSource = [[_arguments objectForKey:@"source"] intValue]; + BOOL usePhaAsset = [[_arguments objectForKey:@"forceFullMetadata"] boolValue]; if ([[_arguments objectForKey:@"maxDuration"] isKindOfClass:[NSNumber class]]) { NSTimeInterval max = [[_arguments objectForKey:@"maxDuration"] doubleValue]; _imagePickerController.videoMaximumDuration = max; @@ -181,7 +193,11 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self checkCameraAuthorization]; break; case SOURCE_GALLERY: - [self checkPhotoAuthorization]; + if (usePhaAsset) { + [self checkPhotoAuthorization]; + break; + } + [self showPhotoLibrary:UIImagePickerClassType]; break; default: result([FlutterError errorWithCode:@"invalid_source" @@ -484,8 +500,12 @@ - (void)imagePickerController:(UIImagePickerController *)picker NSNumber *maxHeight = [_arguments objectForKey:@"maxHeight"]; NSNumber *imageQuality = [_arguments objectForKey:@"imageQuality"]; NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality]; + BOOL usePhaAsset = [[_arguments objectForKey:@"forceFullMetadata"] boolValue]; - PHAsset *originalAsset = [FLTImagePickerPhotoAssetUtil getAssetFromImagePickerInfo:info]; + PHAsset *originalAsset; + if (usePhaAsset) { + originalAsset = [FLTImagePickerPhotoAssetUtil getAssetFromImagePickerInfo:info]; + } if (maxWidth != (id)[NSNull null] || maxHeight != (id)[NSNull null]) { image = [FLTImagePickerImageUtil scaledImage:image diff --git a/packages/image_picker/image_picker/lib/image_picker.dart b/packages/image_picker/image_picker/lib/image_picker.dart index 5bc99d7f0bb2..19989377ba41 100755 --- a/packages/image_picker/image_picker/lib/image_picker.dart +++ b/packages/image_picker/image_picker/lib/image_picker.dart @@ -44,6 +44,11 @@ class ImagePicker { /// image types such as JPEG and on Android PNG and WebP, too. If compression is not supported for the image that is picked, /// a warning message will be logged. /// + /// `forceFullMetadata` defaults to `true`, so the plugin tries to get the full image metadata which may require + /// extra permission requests on certain platforms. + /// If `forceFullMetadata` is set to `false`, the plugin fetches the image in a way that reduces permission requests + /// from the platform (e.g on iOS the plugin won’t ask for the `NSPhotoLibraryUsageDescription` permission). + /// /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. /// Defaults to [CameraDevice.rear]. Note that Android has no documented parameter for an intent to specify if @@ -65,6 +70,7 @@ class ImagePicker { double? maxWidth, double? maxHeight, int? imageQuality, + bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { return platform.pickImage( @@ -72,6 +78,7 @@ class ImagePicker { maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality, + forceFullMetadata: forceFullMetadata, preferredCameraDevice: preferredCameraDevice, ); } @@ -185,6 +192,11 @@ class ImagePicker { /// image types such as JPEG and on Android PNG and WebP, too. If compression is not supported for the image that is picked, /// a warning message will be logged. /// + /// `forceFullMetadata` defaults to `true`, so the plugin tries to get the full image metadata which may require + /// extra permission requests on certain platforms. + /// If `forceFullMetadata` is set to `false`, the plugin fetches the image in a way that reduces permission requests + /// from the platform (e.g on iOS the plugin won’t ask for the `NSPhotoLibraryUsageDescription` permission). + /// /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. /// Defaults to [CameraDevice.rear]. Note that Android has no documented parameter for an intent to specify if @@ -205,6 +217,7 @@ class ImagePicker { double? maxWidth, double? maxHeight, int? imageQuality, + bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { return platform.getImage( @@ -212,6 +225,7 @@ class ImagePicker { maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality, + forceFullMetadata: forceFullMetadata, preferredCameraDevice: preferredCameraDevice, ); } diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index 4becca930261..f43c4f6b2abc 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/master/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.3+3 +version: 0.8.4 environment: sdk: ">=2.12.0 <3.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..21294f99526c 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 @@ -57,14 +57,16 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 1, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), ], ); @@ -103,49 +105,56 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), ], ); @@ -182,6 +191,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 0, + 'forceFullMetadata': true, }), ], ); @@ -201,6 +211,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 1, + 'forceFullMetadata': true, }), ], ); 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 960dfe6917ea..7e960c5b4b69 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.dart @@ -52,14 +52,16 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 1, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), ], ); @@ -98,49 +100,56 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), ], ); @@ -177,6 +186,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 0, + 'forceFullMetadata': true, }), ], ); @@ -196,6 +206,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 1, + 'forceFullMetadata': true, }), ], ); 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 b170ee3256ab..3126c11d828f 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 @@ -53,6 +53,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { double? maxWidth, double? maxHeight, int? imageQuality, + bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { String? capture = computeCaptureAttribute(source, preferredCameraDevice); diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index 97480e044284..6758829f1041 100644 --- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md +++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.3.1 + +* Add `forceFullMetadata` option to `pickImage`. + * To keep this non-breaking `forceFullMetadata` defaults to `true`, so the plugin tries + to get the full image metadata which may require extra permission requests on certain platforms. + * If `forceFullMetadata` is set to `false`, the plugin fetches the image in a way that reduces + permission requests from the platform (e.g on iOS the plugin won’t ask for the `NSPhotoLibraryUsageDescription` permission). + ## 2.3.0 * Updated `LostDataResponse` to include a `files` property, in case more than one file was recovered. 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 292cb814ddeb..fcf3d540fd09 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 @@ -24,6 +24,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { double? maxWidth, double? maxHeight, int? imageQuality, + bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { String? path = await _getImagePath( @@ -31,6 +32,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality, + forceFullMetadata: forceFullMetadata, preferredCameraDevice: preferredCameraDevice, ); return path != null ? PickedFile(path) : null; @@ -85,6 +87,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { double? maxWidth, double? maxHeight, int? imageQuality, + bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { @@ -107,6 +110,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { 'maxWidth': maxWidth, 'maxHeight': maxHeight, 'imageQuality': imageQuality, + 'forceFullMetadata': forceFullMetadata, 'cameraDevice': preferredCameraDevice.index }, ); @@ -183,6 +187,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { double? maxWidth, double? maxHeight, int? imageQuality, + bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { String? path = await _getImagePath( @@ -190,6 +195,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality, + forceFullMetadata: forceFullMetadata, preferredCameraDevice: preferredCameraDevice, ); return path != null ? XFile(path) : null; 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 5c1c8b698442..783e84f6693b 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 @@ -58,6 +58,11 @@ abstract class ImagePickerPlatform extends PlatformInterface { /// image types such as JPEG. If compression is not supported for the image that is picked, /// a warning message will be logged. /// + /// `forceFullMetadata` defaults to `true`, so the plugin tries to get the full image metadata which may require + /// extra permission requests on certain platforms. + /// If `forceFullMetadata` is set to `false`, the plugin fetches the image in a way that reduces permission requests + /// from the platform (e.g on iOS the plugin won’t ask for the `NSPhotoLibraryUsageDescription` permission). + /// /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. /// Defaults to [CameraDevice.rear]. Note that Android has no documented parameter for an intent to specify if @@ -73,6 +78,7 @@ abstract class ImagePickerPlatform extends PlatformInterface { double? maxWidth, double? maxHeight, int? imageQuality, + bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { throw UnimplementedError('pickImage() has not been implemented.'); @@ -164,6 +170,11 @@ abstract class ImagePickerPlatform extends PlatformInterface { /// image types such as JPEG. If compression is not supported for the image that is picked, /// a warning message will be logged. /// + /// `forceFullMetadata` defaults to `true`, so the plugin tries to get the full image metadata which may require + /// extra permission requests on certain platforms. + /// If `forceFullMetadata` is set to `false`, the plugin fetches the image in a way that reduces permission requests + /// from the platform (e.g on iOS the plugin won’t ask for the `NSPhotoLibraryUsageDescription` permission). + /// /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. /// Defaults to [CameraDevice.rear]. Note that Android has no documented parameter for an intent to specify if @@ -179,6 +190,7 @@ abstract class ImagePickerPlatform extends PlatformInterface { double? maxWidth, double? maxHeight, int? imageQuality, + bool forceFullMetadata = true, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { throw UnimplementedError('getImage() has not been implemented.'); diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index 2168ff0f778a..7af04192eb47 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.3.0 +version: 2.3.1 environment: sdk: ">=2.12.0 <3.0.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..a2d9568fc85d 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 @@ -40,14 +40,16 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 1, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), ], ); @@ -93,49 +95,56 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), ], ); @@ -196,6 +205,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 0, + 'forceFullMetadata': true, }), ], ); @@ -215,6 +225,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 1, + 'forceFullMetadata': true, }), ], ); @@ -509,14 +520,16 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 1, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), ], ); @@ -562,49 +575,56 @@ void main() { 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), isMethodCall('pickImage', arguments: { 'source': 0, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': 0, + 'forceFullMetadata': true, }), ], ); @@ -664,6 +684,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 0, + 'forceFullMetadata': true, }), ], ); @@ -683,6 +704,7 @@ void main() { 'maxHeight': null, 'imageQuality': null, 'cameraDevice': 1, + 'forceFullMetadata': true, }), ], );