diff --git a/lib/features/shake_detection/core/shake_detector.dart b/lib/features/shake_detection/core/shake_detector.dart index 4b7b719..023a8ab 100644 --- a/lib/features/shake_detection/core/shake_detector.dart +++ b/lib/features/shake_detection/core/shake_detector.dart @@ -28,6 +28,9 @@ class ShakeDetector { DateTime _lastShakeTime = DateTime.now(); + final StreamController + _detectedShakeStreamController = StreamController.broadcast(); + void start() { if (_accelerometerSubscription != null) { _logger.warning('ShakeDetector is already running'); @@ -36,6 +39,7 @@ class ShakeDetector { _accelerometerSubscription = userAccelerometerEventStream(samplingPeriod: _settings.samplingPeriod) .listen((event) { + _logger.finest('RMS: ${event.rms}'); if (event.rms > _settings.threshold) { _currentShakeCount++; @@ -44,6 +48,7 @@ class ShakeDetector { _logger.fine('Shake detected $_currentShakeCount times'); onShakeDetected?.call(); + _detectedShakeStreamController.add(event); _lastShakeTime = DateTime.now(); _currentShakeCount = 0; @@ -60,6 +65,7 @@ class ShakeDetector { _currentShakeCount = 0; _accelerometerSubscription?.cancel(); _accelerometerSubscription = null; + _detectedShakeStreamController.close(); _logger.fine('ShakeDetector stopped'); } diff --git a/lib/features/shake_detection/providers/shake_detector.dart b/lib/features/shake_detection/providers/shake_detector.dart index 956fc16..8892a92 100644 --- a/lib/features/shake_detection/providers/shake_detector.dart +++ b/lib/features/shake_detection/providers/shake_detector.dart @@ -19,73 +19,111 @@ Logger _logger = Logger('ShakeDetector'); @riverpod class ShakeDetector extends _$ShakeDetector { + bool wasPlayerLoaded = false; + @override core.ShakeDetector? build() { final appSettings = ref.watch(appSettingsProvider); final shakeDetectionSettings = appSettings.shakeDetectionSettings; if (!shakeDetectionSettings.isEnabled) { - _logger.fine('Shake detection is disabled'); - return null; - } - final player = ref.watch(audiobookPlayerProvider); - if (!player.playing && !shakeDetectionSettings.isActiveWhenPaused) { - _logger.fine( - 'Shake detection is disabled when paused and player is not playing', - ); - return null; - } - // if sleep timer is not enabled, shake detection should not be enabled - final sleepTimer = ref.watch(sleepTimerProvider); - if (!shakeDetectionSettings.isPlaybackManagementEnabled && - sleepTimer == null) { - _logger.fine('No playback management is enabled and sleep timer is off, ' - 'so shake detection is disabled'); + _logger.config('Shake detection is disabled'); return null; } - _logger.fine('Creating shake detector'); + // if no book is loaded, shake detection should not be enabled + final player = ref.watch(simpleAudiobookPlayerProvider); + player.playerStateStream.listen((event) { + if (event.processingState == ProcessingState.idle && wasPlayerLoaded) { + _logger.config('Player is now not loaded, invalidating'); + wasPlayerLoaded = false; + ref.invalidateSelf(); + } + if (event.processingState != ProcessingState.idle && !wasPlayerLoaded) { + _logger.config('Player is now loaded, invalidating'); + wasPlayerLoaded = true; + ref.invalidateSelf(); + } + }); + + if (player.book == null) { + _logger.config('No book is loaded, disabling shake detection'); + wasPlayerLoaded = false; + return null; + } else { + _logger.finer('Book is loaded, marking player as loaded'); + wasPlayerLoaded = true; + } + + // if sleep timer is not enabled, shake detection should not be enabled + final sleepTimer = ref.watch(sleepTimerProvider); + if (!shakeDetectionSettings.shakeAction.isPlaybackManagementEnabled && + sleepTimer == null) { + _logger + .config('No playback management is enabled and sleep timer is off, ' + 'so shake detection is disabled'); + return null; + } + + _logger.config('Creating shake detector'); final detector = core.ShakeDetector( shakeDetectionSettings, () { - doShakeAction( + final wasActionComplete = doShakeAction( shakeDetectionSettings.shakeAction, ref: ref, ); - shakeDetectionSettings.feedback.forEach(postShakeFeedback); + if (wasActionComplete) { + shakeDetectionSettings.feedback.forEach(postShakeFeedback); + } }, ); ref.onDispose(detector.dispose); return detector; } - void doShakeAction( + /// Perform the shake action and return whether the action was successful + bool doShakeAction( ShakeAction shakeAction, { required Ref ref, }) { - final player = ref.watch(simpleAudiobookPlayerProvider); + final player = ref.read(simpleAudiobookPlayerProvider); + if (player.book == null && shakeAction.isPlaybackManagementEnabled) { + _logger.warning('No book is loaded'); + return false; + } switch (shakeAction) { case ShakeAction.resetSleepTimer: _logger.fine('Resetting sleep timer'); - ref.read(sleepTimerProvider.notifier).restartTimer(); - break; + var sleepTimer = ref.read(sleepTimerProvider); + if (sleepTimer == null || !sleepTimer.isActive) { + _logger.warning('No sleep timer is running'); + return false; + } + sleepTimer.restartTimer(); + return true; case ShakeAction.fastForward: _logger.fine('Fast forwarding'); + if (!player.playing) { + _logger.warning('Player is not playing'); + return false; + } player.seek(player.position + const Duration(seconds: 30)); - break; + return true; case ShakeAction.rewind: _logger.fine('Rewinding'); - player.seek(player.position - const Duration(seconds: 30)); - break; - case ShakeAction.playPause: - if (player.book == null) { - _logger.warning('No book is loaded'); - break; + if (!player.playing) { + _logger.warning('Player is not playing'); + return false; } + player.seek(player.position - const Duration(seconds: 30)); + return true; + case ShakeAction.playPause: + _logger.fine('Toggling play/pause'); player.togglePlayPause(); - break; + return true; default: - break; + return false; } } @@ -121,18 +159,18 @@ class ShakeDetector extends _$ShakeDetector { } } -extension on ShakeDetectionSettings { +extension on ShakeAction { bool get isActiveWhenPaused { // If the shake action is play/pause, it should be required when not playing - return shakeAction == ShakeAction.playPause; + return this == ShakeAction.playPause; } bool get isPlaybackManagementEnabled { return {ShakeAction.playPause, ShakeAction.fastForward, ShakeAction.rewind} - .contains(shakeAction); + .contains(this); } bool get shouldActOnSleepTimer { - return {ShakeAction.resetSleepTimer}.contains(shakeAction); + return {ShakeAction.resetSleepTimer}.contains(this); } } diff --git a/lib/features/shake_detection/providers/shake_detector.g.dart b/lib/features/shake_detection/providers/shake_detector.g.dart index c3035b8..0f4285f 100644 --- a/lib/features/shake_detection/providers/shake_detector.g.dart +++ b/lib/features/shake_detection/providers/shake_detector.g.dart @@ -6,7 +6,7 @@ part of 'shake_detector.dart'; // RiverpodGenerator // ************************************************************************** -String _$shakeDetectorHash() => r'7bfbdd22f2f43ef3e3858d226d1eb78923e8114d'; +String _$shakeDetectorHash() => r'2a380bab1d4021d05d2ae40fec964a5f33d3730c'; /// See also [ShakeDetector]. @ProviderFor(ShakeDetector) diff --git a/lib/features/sleep_timer/core/sleep_timer.dart b/lib/features/sleep_timer/core/sleep_timer.dart index 728cec1..215c264 100644 --- a/lib/features/sleep_timer/core/sleep_timer.dart +++ b/lib/features/sleep_timer/core/sleep_timer.dart @@ -28,6 +28,9 @@ class SleepTimer { /// The timer that will pause the player Timer? timer; + /// is the sleep timer actively counting down + bool get isActive => timer != null && timer!.isActive; + /// for internal use only /// when the timer was started DateTime? startedAt; diff --git a/lib/features/sleep_timer/providers/sleep_timer_provider.dart b/lib/features/sleep_timer/providers/sleep_timer_provider.dart index 6f1b1b7..92e5eb8 100644 --- a/lib/features/sleep_timer/providers/sleep_timer_provider.dart +++ b/lib/features/sleep_timer/providers/sleep_timer_provider.dart @@ -54,7 +54,7 @@ class SleepTimer extends _$SleepTimer { void restartTimer() { state?.restartTimer(); - ref.notifyListeners(); + // ref.notifyListeners(); // see https://github.com/Dr-Blank/Vaani/pull/40 for more information on why this is commented out } void cancelTimer() { diff --git a/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart b/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart index fe74010..6d820e4 100644 --- a/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart +++ b/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart @@ -6,7 +6,7 @@ part of 'sleep_timer_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$sleepTimerHash() => r'9d9f20267da91e5483151b58b7d4d7c0762c3ca7'; +String _$sleepTimerHash() => r'4f80bcc342e918c70c547b8b24790ccd88aba8c3'; /// See also [SleepTimer]. @ProviderFor(SleepTimer) diff --git a/lib/main.dart b/lib/main.dart index 3228a8a..8eef350 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,7 +20,7 @@ final appLogger = Logger('vaani'); void main() async { WidgetsFlutterBinding.ensureInitialized(); // Configure the root Logger - Logger.root.level = Level.ALL; // Capture all logs + Logger.root.level = Level.FINE; // Capture all logs Logger.root.onRecord.listen((record) { // Print log records to the console debugPrint( diff --git a/lib/settings/models/app_settings.dart b/lib/settings/models/app_settings.dart index 09a07e2..749b227 100644 --- a/lib/settings/models/app_settings.dart +++ b/lib/settings/models/app_settings.dart @@ -197,12 +197,12 @@ class ShakeDetectionSettings with _$ShakeDetectionSettings { @Default(ShakeDirection.horizontal) ShakeDirection direction, @Default(5) double threshold, @Default(ShakeAction.resetSleepTimer) ShakeAction shakeAction, - @Default({ShakeDetectedFeedback.vibrate, ShakeDetectedFeedback.beep}) + @Default({ShakeDetectedFeedback.vibrate}) Set feedback, @Default(0.5) double beepVolume, /// the duration to wait before the shake detection is enabled again - @Default(Duration(seconds: 5)) Duration shakeTriggerCoolDown, + @Default(Duration(seconds: 2)) Duration shakeTriggerCoolDown, /// the number of shakes required to trigger the action @Default(2) int shakeTriggerCount, diff --git a/lib/settings/models/app_settings.freezed.dart b/lib/settings/models/app_settings.freezed.dart index 3c1d9f4..57f0e96 100644 --- a/lib/settings/models/app_settings.freezed.dart +++ b/lib/settings/models/app_settings.freezed.dart @@ -2633,11 +2633,10 @@ class _$ShakeDetectionSettingsImpl implements _ShakeDetectionSettings { this.threshold = 5, this.shakeAction = ShakeAction.resetSleepTimer, final Set feedback = const { - ShakeDetectedFeedback.vibrate, - ShakeDetectedFeedback.beep + ShakeDetectedFeedback.vibrate }, this.beepVolume = 0.5, - this.shakeTriggerCoolDown = const Duration(seconds: 5), + this.shakeTriggerCoolDown = const Duration(seconds: 2), this.shakeTriggerCount = 2, this.samplingPeriod = const Duration(milliseconds: 100)}) : _feedback = feedback; diff --git a/lib/settings/models/app_settings.g.dart b/lib/settings/models/app_settings.g.dart index 3b0784f..7c553dd 100644 --- a/lib/settings/models/app_settings.g.dart +++ b/lib/settings/models/app_settings.g.dart @@ -294,10 +294,10 @@ _$ShakeDetectionSettingsImpl _$$ShakeDetectionSettingsImplFromJson( feedback: (json['feedback'] as List?) ?.map((e) => $enumDecode(_$ShakeDetectedFeedbackEnumMap, e)) .toSet() ?? - const {ShakeDetectedFeedback.vibrate, ShakeDetectedFeedback.beep}, + const {ShakeDetectedFeedback.vibrate}, beepVolume: (json['beepVolume'] as num?)?.toDouble() ?? 0.5, shakeTriggerCoolDown: json['shakeTriggerCoolDown'] == null - ? const Duration(seconds: 5) + ? const Duration(seconds: 2) : Duration( microseconds: (json['shakeTriggerCoolDown'] as num).toInt()), shakeTriggerCount: (json['shakeTriggerCount'] as num?)?.toInt() ?? 2,