Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
// )
}
}
}
Expand Down
44 changes: 29 additions & 15 deletions android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -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 <fields>;
}
5 changes: 3 additions & 2 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<!-- 1. PERMISSIONS GO HERE (Directly inside manifest, NOT inside application) -->
<uses-permission android:name="android.permission.INTERNET"/>

Expand Down Expand Up @@ -58,7 +58,8 @@
<!-- The Service that keeps music playing in background -->
<service android:name="com.ryanheise.audioservice.AudioService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
android:exported="true"
tools:node="merge">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
Expand Down
7 changes: 7 additions & 0 deletions android/app/src/main/res/raw/keep.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/ic_shuffle,
@drawable/ic_shuffle_on,
@drawable/ic_repeat,
@drawable/ic_repeat_on,
@drawable/ic_repeat_one"/>
8 changes: 5 additions & 3 deletions lib/core/di/init_dependencies.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,19 +89,21 @@ Future<void> 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>(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,
),
);

Expand Down
46 changes: 23 additions & 23 deletions lib/features/analytics/domain/services/music_analytics_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -79,7 +79,7 @@ class MusicAnalyticsService with WidgetsBindingObserver {
: Duration.zero;

_accumulatedMilliseconds = 0;
_lastPosition = Duration.zero;
// _lastPosition = Duration.zero;
_playStartTime = _isPlaying ? DateTime.now() : null;
}

Expand All @@ -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) {
Expand All @@ -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();
Expand Down Expand Up @@ -200,6 +200,6 @@ class MusicAnalyticsService with WidgetsBindingObserver {
_currentSongSubscription?.cancel();
_durationSubscription?.cancel();
_completionSubscription?.cancel();
_positionSubscription?.cancel();
// _positionSubscription?.cancel();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,57 @@ class AudioPlayerRepositoryImpl implements AudioPlayerRepository {
// Helper to access custom methods of our handler
MusicPlayerHandler get _handler => _audioHandler as MusicPlayerHandler;

// @override
// Future<void> setQueue(List<SongEntity> 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<void> setQueue(List<SongEntity> 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<void> 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,
Expand Down Expand Up @@ -140,17 +164,17 @@ class AudioPlayerRepositoryImpl implements AudioPlayerRepository {

@override
Stream<int> 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<Duration> get positionStream => AudioService.position;
Expand All @@ -160,10 +184,9 @@ class AudioPlayerRepositoryImpl implements AudioPlayerRepository {
_audioHandler.mediaItem.map((item) => item?.duration ?? Duration.zero);

@override
Stream<void> get playerCompleteStream => _audioHandler.playbackState
.where((state) => state.processingState == AudioProcessingState.completed)
.map((event) => null)
.distinct();
Stream<void> get playerCompleteStream => _audioHandler.playbackState.where(
(state) => state.processingState == AudioProcessingState.completed,
);

@override
Stream<SongEntity?> get currentSongStream =>
Expand Down Expand Up @@ -205,8 +228,8 @@ class AudioPlayerRepositoryImpl implements AudioPlayerRepository {

@override
Future<void> 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,
Expand Down
Loading
Loading