From 3cf95b8a2f26230d7fcbc0280588278eb831b264 Mon Sep 17 00:00:00 2001 From: fadyphil Date: Thu, 12 Mar 2026 23:35:14 +0200 Subject: [PATCH 1/9] chore:ditch the custom progurd file --- android/app/build.gradle.kts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index eec1c78..0113ba3 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -37,10 +37,10 @@ android { signingConfig = signingConfigs.getByName("debug") isMinifyEnabled = true isShrinkResources = true - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) + // proguardFiles( + // getDefaultProguardFile("proguard-android-optimize.txt"), + // "proguard-rules.pro" + // ) } } } From a0d1725f3afd13be6b469fa79e164dc986027d14 Mon Sep 17 00:00:00 2001 From: fadyphil Date: Thu, 12 Mar 2026 23:36:44 +0200 Subject: [PATCH 2/9] chore:(no longer needed) update the proguard file to accomodate all the required classes to prevent them from obfuscation --- android/app/proguard-rules.pro | 44 ++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index a7d845c..499b2a2 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -1,22 +1,36 @@ -# Flutter Play Store deferred components (not used) +# ============================================================ +# Flutter Play Core (Deferred Components) — not used in this app +# Flutter's engine references these but we don't use dynamic delivery. +# R8 sees dangling references and fails; we suppress the warnings. +# ============================================================ -dontwarn com.google.android.play.core.** -# audio_service +# ============================================================ +# Flutter wrapper — keep all Flutter engine classes +# ============================================================ +-keep class io.flutter.app.** { *; } +-keep class io.flutter.plugin.** { *; } +-keep class io.flutter.util.** { *; } +-keep class io.flutter.view.** { *; } +-keep class io.flutter.** { *; } +-keep class io.flutter.plugins.** { *; } + +# ============================================================ +# audio_service — keep the service and all MediaSession classes +# ============================================================ -keep class com.ryanheise.audioservice.** { *; } --keep class com.ryanheise.** { *; } --keep public class * extends androidx.media.MediaBrowserServiceCompat -# just_audio + media3 (newer just_audio uses media3, not exoplayer2) --keep class androidx.media3.** { *; } --dontwarn androidx.media3.** +# ============================================================ +# just_audio / ExoPlayer +# ============================================================ -keep class com.google.android.exoplayer2.** { *; } -dontwarn com.google.android.exoplayer2.** -# on_audio_query --keep class com.lucasjosino.on_audio_query.** { *; } - -# Flutter plugin infrastructure --keep class io.flutter.plugin.** { *; } --keep class io.flutter.embedding.** { *; } - --printusage build/app/outputs/mapping/release/usage.txt +# ============================================================ +# Kotlin coroutines (used by just_audio and audio_service internals) +# ============================================================ +-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} +-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} +-keepclassmembernames class kotlinx.** { + volatile ; +} \ No newline at end of file From c5d9bda71b7674e42613220eb7ae86bb9af31c38 Mon Sep 17 00:00:00 2001 From: fadyphil Date: Thu, 12 Mar 2026 23:38:42 +0200 Subject: [PATCH 3/9] chore:update the manifest to add tool for the node tool to make sure it's used and passed down not shaked nor not used --- android/app/src/main/AndroidManifest.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a2577df..2947403 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + @@ -58,7 +58,8 @@ + android:exported="true" + tools:node="merge"> From c80080e4db6446e86ceda672105e6f26342850f6 Mon Sep 17 00:00:00 2001 From: fadyphil Date: Thu, 12 Mar 2026 23:42:11 +0200 Subject: [PATCH 4/9] chore:change the 'Audio Player' registration from lazy to eager and add the 'androidStopForegroundOnPause' attribute and set it to false (i have no idea what that does but it was made to keep everything smooth not essential for anything) --- lib/core/di/init_dependencies.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/core/di/init_dependencies.dart b/lib/core/di/init_dependencies.dart index e4d610e..c8bf608 100644 --- a/lib/core/di/init_dependencies.dart +++ b/lib/core/di/init_dependencies.dart @@ -89,19 +89,21 @@ Future initDependencies() async { final sharedPreferences = await SharedPreferences.getInstance(); final appRouter = AppRouter(); final mediaStore = MediaStore(); + final audioPlayer = AudioPlayer(); serviceLocator.registerLazySingleton(() => mediaStore); serviceLocator.registerLazySingleton(() => sharedPreferences); serviceLocator.registerLazySingleton(() => OnAudioQuery()); - serviceLocator.registerLazySingleton(() => AudioPlayer()); + serviceLocator.registerSingleton(audioPlayer); final audioHandler = await AudioService.init( - builder: () => MusicPlayerHandler(player: serviceLocator()), - config: const AudioServiceConfig( + builder: () => MusicPlayerHandler(player: audioPlayer), + config: AudioServiceConfig( androidNotificationChannelId: 'com.example.music_player.channel.audio', androidNotificationChannelName: 'Music Playback', androidNotificationOngoing: true, androidShowNotificationBadge: false, + androidStopForegroundOnPause: false, ), ); From 92c118cd431c33d2138288da3170a2554d65efe9 Mon Sep 17 00:00:00 2001 From: fadyphil Date: Thu, 12 Mar 2026 23:43:51 +0200 Subject: [PATCH 5/9] fix:cancel subscription to position (of the song) to reduce the stream overhead --- .../services/music_analytics_service.dart | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/features/analytics/domain/services/music_analytics_service.dart b/lib/features/analytics/domain/services/music_analytics_service.dart index 7a8b10c..10cd3ba 100644 --- a/lib/features/analytics/domain/services/music_analytics_service.dart +++ b/lib/features/analytics/domain/services/music_analytics_service.dart @@ -13,11 +13,11 @@ class MusicAnalyticsService with WidgetsBindingObserver { StreamSubscription? _currentSongSubscription; StreamSubscription? _durationSubscription; StreamSubscription? _completionSubscription; - StreamSubscription? _positionSubscription; + // StreamSubscription? _positionSubscription; SongEntity? _currentSong; Duration _currentSongDuration = Duration.zero; - Duration _lastPosition = Duration.zero; + // Duration _lastPosition = Duration.zero; DateTime? _playStartTime; int _accumulatedMilliseconds = 0; bool _isPlaying = false; @@ -38,9 +38,9 @@ class MusicAnalyticsService with WidgetsBindingObserver { _completionSubscription = _audioRepository.playerCompleteStream.listen( (_) => _onSongCompleted(), ); - _positionSubscription = _audioRepository.positionStream.listen( - _onPositionChanged, - ); + // _positionSubscription = _audioRepository.positionStream.listen( + // _onPositionChanged, + // ); } void _onPlayerStateChanged(bool isPlaying) { @@ -79,7 +79,7 @@ class MusicAnalyticsService with WidgetsBindingObserver { : Duration.zero; _accumulatedMilliseconds = 0; - _lastPosition = Duration.zero; + // _lastPosition = Duration.zero; _playStartTime = _isPlaying ? DateTime.now() : null; } @@ -90,23 +90,23 @@ class MusicAnalyticsService with WidgetsBindingObserver { } } - void _onPositionChanged(Duration position) { - if (_currentSongDuration == Duration.zero) return; + // void _onPositionChanged(Duration position) { + // if (_currentSongDuration == Duration.zero) return; - // Check for wrap-around (Loop detection) - // If position jumps from near end (> 90%) to near start (< 5s) - if (position < _lastPosition) { - final thresholdHigh = _currentSongDuration.inMilliseconds * 0.90; - const thresholdLow = 5000; // 5 seconds + // // Check for wrap-around (Loop detection) + // // If position jumps from near end (> 90%) to near start (< 5s) + // if (position < _lastPosition) { + // final thresholdHigh = _currentSongDuration.inMilliseconds * 0.90; + // const thresholdLow = 5000; // 5 seconds - if (_lastPosition.inMilliseconds > thresholdHigh && - position.inMilliseconds < thresholdLow) { - // Detected Loop or Restart - _onSongCompleted(); - } - } - _lastPosition = position; - } + // if (_lastPosition.inMilliseconds > thresholdHigh && + // position.inMilliseconds < thresholdLow) { + // // Detected Loop or Restart + // _onSongCompleted(); + // } + // } + // _lastPosition = position; + // } void _onSongCompleted() { if (_currentSong != null) { @@ -118,7 +118,7 @@ class MusicAnalyticsService with WidgetsBindingObserver { ); // Reset accumulator to prevent double logging if song changes later _accumulatedMilliseconds = 0; - + // If playing (looping), restart the timer immediately if (_isPlaying) { _playStartTime = DateTime.now(); @@ -200,6 +200,6 @@ class MusicAnalyticsService with WidgetsBindingObserver { _currentSongSubscription?.cancel(); _durationSubscription?.cancel(); _completionSubscription?.cancel(); - _positionSubscription?.cancel(); + // _positionSubscription?.cancel(); } } From 8c4a410cda5a209bdb863b2119c5d7fa243ad33f Mon Sep 17 00:00:00 2001 From: fadyphil Date: Thu, 12 Mar 2026 23:46:32 +0200 Subject: [PATCH 6/9] fix:enchanced the 'getSongById()' (again i don't know but claude suggested switching from 'querysongs()' to 'queryaudiofrom()') --- .../data/datasource/local_music_datasource.dart | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/features/local music/data/datasource/local_music_datasource.dart b/lib/features/local music/data/datasource/local_music_datasource.dart index 8f4fabe..715c5c6 100644 --- a/lib/features/local music/data/datasource/local_music_datasource.dart +++ b/lib/features/local music/data/datasource/local_music_datasource.dart @@ -195,15 +195,19 @@ class LocalMusicDatasourceImpl implements LocalMusicDatasource { // 2. Use OnAudioQuery for others try { - final songs = await _onAudioQuery.querySongs( + final songs = await _onAudioQuery.queryAudiosFrom( + // AudiosFromType.AUDIO_ID, + AudiosFromType.ALBUM_ID, + id, sortType: SongSortType.DATE_ADDED, - orderType: OrderType.DESC_OR_GREATER, - uriType: UriType.EXTERNAL, + orderType: OrderType.ASC_OR_SMALLER, + // uriType: UriType.EXTERNAL, ignoreCase: true, ); + if (songs.isEmpty) return null; - final match = songs.firstWhere((s) => s.id == id); - return SongMapper.toEntity(match); + // final match = songs.firstWhere((s) => s.id == id); + return SongMapper.toEntity(songs.first); } catch (e) { return null; } From 52fdf4afe2c0ed0004d00e0b7d1a5faeae294442 Mon Sep 17 00:00:00 2001 From: fadyphil Date: Thu, 12 Mar 2026 23:48:23 +0200 Subject: [PATCH 7/9] change the 'setQueue()' to achieve a real unique id instead of possible and probable same unique id (more on that in the comments) --- .../repos/audio_player_repository_impl.dart | 73 ++++++++++++------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/lib/features/music_player/data/repos/audio_player_repository_impl.dart b/lib/features/music_player/data/repos/audio_player_repository_impl.dart index 0070918..c55349f 100644 --- a/lib/features/music_player/data/repos/audio_player_repository_impl.dart +++ b/lib/features/music_player/data/repos/audio_player_repository_impl.dart @@ -12,33 +12,57 @@ class AudioPlayerRepositoryImpl implements AudioPlayerRepository { // Helper to access custom methods of our handler MusicPlayerHandler get _handler => _audioHandler as MusicPlayerHandler; + // @override + // Future setQueue(List songs, int initialIndex) async { + // final mediaItems = songs.map((song) { + // final uniqueId = + // DateTime.now().microsecondsSinceEpoch.toString() + + // song.id.toString(); // Simple unique ID + // return MediaItem( + // id: song.id.toString(), + // album: song.album, + // title: song.title, + // artist: song.artist, + // artUri: song.albumId != null + // ? Uri.parse( + // "content://media/external/audio/albumart/${song.albumId}", + // ) + // : null, + // duration: Duration(milliseconds: song.duration.toInt()), + // extras: {'url': song.path, 'uniqueId': uniqueId}, + // ); + // }).toList(); + + // await _handler.setQueueItems(items: mediaItems, initialIndex: initialIndex); + // } + + /// Here is the new suggested method by claude to make sure that each song has its unique id since the old commented one was doing a great job but the problem was that the map() function is sync so in AOT it is so fast that it can give multiple songgs the same "unique id" which defeats the while purpose of a unuique id , so this implementation below solves that problem./// @override Future setQueue(List songs, int initialIndex) async { - final mediaItems = songs.map((song) { - final uniqueId = DateTime.now().microsecondsSinceEpoch.toString() + - song.id.toString(); // Simple unique ID + final mediaItems = songs.asMap().entries.map((entry) { + final index = entry.key; + final song = entry.value; return MediaItem( id: song.id.toString(), - album: song.album, title: song.title, artist: song.artist, + album: song.album, artUri: song.albumId != null ? Uri.parse( "content://media/external/audio/albumart/${song.albumId}", ) : null, duration: Duration(milliseconds: song.duration.toInt()), - extras: {'url': song.path, 'uniqueId': uniqueId}, + extras: {'url': song.path, 'uniqueId': '${song.id}_$index'}, ); }).toList(); - await _handler.setQueueItems(items: mediaItems, initialIndex: initialIndex); } @override Future addQueueItem(SongEntity song) async { - final uniqueId = DateTime.now().microsecondsSinceEpoch.toString() + - song.id.toString(); + final uniqueId = + DateTime.now().microsecondsSinceEpoch.toString() + song.id.toString(); final item = MediaItem( id: song.id.toString(), album: song.album, @@ -140,17 +164,17 @@ class AudioPlayerRepositoryImpl implements AudioPlayerRepository { @override Stream get loopModeStream => _audioHandler.playbackState.map((state) { - switch (state.repeatMode) { - case AudioServiceRepeatMode.none: - return 0; - case AudioServiceRepeatMode.all: - return 1; - case AudioServiceRepeatMode.one: - return 2; - default: - return 0; - } - }).distinct(); + switch (state.repeatMode) { + case AudioServiceRepeatMode.none: + return 0; + case AudioServiceRepeatMode.all: + return 1; + case AudioServiceRepeatMode.one: + return 2; + default: + return 0; + } + }).distinct(); @override Stream get positionStream => AudioService.position; @@ -160,10 +184,9 @@ class AudioPlayerRepositoryImpl implements AudioPlayerRepository { _audioHandler.mediaItem.map((item) => item?.duration ?? Duration.zero); @override - Stream get playerCompleteStream => _audioHandler.playbackState - .where((state) => state.processingState == AudioProcessingState.completed) - .map((event) => null) - .distinct(); + Stream get playerCompleteStream => _audioHandler.playbackState.where( + (state) => state.processingState == AudioProcessingState.completed, + ); @override Stream get currentSongStream => @@ -205,8 +228,8 @@ class AudioPlayerRepositoryImpl implements AudioPlayerRepository { @override Future playNext(SongEntity song) async { - final uniqueId = DateTime.now().microsecondsSinceEpoch.toString() + - song.id.toString(); + final uniqueId = + DateTime.now().microsecondsSinceEpoch.toString() + song.id.toString(); final item = MediaItem( id: song.id.toString(), album: song.album, From 10e8009ded5b2e8d70d064ebc412aeba975eea6b Mon Sep 17 00:00:00 2001 From: fadyphil Date: Thu, 12 Mar 2026 23:50:32 +0200 Subject: [PATCH 8/9] fix:optimized the position stream by adding throttle for the position stream to half the overhead and rebuilds since it is not needed to update every 250ms --- .../presentation/bloc/music_player_bloc.dart | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/lib/features/music_player/presentation/bloc/music_player_bloc.dart b/lib/features/music_player/presentation/bloc/music_player_bloc.dart index 0d60dc9..dff626a 100644 --- a/lib/features/music_player/presentation/bloc/music_player_bloc.dart +++ b/lib/features/music_player/presentation/bloc/music_player_bloc.dart @@ -4,6 +4,7 @@ import 'package:music_player/core/usecases/usecase.dart'; import 'package:music_player/features/analytics/domain/usecases/get_all_song_play_counts.dart'; import 'package:music_player/features/music_player/domain/repos/audio_player_repository.dart'; import 'package:music_player/features/local%20music/domain/use%20cases/get_song_by_id_use_case.dart'; +import 'package:stream_transform/stream_transform.dart'; import 'music_player_event.dart'; import 'music_player_state.dart'; @@ -29,9 +30,11 @@ class MusicPlayerBloc extends Bloc { this._getAllSongPlayCounts, ) : super(const MusicPlayerState()) { // 1. Setup Listeners - _positionSubscription = _audioRepository.positionStream.listen((pos) { - add(MusicPlayerEvent.updatePosition(pos)); - }); + _positionSubscription = _audioRepository.positionStream + .throttle(const Duration(milliseconds: 500)) + .listen((pos) { + add(MusicPlayerEvent.updatePosition(pos)); + }); _durationSubscription = _audioRepository.durationStream.listen((dur) { add(MusicPlayerEvent.updateDuration(dur)); @@ -94,9 +97,7 @@ class MusicPlayerBloc extends Bloc { // Revert or show error // If "Loading interrupted", it's fine. emit( - state.copyWith( - errorMessage: "Failed to initialize queue: $e", - ), + state.copyWith(errorMessage: "Failed to initialize queue: $e"), ); } }, @@ -254,21 +255,25 @@ class MusicPlayerBloc extends Bloc { try { await _audioRepository.reorderQueue(e.oldIndex, e.newIndex); } catch (e) { - emit(state.copyWith( + emit( + state.copyWith( queueActionStatus: QueueStatus.failure, - errorMessage: "Failed to reorder: $e" - )); - emit(state.copyWith(queueActionStatus: QueueStatus.initial)); + errorMessage: "Failed to reorder: $e", + ), + ); + emit(state.copyWith(queueActionStatus: QueueStatus.initial)); } }, playQueueItem: (e) async { try { await _audioRepository.skipToQueueItem(e.index); } catch (e) { - emit(state.copyWith( + emit( + state.copyWith( queueActionStatus: QueueStatus.failure, - errorMessage: "Failed to play queue item: $e" - )); + errorMessage: "Failed to play queue item: $e", + ), + ); emit(state.copyWith(queueActionStatus: QueueStatus.initial)); } }, @@ -321,10 +326,7 @@ class MusicPlayerBloc extends Bloc { cancelTimer: (_) async { _sleepTimer?.cancel(); emit( - state.copyWith( - timerRemaining: null, - isEndTrackTimerActive: false, - ), + state.copyWith(timerRemaining: null, isEndTrackTimerActive: false), ); }, tickTimer: (_) async { From 662e4c917acfcb5ab864d87d509afa556fc4f9ba Mon Sep 17 00:00:00 2001 From: fadyphil Date: Thu, 12 Mar 2026 23:55:45 +0200 Subject: [PATCH 9/9] fix:tell the app during minification and obfuscation to keep the drawalbes i made in the drawable folder, tht was the problem that waas making the background notificatio disappear, explanation: the minification wasa discarding the drawables thinking it is useless assets to lighten the app, and when the notification tried to build using the drawables it didn't find them, resulting in the notification not being built at all, by having this file it makes sure to keep those specific assets for the building of the notification which has worked (this only happened in the release run/build) --- android/app/src/main/res/raw/keep.xml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 android/app/src/main/res/raw/keep.xml diff --git a/android/app/src/main/res/raw/keep.xml b/android/app/src/main/res/raw/keep.xml new file mode 100644 index 0000000..8bba10c --- /dev/null +++ b/android/app/src/main/res/raw/keep.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file