Fix Shake Detector not working when app in background (#40)

* 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
This commit is contained in:
Dr.Blank 2024-09-30 02:34:13 -04:00 committed by GitHub
parent 6c0265fe5f
commit 67d6c9240b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 93 additions and 47 deletions

View file

@ -28,6 +28,9 @@ class ShakeDetector {
DateTime _lastShakeTime = DateTime.now(); DateTime _lastShakeTime = DateTime.now();
final StreamController<UserAccelerometerEvent>
_detectedShakeStreamController = StreamController.broadcast();
void start() { void start() {
if (_accelerometerSubscription != null) { if (_accelerometerSubscription != null) {
_logger.warning('ShakeDetector is already running'); _logger.warning('ShakeDetector is already running');
@ -36,6 +39,7 @@ class ShakeDetector {
_accelerometerSubscription = _accelerometerSubscription =
userAccelerometerEventStream(samplingPeriod: _settings.samplingPeriod) userAccelerometerEventStream(samplingPeriod: _settings.samplingPeriod)
.listen((event) { .listen((event) {
_logger.finest('RMS: ${event.rms}');
if (event.rms > _settings.threshold) { if (event.rms > _settings.threshold) {
_currentShakeCount++; _currentShakeCount++;
@ -44,6 +48,7 @@ class ShakeDetector {
_logger.fine('Shake detected $_currentShakeCount times'); _logger.fine('Shake detected $_currentShakeCount times');
onShakeDetected?.call(); onShakeDetected?.call();
_detectedShakeStreamController.add(event);
_lastShakeTime = DateTime.now(); _lastShakeTime = DateTime.now();
_currentShakeCount = 0; _currentShakeCount = 0;
@ -60,6 +65,7 @@ class ShakeDetector {
_currentShakeCount = 0; _currentShakeCount = 0;
_accelerometerSubscription?.cancel(); _accelerometerSubscription?.cancel();
_accelerometerSubscription = null; _accelerometerSubscription = null;
_detectedShakeStreamController.close();
_logger.fine('ShakeDetector stopped'); _logger.fine('ShakeDetector stopped');
} }

View file

@ -19,73 +19,111 @@ Logger _logger = Logger('ShakeDetector');
@riverpod @riverpod
class ShakeDetector extends _$ShakeDetector { class ShakeDetector extends _$ShakeDetector {
bool wasPlayerLoaded = false;
@override @override
core.ShakeDetector? build() { core.ShakeDetector? build() {
final appSettings = ref.watch(appSettingsProvider); final appSettings = ref.watch(appSettingsProvider);
final shakeDetectionSettings = appSettings.shakeDetectionSettings; final shakeDetectionSettings = appSettings.shakeDetectionSettings;
if (!shakeDetectionSettings.isEnabled) { 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',
);
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');
return null; 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( final detector = core.ShakeDetector(
shakeDetectionSettings, shakeDetectionSettings,
() { () {
doShakeAction( final wasActionComplete = doShakeAction(
shakeDetectionSettings.shakeAction, shakeDetectionSettings.shakeAction,
ref: ref, ref: ref,
); );
shakeDetectionSettings.feedback.forEach(postShakeFeedback); if (wasActionComplete) {
shakeDetectionSettings.feedback.forEach(postShakeFeedback);
}
}, },
); );
ref.onDispose(detector.dispose); ref.onDispose(detector.dispose);
return detector; return detector;
} }
void doShakeAction( /// Perform the shake action and return whether the action was successful
bool doShakeAction(
ShakeAction shakeAction, { ShakeAction shakeAction, {
required Ref ref, 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) { switch (shakeAction) {
case ShakeAction.resetSleepTimer: case ShakeAction.resetSleepTimer:
_logger.fine('Resetting sleep timer'); _logger.fine('Resetting sleep timer');
ref.read(sleepTimerProvider.notifier).restartTimer(); var sleepTimer = ref.read(sleepTimerProvider);
break; if (sleepTimer == null || !sleepTimer.isActive) {
_logger.warning('No sleep timer is running');
return false;
}
sleepTimer.restartTimer();
return true;
case ShakeAction.fastForward: case ShakeAction.fastForward:
_logger.fine('Fast forwarding'); _logger.fine('Fast forwarding');
if (!player.playing) {
_logger.warning('Player is not playing');
return false;
}
player.seek(player.position + const Duration(seconds: 30)); player.seek(player.position + const Duration(seconds: 30));
break; return true;
case ShakeAction.rewind: case ShakeAction.rewind:
_logger.fine('Rewinding'); _logger.fine('Rewinding');
player.seek(player.position - const Duration(seconds: 30)); if (!player.playing) {
break; _logger.warning('Player is not playing');
case ShakeAction.playPause: return false;
if (player.book == null) {
_logger.warning('No book is loaded');
break;
} }
player.seek(player.position - const Duration(seconds: 30));
return true;
case ShakeAction.playPause:
_logger.fine('Toggling play/pause');
player.togglePlayPause(); player.togglePlayPause();
break; return true;
default: default:
break; return false;
} }
} }
@ -121,18 +159,18 @@ class ShakeDetector extends _$ShakeDetector {
} }
} }
extension on ShakeDetectionSettings { extension on ShakeAction {
bool get isActiveWhenPaused { bool get isActiveWhenPaused {
// If the shake action is play/pause, it should be required when not playing // 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 { bool get isPlaybackManagementEnabled {
return {ShakeAction.playPause, ShakeAction.fastForward, ShakeAction.rewind} return {ShakeAction.playPause, ShakeAction.fastForward, ShakeAction.rewind}
.contains(shakeAction); .contains(this);
} }
bool get shouldActOnSleepTimer { bool get shouldActOnSleepTimer {
return {ShakeAction.resetSleepTimer}.contains(shakeAction); return {ShakeAction.resetSleepTimer}.contains(this);
} }
} }

View file

@ -6,7 +6,7 @@ part of 'shake_detector.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$shakeDetectorHash() => r'7bfbdd22f2f43ef3e3858d226d1eb78923e8114d'; String _$shakeDetectorHash() => r'2a380bab1d4021d05d2ae40fec964a5f33d3730c';
/// See also [ShakeDetector]. /// See also [ShakeDetector].
@ProviderFor(ShakeDetector) @ProviderFor(ShakeDetector)

View file

@ -28,6 +28,9 @@ class SleepTimer {
/// The timer that will pause the player /// The timer that will pause the player
Timer? timer; Timer? timer;
/// is the sleep timer actively counting down
bool get isActive => timer != null && timer!.isActive;
/// for internal use only /// for internal use only
/// when the timer was started /// when the timer was started
DateTime? startedAt; DateTime? startedAt;

View file

@ -54,7 +54,7 @@ class SleepTimer extends _$SleepTimer {
void restartTimer() { void restartTimer() {
state?.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() { void cancelTimer() {

View file

@ -6,7 +6,7 @@ part of 'sleep_timer_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$sleepTimerHash() => r'9d9f20267da91e5483151b58b7d4d7c0762c3ca7'; String _$sleepTimerHash() => r'4f80bcc342e918c70c547b8b24790ccd88aba8c3';
/// See also [SleepTimer]. /// See also [SleepTimer].
@ProviderFor(SleepTimer) @ProviderFor(SleepTimer)

View file

@ -20,7 +20,7 @@ final appLogger = Logger('vaani');
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// Configure the root Logger // 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) { Logger.root.onRecord.listen((record) {
// Print log records to the console // Print log records to the console
debugPrint( debugPrint(

View file

@ -197,12 +197,12 @@ class ShakeDetectionSettings with _$ShakeDetectionSettings {
@Default(ShakeDirection.horizontal) ShakeDirection direction, @Default(ShakeDirection.horizontal) ShakeDirection direction,
@Default(5) double threshold, @Default(5) double threshold,
@Default(ShakeAction.resetSleepTimer) ShakeAction shakeAction, @Default(ShakeAction.resetSleepTimer) ShakeAction shakeAction,
@Default({ShakeDetectedFeedback.vibrate, ShakeDetectedFeedback.beep}) @Default({ShakeDetectedFeedback.vibrate})
Set<ShakeDetectedFeedback> feedback, Set<ShakeDetectedFeedback> feedback,
@Default(0.5) double beepVolume, @Default(0.5) double beepVolume,
/// the duration to wait before the shake detection is enabled again /// 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 /// the number of shakes required to trigger the action
@Default(2) int shakeTriggerCount, @Default(2) int shakeTriggerCount,

View file

@ -2633,11 +2633,10 @@ class _$ShakeDetectionSettingsImpl implements _ShakeDetectionSettings {
this.threshold = 5, this.threshold = 5,
this.shakeAction = ShakeAction.resetSleepTimer, this.shakeAction = ShakeAction.resetSleepTimer,
final Set<ShakeDetectedFeedback> feedback = const { final Set<ShakeDetectedFeedback> feedback = const {
ShakeDetectedFeedback.vibrate, ShakeDetectedFeedback.vibrate
ShakeDetectedFeedback.beep
}, },
this.beepVolume = 0.5, this.beepVolume = 0.5,
this.shakeTriggerCoolDown = const Duration(seconds: 5), this.shakeTriggerCoolDown = const Duration(seconds: 2),
this.shakeTriggerCount = 2, this.shakeTriggerCount = 2,
this.samplingPeriod = const Duration(milliseconds: 100)}) this.samplingPeriod = const Duration(milliseconds: 100)})
: _feedback = feedback; : _feedback = feedback;

View file

@ -294,10 +294,10 @@ _$ShakeDetectionSettingsImpl _$$ShakeDetectionSettingsImplFromJson(
feedback: (json['feedback'] as List<dynamic>?) feedback: (json['feedback'] as List<dynamic>?)
?.map((e) => $enumDecode(_$ShakeDetectedFeedbackEnumMap, e)) ?.map((e) => $enumDecode(_$ShakeDetectedFeedbackEnumMap, e))
.toSet() ?? .toSet() ??
const {ShakeDetectedFeedback.vibrate, ShakeDetectedFeedback.beep}, const {ShakeDetectedFeedback.vibrate},
beepVolume: (json['beepVolume'] as num?)?.toDouble() ?? 0.5, beepVolume: (json['beepVolume'] as num?)?.toDouble() ?? 0.5,
shakeTriggerCoolDown: json['shakeTriggerCoolDown'] == null shakeTriggerCoolDown: json['shakeTriggerCoolDown'] == null
? const Duration(seconds: 5) ? const Duration(seconds: 2)
: Duration( : Duration(
microseconds: (json['shakeTriggerCoolDown'] as num).toInt()), microseconds: (json['shakeTriggerCoolDown'] as num).toInt()),
shakeTriggerCount: (json['shakeTriggerCount'] as num?)?.toInt() ?? 2, shakeTriggerCount: (json['shakeTriggerCount'] as num?)?.toInt() ?? 2,