From dc53982ebbcb8efb85ecf1805f7c7a6fdd01204a Mon Sep 17 00:00:00 2001 From: Vadim Date: Wed, 5 Jul 2023 12:46:03 +0200 Subject: [PATCH] implemented `VolumeKeysNotifier` --- lib/interactors/metering_interactor.dart | 6 +- lib/screens/metering/bloc_metering.dart | 20 +-- .../bloc_container_camera.dart | 37 ++--- .../provider_container_camera.dart | 2 + .../listener_volume_keys.dart | 28 ---- .../notifier_volume_keys.dart | 34 +++++ lib/screens/metering/flow_metering.dart | 25 ++-- test/screens/metering/bloc_metering_test.dart | 68 +++++++++- .../camera/bloc_container_camera_test.dart | 128 +++++++++++++++++- 9 files changed, 276 insertions(+), 72 deletions(-) delete mode 100644 lib/screens/metering/components/shared/volume_keys_listener/listener_volume_keys.dart create mode 100644 lib/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart diff --git a/lib/interactors/metering_interactor.dart b/lib/interactors/metering_interactor.dart index 53e0538..02cbde9 100644 --- a/lib/interactors/metering_interactor.dart +++ b/lib/interactors/metering_interactor.dart @@ -49,11 +49,7 @@ class MeteringInteractor { set film(Film value) => _userPreferencesService.film = value; VolumeAction get volumeAction => _userPreferencesService.volumeAction; - Stream volumeKeysStream() => _volumeEventsService - .volumeButtonsEventStream() - .where((event) => event == 24 || event == 25) - .map((event) => event == 24 ? VolumeKey.up : VolumeKey.down); - + /// Executes vibration if haptics are enabled in settings Future quickVibration() async { if (_userPreferencesService.haptics) await _hapticsService.quickVibration(); diff --git a/lib/screens/metering/bloc_metering.dart b/lib/screens/metering/bloc_metering.dart index 5228165..827a317 100644 --- a/lib/screens/metering/bloc_metering.dart +++ b/lib/screens/metering/bloc_metering.dart @@ -11,24 +11,20 @@ import 'package:lightmeter/screens/metering/communication/event_communication_me as communication_events; import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' as communication_states; -import 'package:lightmeter/screens/metering/components/shared/volume_keys_listener/listener_volume_keys.dart'; +import 'package:lightmeter/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart'; import 'package:lightmeter/screens/metering/event_metering.dart'; import 'package:lightmeter/screens/metering/state_metering.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class MeteringBloc extends Bloc { final MeteringInteractor _meteringInteractor; + final VolumeKeysNotifier _volumeKeysNotifier; final MeteringCommunicationBloc _communicationBloc; late final StreamSubscription _communicationSubscription; - late final VolumeKeysListener _volumeKeysListener = VolumeKeysListener( - _meteringInteractor, - action: VolumeAction.shutter, - onKey: (value) => add(const MeasureEvent()), - ); - MeteringBloc( this._meteringInteractor, + this._volumeKeysNotifier, this._communicationBloc, ) : super( MeteringDataState( @@ -39,6 +35,7 @@ class MeteringBloc extends Bloc { isMetering: false, ), ) { + _volumeKeysNotifier.addListener(onVolumeKey); _communicationSubscription = _communicationBloc.stream .where((state) => state is communication_states.ScreenState) .map((state) => state as communication_states.ScreenState) @@ -72,7 +69,7 @@ class MeteringBloc extends Bloc { @override Future close() async { - await _volumeKeysListener.dispose(); + _volumeKeysNotifier.removeListener(onVolumeKey); await _communicationSubscription.cancel(); return super.close(); } @@ -229,4 +226,11 @@ class MeteringBloc extends Bloc { ), ); } + + @visibleForTesting + void onVolumeKey() { + if (_meteringInteractor.volumeAction == VolumeAction.shutter) { + add(const MeasureEvent()); + } + } } diff --git a/lib/screens/metering/components/camera_container/bloc_container_camera.dart b/lib/screens/metering/components/camera_container/bloc_container_camera.dart index a23ac20..0e409cc 100644 --- a/lib/screens/metering/components/camera_container/bloc_container_camera.dart +++ b/lib/screens/metering/components/camera_container/bloc_container_camera.dart @@ -19,12 +19,14 @@ import 'package:lightmeter/screens/metering/components/camera_container/event_co import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart'; import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart'; import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart'; -import 'package:lightmeter/screens/metering/components/shared/volume_keys_listener/listener_volume_keys.dart'; +import 'package:lightmeter/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart'; import 'package:lightmeter/utils/log_2.dart'; class CameraContainerBloc extends EvSourceBlocBase { final MeteringInteractor _meteringInteractor; + final VolumeKeysNotifier _volumeKeysNotifier; late final _WidgetsBindingObserver _observer; + CameraController? _cameraController; static const _maxZoom = 7.0; @@ -38,26 +40,15 @@ class CameraContainerBloc extends EvSourceBlocBase close() async { WidgetsBinding.instance.removeObserver(_observer); - _volumeKeysListener.dispose(); + _volumeKeysNotifier.removeListener(onVolumeKey); unawaited(_cameraController?.dispose().then((_) => _cameraController = null)); communicationBloc.add(communication_event.MeteringEndedEvent(_ev100)); return super.close(); @@ -169,7 +160,9 @@ class CameraContainerBloc extends EvSourceBlocBase _onZoomChanged(ZoomChangedEvent event, Emitter emit) async { - if (_cameraController != null) { + if (_cameraController != null && + event.value >= _zoomRange!.start && + event.value <= _zoomRange!.end) { _cameraController!.setZoomLevel(event.value); _currentZoom = event.value; _emitActiveState(emit); @@ -240,6 +233,18 @@ class CameraContainerBloc extends EvSourceBlocBase CameraContainerBloc( context.get(), + context.get(), context.read(), )..add(const RequestPermissionEvent()), child: CameraContainer( diff --git a/lib/screens/metering/components/shared/volume_keys_listener/listener_volume_keys.dart b/lib/screens/metering/components/shared/volume_keys_listener/listener_volume_keys.dart deleted file mode 100644 index a6b6fe2..0000000 --- a/lib/screens/metering/components/shared/volume_keys_listener/listener_volume_keys.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:lightmeter/data/models/volume_action.dart'; -import 'package:lightmeter/interactors/metering_interactor.dart'; - -class VolumeKeysListener { - final MeteringInteractor _meteringInteractor; - late final StreamSubscription _volumeKeysSubscription; - - VolumeKeysListener( - this._meteringInteractor, { - required VolumeAction action, - required ValueChanged onKey, - }) { - _volumeKeysSubscription = _meteringInteractor.volumeKeysStream().listen((event) { - if (_meteringInteractor.volumeAction == action) { - onKey(event); - } - }); - - // TODO: add RouteObserver and disable overriden action if SettingScreen is opened - } - - Future dispose() async { - await _volumeKeysSubscription.cancel(); - } -} diff --git a/lib/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart b/lib/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart new file mode 100644 index 0000000..a60f70e --- /dev/null +++ b/lib/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:lightmeter/data/models/volume_action.dart'; +import 'package:lightmeter/data/volume_events_service.dart'; + +class VolumeKeysNotifier extends ChangeNotifier { + final VolumeEventsService volumeEventsService; + late final StreamSubscription _volumeKeysSubscription; + VolumeKey _value = VolumeKey.up; + + VolumeKeysNotifier(this.volumeEventsService) { + // TODO: add RouteObserver and disable overriden action if SettingScreen is opened + _volumeKeysSubscription = volumeEventsService + .volumeButtonsEventStream() + .where((event) => event == 24 || event == 25) + .map((event) => event == 24 ? VolumeKey.up : VolumeKey.down) + .listen((event) { + value = event; + }); + } + + VolumeKey get value => _value; + set value(VolumeKey newValue) { + _value = newValue; + notifyListeners(); + } + + @override + Future dispose() async { + await _volumeKeysSubscription.cancel(); + super.dispose(); + } +} diff --git a/lib/screens/metering/flow_metering.dart b/lib/screens/metering/flow_metering.dart index f4dae72..780f537 100644 --- a/lib/screens/metering/flow_metering.dart +++ b/lib/screens/metering/flow_metering.dart @@ -9,6 +9,7 @@ import 'package:lightmeter/data/volume_events_service.dart'; import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:lightmeter/screens/metering/bloc_metering.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; +import 'package:lightmeter/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart'; import 'package:lightmeter/screens/metering/screen_metering.dart'; import 'package:lightmeter/utils/inherited_generics.dart'; @@ -31,17 +32,21 @@ class _MeteringFlowState extends State { context.get(), context.get(), ), - child: MultiBlocProvider( - providers: [ - BlocProvider(create: (_) => MeteringCommunicationBloc()), - BlocProvider( - create: (context) => MeteringBloc( - context.get(), - context.read(), + child: InheritedWidgetBase( + data: VolumeKeysNotifier(context.get()), + child: MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => MeteringCommunicationBloc()), + BlocProvider( + create: (context) => MeteringBloc( + context.get(), + context.get(), + context.read(), + ), ), - ), - ], - child: const MeteringScreen(), + ], + child: const MeteringScreen(), + ), ), ); } diff --git a/test/screens/metering/bloc_metering_test.dart b/test/screens/metering/bloc_metering_test.dart index 7e87a27..60c6f34 100644 --- a/test/screens/metering/bloc_metering_test.dart +++ b/test/screens/metering/bloc_metering_test.dart @@ -1,5 +1,6 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:lightmeter/data/models/film.dart'; +import 'package:lightmeter/data/models/volume_action.dart'; import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:lightmeter/screens/metering/bloc_metering.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; @@ -7,20 +8,24 @@ import 'package:lightmeter/screens/metering/communication/event_communication_me as communication_events; import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' as communication_states; +import 'package:lightmeter/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart'; import 'package:lightmeter/screens/metering/event_metering.dart'; import 'package:lightmeter/screens/metering/state_metering.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; +class _MockMeteringInteractor extends Mock implements MeteringInteractor {} + +class _MockVolumeKeysNotifier extends Mock implements VolumeKeysNotifier {} + class _MockMeteringCommunicationBloc extends MockBloc< communication_events.MeteringCommunicationEvent, communication_states.MeteringCommunicationState> implements MeteringCommunicationBloc {} -class _MockMeteringInteractor extends Mock implements MeteringInteractor {} - void main() { late _MockMeteringInteractor meteringInteractor; + late _MockVolumeKeysNotifier volumeKeysNotifier; late _MockMeteringCommunicationBloc communicationBloc; late MeteringBloc bloc; const iso100 = IsoValue(100, StopType.full); @@ -34,16 +39,19 @@ void main() { when(meteringInteractor.responseVibration).thenAnswer((_) async {}); when(meteringInteractor.errorVibration).thenAnswer((_) async {}); + volumeKeysNotifier = _MockVolumeKeysNotifier(); communicationBloc = _MockMeteringCommunicationBloc(); - + bloc = MeteringBloc( meteringInteractor, + volumeKeysNotifier, communicationBloc, ); }); tearDown(() { bloc.close(); + //volumeKeysNotifier.dispose(); communicationBloc.close(); }); @@ -606,4 +614,58 @@ void main() { ); }, ); + + group( + '`Volume keys shutter action`', + () { + blocTest( + 'Add/remove listener', + build: () => bloc, + verify: (_) { + verify(() => volumeKeysNotifier.addListener(bloc.onVolumeKey)).called(1); + verify(() => volumeKeysNotifier.removeListener(bloc.onVolumeKey)).called(1); + }, + expect: () => [], + ); + + blocTest( + 'onVolumeKey & VolumeAction.shutter', + build: () => bloc, + act: (bloc) async { + bloc.onVolumeKey(); + }, + setUp: () { + when(() => meteringInteractor.volumeAction).thenReturn(VolumeAction.shutter); + }, + verify: (_) {}, + expect: () => [isA()], + ); + + blocTest( + 'onVolumeKey & VolumeAction.zoom', + build: () => bloc, + act: (bloc) async { + bloc.onVolumeKey(); + }, + setUp: () { + when(() => meteringInteractor.volumeAction).thenReturn(VolumeAction.zoom); + }, + verify: (_) {}, + expect: () => [], + ); + + blocTest( + 'onVolumeKey & VolumeAction.none', + build: () => bloc, + act: (bloc) async { + bloc.onVolumeKey(); + }, + setUp: () { + when(() => meteringInteractor.volumeAction).thenReturn(VolumeAction.none); + }, + verify: (_) {}, + expect: () => [], + ); + }, + ); } diff --git a/test/screens/metering/components/camera/bloc_container_camera_test.dart b/test/screens/metering/components/camera/bloc_container_camera_test.dart index e720ef5..1264082 100644 --- a/test/screens/metering/components/camera/bloc_container_camera_test.dart +++ b/test/screens/metering/components/camera/bloc_container_camera_test.dart @@ -2,6 +2,7 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:lightmeter/data/models/volume_action.dart'; import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' @@ -12,18 +13,22 @@ import 'package:lightmeter/screens/metering/components/camera_container/bloc_con import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart'; import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart'; +import 'package:lightmeter/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart'; import 'package:mocktail/mocktail.dart'; +class _MockMeteringInteractor extends Mock implements MeteringInteractor {} + +class _MockVolumeKeysNotifier extends Mock implements VolumeKeysNotifier {} + class _MockMeteringCommunicationBloc extends MockBloc< communication_events.MeteringCommunicationEvent, communication_states.MeteringCommunicationState> implements MeteringCommunicationBloc {} -class _MockMeteringInteractor extends Mock implements MeteringInteractor {} - void main() { TestWidgetsFlutterBinding.ensureInitialized(); late _MockMeteringInteractor meteringInteractor; + late _MockVolumeKeysNotifier volumeKeysNotifier; late _MockMeteringCommunicationBloc communicationBloc; late CameraContainerBloc bloc; @@ -112,6 +117,7 @@ void main() { setUpAll(() { meteringInteractor = _MockMeteringInteractor(); + volumeKeysNotifier = _MockVolumeKeysNotifier(); communicationBloc = _MockMeteringCommunicationBloc(); when(() => meteringInteractor.cameraEvCalibration).thenReturn(0.0); @@ -121,6 +127,7 @@ void main() { setUp(() { bloc = CameraContainerBloc( meteringInteractor, + volumeKeysNotifier, communicationBloc, ); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger @@ -476,6 +483,123 @@ void main() { ); }, ); + + group( + '`Volume keys shutter action`', + () { + blocTest( + 'Add/remove listener', + build: () => bloc, + verify: (_) { + verify(() => volumeKeysNotifier.addListener(bloc.onVolumeKey)).called(1); + verify(() => volumeKeysNotifier.removeListener(bloc.onVolumeKey)).called(1); + }, + expect: () => [], + ); + + blocTest( + 'onVolumeKey & VolumeAction.shutter', + build: () => bloc, + act: (bloc) async { + bloc.onVolumeKey(); + }, + setUp: () { + when(() => meteringInteractor.volumeAction).thenReturn(VolumeAction.shutter); + }, + verify: (_) {}, + expect: () => [], + ); + + blocTest( + 'onVolumeKey.up & VolumeAction.zoom', + build: () => bloc, + act: (bloc) async { + bloc.add(const InitializeEvent()); + await Future.delayed(Duration.zero); + bloc.onVolumeKey(); + await Future.delayed(Duration.zero); + bloc.onVolumeKey(); + await Future.delayed(Duration.zero); + bloc.onVolumeKey(); + }, + setUp: () { + when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true); + when(() => meteringInteractor.volumeAction).thenReturn(VolumeAction.zoom); + when(() => volumeKeysNotifier.value).thenReturn(VolumeKey.up); + }, + verify: (_) {}, + expect: () => [ + ...initializedStateSequence, + isA() + .having((state) => state.zoomRange, 'zoomRange', const RangeValues(1.0, 7.0)) + .having((state) => state.currentZoom, 'currentZoom', 1.5) + .having( + (state) => state.exposureOffsetRange, + 'exposureOffsetRange', + const RangeValues(-4.0, 4.0), + ) + .having((state) => state.exposureOffsetStep, 'exposureOffsetStep', 0.1666666) + .having((state) => state.currentExposureOffset, 'currentExposureOffset', 0.0), + isA() + .having((state) => state.zoomRange, 'zoomRange', const RangeValues(1.0, 7.0)) + .having((state) => state.currentZoom, 'currentZoom', 2.0) + .having( + (state) => state.exposureOffsetRange, + 'exposureOffsetRange', + const RangeValues(-4.0, 4.0), + ) + .having((state) => state.exposureOffsetStep, 'exposureOffsetStep', 0.1666666) + .having((state) => state.currentExposureOffset, 'currentExposureOffset', 0.0), + isA() + .having((state) => state.zoomRange, 'zoomRange', const RangeValues(1.0, 7.0)) + .having((state) => state.currentZoom, 'currentZoom', 2.5) + .having( + (state) => state.exposureOffsetRange, + 'exposureOffsetRange', + const RangeValues(-4.0, 4.0), + ) + .having((state) => state.exposureOffsetStep, 'exposureOffsetStep', 0.1666666) + .having((state) => state.currentExposureOffset, 'currentExposureOffset', 0.0), + ], + ); + + blocTest( + 'onVolumeKey.down & VolumeAction.zoom', + build: () => bloc, + act: (bloc) async { + bloc.add(const InitializeEvent()); + await Future.delayed(Duration.zero); + bloc.onVolumeKey(); + await Future.delayed(Duration.zero); + bloc.onVolumeKey(); + await Future.delayed(Duration.zero); + bloc.onVolumeKey(); + }, + setUp: () { + when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true); + when(() => meteringInteractor.volumeAction).thenReturn(VolumeAction.zoom); + when(() => volumeKeysNotifier.value).thenReturn(VolumeKey.down); + }, + verify: (_) {}, + expect: () => [ + ...initializedStateSequence, + ], + ); + + blocTest( + 'onVolumeKey & VolumeAction.none', + build: () => bloc, + act: (bloc) async { + bloc.onVolumeKey(); + }, + setUp: () { + when(() => meteringInteractor.volumeAction).thenReturn(VolumeAction.none); + }, + verify: (_) {}, + expect: () => [], + ); + }, + ); } extension _MethodChannelMock on MethodChannel {