react-native-nitro-player 0.7.1-alpha.1 → 0.7.1-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/AndroidManifest.xml +15 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +5 -6
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +68 -49
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +67 -21
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +27 -5
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +88 -49
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +40 -10
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +46 -45
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ListenerRegistry.kt +4 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +38 -32
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +146 -81
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +24 -13
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +16 -4
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +101 -72
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +42 -22
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +55 -24
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +27 -8
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +73 -62
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +51 -48
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +3 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +14 -3
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +94 -23
- package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +43 -34
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/ExoPlayerBuilder.kt +49 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionCallbackFactory.kt +167 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +11 -450
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/NitroPlayerNotificationProvider.kt +200 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/PlaybackService.kt +101 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +87 -85
- package/ios/core/TrackPlayerQueueBuild.swift +16 -2
- package/package.json +1 -1
- package/src/hooks/useEqualizer.ts +15 -12
- package/src/specs/AndroidAutoMediaLibrary.nitro.ts +3 -2
- package/src/specs/DownloadManager.nitro.ts +4 -2
- package/src/specs/Equalizer.nitro.ts +4 -2
- package/src/specs/TrackPlayer.nitro.ts +18 -6
|
@@ -11,16 +11,17 @@ import com.margelo.nitro.nitroplayer.TrackItem
|
|
|
11
11
|
|
|
12
12
|
// ── Playlist loading ──────────────────────────────────────────────────────
|
|
13
13
|
|
|
14
|
-
suspend fun TrackPlayerCore.loadPlaylist(playlistId: String) =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
suspend fun TrackPlayerCore.loadPlaylist(playlistId: String) =
|
|
15
|
+
withPlayerContext {
|
|
16
|
+
playNextStack.clear()
|
|
17
|
+
upNextQueue.clear()
|
|
18
|
+
currentTemporaryType = TrackPlayerCore.TemporaryType.NONE
|
|
19
|
+
val playlist = playlistManager.getPlaylist(playlistId) ?: return@withPlayerContext
|
|
20
|
+
currentPlaylistId = playlistId
|
|
21
|
+
updatePlayerQueue(playlist.tracks)
|
|
22
|
+
checkUpcomingTracksForUrls(lookaheadCount)
|
|
23
|
+
notifyTemporaryQueueChange()
|
|
24
|
+
}
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Debounced update — coalesces rapid back-to-back mutations into one player rebuild.
|
|
@@ -37,8 +38,9 @@ fun TrackPlayerCore.updatePlaylist(playlistId: String) {
|
|
|
37
38
|
suspend fun TrackPlayerCore.playNext(trackId: String) = withPlayerContext { playNextInternal(trackId) }
|
|
38
39
|
|
|
39
40
|
internal fun TrackPlayerCore.playNextInternal(trackId: String) {
|
|
40
|
-
val track =
|
|
41
|
-
|
|
41
|
+
val track =
|
|
42
|
+
findTrackById(trackId)
|
|
43
|
+
?: throw IllegalArgumentException("Track $trackId not found")
|
|
42
44
|
playNextStack.add(0, track)
|
|
43
45
|
if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
|
|
44
46
|
notifyTemporaryQueueChange()
|
|
@@ -49,8 +51,9 @@ internal fun TrackPlayerCore.playNextInternal(trackId: String) {
|
|
|
49
51
|
suspend fun TrackPlayerCore.addToUpNext(trackId: String) = withPlayerContext { addToUpNextInternal(trackId) }
|
|
50
52
|
|
|
51
53
|
internal fun TrackPlayerCore.addToUpNextInternal(trackId: String) {
|
|
52
|
-
val track =
|
|
53
|
-
|
|
54
|
+
val track =
|
|
55
|
+
findTrackById(trackId)
|
|
56
|
+
?: throw IllegalArgumentException("Track $trackId not found")
|
|
54
57
|
upNextQueue.add(track)
|
|
55
58
|
if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
|
|
56
59
|
notifyTemporaryQueueChange()
|
|
@@ -58,35 +61,39 @@ internal fun TrackPlayerCore.addToUpNextInternal(trackId: String) {
|
|
|
58
61
|
|
|
59
62
|
// ── Remove / clear ────────────────────────────────────────────────────────
|
|
60
63
|
|
|
61
|
-
suspend fun TrackPlayerCore.removeFromPlayNext(trackId: String): Boolean =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
64
|
+
suspend fun TrackPlayerCore.removeFromPlayNext(trackId: String): Boolean =
|
|
65
|
+
withPlayerContext {
|
|
66
|
+
val idx = playNextStack.indexOfFirst { it.id == trackId }
|
|
67
|
+
if (idx < 0) return@withPlayerContext false
|
|
68
|
+
playNextStack.removeAt(idx)
|
|
69
|
+
if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
|
|
70
|
+
notifyTemporaryQueueChange()
|
|
71
|
+
true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
suspend fun TrackPlayerCore.removeFromUpNext(trackId: String): Boolean =
|
|
75
|
+
withPlayerContext {
|
|
76
|
+
val idx = upNextQueue.indexOfFirst { it.id == trackId }
|
|
77
|
+
if (idx < 0) return@withPlayerContext false
|
|
78
|
+
upNextQueue.removeAt(idx)
|
|
79
|
+
if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
|
|
80
|
+
notifyTemporaryQueueChange()
|
|
81
|
+
true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
suspend fun TrackPlayerCore.clearPlayNext() =
|
|
85
|
+
withPlayerContext {
|
|
86
|
+
playNextStack.clear()
|
|
87
|
+
if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
|
|
88
|
+
notifyTemporaryQueueChange()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
suspend fun TrackPlayerCore.clearUpNext() =
|
|
92
|
+
withPlayerContext {
|
|
93
|
+
upNextQueue.clear()
|
|
94
|
+
if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
|
|
95
|
+
notifyTemporaryQueueChange()
|
|
96
|
+
}
|
|
90
97
|
|
|
91
98
|
// ── Reorder ───────────────────────────────────────────────────────────────
|
|
92
99
|
|
|
@@ -94,25 +101,29 @@ suspend fun TrackPlayerCore.clearUpNext() = withPlayerContext {
|
|
|
94
101
|
* Reorder within the combined virtual list [playNextStack + upNextQueue].
|
|
95
102
|
* newIndex is 0-based within that combined list.
|
|
96
103
|
*/
|
|
97
|
-
suspend fun TrackPlayerCore.reorderTemporaryTrack(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
104
|
+
suspend fun TrackPlayerCore.reorderTemporaryTrack(
|
|
105
|
+
trackId: String,
|
|
106
|
+
newIndex: Int,
|
|
107
|
+
): Boolean =
|
|
108
|
+
withPlayerContext {
|
|
109
|
+
val combined = (playNextStack + upNextQueue).toMutableList()
|
|
110
|
+
val fromIdx = combined.indexOfFirst { it.id == trackId }
|
|
111
|
+
if (fromIdx < 0) return@withPlayerContext false
|
|
112
|
+
val track = combined.removeAt(fromIdx)
|
|
113
|
+
val clampedIndex = newIndex.coerceIn(0, combined.size)
|
|
114
|
+
combined.add(clampedIndex, track)
|
|
115
|
+
|
|
116
|
+
// Split back at original playNextStack.size boundary (reduced if an item was moved out)
|
|
117
|
+
val pnSize = playNextStack.size
|
|
118
|
+
playNextStack.clear()
|
|
119
|
+
upNextQueue.clear()
|
|
120
|
+
playNextStack.addAll(combined.take(pnSize))
|
|
121
|
+
upNextQueue.addAll(combined.drop(pnSize))
|
|
122
|
+
|
|
123
|
+
if (isExoInitialized && exo.currentMediaItem != null) rebuildQueueFromCurrentPosition()
|
|
124
|
+
notifyTemporaryQueueChange()
|
|
125
|
+
true
|
|
126
|
+
}
|
|
116
127
|
|
|
117
128
|
// ── Read-only accessors ────────────────────────────────────────────────────
|
|
118
129
|
|
|
@@ -12,62 +12,65 @@ import com.margelo.nitro.nitroplayer.TrackItem
|
|
|
12
12
|
|
|
13
13
|
// ── Track updates (URL resolution) ────────────────────────────────────────
|
|
14
14
|
|
|
15
|
-
suspend fun TrackPlayerCore.updateTracks(tracks: List<TrackItem>) =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
15
|
+
suspend fun TrackPlayerCore.updateTracks(tracks: List<TrackItem>) =
|
|
16
|
+
withPlayerContext {
|
|
17
|
+
val currentTrack = getCurrentTrack()
|
|
18
|
+
val currentTrackId = currentTrack?.id
|
|
19
|
+
val currentTrackIsEmpty = currentTrack?.url.isNullOrEmpty()
|
|
20
|
+
val currentTrackUpdate = if (currentTrackId != null) tracks.find { it.id == currentTrackId } else null
|
|
21
|
+
|
|
22
|
+
val safeTracks =
|
|
23
|
+
tracks.filter { track ->
|
|
24
|
+
when {
|
|
25
|
+
track.id == currentTrackId && !currentTrackIsEmpty -> false
|
|
26
|
+
|
|
27
|
+
// preserve gapless
|
|
28
|
+
track.id == currentTrackId && currentTrackIsEmpty -> track.url.isNotEmpty()
|
|
29
|
+
|
|
30
|
+
track.url.isEmpty() -> false
|
|
31
|
+
|
|
32
|
+
else -> true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (safeTracks.isEmpty()) return@withPlayerContext
|
|
36
|
+
|
|
37
|
+
val affectedPlaylists: Map<String, Int> = playlistManager.updateTracks(safeTracks)
|
|
38
|
+
|
|
39
|
+
// Replace current track's MediaItem if it was empty-URL and now has a URL
|
|
40
|
+
if (currentTrackUpdate != null && currentTrackIsEmpty && currentTrackUpdate.url.isNotEmpty()) {
|
|
41
|
+
val exoIndex = exo.currentMediaItemIndex
|
|
42
|
+
if (exoIndex >= 0) {
|
|
43
|
+
val playlistId = currentPlaylistId ?: ""
|
|
44
|
+
val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${currentTrackUpdate.id}" else currentTrackUpdate.id
|
|
45
|
+
exo.replaceMediaItem(exoIndex, makeMediaItem(currentTrackUpdate, mediaId))
|
|
46
|
+
if (exo.playbackState == Player.STATE_IDLE) exo.prepare()
|
|
47
|
+
}
|
|
41
48
|
}
|
|
42
|
-
}
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
if (currentPlaylistId != null && affectedPlaylists.containsKey(currentPlaylistId)) {
|
|
51
|
+
val refreshedPlaylist = playlistManager.getPlaylist(currentPlaylistId!!)
|
|
52
|
+
if (refreshedPlaylist != null) {
|
|
53
|
+
currentTracks = refreshedPlaylist.tracks
|
|
54
|
+
val updatedById = currentTracks.associateBy { it.id }
|
|
55
|
+
playNextStack.forEachIndexed { i, t -> updatedById[t.id]?.let { if (it !== t) playNextStack[i] = it } }
|
|
56
|
+
upNextQueue.forEachIndexed { i, t -> updatedById[t.id]?.let { if (it !== t) upNextQueue[i] = it } }
|
|
57
|
+
}
|
|
58
|
+
rebuildQueueFromCurrentPosition()
|
|
51
59
|
}
|
|
52
|
-
rebuildQueueFromCurrentPosition()
|
|
53
60
|
}
|
|
54
|
-
}
|
|
55
61
|
|
|
56
62
|
// ── Track queries ─────────────────────────────────────────────────────────
|
|
57
63
|
|
|
58
|
-
suspend fun TrackPlayerCore.getTracksById(trackIds: List<String>): List<TrackItem> =
|
|
59
|
-
withPlayerContext { playlistManager.getTracksById(trackIds) as List<TrackItem> }
|
|
64
|
+
suspend fun TrackPlayerCore.getTracksById(trackIds: List<String>): List<TrackItem> = withPlayerContext { playlistManager.getTracksById(trackIds) as List<TrackItem> }
|
|
60
65
|
|
|
61
|
-
suspend fun TrackPlayerCore.getTracksNeedingUrls(): List<TrackItem> =
|
|
62
|
-
withPlayerContext { getTracksNeedingUrlsInternal() }
|
|
66
|
+
suspend fun TrackPlayerCore.getTracksNeedingUrls(): List<TrackItem> = withPlayerContext { getTracksNeedingUrlsInternal() }
|
|
63
67
|
|
|
64
68
|
internal fun TrackPlayerCore.getTracksNeedingUrlsInternal(): List<TrackItem> {
|
|
65
69
|
val pid = currentPlaylistId ?: return emptyList()
|
|
66
70
|
return playlistManager.getPlaylist(pid)?.tracks?.filter { it.url.isEmpty() } ?: emptyList()
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
suspend fun TrackPlayerCore.getNextTracks(count: Int): List<TrackItem> =
|
|
70
|
-
withPlayerContext { getNextTracksInternal(count) }
|
|
73
|
+
suspend fun TrackPlayerCore.getNextTracks(count: Int): List<TrackItem> = withPlayerContext { getNextTracksInternal(count) }
|
|
71
74
|
|
|
72
75
|
internal fun TrackPlayerCore.getNextTracksInternal(count: Int): List<TrackItem> {
|
|
73
76
|
val actualQueue = getActualQueueInternal()
|
|
@@ -84,15 +87,15 @@ suspend fun TrackPlayerCore.getCurrentTrackIndex(): Int = withPlayerContext { cu
|
|
|
84
87
|
// ── URL lookahead ─────────────────────────────────────────────────────────
|
|
85
88
|
|
|
86
89
|
internal fun TrackPlayerCore.checkUpcomingTracksForUrls(lookahead: Int = 5) {
|
|
87
|
-
val upcomingTracks =
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
val upcomingTracks =
|
|
91
|
+
if (currentTrackIndex < 0) {
|
|
92
|
+
currentTracks.take(lookahead)
|
|
93
|
+
} else {
|
|
94
|
+
getNextTracksInternal(lookahead)
|
|
95
|
+
}
|
|
92
96
|
val currentTrack = getCurrentTrack()
|
|
93
97
|
val currentNeedsUrl = currentTrack != null && currentTrack.url.isEmpty()
|
|
94
98
|
val candidates = if (currentNeedsUrl) listOf(currentTrack!!) + upcomingTracks else upcomingTracks
|
|
95
99
|
val needUrls = candidates.filter { it.url.isEmpty() }
|
|
96
100
|
if (needUrls.isNotEmpty()) notifyTracksNeedUpdate(needUrls, lookahead)
|
|
97
101
|
}
|
|
98
|
-
|
|
@@ -7,13 +7,13 @@ import com.margelo.nitro.nitroplayer.*
|
|
|
7
7
|
import com.margelo.nitro.nitroplayer.core.NitroPlayerLogger
|
|
8
8
|
import com.margelo.nitro.nitroplayer.playlist.PlaylistManager
|
|
9
9
|
import com.margelo.nitro.nitroplayer.storage.NitroPlayerStorage
|
|
10
|
-
import org.json.JSONArray
|
|
11
|
-
import org.json.JSONObject
|
|
12
|
-
import java.io.File
|
|
13
10
|
import kotlinx.coroutines.CoroutineScope
|
|
14
11
|
import kotlinx.coroutines.Dispatchers
|
|
15
12
|
import kotlinx.coroutines.SupervisorJob
|
|
16
13
|
import kotlinx.coroutines.launch
|
|
14
|
+
import org.json.JSONArray
|
|
15
|
+
import org.json.JSONObject
|
|
16
|
+
import java.io.File
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Manages persistence of downloaded track metadata using file storage
|
|
@@ -111,6 +111,7 @@ class DownloadManagerCore private constructor(
|
|
|
111
111
|
workDataOf(
|
|
112
112
|
DownloadWorker.KEY_DOWNLOAD_ID to downloadId,
|
|
113
113
|
DownloadWorker.KEY_TRACK_ID to track.id,
|
|
114
|
+
DownloadWorker.KEY_TRACK_TITLE to track.title,
|
|
114
115
|
DownloadWorker.KEY_URL to track.url,
|
|
115
116
|
DownloadWorker.KEY_PLAYLIST_ID to (playlistId ?: ""),
|
|
116
117
|
DownloadWorker.KEY_STORAGE_LOCATION to (config.storageLocation?.name ?: StorageLocation.PRIVATE.name),
|
|
@@ -169,6 +170,7 @@ class DownloadManagerCore private constructor(
|
|
|
169
170
|
workDataOf(
|
|
170
171
|
DownloadWorker.KEY_DOWNLOAD_ID to downloadId,
|
|
171
172
|
DownloadWorker.KEY_TRACK_ID to track.id,
|
|
173
|
+
DownloadWorker.KEY_TRACK_TITLE to track.title,
|
|
172
174
|
DownloadWorker.KEY_URL to track.url,
|
|
173
175
|
DownloadWorker.KEY_PLAYLIST_ID to (playlistId ?: ""),
|
|
174
176
|
DownloadWorker.KEY_STORAGE_LOCATION to (config.storageLocation?.name ?: StorageLocation.PRIVATE.name),
|
|
@@ -236,9 +238,18 @@ class DownloadManagerCore private constructor(
|
|
|
236
238
|
|
|
237
239
|
for (m in activeTasks.values) {
|
|
238
240
|
when (m.state) {
|
|
239
|
-
DownloadState.PENDING ->
|
|
240
|
-
|
|
241
|
-
|
|
241
|
+
DownloadState.PENDING -> {
|
|
242
|
+
pendingCount++
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
DownloadState.DOWNLOADING -> {
|
|
246
|
+
activeCount++
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
DownloadState.FAILED -> {
|
|
250
|
+
failedCount++
|
|
251
|
+
}
|
|
252
|
+
|
|
242
253
|
else -> {}
|
|
243
254
|
}
|
|
244
255
|
totalBytes += m.totalBytes ?: 0.0
|
|
@@ -13,7 +13,6 @@ import com.margelo.nitro.nitroplayer.*
|
|
|
13
13
|
import kotlinx.coroutines.Dispatchers
|
|
14
14
|
import kotlinx.coroutines.withContext
|
|
15
15
|
import java.io.BufferedInputStream
|
|
16
|
-
import java.io.File
|
|
17
16
|
import java.io.FileOutputStream
|
|
18
17
|
import java.net.HttpURLConnection
|
|
19
18
|
import java.net.URL
|
|
@@ -28,26 +27,34 @@ class DownloadWorker(
|
|
|
28
27
|
companion object {
|
|
29
28
|
const val KEY_DOWNLOAD_ID = "download_id"
|
|
30
29
|
const val KEY_TRACK_ID = "track_id"
|
|
30
|
+
const val KEY_TRACK_TITLE = "track_title"
|
|
31
31
|
const val KEY_URL = "url"
|
|
32
32
|
const val KEY_PLAYLIST_ID = "playlist_id"
|
|
33
33
|
const val KEY_STORAGE_LOCATION = "storage_location"
|
|
34
34
|
|
|
35
35
|
private const val NOTIFICATION_CHANNEL_ID = "nitro_player_downloads"
|
|
36
|
-
private const val
|
|
36
|
+
private const val BASE_NOTIFICATION_ID = 2001
|
|
37
37
|
private const val BUFFER_SIZE = 8192
|
|
38
38
|
private val CONTENT_DISPOSITION_REGEX = Regex("filename=\"?([^\";]+)\"?")
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
private val downloadManager = DownloadManagerCore.getInstance(context)
|
|
42
42
|
private val fileManager = DownloadFileManager.getInstance(context)
|
|
43
|
+
private val notificationManager =
|
|
44
|
+
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
45
|
+
/** Stable notification ID per download — derived from trackId hash. */
|
|
46
|
+
private var notificationId = BASE_NOTIFICATION_ID
|
|
43
47
|
|
|
44
48
|
override suspend fun doWork(): Result =
|
|
45
49
|
withContext(Dispatchers.IO) {
|
|
46
50
|
val downloadId = inputData.getString(KEY_DOWNLOAD_ID) ?: return@withContext Result.failure()
|
|
47
51
|
val trackId = inputData.getString(KEY_TRACK_ID) ?: return@withContext Result.failure()
|
|
52
|
+
val trackTitle = inputData.getString(KEY_TRACK_TITLE) ?: "Unknown track"
|
|
48
53
|
val urlString = inputData.getString(KEY_URL) ?: return@withContext Result.failure()
|
|
49
54
|
val storageLocationStr = inputData.getString(KEY_STORAGE_LOCATION) ?: StorageLocation.PRIVATE.name
|
|
50
55
|
|
|
56
|
+
notificationId = BASE_NOTIFICATION_ID + trackId.hashCode().and(0xFFFF)
|
|
57
|
+
|
|
51
58
|
val storageLocation =
|
|
52
59
|
try {
|
|
53
60
|
StorageLocation.valueOf(storageLocationStr)
|
|
@@ -56,14 +63,21 @@ class DownloadWorker(
|
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
try {
|
|
59
|
-
// Set foreground notification
|
|
60
|
-
|
|
66
|
+
// Set foreground notification — if POST_NOTIFICATIONS is denied,
|
|
67
|
+
// WorkManager still runs the task; the notification just won't show.
|
|
68
|
+
try {
|
|
69
|
+
setForeground(createForegroundInfo(trackTitle, 0, true))
|
|
70
|
+
} catch (_: Exception) {
|
|
71
|
+
// Foreground promotion failed (e.g. missing permission on some OEMs).
|
|
72
|
+
// Download continues in background.
|
|
73
|
+
}
|
|
61
74
|
|
|
62
75
|
// Perform download
|
|
63
|
-
val localPath = downloadFile(downloadId, trackId, urlString, storageLocation)
|
|
76
|
+
val localPath = downloadFile(downloadId, trackId, trackTitle, urlString, storageLocation)
|
|
64
77
|
|
|
65
78
|
if (localPath != null) {
|
|
66
79
|
downloadManager.onComplete(downloadId, trackId, localPath)
|
|
80
|
+
showCompletionNotification(trackTitle)
|
|
67
81
|
Result.success()
|
|
68
82
|
} else {
|
|
69
83
|
val error =
|
|
@@ -74,6 +88,7 @@ class DownloadWorker(
|
|
|
74
88
|
isRetryable = true,
|
|
75
89
|
)
|
|
76
90
|
downloadManager.onError(downloadId, trackId, error)
|
|
91
|
+
showErrorNotification(trackTitle)
|
|
77
92
|
Result.retry()
|
|
78
93
|
}
|
|
79
94
|
} catch (e: Exception) {
|
|
@@ -93,6 +108,7 @@ class DownloadWorker(
|
|
|
93
108
|
isRetryable = errorReason in listOf(DownloadErrorReason.NETWORK_ERROR, DownloadErrorReason.TIMEOUT),
|
|
94
109
|
)
|
|
95
110
|
downloadManager.onError(downloadId, trackId, error)
|
|
111
|
+
showErrorNotification(trackTitle)
|
|
96
112
|
|
|
97
113
|
if (error.isRetryable) {
|
|
98
114
|
Result.retry()
|
|
@@ -105,6 +121,7 @@ class DownloadWorker(
|
|
|
105
121
|
private suspend fun downloadFile(
|
|
106
122
|
downloadId: String,
|
|
107
123
|
trackId: String,
|
|
124
|
+
trackTitle: String,
|
|
108
125
|
urlString: String,
|
|
109
126
|
storageLocation: StorageLocation,
|
|
110
127
|
): String? =
|
|
@@ -167,10 +184,12 @@ class DownloadWorker(
|
|
|
167
184
|
outputStream.write(buffer, 0, bytesRead)
|
|
168
185
|
bytesDownloaded += bytesRead
|
|
169
186
|
|
|
170
|
-
// Update progress every 250ms
|
|
187
|
+
// Update progress every 250ms — both the callback and the notification
|
|
171
188
|
val now = System.currentTimeMillis()
|
|
172
189
|
if (now - lastProgressUpdate >= 250) {
|
|
173
190
|
downloadManager.onProgress(downloadId, trackId, bytesDownloaded, totalBytes)
|
|
191
|
+
val percent = if (totalBytes > 0) ((bytesDownloaded * 100) / totalBytes).toInt() else 0
|
|
192
|
+
updateProgressNotification(trackTitle, percent)
|
|
174
193
|
lastProgressUpdate = now
|
|
175
194
|
}
|
|
176
195
|
}
|
|
@@ -194,31 +213,85 @@ class DownloadWorker(
|
|
|
194
213
|
}
|
|
195
214
|
}
|
|
196
215
|
|
|
197
|
-
|
|
198
|
-
|
|
216
|
+
// ── Notification helpers ──────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
private fun createForegroundInfo(
|
|
219
|
+
trackTitle: String,
|
|
220
|
+
percent: Int,
|
|
221
|
+
indeterminate: Boolean,
|
|
222
|
+
): ForegroundInfo {
|
|
223
|
+
ensureNotificationChannel()
|
|
199
224
|
|
|
200
|
-
val notification =
|
|
201
|
-
NotificationCompat
|
|
202
|
-
.Builder(context, NOTIFICATION_CHANNEL_ID)
|
|
203
|
-
.setContentTitle("Downloading")
|
|
204
|
-
.setContentText("Downloading track...")
|
|
205
|
-
.setSmallIcon(android.R.drawable.stat_sys_download)
|
|
206
|
-
.setOngoing(true)
|
|
207
|
-
.setProgress(100, 0, true)
|
|
208
|
-
.build()
|
|
225
|
+
val notification = buildProgressNotification(trackTitle, percent, indeterminate)
|
|
209
226
|
|
|
210
227
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
211
228
|
ForegroundInfo(
|
|
212
|
-
|
|
229
|
+
notificationId,
|
|
213
230
|
notification,
|
|
214
231
|
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
|
|
215
232
|
)
|
|
216
233
|
} else {
|
|
217
|
-
ForegroundInfo(
|
|
234
|
+
ForegroundInfo(notificationId, notification)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private fun buildProgressNotification(
|
|
239
|
+
trackTitle: String,
|
|
240
|
+
percent: Int,
|
|
241
|
+
indeterminate: Boolean,
|
|
242
|
+
) = NotificationCompat
|
|
243
|
+
.Builder(context, NOTIFICATION_CHANNEL_ID)
|
|
244
|
+
.setContentTitle("Downloading")
|
|
245
|
+
.setContentText(trackTitle)
|
|
246
|
+
.setSubText(if (!indeterminate) "$percent%" else null)
|
|
247
|
+
.setSmallIcon(android.R.drawable.stat_sys_download)
|
|
248
|
+
.setOngoing(true)
|
|
249
|
+
.setOnlyAlertOnce(true)
|
|
250
|
+
.setProgress(100, percent, indeterminate)
|
|
251
|
+
.build()
|
|
252
|
+
|
|
253
|
+
private fun updateProgressNotification(trackTitle: String, percent: Int) {
|
|
254
|
+
try {
|
|
255
|
+
notificationManager.notify(
|
|
256
|
+
notificationId,
|
|
257
|
+
buildProgressNotification(trackTitle, percent, false),
|
|
258
|
+
)
|
|
259
|
+
} catch (_: SecurityException) {
|
|
260
|
+
// POST_NOTIFICATIONS not granted — download continues silently
|
|
218
261
|
}
|
|
219
262
|
}
|
|
220
263
|
|
|
221
|
-
private fun
|
|
264
|
+
private fun showCompletionNotification(trackTitle: String) {
|
|
265
|
+
try {
|
|
266
|
+
notificationManager.notify(
|
|
267
|
+
notificationId,
|
|
268
|
+
NotificationCompat
|
|
269
|
+
.Builder(context, NOTIFICATION_CHANNEL_ID)
|
|
270
|
+
.setContentTitle("Download complete")
|
|
271
|
+
.setContentText(trackTitle)
|
|
272
|
+
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
|
273
|
+
.setAutoCancel(true)
|
|
274
|
+
.build(),
|
|
275
|
+
)
|
|
276
|
+
} catch (_: SecurityException) { }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private fun showErrorNotification(trackTitle: String) {
|
|
280
|
+
try {
|
|
281
|
+
notificationManager.notify(
|
|
282
|
+
notificationId,
|
|
283
|
+
NotificationCompat
|
|
284
|
+
.Builder(context, NOTIFICATION_CHANNEL_ID)
|
|
285
|
+
.setContentTitle("Download failed")
|
|
286
|
+
.setContentText(trackTitle)
|
|
287
|
+
.setSmallIcon(android.R.drawable.stat_notify_error)
|
|
288
|
+
.setAutoCancel(true)
|
|
289
|
+
.build(),
|
|
290
|
+
)
|
|
291
|
+
} catch (_: SecurityException) { }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private fun ensureNotificationChannel() {
|
|
222
295
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
223
296
|
val channel =
|
|
224
297
|
NotificationChannel(
|
|
@@ -228,9 +301,7 @@ class DownloadWorker(
|
|
|
228
301
|
).apply {
|
|
229
302
|
description = "Download progress notifications"
|
|
230
303
|
}
|
|
231
|
-
|
|
232
|
-
val notificationManager = context.getSystemService(NotificationManager::class.java)
|
|
233
|
-
notificationManager?.createNotificationChannel(channel)
|
|
304
|
+
notificationManager.createNotificationChannel(channel)
|
|
234
305
|
}
|
|
235
306
|
}
|
|
236
307
|
}
|