From 19c6da29f74f2bb67f082484517bb6073f5740f9 Mon Sep 17 00:00:00 2001 From: Ashton Kinslow Date: Sat, 17 Oct 2026 20:25:04 -0500 Subject: [PATCH 1/2] fix(mobile): restart episodes when resume is disabled --- .../audio/native_audio_player_service.dart | 4 +- .../services/audio/native_playback_test.dart | 64 +++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 mobile/test/services/audio/native_playback_test.dart diff --git a/mobile/lib/services/audio/native_audio_player_service.dart b/mobile/lib/services/audio/native_audio_player_service.dart index c29bfb5f6..fe7030b1d 100644 --- a/mobile/lib/services/audio/native_audio_player_service.dart +++ b/mobile/lib/services/audio/native_audio_player_service.dart @@ -425,9 +425,7 @@ class NativeAudioPlayerService extends AudioPlayerService { // Get best position (furthest of local vs server) final bestPosition = resume ? await _getBestEpisodePosition(episode) : 0; - if (bestPosition > _currentEpisode!.position) { - _currentEpisode!.position = bestPosition; - } + _currentEpisode!.position = bestPosition; await repository.saveEpisode(_currentEpisode!); diff --git a/mobile/test/services/audio/native_playback_test.dart b/mobile/test/services/audio/native_playback_test.dart new file mode 100644 index 000000000..f28ff6aab --- /dev/null +++ b/mobile/test/services/audio/native_playback_test.dart @@ -0,0 +1,64 @@ +import 'dart:async'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinepods_mobile/repository/repository.dart'; +import 'package:pinepods_mobile/services/audio/native_audio_player_service.dart'; +import 'package:pinepods_mobile/services/settings/settings_service.dart'; +import '../../support/factories.dart'; + +class MockRepository extends Mock implements Repository {} + +class MockSettings extends Mock implements SettingsService {} + +void main() { + for (final resume in [false, true]) { + testWidgets('native playback honors resume=$resume', (tester) async { + final repository = MockRepository(); + final settings = MockSettings(); + final episode = buildEpisode(position: 120000); + when(() => repository.loadQueue()).thenAnswer((_) async => []); + when( + () => repository.saveEpisode(episode), + ).thenAnswer((_) async => episode); + when( + () => settings.settingsListener, + ).thenAnswer((_) => const Stream.empty()); + when(() => settings.playbackSpeed).thenReturn(1.75); + when(() => settings.trimSilence).thenReturn(false); + when(() => settings.volumeBoost).thenReturn(false); + when(() => settings.fastForwardInterval).thenReturn(30); + when(() => settings.rewindInterval).thenReturn(10); + final calls = []; + final messenger = + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + messenger.setMockMethodCallHandler(NativeAudioPlayerService.platform, ( + call, + ) async { + calls.add(call); + if (call.method == 'getPosition') return 0; + return null; + }); + const events = MethodChannel('com.pinepods/audio_events'); + messenger.setMockMethodCallHandler(events, (_) async => null); + addTearDown(() { + messenger.setMockMethodCallHandler( + NativeAudioPlayerService.platform, + null, + ); + messenger.setMockMethodCallHandler(events, null); + }); + final service = NativeAudioPlayerService( + repository: repository, + settingsService: settings, + ); + await tester.pump(const Duration(milliseconds: 1)); + await service.playEpisode(episode: episode, resume: resume); + final play = calls.singleWhere((c) => c.method == 'playEpisode'); + expect(play.arguments['startPosition'], resume ? 120000 : 0); + expect(episode.position, resume ? 120000 : 0); + await service.stop(); + await tester.pump(const Duration(milliseconds: 1)); + }); + } +} From e9ca2c23cdbb999fd44ec6af499e1180a367f185 Mon Sep 17 00:00:00 2001 From: Ashton Kinslow Date: Sat, 17 Oct 2026 21:06:02 -0500 Subject: [PATCH 2/2] fix(mobile): retain newer progress during resume lookup --- .../audio/native_audio_player_service.dart | 4 ++- .../services/audio/native_playback_test.dart | 32 ++++++++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/mobile/lib/services/audio/native_audio_player_service.dart b/mobile/lib/services/audio/native_audio_player_service.dart index fe7030b1d..74aa02542 100644 --- a/mobile/lib/services/audio/native_audio_player_service.dart +++ b/mobile/lib/services/audio/native_audio_player_service.dart @@ -425,7 +425,9 @@ class NativeAudioPlayerService extends AudioPlayerService { // Get best position (furthest of local vs server) final bestPosition = resume ? await _getBestEpisodePosition(episode) : 0; - _currentEpisode!.position = bestPosition; + if (!resume || bestPosition > _currentEpisode!.position) { + _currentEpisode!.position = bestPosition; + } await repository.saveEpisode(_currentEpisode!); diff --git a/mobile/test/services/audio/native_playback_test.dart b/mobile/test/services/audio/native_playback_test.dart index f28ff6aab..ec912d6e3 100644 --- a/mobile/test/services/audio/native_playback_test.dart +++ b/mobile/test/services/audio/native_playback_test.dart @@ -5,18 +5,27 @@ import 'package:mocktail/mocktail.dart'; import 'package:pinepods_mobile/repository/repository.dart'; import 'package:pinepods_mobile/services/audio/native_audio_player_service.dart'; import 'package:pinepods_mobile/services/settings/settings_service.dart'; +import 'package:pinepods_mobile/services/pinepods/pinepods_audio_service.dart'; import '../../support/factories.dart'; class MockRepository extends Mock implements Repository {} class MockSettings extends Mock implements SettingsService {} +class MockPinepodsAudio extends Mock implements PinepodsAudioService {} + void main() { - for (final resume in [false, true]) { - testWidgets('native playback honors resume=$resume', (tester) async { + for (final (resume, advancesDuringLookup) in [ + (false, false), + (true, false), + (true, true), + ]) { + testWidgets('resume=$resume preserves newer progress=$advancesDuringLookup', ( + tester, + ) async { final repository = MockRepository(); final settings = MockSettings(); - final episode = buildEpisode(position: 120000); + final episode = buildEpisode(guid: 'pinepods_101', position: 120000); when(() => repository.loadQueue()).thenAnswer((_) async => []); when( () => repository.saveEpisode(episode), @@ -53,10 +62,23 @@ void main() { settingsService: settings, ); await tester.pump(const Duration(milliseconds: 1)); + if (advancesDuringLookup) { + final pinepods = MockPinepodsAudio(); + when( + () => pinepods.getServerPositionForEpisode(101, 0, false), + ).thenAnswer((_) async { + // A local position save can finish while the server lookup is pending. + episode.position = 150000; + return 100; + }); + service.setPinepodsAudioService(pinepods); + } await service.playEpisode(episode: episode, resume: resume); final play = calls.singleWhere((c) => c.method == 'playEpisode'); - expect(play.arguments['startPosition'], resume ? 120000 : 0); - expect(episode.position, resume ? 120000 : 0); + final expected = advancesDuringLookup ? 150000 : (resume ? 120000 : 0); + expect(play.arguments['startPosition'], expected); + expect(episode.position, expected); + service.setPinepodsAudioService(null); await service.stop(); await tester.pump(const Duration(milliseconds: 1)); });