@@ -141,6 +141,13 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler with SeekHandler, Queue
141141 final _volumeNormalizationLogger = Logger ("VolumeNormalization" );
142142 final _outputLogger = Logger ("Output" );
143143
144+ /// Time window used to ignore spurious play/pause callbacks immediately
145+ /// after a skip command from certain Bluetooth headsets. Behavior before this fix was that a double-tap on such headsets would trigger a skip followed by an unintended pause, because the headset sent play/pause events immediately after the skip event. With this guard, if a play/pause event is received within this window after a skip command, it will be ignored.
146+ static const Duration _skipPlayPauseGuardWindow = Duration (milliseconds: 50 );
147+
148+ /// Timestamp of the most recent explicit skip command (next/previous).
149+ DateTime ? _lastSkipCommandAt;
150+
144151 // Init the new sleep timer with a length of 0
145152 // SleepTimer sleepTimer = SleepTimer(SleepTimerType.duration, 0);
146153
@@ -164,6 +171,24 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler with SeekHandler, Queue
164171
165172 final outputSwitcherChannel = MethodChannel ('com.unicornsonlsd.finamp/output_switcher' );
166173
174+ /// Some Bluetooth headsets send skip and pause/play media button events in
175+ /// very quick succession for a double-tap skip gesture. This guard ignores a
176+ /// trailing play/pause event if it arrives right after skip, preventing an
177+ /// unintended pause while still allowing normal controls outside the window.
178+ bool get _shouldIgnorePlayPauseAfterRecentSkip {
179+ final lastSkipCommandAt = _lastSkipCommandAt;
180+ if (lastSkipCommandAt == null ) return false ;
181+
182+ final elapsed = DateTime .now ().difference (lastSkipCommandAt);
183+ if (elapsed <= _skipPlayPauseGuardWindow) {
184+ _audioServiceBackgroundTaskLogger.fine (
185+ "Ignoring play/pause because skip was ${elapsed .inMilliseconds }ms ago (threshold ${_skipPlayPauseGuardWindow .inMilliseconds }ms)" ,
186+ );
187+ return true ;
188+ }
189+ return false ;
190+ }
191+
167192 Future <void > showOutputSwitcherDialog () async {
168193 if (! Platform .isAndroid) {
169194 return ;
@@ -583,6 +608,12 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler with SeekHandler, Queue
583608
584609 @override
585610 Future <void > play ({bool disableFade = false }) async {
611+ _audioServiceBackgroundTaskLogger.fine (
612+ "play() start: disableFade=$disableFade , playing=${_player .playing }, fadeDirection=${fadeState .value .fadeDirection }, currentIndex=${_player .currentIndex }, position=${_player .position }" ,
613+ );
614+ if (_shouldIgnorePlayPauseAfterRecentSkip) {
615+ return ;
616+ }
586617 if (! disableFade && FinampSettingsHelper .finampSettings.audioFadeInDuration > Duration .zero) {
587618 return fadeInAndPlay ();
588619 } else {
@@ -608,6 +639,12 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler with SeekHandler, Queue
608639
609640 @override
610641 Future <void > pause ({bool disableFade = false }) async {
642+ _audioServiceBackgroundTaskLogger.fine (
643+ "pause() start: disableFade=$disableFade , playing=${_player .playing }, fadeDirection=${fadeState .value .fadeDirection }, currentIndex=${_player .currentIndex }, position=${_player .position }" ,
644+ );
645+ if (_shouldIgnorePlayPauseAfterRecentSkip) {
646+ return ;
647+ }
611648 if (! disableFade && FinampSettingsHelper .finampSettings.audioFadeOutDuration > Duration .zero) {
612649 return fadeOutAndPause ();
613650 } else {
@@ -771,6 +808,10 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler with SeekHandler, Queue
771808
772809 @override
773810 Future <void > skipToPrevious ({bool forceSkip = false }) async {
811+ _audioServiceBackgroundTaskLogger.fine (
812+ "skipToPrevious() start: forceSkip=$forceSkip , playing=${_player .playing }, fadeDirection=${fadeState .value .fadeDirection }, hasPrevious=${_player .hasPrevious }, loopMode=${_player .loopMode }, currentIndex=${_player .currentIndex }, position=${_player .position }" ,
813+ );
814+ _lastSkipCommandAt = DateTime .now ();
774815 bool doSkip = true ;
775816
776817 try {
@@ -803,6 +844,10 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler with SeekHandler, Queue
803844
804845 @override
805846 Future <void > skipToNext () async {
847+ _audioServiceBackgroundTaskLogger.fine (
848+ "skipToNext() start: playing=${_player .playing }, fadeDirection=${fadeState .value .fadeDirection }, hasNext=${_player .hasNext }, loopMode=${_player .loopMode }, currentIndex=${_player .currentIndex }, position=${_player .position }" ,
849+ );
850+ _lastSkipCommandAt = DateTime .now ();
806851 try {
807852 if (_player.loopMode == LoopMode .one || ! _player.hasNext) {
808853 // if the user manually skips to the next track, they probably want to actually skip to the next track
0 commit comments