Skip to content

Commit 5533be7

Browse files
authored
Merge pull request #16 from fadyphil/feat/analytics-overhaul
Feat/analytics overhaul(notification fix)
2 parents 8cafeb4 + 662e4c9 commit 5533be7

9 files changed

Lines changed: 147 additions & 94 deletions

File tree

android/app/build.gradle.kts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ android {
3737
signingConfig = signingConfigs.getByName("debug")
3838
isMinifyEnabled = true
3939
isShrinkResources = true
40-
proguardFiles(
41-
getDefaultProguardFile("proguard-android-optimize.txt"),
42-
"proguard-rules.pro"
43-
)
40+
// proguardFiles(
41+
// getDefaultProguardFile("proguard-android-optimize.txt"),
42+
// "proguard-rules.pro"
43+
// )
4444
}
4545
}
4646
}

android/app/proguard-rules.pro

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,36 @@
1-
# Flutter Play Store deferred components (not used)
1+
# ============================================================
2+
# Flutter Play Core (Deferred Components) — not used in this app
3+
# Flutter's engine references these but we don't use dynamic delivery.
4+
# R8 sees dangling references and fails; we suppress the warnings.
5+
# ============================================================
26
-dontwarn com.google.android.play.core.**
37

4-
# audio_service
8+
# ============================================================
9+
# Flutter wrapper — keep all Flutter engine classes
10+
# ============================================================
11+
-keep class io.flutter.app.** { *; }
12+
-keep class io.flutter.plugin.** { *; }
13+
-keep class io.flutter.util.** { *; }
14+
-keep class io.flutter.view.** { *; }
15+
-keep class io.flutter.** { *; }
16+
-keep class io.flutter.plugins.** { *; }
17+
18+
# ============================================================
19+
# audio_service — keep the service and all MediaSession classes
20+
# ============================================================
521
-keep class com.ryanheise.audioservice.** { *; }
6-
-keep class com.ryanheise.** { *; }
7-
-keep public class * extends androidx.media.MediaBrowserServiceCompat
822

9-
# just_audio + media3 (newer just_audio uses media3, not exoplayer2)
10-
-keep class androidx.media3.** { *; }
11-
-dontwarn androidx.media3.**
23+
# ============================================================
24+
# just_audio / ExoPlayer
25+
# ============================================================
1226
-keep class com.google.android.exoplayer2.** { *; }
1327
-dontwarn com.google.android.exoplayer2.**
1428

15-
# on_audio_query
16-
-keep class com.lucasjosino.on_audio_query.** { *; }
17-
18-
# Flutter plugin infrastructure
19-
-keep class io.flutter.plugin.** { *; }
20-
-keep class io.flutter.embedding.** { *; }
21-
22-
-printusage build/app/outputs/mapping/release/usage.txt
29+
# ============================================================
30+
# Kotlin coroutines (used by just_audio and audio_service internals)
31+
# ============================================================
32+
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
33+
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
34+
-keepclassmembernames class kotlinx.** {
35+
volatile <fields>;
36+
}

android/app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
22
<!-- 1. PERMISSIONS GO HERE (Directly inside manifest, NOT inside application) -->
33
<uses-permission android:name="android.permission.INTERNET"/>
44

@@ -58,7 +58,8 @@
5858
<!-- The Service that keeps music playing in background -->
5959
<service android:name="com.ryanheise.audioservice.AudioService"
6060
android:foregroundServiceType="mediaPlayback"
61-
android:exported="true">
61+
android:exported="true"
62+
tools:node="merge">
6263
<intent-filter>
6364
<action android:name="android.media.browse.MediaBrowserService" />
6465
</intent-filter>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources xmlns:tools="http://schemas.android.com/tools"
3+
tools:keep="@drawable/ic_shuffle,
4+
@drawable/ic_shuffle_on,
5+
@drawable/ic_repeat,
6+
@drawable/ic_repeat_on,
7+
@drawable/ic_repeat_one"/>

lib/core/di/init_dependencies.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,19 +89,21 @@ Future<void> initDependencies() async {
8989
final sharedPreferences = await SharedPreferences.getInstance();
9090
final appRouter = AppRouter();
9191
final mediaStore = MediaStore();
92+
final audioPlayer = AudioPlayer();
9293
serviceLocator.registerLazySingleton(() => mediaStore);
9394

9495
serviceLocator.registerLazySingleton(() => sharedPreferences);
9596

9697
serviceLocator.registerLazySingleton(() => OnAudioQuery());
97-
serviceLocator.registerLazySingleton(() => AudioPlayer());
98+
serviceLocator.registerSingleton<AudioPlayer>(audioPlayer);
9899
final audioHandler = await AudioService.init(
99-
builder: () => MusicPlayerHandler(player: serviceLocator()),
100-
config: const AudioServiceConfig(
100+
builder: () => MusicPlayerHandler(player: audioPlayer),
101+
config: AudioServiceConfig(
101102
androidNotificationChannelId: 'com.example.music_player.channel.audio',
102103
androidNotificationChannelName: 'Music Playback',
103104
androidNotificationOngoing: true,
104105
androidShowNotificationBadge: false,
106+
androidStopForegroundOnPause: false,
105107
),
106108
);
107109

lib/features/analytics/domain/services/music_analytics_service.dart

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ class MusicAnalyticsService with WidgetsBindingObserver {
1313
StreamSubscription? _currentSongSubscription;
1414
StreamSubscription? _durationSubscription;
1515
StreamSubscription? _completionSubscription;
16-
StreamSubscription? _positionSubscription;
16+
// StreamSubscription? _positionSubscription;
1717

1818
SongEntity? _currentSong;
1919
Duration _currentSongDuration = Duration.zero;
20-
Duration _lastPosition = Duration.zero;
20+
// Duration _lastPosition = Duration.zero;
2121
DateTime? _playStartTime;
2222
int _accumulatedMilliseconds = 0;
2323
bool _isPlaying = false;
@@ -38,9 +38,9 @@ class MusicAnalyticsService with WidgetsBindingObserver {
3838
_completionSubscription = _audioRepository.playerCompleteStream.listen(
3939
(_) => _onSongCompleted(),
4040
);
41-
_positionSubscription = _audioRepository.positionStream.listen(
42-
_onPositionChanged,
43-
);
41+
// _positionSubscription = _audioRepository.positionStream.listen(
42+
// _onPositionChanged,
43+
// );
4444
}
4545

4646
void _onPlayerStateChanged(bool isPlaying) {
@@ -79,7 +79,7 @@ class MusicAnalyticsService with WidgetsBindingObserver {
7979
: Duration.zero;
8080

8181
_accumulatedMilliseconds = 0;
82-
_lastPosition = Duration.zero;
82+
// _lastPosition = Duration.zero;
8383
_playStartTime = _isPlaying ? DateTime.now() : null;
8484
}
8585

@@ -90,23 +90,23 @@ class MusicAnalyticsService with WidgetsBindingObserver {
9090
}
9191
}
9292

93-
void _onPositionChanged(Duration position) {
94-
if (_currentSongDuration == Duration.zero) return;
93+
// void _onPositionChanged(Duration position) {
94+
// if (_currentSongDuration == Duration.zero) return;
9595

96-
// Check for wrap-around (Loop detection)
97-
// If position jumps from near end (> 90%) to near start (< 5s)
98-
if (position < _lastPosition) {
99-
final thresholdHigh = _currentSongDuration.inMilliseconds * 0.90;
100-
const thresholdLow = 5000; // 5 seconds
96+
// // Check for wrap-around (Loop detection)
97+
// // If position jumps from near end (> 90%) to near start (< 5s)
98+
// if (position < _lastPosition) {
99+
// final thresholdHigh = _currentSongDuration.inMilliseconds * 0.90;
100+
// const thresholdLow = 5000; // 5 seconds
101101

102-
if (_lastPosition.inMilliseconds > thresholdHigh &&
103-
position.inMilliseconds < thresholdLow) {
104-
// Detected Loop or Restart
105-
_onSongCompleted();
106-
}
107-
}
108-
_lastPosition = position;
109-
}
102+
// if (_lastPosition.inMilliseconds > thresholdHigh &&
103+
// position.inMilliseconds < thresholdLow) {
104+
// // Detected Loop or Restart
105+
// _onSongCompleted();
106+
// }
107+
// }
108+
// _lastPosition = position;
109+
// }
110110

111111
void _onSongCompleted() {
112112
if (_currentSong != null) {
@@ -118,7 +118,7 @@ class MusicAnalyticsService with WidgetsBindingObserver {
118118
);
119119
// Reset accumulator to prevent double logging if song changes later
120120
_accumulatedMilliseconds = 0;
121-
121+
122122
// If playing (looping), restart the timer immediately
123123
if (_isPlaying) {
124124
_playStartTime = DateTime.now();
@@ -200,6 +200,6 @@ class MusicAnalyticsService with WidgetsBindingObserver {
200200
_currentSongSubscription?.cancel();
201201
_durationSubscription?.cancel();
202202
_completionSubscription?.cancel();
203-
_positionSubscription?.cancel();
203+
// _positionSubscription?.cancel();
204204
}
205205
}

lib/features/local music/data/datasource/local_music_datasource.dart

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -195,15 +195,19 @@ class LocalMusicDatasourceImpl implements LocalMusicDatasource {
195195

196196
// 2. Use OnAudioQuery for others
197197
try {
198-
final songs = await _onAudioQuery.querySongs(
198+
final songs = await _onAudioQuery.queryAudiosFrom(
199+
// AudiosFromType.AUDIO_ID,
200+
AudiosFromType.ALBUM_ID,
201+
id,
199202
sortType: SongSortType.DATE_ADDED,
200-
orderType: OrderType.DESC_OR_GREATER,
201-
uriType: UriType.EXTERNAL,
203+
orderType: OrderType.ASC_OR_SMALLER,
204+
// uriType: UriType.EXTERNAL,
202205
ignoreCase: true,
203206
);
207+
if (songs.isEmpty) return null;
204208

205-
final match = songs.firstWhere((s) => s.id == id);
206-
return SongMapper.toEntity(match);
209+
// final match = songs.firstWhere((s) => s.id == id);
210+
return SongMapper.toEntity(songs.first);
207211
} catch (e) {
208212
return null;
209213
}

lib/features/music_player/data/repos/audio_player_repository_impl.dart

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,33 +12,57 @@ class AudioPlayerRepositoryImpl implements AudioPlayerRepository {
1212
// Helper to access custom methods of our handler
1313
MusicPlayerHandler get _handler => _audioHandler as MusicPlayerHandler;
1414

15+
// @override
16+
// Future<void> setQueue(List<SongEntity> songs, int initialIndex) async {
17+
// final mediaItems = songs.map((song) {
18+
// final uniqueId =
19+
// DateTime.now().microsecondsSinceEpoch.toString() +
20+
// song.id.toString(); // Simple unique ID
21+
// return MediaItem(
22+
// id: song.id.toString(),
23+
// album: song.album,
24+
// title: song.title,
25+
// artist: song.artist,
26+
// artUri: song.albumId != null
27+
// ? Uri.parse(
28+
// "content://media/external/audio/albumart/${song.albumId}",
29+
// )
30+
// : null,
31+
// duration: Duration(milliseconds: song.duration.toInt()),
32+
// extras: {'url': song.path, 'uniqueId': uniqueId},
33+
// );
34+
// }).toList();
35+
36+
// await _handler.setQueueItems(items: mediaItems, initialIndex: initialIndex);
37+
// }
38+
39+
/// 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.///
1540
@override
1641
Future<void> setQueue(List<SongEntity> songs, int initialIndex) async {
17-
final mediaItems = songs.map((song) {
18-
final uniqueId = DateTime.now().microsecondsSinceEpoch.toString() +
19-
song.id.toString(); // Simple unique ID
42+
final mediaItems = songs.asMap().entries.map((entry) {
43+
final index = entry.key;
44+
final song = entry.value;
2045
return MediaItem(
2146
id: song.id.toString(),
22-
album: song.album,
2347
title: song.title,
2448
artist: song.artist,
49+
album: song.album,
2550
artUri: song.albumId != null
2651
? Uri.parse(
2752
"content://media/external/audio/albumart/${song.albumId}",
2853
)
2954
: null,
3055
duration: Duration(milliseconds: song.duration.toInt()),
31-
extras: {'url': song.path, 'uniqueId': uniqueId},
56+
extras: {'url': song.path, 'uniqueId': '${song.id}_$index'},
3257
);
3358
}).toList();
34-
3559
await _handler.setQueueItems(items: mediaItems, initialIndex: initialIndex);
3660
}
3761

3862
@override
3963
Future<void> addQueueItem(SongEntity song) async {
40-
final uniqueId = DateTime.now().microsecondsSinceEpoch.toString() +
41-
song.id.toString();
64+
final uniqueId =
65+
DateTime.now().microsecondsSinceEpoch.toString() + song.id.toString();
4266
final item = MediaItem(
4367
id: song.id.toString(),
4468
album: song.album,
@@ -140,17 +164,17 @@ class AudioPlayerRepositoryImpl implements AudioPlayerRepository {
140164

141165
@override
142166
Stream<int> get loopModeStream => _audioHandler.playbackState.map((state) {
143-
switch (state.repeatMode) {
144-
case AudioServiceRepeatMode.none:
145-
return 0;
146-
case AudioServiceRepeatMode.all:
147-
return 1;
148-
case AudioServiceRepeatMode.one:
149-
return 2;
150-
default:
151-
return 0;
152-
}
153-
}).distinct();
167+
switch (state.repeatMode) {
168+
case AudioServiceRepeatMode.none:
169+
return 0;
170+
case AudioServiceRepeatMode.all:
171+
return 1;
172+
case AudioServiceRepeatMode.one:
173+
return 2;
174+
default:
175+
return 0;
176+
}
177+
}).distinct();
154178

155179
@override
156180
Stream<Duration> get positionStream => AudioService.position;
@@ -160,10 +184,9 @@ class AudioPlayerRepositoryImpl implements AudioPlayerRepository {
160184
_audioHandler.mediaItem.map((item) => item?.duration ?? Duration.zero);
161185

162186
@override
163-
Stream<void> get playerCompleteStream => _audioHandler.playbackState
164-
.where((state) => state.processingState == AudioProcessingState.completed)
165-
.map((event) => null)
166-
.distinct();
187+
Stream<void> get playerCompleteStream => _audioHandler.playbackState.where(
188+
(state) => state.processingState == AudioProcessingState.completed,
189+
);
167190

168191
@override
169192
Stream<SongEntity?> get currentSongStream =>
@@ -205,8 +228,8 @@ class AudioPlayerRepositoryImpl implements AudioPlayerRepository {
205228

206229
@override
207230
Future<void> playNext(SongEntity song) async {
208-
final uniqueId = DateTime.now().microsecondsSinceEpoch.toString() +
209-
song.id.toString();
231+
final uniqueId =
232+
DateTime.now().microsecondsSinceEpoch.toString() + song.id.toString();
210233
final item = MediaItem(
211234
id: song.id.toString(),
212235
album: song.album,

0 commit comments

Comments
 (0)