mirror of
https://github.com/Dr-Blank/Vaani.git
synced 2025-12-06 02:59:28 +00:00
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:
parent
6c0265fe5f
commit
67d6c9240b
10 changed files with 93 additions and 47 deletions
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue