ML-11 Implement volume buttons actions (#86)

* [Android] wip

* implemented `VolumeEventsService`

* implemented `VolumeKeysListener` (wip)

* Added screenshots links

* [Android] nullable typo

* implemented `VolumeKeysNotifier`

* deinitialize camera when on Settings screen

* disable volume handling when on Settings screen

* used "platform" package to mock `isAndroid`

* init/deinit camera on settings open

* allow volume action override only on metering screen

* lints

* cleanup

* await dispose

* tests

* reduced `SwitchListTile.contentPadding`

* fixed tests

* removed `VolumeAction.zoom`

* added social preview

* typo

* fixed `CameraContainerBloc` tests

* added `Stream.empty()` tests
This commit is contained in:
Vadim 2023-07-09 13:39:33 +02:00 committed by GitHub
parent ed83540dde
commit e001c153fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 758 additions and 124 deletions

View file

@ -1,58 +1,48 @@
<p align="center">
<img src="assets/launcher_icon_circle.png" width="100" height="100">
</p>
<p align="center", style="font-size:60px;">
<b>Material Lightmeter</b>
</p>
<img src="resources/social_preview.png" width="100%" />
# Table of contents
- [Table of contents](#table-of-contents)
- [Backstory](#backstory)
- [Legacy features](#legacy-features)
- [Screenshots](#screenshots)
- [Build](#build)
- [Contribution](#contribution)
- [iOS Limitations](#ios-limitations)
# Backstory
Some time ago I've started developing the [Material Lightmeter](https://play.google.com/store/apps/details?id=com.vodemn.lightmeter&hl=en&gl=US) app. Unfortunately, the last update of this app was almost a year prior to creation of this repo. So after reading some positive review on Google Play saying that "this is an excellent app, too bad it is no longer updated", I've decided to make an update and also make this app open source. Maybe someone sometime will decide to contribute to this project.
But as the existing repo contained some sensitive data, that I've pushed due to lack of experience, I had to make a new one. And if creating a new repo, why not rewrite the app from scratch?)
But as the existing repo contained some sensitive data, that I've pushed due to lack of experience, I had to make a new one. And if creating a new repo, why not rewrite the app from scratch?
Without further delay behold my new Lightmeter app inspired by Material You (a.k.a. M3)
# Legacy features
# Screenshots
The list of features that the old lightmeter app has and that have to be implemeneted in the M3 lightmeter.
<p float="center">
<img src="https://lh3.googleusercontent.com/8Sd-pmNcQ0xAr5opuTeJKWr2OXeQvCoFSdVDSoKQSHHKeNmqF71hqeAdm3yjunY12zY" width="18.8%" />
<img src="https://lh3.googleusercontent.com/rqBv8pT0AdcBy0xEgQY2unV-YEQ5KfUkandAxJ62yYCiSF72HClA_tkb4JT_3UPaIfFP" width="18.8%" />
<img src="https://lh3.googleusercontent.com/-SnYbYSugVfdwYi6m_rd9CzpCZMCIfudhnq0zRIlzEtLSXhrwziWVd2hotygfqiSofI" width="18.8%" />
<img src="https://lh3.googleusercontent.com/UXxptL_dAIJDtrmpEZuSz39Iq4HuPb3ZPeuANfE9XH0De0uZQT83LNdu1AObBPobpg" width="18.8%" />
<img src="https://lh3.googleusercontent.com/15g_SPV8knDLFbz1_-wGNJFsJeyVWZ_y--TGHpk75MaaIdMDyTXY2_TL-Aw8bpOhpw" width="18.8%" />
</p>
### Metering
- [x] ISO selecting
- [x] Reciprocity for different films
- [x] Reflected light metering
- [x] Incident light metering
### Adjust
- [x] Light sources EV calibration
- [ ] Customizable aperture range
- [ ] Customizable shutter speed range
- [x] ND filter select
### General
- [x] Caffeine
- [x] Vibration
- [ ] Volume button actions
### Theme
- [x] Dark theme
- [x] Picking primary color
- [x] Russian language
## Build
# Build
As part of this project is private, you will be able to run this app from the _main_dev.dart_ file (i.e. --flavor dev). Also to avoid fatal errors the _main_prod.dart_ file is excluded from analysis.
## Contribution
# Contribution
To report a bug or suggest a new feature open a new [issue](https://github.com/vodemn/m3_lightmeter/issues).
In case you want to help develop this project you need to follow this [style guide](doc/style_guide.md).
# iOS Limitations
A list of features, that Android version of the app has and that iOS does not.
## Incident light metering
Apple does not provide API for reading Lux stream form the ambient light sensor. Lux can be calculated based on front camera image stream, but this would be a reflected light. So there is no way incident light metering can be implemented on iOS.
## Volume buttons action
This can be [implemented](https://stackoverflow.com/questions/70161271/ios-override-hardware-volume-buttons-same-as-zello) but the app will be rejected due to [2.5.9](https://developer.apple.com/app-store/review/guidelines/#software-requirements)

View file

@ -1,13 +1,22 @@
package com.vodemn.lightmeter
import android.os.Bundle
import android.view.KeyEvent
import android.view.WindowManager
import androidx.core.view.WindowCompat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
private lateinit var keepScreenOnChannel: MethodChannel
private lateinit var volumeHandlingChannel: MethodChannel
private lateinit var volumeEventChannel: EventChannel
private var volumeEventsEmitter: EventSink? = null
private var handleVolume = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
@ -15,10 +24,11 @@ class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(
keepScreenOnChannel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"com.vodemn.lightmeter/keepScreenOn"
).setMethodCallHandler { call, result ->
)
keepScreenOnChannel.setMethodCallHandler { call, result ->
when (call.method) {
"isKeepScreenOn" -> result.success((window.attributes.flags and WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0)
"setKeepScreenOn" -> {
@ -33,5 +43,53 @@ class MainActivity : FlutterActivity() {
else -> result.notImplemented()
}
}
volumeHandlingChannel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"com.vodemn.lightmeter/volumeHandling"
)
volumeHandlingChannel.setMethodCallHandler { call, result ->
when (call.method) {
"setVolumeHandling" -> {
handleVolume = call.arguments as Boolean
result.success(handleVolume)
}
else -> result.notImplemented()
}
}
volumeEventChannel = EventChannel(
flutterEngine.dartExecutor.binaryMessenger,
"com.vodemn.lightmeter/volumeEvents"
)
volumeEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(listener: Any?, eventSink: EventSink) {
volumeEventsEmitter = eventSink
}
override fun onCancel(listener: Any?) {
volumeEventsEmitter = null
}
})
}
override fun onDestroy() {
keepScreenOnChannel.setMethodCallHandler(null)
volumeHandlingChannel.setMethodCallHandler(null)
volumeEventChannel.setStreamHandler(null)
super.onDestroy()
}
override fun onKeyDown(code: Int, event: KeyEvent): Boolean {
return when (val keyCode: Int = event.keyCode) {
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> {
if (handleVolume) {
volumeEventsEmitter?.success(keyCode)
true
} else {
super.onKeyDown(code, event)
}
}
else -> super.onKeyDown(code, event)
}
}
}

View file

@ -1,9 +1,15 @@
import 'package:light_sensor/light_sensor.dart';
import 'package:platform/platform.dart';
class LightSensorService {
const LightSensorService();
final LocalPlatform localPlatform;
const LightSensorService(this.localPlatform);
Future<bool> hasSensor() async {
if (!localPlatform.isAndroid) {
return false;
}
try {
return await LightSensor.hasSensor ?? false;
} catch (_) {
@ -11,5 +17,10 @@ class LightSensorService {
}
}
Stream<int> luxStream() => LightSensor.lightSensorStream;
Stream<int> luxStream() {
if (!localPlatform.isAndroid) {
return const Stream<int>.empty();
}
return LightSensor.lightSensorStream;
}
}

View file

@ -0,0 +1,3 @@
enum VolumeAction { shutter, none }
enum VolumeKey { up, down }

View file

@ -6,6 +6,7 @@ import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/data/models/supported_locale.dart';
import 'package:lightmeter/data/models/theme_type.dart';
import 'package:lightmeter/data/models/volume_action.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -22,6 +23,7 @@ class UserPreferencesService {
static const caffeineKey = "caffeine";
static const hapticsKey = "haptics";
static const volumeActionKey = "volumeAction";
static const localeKey = "locale";
static const themeTypeKey = "themeType";
@ -82,9 +84,6 @@ class UserPreferencesService {
EvSourceType.values[_sharedPreferences.getInt(evSourceTypeKey) ?? 0];
set evSourceType(EvSourceType value) => _sharedPreferences.setInt(evSourceTypeKey, value.index);
bool get caffeine => _sharedPreferences.getBool(caffeineKey) ?? false;
set caffeine(bool value) => _sharedPreferences.setBool(caffeineKey, value);
StopType get stopType => StopType.values[_sharedPreferences.getInt(stopTypeKey) ?? 2];
set stopType(StopType value) => _sharedPreferences.setInt(stopTypeKey, value.index);
@ -105,9 +104,19 @@ class UserPreferencesService {
set meteringScreenLayout(MeteringScreenLayoutConfig value) =>
_sharedPreferences.setString(meteringScreenLayoutKey, json.encode(value.toJson()));
bool get caffeine => _sharedPreferences.getBool(caffeineKey) ?? false;
set caffeine(bool value) => _sharedPreferences.setBool(caffeineKey, value);
bool get haptics => _sharedPreferences.getBool(hapticsKey) ?? true;
set haptics(bool value) => _sharedPreferences.setBool(hapticsKey, value);
VolumeAction get volumeAction => VolumeAction.values.firstWhere(
(e) => e.toString() == _sharedPreferences.getString(volumeActionKey),
orElse: () => VolumeAction.shutter,
);
set volumeAction(VolumeAction value) =>
_sharedPreferences.setString(volumeActionKey, value.toString());
SupportedLocale get locale => SupportedLocale.values.firstWhere(
(e) => e.toString() == _sharedPreferences.getString(localeKey),
orElse: () => SupportedLocale.en,

View file

@ -0,0 +1,40 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:platform/platform.dart';
class VolumeEventsService {
final LocalPlatform localPlatform;
@visibleForTesting
static const volumeHandlingChannel = MethodChannel("com.vodemn.lightmeter/volumeHandling");
@visibleForTesting
static const volumeEventsChannel = EventChannel("com.vodemn.lightmeter/volumeEvents");
const VolumeEventsService(this.localPlatform);
/// If set to `false` we allow system to handle key events.
/// Returns current status of volume handling.
Future<bool> setVolumeHandling(bool enableHandling) async {
if (!localPlatform.isAndroid) {
return false;
}
return volumeHandlingChannel
.invokeMethod<bool>("setVolumeHandling", enableHandling)
.then((value) => value!);
}
/// Emits new events on
/// KEYCODE_VOLUME_UP = 24;
/// KEYCODE_VOLUME_DOWN = 25;
/// pressed
Stream<int> volumeButtonsEventStream() {
if (!localPlatform.isAndroid) {
return const Stream.empty();
}
return volumeEventsChannel
.receiveBroadcastStream()
.cast<int>()
.where((event) => event == 24 || event == 25);
}
}

View file

@ -1,12 +1,12 @@
import 'dart:io';
import 'package:app_settings/app_settings.dart';
import 'package:lightmeter/data/caffeine_service.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:lightmeter/data/models/film.dart';
import 'package:lightmeter/data/models/volume_action.dart';
import 'package:lightmeter/data/permissions_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/data/volume_events_service.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:permission_handler/permission_handler.dart';
@ -16,6 +16,7 @@ class MeteringInteractor {
final HapticsService _hapticsService;
final PermissionsService _permissionsService;
final LightSensorService _lightSensorService;
final VolumeEventsService _volumeEventsService;
MeteringInteractor(
this._userPreferencesService,
@ -23,10 +24,13 @@ class MeteringInteractor {
this._hapticsService,
this._permissionsService,
this._lightSensorService,
this._volumeEventsService,
) {
if (_userPreferencesService.caffeine) {
_caffeineService.keepScreenOn(true);
}
_volumeEventsService
.setVolumeHandling(_userPreferencesService.volumeAction != VolumeAction.none);
}
double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration;
@ -42,6 +46,8 @@ class MeteringInteractor {
Film get film => _userPreferencesService.film;
set film(Film value) => _userPreferencesService.film = value;
VolumeAction get volumeAction => _userPreferencesService.volumeAction;
/// Executes vibration if haptics are enabled in settings
Future<void> quickVibration() async {
if (_userPreferencesService.haptics) await _hapticsService.quickVibration();
@ -73,13 +79,7 @@ class MeteringInteractor {
AppSettings.openAppSettings();
}
Future<bool> hasAmbientLightSensor() async {
if (Platform.isAndroid) {
return _lightSensorService.hasSensor();
} else {
return false;
}
}
Future<bool> hasAmbientLightSensor() async => _lightSensorService.hasSensor();
Stream<int> luxStream() => _lightSensorService.luxStream();
}

View file

@ -1,16 +1,20 @@
import 'package:lightmeter/data/caffeine_service.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/models/volume_action.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/data/volume_events_service.dart';
class SettingsInteractor {
final UserPreferencesService _userPreferencesService;
final CaffeineService _caffeineService;
final HapticsService _hapticsService;
final VolumeEventsService _volumeEventsService;
const SettingsInteractor(
this._userPreferencesService,
this._caffeineService,
this._hapticsService,
this._volumeEventsService,
);
double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration;
@ -27,6 +31,20 @@ class SettingsInteractor {
});
}
Future<void> disableVolumeHandling() async {
await _volumeEventsService.setVolumeHandling(false);
}
Future<void> restoreVolumeHandling() async {
await _volumeEventsService
.setVolumeHandling(_userPreferencesService.volumeAction != VolumeAction.none);
}
VolumeAction get volumeAction => _userPreferencesService.volumeAction;
Future<void> setVolumeAction(VolumeAction value) async {
await _volumeEventsService.setVolumeHandling(value != VolumeAction.none);
_userPreferencesService.volumeAction = value;
}
bool get isHapticsEnabled => _userPreferencesService.haptics;
void enableHaptics(bool enable) {
_userPreferencesService.haptics = enable;

View file

@ -56,6 +56,7 @@
"general": "General",
"keepScreenOn": "Keep screen on",
"haptics": "Haptics",
"volumeKeysAction": "Shutter by volume keys",
"language": "Language",
"chooseLanguage": "Choose language",
"theme": "Theme",

View file

@ -56,6 +56,7 @@
"general": "Général",
"keepScreenOn": "Garder l'écran allumé",
"haptics": "Haptiques",
"volumeKeysAction": "Obturateur par boutons de volume",
"language": "Langue",
"chooseLanguage": "Choisissez la langue",
"theme": "Thème",

View file

@ -56,6 +56,7 @@
"general": "Общие",
"keepScreenOn": "Запрет блокировки",
"haptics": "Вибрация",
"volumeKeysAction": "Затвор по кнопкам громкости",
"language": "Язык",
"chooseLanguage": "Выберите язык",
"theme": "Тема",

View file

@ -6,6 +6,5 @@ import 'package:lightmeter/firebase.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeFirebase();
runApp(const Application(Environment.prod()));
}

View file

@ -1,11 +1,10 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:lightmeter/data/caffeine_service.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:lightmeter/data/permissions_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/data/volume_events_service.dart';
import 'package:lightmeter/environment.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/providers/ev_source_type_provider.dart';
@ -14,6 +13,7 @@ import 'package:lightmeter/providers/stop_type_provider.dart';
import 'package:lightmeter/providers/supported_locale_provider.dart';
import 'package:lightmeter/providers/theme_provider.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
import 'package:platform/platform.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LightmeterProviders extends StatelessWidget {
@ -27,7 +27,7 @@ class LightmeterProviders extends StatelessWidget {
return FutureBuilder(
future: Future.wait([
SharedPreferences.getInstance(),
if (Platform.isAndroid) const LightSensorService().hasSensor() else Future.value(false),
const LightSensorService(LocalPlatform()).hasSensor(),
]),
builder: (_, snapshot) {
if (snapshot.data != null) {
@ -36,11 +36,13 @@ class LightmeterProviders extends StatelessWidget {
child: InheritedWidgetBase<UserPreferencesService>(
data: UserPreferencesService(snapshot.data![0] as SharedPreferences),
child: InheritedWidgetBase<LightSensorService>(
data: const LightSensorService(),
data: const LightSensorService(LocalPlatform()),
child: InheritedWidgetBase<CaffeineService>(
data: const CaffeineService(),
child: InheritedWidgetBase<HapticsService>(
data: const HapticsService(),
child: InheritedWidgetBase<VolumeEventsService>(
data: const VolumeEventsService(LocalPlatform()),
child: InheritedWidgetBase<PermissionsService>(
data: const PermissionsService(),
child: MeteringScreenLayoutProvider(
@ -63,6 +65,7 @@ class LightmeterProviders extends StatelessWidget {
),
),
),
),
);
} else if (snapshot.error != null) {
return Center(child: Text(snapshot.error!.toString()));

View file

@ -4,23 +4,27 @@ import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.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/communication/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
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';
class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
final MeteringInteractor _meteringInteractor;
final VolumeKeysNotifier _volumeKeysNotifier;
final MeteringCommunicationBloc _communicationBloc;
late final StreamSubscription<communication_states.ScreenState> _communicationSubscription;
MeteringBloc(
this._meteringInteractor,
this._volumeKeysNotifier,
this._communicationBloc,
) : super(
MeteringDataState(
@ -31,6 +35,7 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
isMetering: false,
),
) {
_volumeKeysNotifier.addListener(onVolumeKey);
_communicationSubscription = _communicationBloc.stream
.where((state) => state is communication_states.ScreenState)
.map((state) => state as communication_states.ScreenState)
@ -43,6 +48,8 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
on<MeasureEvent>(_onMeasure, transformer: droppable());
on<MeasuredEvent>(_onMeasured);
on<MeasureErrorEvent>(_onMeasureError);
on<SettingsOpenedEvent>(_onSettingsOpened);
on<SettingsClosedEvent>(_onSettingsClosed);
}
@override
@ -64,6 +71,7 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
@override
Future<void> close() async {
_volumeKeysNotifier.removeListener(onVolumeKey);
await _communicationSubscription.cancel();
return super.close();
}
@ -220,4 +228,19 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
),
);
}
@visibleForTesting
void onVolumeKey() {
if (_meteringInteractor.volumeAction == VolumeAction.shutter) {
add(const MeasureEvent());
}
}
void _onSettingsOpened(SettingsOpenedEvent _, Emitter __) {
_communicationBloc.add(const communication_events.SettingsOpenedEvent());
}
void _onSettingsClosed(SettingsClosedEvent _, Emitter __) {
_communicationBloc.add(const communication_events.SettingsClosedEvent());
}
}

View file

@ -11,5 +11,7 @@ class MeteringCommunicationBloc
on<MeasureEvent>((_, emit) => emit(MeasureState()));
on<MeteringInProgressEvent>((event, emit) => emit(MeteringInProgressState(event.ev100)));
on<MeteringEndedEvent>((event, emit) => emit(MeteringEndedState(event.ev100)));
on<SettingsOpenedEvent>((_, emit) => emit(const SettingsOpenedState()));
on<SettingsClosedEvent>((_, emit) => emit(const SettingsClosedState()));
}
}

View file

@ -47,3 +47,11 @@ class MeteringEndedEvent extends MeasuredEvent {
@override
int get hashCode => Object.hash(ev100, runtimeType);
}
class SettingsOpenedEvent extends ScreenEvent {
const SettingsOpenedEvent();
}
class SettingsClosedEvent extends ScreenEvent {
const SettingsClosedEvent();
}

View file

@ -51,3 +51,11 @@ class MeteringEndedState extends MeasuredState {
@override
int get hashCode => Object.hash(ev100, runtimeType);
}
class SettingsOpenedState extends SourceState {
const SettingsOpenedState();
}
class SettingsClosedState extends SourceState {
const SettingsClosedState();
}

View file

@ -23,6 +23,7 @@ import 'package:lightmeter/utils/log_2.dart';
class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraContainerState> {
final MeteringInteractor _meteringInteractor;
late final _WidgetsBindingObserver _observer;
CameraController? _cameraController;
static const _maxZoom = 7.0;
@ -36,6 +37,8 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
double? _ev100 = 0.0;
bool _settingsOpened = false;
CameraContainerBloc(
this._meteringInteractor,
MeteringCommunicationBloc communicationBloc,
@ -65,7 +68,8 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
@override
void onCommunicationState(communication_states.SourceState communicationState) {
if (communicationState is communication_states.MeasureState) {
switch (communicationState) {
case communication_states.MeasureState():
if (_canTakePhoto) {
_takePhoto().then((ev100Raw) {
if (ev100Raw != null) {
@ -77,6 +81,13 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
}
});
}
case communication_states.SettingsOpenedState():
_settingsOpened = true;
add(const DeinitializeEvent());
case communication_states.SettingsClosedState():
_settingsOpened = false;
add(const InitializeEvent());
default:
}
}
@ -148,12 +159,15 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
}
Future<void> _onDeinitialize(DeinitializeEvent _, Emitter emit) async {
emit(const CameraLoadingState());
unawaited(_cameraController?.dispose().then((_) => _cameraController = null));
emit(const CameraInitState());
communicationBloc.add(communication_event.MeteringEndedEvent(_ev100));
await _cameraController?.dispose().then((_) => _cameraController = null);
}
Future<void> _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);
@ -215,6 +229,7 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
}
Future<void> _appLifecycleStateObserver(AppLifecycleState state) async {
if (!_settingsOpened) {
switch (state) {
case AppLifecycleState.resumed:
add(const InitializeEvent());
@ -225,6 +240,7 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
}
}
}
}
/// This is needed only because we cannot use `with` with mixins
class _WidgetsBindingObserver with WidgetsBindingObserver {

View file

@ -25,37 +25,44 @@ class LightSensorContainerBloc
communicationBloc,
const LightSensorContainerState(null),
) {
on<StartLuxMeteringEvent>(_onStartLuxMeteringEvent);
on<LuxMeteringEvent>(_onLuxMeteringEvent);
on<CancelLuxMeteringEvent>(_onCancelLuxMeteringEvent);
}
@override
void onCommunicationState(communication_states.SourceState communicationState) {
if (communicationState is communication_states.MeasureState) {
switch (communicationState) {
case communication_states.MeasureState():
if (_luxSubscriptions == null) {
_startMetering();
add(const StartLuxMeteringEvent());
} else {
_cancelMetering();
add(const CancelLuxMeteringEvent());
}
case communication_states.SettingsOpenedState():
add(const CancelLuxMeteringEvent());
default:
}
}
@override
Future<void> close() async {
_cancelMetering();
communicationBloc.add(communication_event.MeteringEndedEvent(state.ev100));
_luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null);
return super.close();
}
void _onStartLuxMeteringEvent(StartLuxMeteringEvent event, _) {
_luxSubscriptions = _meteringInteractor.luxStream().listen((lux) => add(LuxMeteringEvent(lux)));
}
void _onLuxMeteringEvent(LuxMeteringEvent event, Emitter<LightSensorContainerState> emit) {
final ev100 = log2(event.lux.toDouble() / 2.5) + _meteringInteractor.lightSensorEvCalibration;
emit(LightSensorContainerState(ev100));
communicationBloc.add(communication_event.MeteringInProgressEvent(ev100));
}
void _startMetering() {
_luxSubscriptions = _meteringInteractor.luxStream().listen((lux) => add(LuxMeteringEvent(lux)));
}
void _cancelMetering() {
void _onCancelLuxMeteringEvent(CancelLuxMeteringEvent event, _) {
communicationBloc.add(communication_event.MeteringEndedEvent(state.ev100));
_luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null);
}

View file

@ -2,8 +2,16 @@ abstract class LightSensorContainerEvent {
const LightSensorContainerEvent();
}
class StartLuxMeteringEvent extends LightSensorContainerEvent {
const StartLuxMeteringEvent();
}
class LuxMeteringEvent extends LightSensorContainerEvent {
final int lux;
const LuxMeteringEvent(this.lux);
}
class CancelLuxMeteringEvent extends LightSensorContainerEvent {
const CancelLuxMeteringEvent();
}

View file

@ -0,0 +1,32 @@
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 with RouteAware {
final VolumeEventsService volumeEventsService;
late final StreamSubscription<VolumeKey> _volumeKeysSubscription;
VolumeKey _value = VolumeKey.up;
VolumeKeysNotifier(this.volumeEventsService) {
_volumeKeysSubscription = volumeEventsService
.volumeButtonsEventStream()
.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<void> dispose() async {
await _volumeKeysSubscription.cancel();
super.dispose();
}
}

View file

@ -45,3 +45,11 @@ class MeasureErrorEvent extends MeteringEvent {
const MeasureErrorEvent({required this.isMetering});
}
class SettingsOpenedEvent extends MeteringEvent {
const SettingsOpenedEvent();
}
class SettingsClosedEvent extends MeteringEvent {
const SettingsClosedEvent();
}

View file

@ -5,9 +5,11 @@ import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:lightmeter/data/permissions_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
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';
@ -28,19 +30,24 @@ class _MeteringFlowState extends State<MeteringFlow> {
context.get<HapticsService>(),
context.get<PermissionsService>(),
context.get<LightSensorService>(),
context.get<VolumeEventsService>(),
),
child: InheritedWidgetBase<VolumeKeysNotifier>(
data: VolumeKeysNotifier(context.get<VolumeEventsService>()),
child: MultiBlocProvider(
providers: [
BlocProvider(create: (_) => MeteringCommunicationBloc()),
BlocProvider(
create: (context) => MeteringBloc(
context.get<MeteringInteractor>(),
context.get<VolumeKeysNotifier>(),
context.read<MeteringCommunicationBloc>(),
),
),
],
child: const MeteringScreen(),
),
),
);
}
}

View file

@ -50,7 +50,12 @@ class MeteringScreen extends StatelessWidget {
? EvSourceTypeProvider.of(context).toggleType
: null,
onMeasure: () => context.read<MeteringBloc>().add(const MeasureEvent()),
onSettings: () => Navigator.pushNamed(context, 'settings'),
onSettings: () {
context.read<MeteringBloc>().add(const SettingsOpenedEvent());
Navigator.pushNamed(context, 'settings').then((value) {
context.read<MeteringBloc>().add(const SettingsClosedEvent());
});
},
),
),
],

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/settings/components/general/components/caffeine/bloc_list_tile_caffeine.dart';
@ -15,6 +16,7 @@ class CaffeineListTile extends StatelessWidget {
title: Text(S.of(context).keepScreenOn),
value: state,
onChanged: context.read<CaffeineListTileBloc>().onCaffeineChanged,
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
),
);
}

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/settings/components/general/components/haptics/bloc_list_tile_haptics.dart';
@ -15,6 +16,7 @@ class HapticsListTile extends StatelessWidget {
title: Text(S.of(context).haptics),
value: state,
onChanged: context.read<HapticsListTileBloc>().onHapticsChanged,
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
),
);
}

View file

@ -0,0 +1,20 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/volume_action.dart';
import 'package:lightmeter/interactors/settings_interactor.dart';
class VolumeActionsListTileBloc extends Cubit<bool> {
final SettingsInteractor _settingsInteractor;
VolumeActionsListTileBloc(
this._settingsInteractor,
) : super(_settingsInteractor.volumeAction == VolumeAction.shutter);
void onVolumeActionChanged(bool value) {
_settingsInteractor.setVolumeAction(value ? VolumeAction.shutter : VolumeAction.none);
// while in settings we allow system to handle volume
// so that volume keys action works only when necessary - on the metering screen
_settingsInteractor.disableVolumeHandling();
emit(value);
}
}

View file

@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/interactors/settings_interactor.dart';
import 'package:lightmeter/screens/settings/components/general/components/volume_actions/bloc_list_tile_volume_actions.dart';
import 'package:lightmeter/screens/settings/components/general/components/volume_actions/widget_list_tile_volume_actions.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
class VolumeActionsListTileProvider extends StatelessWidget {
const VolumeActionsListTileProvider({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => VolumeActionsListTileBloc(context.get<SettingsInteractor>()),
child: const VolumeActionsListTile(),
);
}
}

View file

@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/settings/components/general/components/volume_actions/bloc_list_tile_volume_actions.dart';
class VolumeActionsListTile extends StatelessWidget {
const VolumeActionsListTile({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<VolumeActionsListTileBloc, bool>(
builder: (context, state) => SwitchListTile(
secondary: const Icon(Icons.volume_up),
title: Text(S.of(context).volumeKeysAction),
value: state,
onChanged: context.read<VolumeActionsListTileBloc>().onVolumeActionChanged,
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
),
);
}
}

View file

@ -1,8 +1,11 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/general/components/caffeine/provider_list_tile_caffeine.dart';
import 'package:lightmeter/screens/settings/components/general/components/haptics/provider_list_tile_haptics.dart';
import 'package:lightmeter/screens/settings/components/general/components/language/widget_list_tile_language.dart';
import 'package:lightmeter/screens/settings/components/general/components/volume_actions/provider_list_tile_volume_actions.dart';
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
class GeneralSettingsSection extends StatelessWidget {
@ -12,10 +15,11 @@ class GeneralSettingsSection extends StatelessWidget {
Widget build(BuildContext context) {
return SettingsSection(
title: S.of(context).general,
children: const [
CaffeineListTileProvider(),
HapticsListTileProvider(),
LanguageListTile(),
children: [
const CaffeineListTileProvider(),
const HapticsListTileProvider(),
if (Platform.isAndroid) const VolumeActionsListTileProvider(),
const LanguageListTile(),
],
);
}

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/dynamic_colors_state.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/theme_provider.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
class DynamicColorListTile extends StatelessWidget {
@ -14,6 +15,7 @@ class DynamicColorListTile extends StatelessWidget {
title: Text(S.of(context).dynamicColor),
value: context.listen<DynamicColorState>() == DynamicColorState.enabled,
onChanged: ThemeProvider.of(context).enableDynamicColor,
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
);
}
}

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:lightmeter/data/caffeine_service.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/data/volume_events_service.dart';
import 'package:lightmeter/interactors/settings_interactor.dart';
import 'package:lightmeter/screens/settings/screen_settings.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
@ -16,6 +17,7 @@ class SettingsFlow extends StatelessWidget {
context.get<UserPreferencesService>(),
context.get<CaffeineService>(),
context.get<HapticsService>(),
context.get<VolumeEventsService>(),
),
child: const SettingsScreen(),
);

View file

@ -1,14 +1,33 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/interactors/settings_interactor.dart';
import 'package:lightmeter/screens/settings/components/about/widget_settings_section_about.dart';
import 'package:lightmeter/screens/settings/components/general/widget_settings_section_general.dart';
import 'package:lightmeter/screens/settings/components/metering/widget_settings_section_metering.dart';
import 'package:lightmeter/screens/settings/components/theme/widget_settings_section_theme.dart';
import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart';
import 'package:lightmeter/utils/inherited_generics.dart';
class SettingsScreen extends StatelessWidget {
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
context.get<SettingsInteractor>().disableVolumeHandling();
}
@override
void deactivate() {
context.get<SettingsInteractor>().restoreVolumeHandling();
super.deactivate();
}
@override
Widget build(BuildContext context) {
return ScaffoldMessenger(

View file

@ -30,6 +30,7 @@ dependencies:
material_color_utilities: 0.2.0
package_info_plus: 4.0.1
permission_handler: 10.2.0
platform: 3.1.0
shared_preferences: 2.1.1
url_launcher: 6.1.11
uuid: 3.0.7

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View file

@ -1,10 +1,15 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:mocktail/mocktail.dart';
import 'package:platform/platform.dart';
class _MockLocalPlatform extends Mock implements LocalPlatform {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late _MockLocalPlatform localPlatform;
late LightSensorService service;
const methodChannel = MethodChannel('system_feature');
@ -12,7 +17,8 @@ void main() {
//const eventChannel = EventChannel('light.eventChannel');
setUp(() {
service = const LightSensorService();
localPlatform = _MockLocalPlatform();
service = LightSensorService(localPlatform);
});
tearDown(() {
@ -23,7 +29,8 @@ void main() {
group(
'hasSensor()',
() {
test('true', () async {
test('true - Android', () async {
when(() => localPlatform.isAndroid).thenReturn(true);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(methodChannel, null);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
@ -38,7 +45,8 @@ void main() {
expectLater(service.hasSensor(), completion(true));
});
test('false', () async {
test('false - Android', () async {
when(() => localPlatform.isAndroid).thenReturn(true);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(methodChannel, null);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
@ -52,7 +60,9 @@ void main() {
});
expectLater(service.hasSensor(), completion(false));
});
test('null', () async {
test('null - Android', () async {
when(() => localPlatform.isAndroid).thenReturn(true);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(methodChannel, null);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
@ -66,6 +76,23 @@ void main() {
});
expectLater(service.hasSensor(), completion(false));
});
test('false - iOS', () async {
when(() => localPlatform.isAndroid).thenReturn(false);
expectLater(service.hasSensor(), completion(false));
});
},
);
group('luxStream', () {
// test('Android', () async {
// when(() => localPlatform.isAndroid).thenReturn(true);
// expect(service.luxStream(), const Stream.empty());
// });
test('iOS', () async {
when(() => localPlatform.isAndroid).thenReturn(false);
expect(service.luxStream(), const Stream<int>.empty());
});
});
}

View file

@ -0,0 +1,73 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lightmeter/data/volume_events_service.dart';
import 'package:mocktail/mocktail.dart';
import 'package:platform/platform.dart';
class _MockLocalPlatform extends Mock implements LocalPlatform {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late _MockLocalPlatform localPlatform;
late VolumeEventsService service;
Future<Object?>? methodCallSuccessHandler(MethodCall methodCall) async {
switch (methodCall.method) {
case "setVolumeHandling":
return methodCall.arguments as bool;
default:
throw UnimplementedError();
}
}
setUp(() {
localPlatform = _MockLocalPlatform();
service = VolumeEventsService(localPlatform);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
VolumeEventsService.volumeHandlingChannel,
methodCallSuccessHandler,
);
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
VolumeEventsService.volumeHandlingChannel,
null,
);
});
group('setVolumeHandling', () {
test('true - Android', () async {
when(() => localPlatform.isAndroid).thenReturn(true);
expectLater(service.setVolumeHandling(true), completion(true));
});
test('true - iOS', () async {
when(() => localPlatform.isAndroid).thenReturn(false);
expectLater(service.setVolumeHandling(true), completion(false));
});
test('false - Android', () async {
when(() => localPlatform.isAndroid).thenReturn(true);
expectLater(service.setVolumeHandling(false), completion(false));
});
test('false - iOS', () async {
when(() => localPlatform.isAndroid).thenReturn(false);
expectLater(service.setVolumeHandling(false), completion(false));
});
});
group('volumeButtonsEventStream', () {
// test('Android', () async {
// when(() => localPlatform.isAndroid).thenReturn(true);
// expect(service.volumeButtonsEventStream(), const Stream.empty());
// });
test('iOS', () async {
when(() => localPlatform.isAndroid).thenReturn(false);
expect(service.volumeButtonsEventStream(), const Stream<int>.empty());
});
});
}

View file

@ -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,66 @@ void main() {
);
},
);
group(
'`Volume keys shutter action`',
() {
blocTest<MeteringBloc, MeteringState>(
'Add/remove listener',
build: () => bloc,
verify: (_) {
verify(() => volumeKeysNotifier.addListener(bloc.onVolumeKey)).called(1);
verify(() => volumeKeysNotifier.removeListener(bloc.onVolumeKey)).called(1);
},
expect: () => [],
);
blocTest<MeteringBloc, MeteringState>(
'onVolumeKey & VolumeAction.shutter',
build: () => bloc,
act: (bloc) async {
bloc.onVolumeKey();
},
setUp: () {
when(() => meteringInteractor.volumeAction).thenReturn(VolumeAction.shutter);
},
verify: (_) {},
expect: () => [isA<LoadingState>()],
);
blocTest<MeteringBloc, MeteringState>(
'onVolumeKey & VolumeAction.none',
build: () => bloc,
act: (bloc) async {
bloc.onVolumeKey();
},
setUp: () {
when(() => meteringInteractor.volumeAction).thenReturn(VolumeAction.none);
},
verify: (_) {},
expect: () => [],
);
},
);
group(
'`SettingOpenedEvent`/`SettingsClosedEvent`',
() {
blocTest<MeteringBloc, MeteringState>(
'Settings opened & closed',
build: () => bloc,
act: (bloc) async {
bloc.add(const SettingsOpenedEvent());
bloc.add(const SettingsClosedEvent());
},
verify: (_) {
verify(() => communicationBloc.add(const communication_events.SettingsOpenedEvent()))
.called(1);
verify(() => communicationBloc.add(const communication_events.SettingsClosedEvent()))
.called(1);
},
expect: () => [],
);
},
);
}

View file

@ -98,4 +98,30 @@ void main() {
);
},
);
group(
'`SettingsOpenedEvent`/`SettingsClosedEvent`',
() {
blocTest<MeteringCommunicationBloc, MeteringCommunicationState>(
'Multiple consequtive settings events',
build: () => bloc,
act: (bloc) async {
bloc.add(const SettingsOpenedEvent());
bloc.add(const SettingsOpenedEvent());
bloc.add(const SettingsOpenedEvent());
bloc.add(const SettingsClosedEvent());
bloc.add(const SettingsClosedEvent());
bloc.add(const SettingsClosedEvent());
bloc.add(const SettingsOpenedEvent());
bloc.add(const SettingsClosedEvent());
},
expect: () => [
isA<SettingsOpenedState>(),
isA<SettingsClosedState>(),
isA<SettingsOpenedState>(),
isA<SettingsClosedState>(),
],
);
},
);
}

View file

@ -14,12 +14,12 @@ import 'package:lightmeter/screens/metering/components/camera_container/models/c
import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart';
import 'package:mocktail/mocktail.dart';
class _MockMeteringInteractor extends Mock implements MeteringInteractor {}
class _MockMeteringCommunicationBloc extends MockBloc<
communication_events.MeteringCommunicationEvent,
communication_states.MeteringCommunicationState> implements MeteringCommunicationBloc {}
class _MockMeteringInteractor extends Mock implements MeteringInteractor {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
@ -310,6 +310,30 @@ void main() {
},
expect: () => [
...initializedStateSequence,
const CameraInitState(),
...initializedStateSequence,
],
);
blocTest<CameraContainerBloc, CameraContainerState>(
'onCommunicationState',
setUp: () {
when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true);
},
build: () => bloc,
act: (bloc) async {
bloc.add(const InitializeEvent());
await Future.delayed(Duration.zero);
bloc.onCommunicationState(const communication_states.SettingsOpenedState());
await Future.delayed(Duration.zero);
bloc.onCommunicationState(const communication_states.SettingsClosedState());
},
verify: (_) {
verify(() => meteringInteractor.checkCameraPermission()).called(2);
},
expect: () => [
...initializedStateSequence,
const CameraInitState(),
...initializedStateSequence,
],
);

View file

@ -78,4 +78,67 @@ void main() {
);
},
);
group(
'`communication_states.SettingsOpenedState()`',
() {
const List<int> luxIterable = [1, 2, 2, 2, 3];
final List<double> resultList = luxIterable.map((lux) => log2(lux / 2.5)).toList();
blocTest<LightSensorContainerBloc, LightSensorContainerState>(
'Metering is already canceled',
build: () => bloc,
setUp: () {
when(() => meteringInteractor.luxStream())
.thenAnswer((_) => Stream.fromIterable(luxIterable));
when(() => meteringInteractor.lightSensorEvCalibration).thenReturn(0.0);
},
act: (bloc) async {
bloc.onCommunicationState(const communication_states.SettingsOpenedState());
},
verify: (_) {
verifyNever(() => meteringInteractor.luxStream().listen((_) {}));
verifyNever(() => meteringInteractor.lightSensorEvCalibration);
verify(() {
communicationBloc.add(const communication_events.MeteringEndedEvent(null));
}).called(2); // +1 from dispose
},
expect: () => [],
);
blocTest<LightSensorContainerBloc, LightSensorContainerState>(
'Metering is in progress',
build: () => bloc,
setUp: () {
when(() => meteringInteractor.luxStream())
.thenAnswer((_) => Stream.fromIterable(luxIterable));
when(() => meteringInteractor.lightSensorEvCalibration).thenReturn(0.0);
},
act: (bloc) async {
bloc.onCommunicationState(const communication_states.MeasureState());
await Future.delayed(Duration.zero);
bloc.onCommunicationState(const communication_states.SettingsOpenedState());
bloc.onCommunicationState(const communication_states.SettingsClosedState());
},
verify: (_) {
verify(() => meteringInteractor.luxStream().listen((_) {})).called(1);
verify(() => meteringInteractor.lightSensorEvCalibration).called(5);
verify(() {
communicationBloc.add(communication_events.MeteringInProgressEvent(resultList.first));
}).called(1);
verify(() {
communicationBloc.add(communication_events.MeteringInProgressEvent(resultList[1]));
}).called(3);
verify(() {
communicationBloc.add(communication_events.MeteringInProgressEvent(resultList.last));
}).called(1);
verify(() {
communicationBloc.add(communication_events.MeteringEndedEvent(resultList.last));
}).called(3); // +1 from settings closed, +1 from dispose
},
expect: () => resultList.map(
(e) => isA<LightSensorContainerState>().having((state) => state.ev100, 'ev100', e),
),
);
},
);
}