From c425b63fc2a1e9ace2342002f78ab9beda416a47 Mon Sep 17 00:00:00 2001 From: Moritz Weber Date: Tue, 7 Apr 2020 22:06:43 +0200 Subject: [PATCH] notification controls, AudioStore --- lib/domain/usecases/play_song.dart | 28 ---------- lib/main.dart | 6 ++- lib/presentation/pages/songs_page.dart | 12 +++-- lib/presentation/state/audio_store.dart | 26 ++++++++++ lib/presentation/state/audio_store.g.dart | 24 +++++++++ lib/presentation/state/music_data_store.dart | 21 +++----- .../state/music_data_store.g.dart | 18 +++---- .../widgets/injection_widget.dart | 31 ++++++++--- lib/system/datasources/audio_manager.dart | 45 +++++++++++++++- lib/system/models/song_model.dart | 3 +- test/domain/usecases/play_song_test.dart | 51 ------------------- test/system/models/song_model_test.dart | 2 +- 12 files changed, 143 insertions(+), 124 deletions(-) delete mode 100644 lib/domain/usecases/play_song.dart create mode 100644 lib/presentation/state/audio_store.dart create mode 100644 lib/presentation/state/audio_store.g.dart delete mode 100644 test/domain/usecases/play_song_test.dart diff --git a/lib/domain/usecases/play_song.dart b/lib/domain/usecases/play_song.dart deleted file mode 100644 index 4923a04..0000000 --- a/lib/domain/usecases/play_song.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:dartz/dartz.dart'; -import 'package:equatable/equatable.dart'; -import 'package:mosh/domain/repositories/audio_repository.dart'; - -import '../../core/error/failures.dart'; -import '../../core/usecase.dart'; -import '../entities/song.dart'; - -class PlaySong implements UseCase { - PlaySong(this.audioRepository); - - final AudioRepository audioRepository; - - @override - Future> call(Params params) async { - return audioRepository.playSong(params.index, params.songList); - } -} - -class Params extends Equatable { - const Params(this.index, this.songList); - - final int index; - final List songList; - - @override - List get props => [index, songList]; -} diff --git a/lib/main.dart b/lib/main.dart index 9a90726..f07b720 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -62,7 +62,11 @@ class _RootPageState extends State { _musicStore.fetchSongs(); // TODO: don't do this here... - AudioService.start(backgroundTaskEntrypoint: _backgroundTaskEntrypoint); + AudioService.start( + backgroundTaskEntrypoint: _backgroundTaskEntrypoint, + enableQueue: true, + androidStopOnRemoveTask: true, + ); super.didChangeDependencies(); } diff --git a/lib/presentation/pages/songs_page.dart b/lib/presentation/pages/songs_page.dart index a1c77be..6e709ae 100644 --- a/lib/presentation/pages/songs_page.dart +++ b/lib/presentation/pages/songs_page.dart @@ -3,11 +3,12 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:provider/provider.dart'; import '../../domain/entities/song.dart'; +import '../state/audio_store.dart'; import '../state/music_data_store.dart'; import '../widgets/album_art_list_tile.dart'; class SongsPage extends StatefulWidget { - SongsPage({Key key}) : super(key: key); + const SongsPage({Key key}) : super(key: key); @override _SongsPageState createState() => _SongsPageState(); @@ -19,12 +20,13 @@ class _SongsPageState extends State @override Widget build(BuildContext context) { print('SongsPage.build'); - final MusicDataStore store = Provider.of(context); + final MusicDataStore musicDataStore = Provider.of(context); + final AudioStore audioStore = Provider.of(context); super.build(context); return Observer(builder: (_) { print('SongsPage.build -> Observer.builder'); - final bool isFetching = store.isFetchingSongs; + final bool isFetching = musicDataStore.isFetchingSongs; if (isFetching) { return Column( @@ -35,7 +37,7 @@ class _SongsPageState extends State ], ); } else { - final List songs = store.songs; + final List songs = musicDataStore.songs; return ListView.separated( itemCount: songs.length, itemBuilder: (_, int index) { @@ -44,7 +46,7 @@ class _SongsPageState extends State title: song.title, subtitle: '${song.artist} • ${song.album}', albumArtPath: song.albumArtPath, - onTap: () => store.playSong(index, songs), + onTap: () => audioStore.playSong(index, songs), ); }, separatorBuilder: (BuildContext context, int index) => const Divider( diff --git a/lib/presentation/state/audio_store.dart b/lib/presentation/state/audio_store.dart new file mode 100644 index 0000000..73211ac --- /dev/null +++ b/lib/presentation/state/audio_store.dart @@ -0,0 +1,26 @@ +import 'package:meta/meta.dart'; +import 'package:mobx/mobx.dart'; + +import '../../domain/entities/song.dart'; +import '../../domain/repositories/audio_repository.dart'; +import '../../domain/repositories/music_data_repository.dart'; + +part 'audio_store.g.dart'; + +class AudioStore extends _AudioStore with _$AudioStore { + AudioStore({@required MusicDataRepository musicDataRepository, @required AudioRepository audioRepository}) + : super(musicDataRepository, audioRepository); +} + +abstract class _AudioStore with Store { + _AudioStore(this._musicDataRepository, this._audioRepository); + + final MusicDataRepository _musicDataRepository; + final AudioRepository _audioRepository; + + @action + Future playSong(int index, List songList) async { + _audioRepository.playSong(index, songList); + } + +} \ No newline at end of file diff --git a/lib/presentation/state/audio_store.g.dart b/lib/presentation/state/audio_store.g.dart new file mode 100644 index 0000000..e372b2f --- /dev/null +++ b/lib/presentation/state/audio_store.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'audio_store.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic + +mixin _$AudioStore on _AudioStore, Store { + final _$playSongAsyncAction = AsyncAction('playSong'); + + @override + Future playSong(int index, List songList) { + return _$playSongAsyncAction.run(() => super.playSong(index, songList)); + } + + @override + String toString() { + final string = ''; + return '{$string}'; + } +} diff --git a/lib/presentation/state/music_data_store.dart b/lib/presentation/state/music_data_store.dart index 0e785b4..b5fa90b 100644 --- a/lib/presentation/state/music_data_store.dart +++ b/lib/presentation/state/music_data_store.dart @@ -1,12 +1,10 @@ import 'package:dartz/dartz.dart'; import 'package:meta/meta.dart'; import 'package:mobx/mobx.dart'; -import 'package:mosh/domain/usecases/play_song.dart'; import '../../core/error/failures.dart'; import '../../domain/entities/album.dart'; import '../../domain/entities/song.dart'; -import '../../domain/repositories/audio_repository.dart'; import '../../domain/repositories/music_data_repository.dart'; import '../../domain/usecases/get_albums.dart'; import '../../domain/usecases/get_songs.dart'; @@ -14,22 +12,20 @@ import '../../domain/usecases/update_database.dart'; part 'music_data_store.g.dart'; -class MusicDataStore extends _MusicStore with _$MusicDataStore { - MusicDataStore({@required MusicDataRepository musicDataRepository, @required AudioRepository audioRepository}) - : super(musicDataRepository, audioRepository); +class MusicDataStore extends _MusicDataStore with _$MusicDataStore { + MusicDataStore({@required MusicDataRepository musicDataRepository}) + : super(musicDataRepository); } -abstract class _MusicStore with Store { - _MusicStore(MusicDataRepository _musicDataRepository, AudioRepository _audioRepository) +abstract class _MusicDataStore with Store { + _MusicDataStore(MusicDataRepository _musicDataRepository) : _updateDatabase = UpdateDatabase(_musicDataRepository), _getAlbums = GetAlbums(_musicDataRepository), - _getSongs = GetSongs(_musicDataRepository), - _playSong = PlaySong(_audioRepository); + _getSongs = GetSongs(_musicDataRepository); final UpdateDatabase _updateDatabase; final GetAlbums _getAlbums; final GetSongs _getSongs; - final PlaySong _playSong; @observable ObservableFuture> albumsFuture; @@ -81,9 +77,4 @@ abstract class _MusicStore with Store { isFetchingSongs = false; } - - @action - Future playSong(int index, List songList) async { - await _playSong(Params(index, songList)); - } } diff --git a/lib/presentation/state/music_data_store.g.dart b/lib/presentation/state/music_data_store.g.dart index c349aef..12c48d2 100644 --- a/lib/presentation/state/music_data_store.g.dart +++ b/lib/presentation/state/music_data_store.g.dart @@ -8,8 +8,8 @@ part of 'music_data_store.dart'; // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic -mixin _$MusicDataStore on _MusicStore, Store { - final _$albumsFutureAtom = Atom(name: '_MusicStore.albumsFuture'); +mixin _$MusicDataStore on _MusicDataStore, Store { + final _$albumsFutureAtom = Atom(name: '_MusicDataStore.albumsFuture'); @override ObservableFuture> get albumsFuture { @@ -26,7 +26,7 @@ mixin _$MusicDataStore on _MusicStore, Store { }, _$albumsFutureAtom, name: '${_$albumsFutureAtom.name}_set'); } - final _$songsAtom = Atom(name: '_MusicStore.songs'); + final _$songsAtom = Atom(name: '_MusicDataStore.songs'); @override ObservableList get songs { @@ -43,7 +43,7 @@ mixin _$MusicDataStore on _MusicStore, Store { }, _$songsAtom, name: '${_$songsAtom.name}_set'); } - final _$isFetchingSongsAtom = Atom(name: '_MusicStore.isFetchingSongs'); + final _$isFetchingSongsAtom = Atom(name: '_MusicDataStore.isFetchingSongs'); @override bool get isFetchingSongs { @@ -60,7 +60,8 @@ mixin _$MusicDataStore on _MusicStore, Store { }, _$isFetchingSongsAtom, name: '${_$isFetchingSongsAtom.name}_set'); } - final _$isUpdatingDatabaseAtom = Atom(name: '_MusicStore.isUpdatingDatabase'); + final _$isUpdatingDatabaseAtom = + Atom(name: '_MusicDataStore.isUpdatingDatabase'); @override bool get isUpdatingDatabase { @@ -99,13 +100,6 @@ mixin _$MusicDataStore on _MusicStore, Store { return _$fetchSongsAsyncAction.run(() => super.fetchSongs()); } - final _$playSongAsyncAction = AsyncAction('playSong'); - - @override - Future playSong(int index, List songList) { - return _$playSongAsyncAction.run(() => super.playSong(index, songList)); - } - @override String toString() { final string = diff --git a/lib/presentation/widgets/injection_widget.dart b/lib/presentation/widgets/injection_widget.dart index e5df4cc..1caf6b6 100644 --- a/lib/presentation/widgets/injection_widget.dart +++ b/lib/presentation/widgets/injection_widget.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_audio_query/flutter_audio_query.dart'; +import 'package:mosh/domain/repositories/audio_repository.dart'; +import 'package:mosh/domain/repositories/music_data_repository.dart'; +import 'package:mosh/presentation/state/audio_store.dart'; import 'package:mosh/presentation/state/music_data_store.dart'; import 'package:mosh/system/datasources/audio_manager.dart'; import 'package:mosh/system/datasources/local_music_fetcher.dart'; @@ -15,15 +18,29 @@ class InjectionWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Provider( + final MusicDataRepository musicDataRepository = MusicDataRepositoryImpl( + localMusicFetcher: LocalMusicFetcherImpl(FlutterAudioQuery()), + musicDataSource: MoorMusicDataSource(), + ); + + final AudioRepository audioRepository = + AudioRepositoryImpl(AudioManagerImpl()); + + return MultiProvider( child: child, - create: (BuildContext context) => MusicDataStore( - musicDataRepository: MusicDataRepositoryImpl( - localMusicFetcher: LocalMusicFetcherImpl(FlutterAudioQuery()), - musicDataSource: MoorMusicDataSource(), + providers: [ + Provider( + create: (BuildContext context) => MusicDataStore( + musicDataRepository: musicDataRepository, + ), ), - audioRepository: AudioRepositoryImpl(AudioManagerImpl()), - ), + Provider( + create: (BuildContext context) => AudioStore( + musicDataRepository: musicDataRepository, + audioRepository: audioRepository, + ), + ), + ], ); } } diff --git a/lib/system/datasources/audio_manager.dart b/lib/system/datasources/audio_manager.dart index 363761b..2f4a55f 100644 --- a/lib/system/datasources/audio_manager.dart +++ b/lib/system/datasources/audio_manager.dart @@ -11,7 +11,7 @@ class AudioManagerImpl implements AudioManager { Future playSong(int index, List songList) async { final List queue = songList.map((s) => s.toMediaItem()).toList(); - // await AudioService.addQueueItem(queue[index]); + await AudioService.addQueueItem(queue[index]); AudioService.playFromMediaId(queue[index].id); } } @@ -25,6 +25,12 @@ class AudioPlayerTask extends BackgroundAudioTask { @override Future onStart() async { print('onStart'); + + AudioServiceBackground.setState( + controls: [pauseControl, stopControl], + basicState: BasicPlaybackState.playing, + ); + await _completer.future; } @@ -41,8 +47,43 @@ class AudioPlayerTask extends BackgroundAudioTask { @override Future onPlayFromMediaId(String mediaId) async { - // _audioPlayer.setFilePath(_mediaItems[mediaId].id); + + // await _audioPlayer.setFilePath(_mediaItems[mediaId]); + AudioServiceBackground.setMediaItem(_mediaItems[mediaId]); + await _audioPlayer.setFilePath(mediaId); _audioPlayer.play(); } + + @override + Future onPlay() async { + AudioServiceBackground.setState( + controls: [pauseControl, stopControl], + basicState: BasicPlaybackState.playing); + _audioPlayer.play(); + } + + @override + Future onPause() async { + AudioServiceBackground.setState( + controls: [playControl, stopControl], + basicState: BasicPlaybackState.paused); + await _audioPlayer.pause(); + } } + +MediaControl playControl = const MediaControl( + androidIcon: 'drawable/ic_action_play_arrow', + label: 'Play', + action: MediaAction.play, +); +MediaControl pauseControl = const MediaControl( + androidIcon: 'drawable/ic_action_pause', + label: 'Pause', + action: MediaAction.pause, +); +MediaControl stopControl = const MediaControl( + androidIcon: 'drawable/ic_action_stop', + label: 'Stop', + action: MediaAction.stop, +); diff --git a/lib/system/models/song_model.dart b/lib/system/models/song_model.dart index ec7324f..8ce607b 100644 --- a/lib/system/models/song_model.dart +++ b/lib/system/models/song_model.dart @@ -57,12 +57,11 @@ class SongModel extends Song { trackNumber: Value(trackNumber), ); - // TODO: test! MediaItem toMediaItem() => MediaItem( title: title, album: album, artist: artist, - artUri: albumArtPath, + artUri: 'file://$albumArtPath', id: path, ); } diff --git a/test/domain/usecases/play_song_test.dart b/test/domain/usecases/play_song_test.dart deleted file mode 100644 index 0409a7d..0000000 --- a/test/domain/usecases/play_song_test.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:mosh/domain/entities/song.dart'; -import 'package:mosh/domain/repositories/audio_repository.dart'; -import 'package:mosh/domain/usecases/play_song.dart'; - -import '../../test_constants.dart'; - -class MockAudioRepository extends Mock implements AudioRepository {} - -void main() { - PlaySong usecase; - MockAudioRepository mockAudioRepository; - - setUp(() { - mockAudioRepository = MockAudioRepository(); - usecase = PlaySong(mockAudioRepository); - }); - - const tIndex = 0; - - final tSongList = [ - Song( - album: ALBUM_TITLE_3, - artist: ARTIST_3, - title: SONG_TITLE_3, - path: PATH_3, - albumArtPath: ALBUM_ART_PATH_3, - trackNumber: TRACKNUMBER_3, - ), - Song( - album: ALBUM_TITLE_4, - artist: ARTIST_4, - title: SONG_TITLE_4, - path: PATH_4, - albumArtPath: ALBUM_ART_PATH_4, - trackNumber: TRACKNUMBER_4, - ), - ]; - - test( - 'should forward index and song list to repository', - () async { - // act - await usecase(Params(tIndex, tSongList)); - // assert - verify(mockAudioRepository.playSong(tIndex, tSongList)); - verifyNoMoreInteractions(mockAudioRepository); - }, - ); -} diff --git a/test/system/models/song_model_test.dart b/test/system/models/song_model_test.dart index d1b21ac..fe35bf8 100644 --- a/test/system/models/song_model_test.dart +++ b/test/system/models/song_model_test.dart @@ -74,7 +74,7 @@ void main() { title: SONG_TITLE_3, album: ALBUM_TITLE_3, artist: ARTIST_3, - artUri: ALBUM_ART_PATH_3, + artUri: 'file://$ALBUM_ART_PATH_3', ); final songModel = SongModel(