Skip to content

Commit

Permalink
Fix Shake Detector not working when app in background (#40)
Browse files Browse the repository at this point in the history
* feat: update shake detection settings to reduce cooldown and feedback options

* fix: shake detector not detecting in background

* enhance shake action handling to avoid unnecessary feedback

* disable shake detector when player not playing anything

* refactor: remove outdated TODO regarding shake detection optimization

* refactor: comment out notifyListeners call in restartTimer method for clarity
  • Loading branch information
Dr-Blank authored Sep 30, 2024
1 parent 6c0265f commit 67d6c92
Show file tree
Hide file tree
Showing 10 changed files with 85 additions and 39 deletions.
6 changes: 6 additions & 0 deletions lib/features/shake_detection/core/shake_detector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ class ShakeDetector {

DateTime _lastShakeTime = DateTime.now();

final StreamController<UserAccelerometerEvent>
_detectedShakeStreamController = StreamController.broadcast();

void start() {
if (_accelerometerSubscription != null) {
_logger.warning('ShakeDetector is already running');
Expand All @@ -36,6 +39,7 @@ class ShakeDetector {
_accelerometerSubscription =
userAccelerometerEventStream(samplingPeriod: _settings.samplingPeriod)
.listen((event) {
_logger.finest('RMS: ${event.rms}');
if (event.rms > _settings.threshold) {
_currentShakeCount++;

Expand All @@ -44,6 +48,7 @@ class ShakeDetector {
_logger.fine('Shake detected $_currentShakeCount times');

onShakeDetected?.call();
_detectedShakeStreamController.add(event);

_lastShakeTime = DateTime.now();
_currentShakeCount = 0;
Expand All @@ -60,6 +65,7 @@ class ShakeDetector {
_currentShakeCount = 0;
_accelerometerSubscription?.cancel();
_accelerometerSubscription = null;
_detectedShakeStreamController.close();
_logger.fine('ShakeDetector stopped');
}

Expand Down
94 changes: 66 additions & 28 deletions lib/features/shake_detection/providers/shake_detector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');
_logger.config('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',
);

// 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.isPlaybackManagementEnabled &&
if (!shakeDetectionSettings.shakeAction.isPlaybackManagementEnabled &&
sleepTimer == null) {
_logger.fine('No playback management is enabled and sleep timer is off, '
'so shake detection is disabled');
_logger
.config('No playback management is enabled and sleep timer is off, '
'so shake detection is disabled');
return null;
}

_logger.fine('Creating shake detector');
_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');
if (!player.playing) {
_logger.warning('Player is not playing');
return false;
}
player.seek(player.position - const Duration(seconds: 30));
break;
return true;
case ShakeAction.playPause:
if (player.book == null) {
_logger.warning('No book is loaded');
break;
}
_logger.fine('Toggling play/pause');
player.togglePlayPause();
break;
return true;
default:
break;
return false;
}
}

Expand Down Expand Up @@ -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);
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions lib/features/sleep_timer/core/sleep_timer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions lib/settings/models/app_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<ShakeDetectedFeedback> 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,
Expand Down
5 changes: 2 additions & 3 deletions lib/settings/models/app_settings.freezed.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2633,11 +2633,10 @@ class _$ShakeDetectionSettingsImpl implements _ShakeDetectionSettings {
this.threshold = 5,
this.shakeAction = ShakeAction.resetSleepTimer,
final Set<ShakeDetectedFeedback> 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;
Expand Down
4 changes: 2 additions & 2 deletions lib/settings/models/app_settings.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 67d6c92

Please sign in to comment.