react-native-nitro-player 0.3.0-alpha.9 → 0.4.0

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.
Files changed (257) hide show
  1. package/README.md +444 -4
  2. package/android/build.gradle +4 -1
  3. package/android/src/main/AndroidManifest.xml +16 -1
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +2 -0
  5. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +8 -0
  6. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +225 -0
  7. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +105 -0
  8. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +6 -6
  9. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +37 -12
  10. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +970 -213
  11. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +475 -0
  12. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadFileManager.kt +159 -0
  13. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +489 -0
  14. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +209 -0
  15. package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +486 -0
  16. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaBrowserService.kt +3 -1
  17. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +14 -6
  18. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +27 -0
  19. package/ios/HybridDownloadManager.swift +226 -0
  20. package/ios/HybridEqualizer.swift +111 -0
  21. package/ios/HybridTrackPlayer.swift +36 -8
  22. package/ios/core/TrackPlayerCore.swift +998 -276
  23. package/ios/download/DownloadDatabase.swift +493 -0
  24. package/ios/download/DownloadFileManager.swift +241 -0
  25. package/ios/download/DownloadManagerCore.swift +923 -0
  26. package/ios/equalizer/EqualizerCore.swift +685 -0
  27. package/ios/media/MediaSessionManager.swift +40 -28
  28. package/ios/playlist/PlaylistManager.swift +40 -9
  29. package/ios/queue/HybridPlayerQueue.swift +33 -13
  30. package/lib/hooks/downloadCallbackManager.d.ts +36 -0
  31. package/lib/hooks/downloadCallbackManager.js +108 -0
  32. package/lib/hooks/equalizerCallbackManager.d.ts +37 -0
  33. package/lib/hooks/equalizerCallbackManager.js +109 -0
  34. package/lib/hooks/index.d.ts +16 -0
  35. package/lib/hooks/index.js +10 -0
  36. package/lib/hooks/useActualQueue.d.ts +48 -0
  37. package/lib/hooks/useActualQueue.js +98 -0
  38. package/lib/hooks/useDownloadActions.d.ts +26 -0
  39. package/lib/hooks/useDownloadActions.js +117 -0
  40. package/lib/hooks/useDownloadProgress.d.ts +25 -0
  41. package/lib/hooks/useDownloadProgress.js +79 -0
  42. package/lib/hooks/useDownloadStorage.d.ts +19 -0
  43. package/lib/hooks/useDownloadStorage.js +60 -0
  44. package/lib/hooks/useDownloadedTracks.d.ts +25 -0
  45. package/lib/hooks/useDownloadedTracks.js +69 -0
  46. package/lib/hooks/useEqualizer.d.ts +25 -0
  47. package/lib/hooks/useEqualizer.js +124 -0
  48. package/lib/hooks/useEqualizerPresets.d.ts +22 -0
  49. package/lib/hooks/useEqualizerPresets.js +96 -0
  50. package/lib/hooks/useNowPlaying.js +3 -2
  51. package/lib/hooks/useOnChangeTrack.js +15 -12
  52. package/lib/hooks/useOnPlaybackStateChange.js +16 -13
  53. package/lib/hooks/usePlaylist.d.ts +48 -0
  54. package/lib/hooks/usePlaylist.js +136 -0
  55. package/lib/index.d.ts +6 -0
  56. package/lib/index.js +6 -0
  57. package/lib/specs/DownloadManager.nitro.d.ts +152 -0
  58. package/lib/specs/DownloadManager.nitro.js +1 -0
  59. package/lib/specs/Equalizer.nitro.d.ts +43 -0
  60. package/lib/specs/Equalizer.nitro.js +1 -0
  61. package/lib/specs/TrackPlayer.nitro.d.ts +6 -2
  62. package/lib/types/DownloadTypes.d.ts +110 -0
  63. package/lib/types/DownloadTypes.js +1 -0
  64. package/lib/types/EqualizerTypes.d.ts +52 -0
  65. package/lib/types/EqualizerTypes.js +1 -0
  66. package/lib/types/PlayerQueue.d.ts +4 -0
  67. package/nitro.json +8 -0
  68. package/nitrogen/generated/android/NitroPlayer+autolinking.cmake +10 -1
  69. package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +32 -2
  70. package/nitrogen/generated/android/c++/JCurrentPlayingType.hpp +65 -0
  71. package/nitrogen/generated/android/c++/JDownloadConfig.hpp +92 -0
  72. package/nitrogen/generated/android/c++/JDownloadError.hpp +71 -0
  73. package/nitrogen/generated/android/c++/JDownloadErrorReason.hpp +74 -0
  74. package/nitrogen/generated/android/c++/JDownloadProgress.hpp +79 -0
  75. package/nitrogen/generated/android/c++/JDownloadQueueStatus.hpp +81 -0
  76. package/nitrogen/generated/android/c++/JDownloadState.hpp +71 -0
  77. package/nitrogen/generated/android/c++/JDownloadStorageInfo.hpp +73 -0
  78. package/nitrogen/generated/android/c++/JDownloadTask.hpp +108 -0
  79. package/nitrogen/generated/android/c++/JDownloadedPlaylist.hpp +111 -0
  80. package/nitrogen/generated/android/c++/JDownloadedTrack.hpp +92 -0
  81. package/nitrogen/generated/android/c++/JEqualizerBand.hpp +69 -0
  82. package/nitrogen/generated/android/c++/JEqualizerPreset.hpp +78 -0
  83. package/nitrogen/generated/android/c++/JEqualizerState.hpp +91 -0
  84. package/nitrogen/generated/android/c++/JFunc_void_DownloadProgress.hpp +80 -0
  85. package/nitrogen/generated/android/c++/JFunc_void_DownloadedTrack.hpp +89 -0
  86. package/nitrogen/generated/android/c++/JFunc_void_TrackItem_std__optional_Reason_.hpp +2 -0
  87. package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__std__string__.hpp +81 -0
  88. package/nitrogen/generated/android/c++/JFunc_void_std__string_Playlist_std__optional_QueueOperation_.hpp +2 -0
  89. package/nitrogen/generated/android/c++/JFunc_void_std__string_std__string_DownloadState_std__optional_DownloadError_.hpp +83 -0
  90. package/nitrogen/generated/android/c++/JFunc_void_std__vector_EqualizerBand_.hpp +97 -0
  91. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Playlist__std__optional_QueueOperation_.hpp +2 -0
  92. package/nitrogen/generated/android/c++/JGainRange.hpp +61 -0
  93. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +470 -0
  94. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +99 -0
  95. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +204 -0
  96. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +82 -0
  97. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +2 -0
  98. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +117 -15
  99. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +6 -2
  100. package/nitrogen/generated/android/c++/JPlaybackSource.hpp +62 -0
  101. package/nitrogen/generated/android/c++/JPlayerState.hpp +11 -3
  102. package/nitrogen/generated/android/c++/JPlaylist.hpp +2 -0
  103. package/nitrogen/generated/android/c++/JPresetType.hpp +59 -0
  104. package/nitrogen/generated/android/c++/JStorageLocation.hpp +59 -0
  105. package/nitrogen/generated/android/c++/JTrackItem.hpp +9 -3
  106. package/nitrogen/generated/android/c++/JTrackPlayerState.hpp +3 -3
  107. package/nitrogen/generated/android/c++/JVariant_NullType_Double.cpp +26 -0
  108. package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +69 -0
  109. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.cpp +26 -0
  110. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.hpp +74 -0
  111. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.cpp +26 -0
  112. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.hpp +84 -0
  113. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.cpp +26 -0
  114. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.hpp +85 -0
  115. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.cpp +26 -0
  116. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.hpp +80 -0
  117. package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.hpp +2 -0
  118. package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.hpp +2 -0
  119. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/CurrentPlayingType.kt +23 -0
  120. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadConfig.kt +59 -0
  121. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadError.kt +47 -0
  122. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadErrorReason.kt +26 -0
  123. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadProgress.kt +53 -0
  124. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadQueueStatus.kt +56 -0
  125. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadState.kt +25 -0
  126. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadStorageInfo.kt +50 -0
  127. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadTask.kt +65 -0
  128. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadedPlaylist.kt +53 -0
  129. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadedTrack.kt +56 -0
  130. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerBand.kt +47 -0
  131. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerPreset.kt +44 -0
  132. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerState.kt +44 -0
  133. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_DownloadProgress.kt +80 -0
  134. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_DownloadedTrack.kt +80 -0
  135. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__optional_std__variant_nitro__NullType__std__string__.kt +80 -0
  136. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__string_std__string_DownloadState_std__optional_DownloadError_.kt +80 -0
  137. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_EqualizerBand_.kt +80 -0
  138. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/GainRange.kt +41 -0
  139. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +210 -0
  140. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +141 -0
  141. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +19 -2
  142. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlaybackSource.kt +22 -0
  143. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerState.kt +6 -3
  144. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PresetType.kt +21 -0
  145. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/StorageLocation.kt +21 -0
  146. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackItem.kt +7 -3
  147. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackPlayerState.kt +2 -2
  148. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Double.kt +59 -0
  149. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadError.kt +59 -0
  150. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadTask.kt +59 -0
  151. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedPlaylist.kt +59 -0
  152. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedTrack.kt +59 -0
  153. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +138 -8
  154. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +1046 -121
  155. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +66 -0
  156. package/nitrogen/generated/ios/NitroPlayerAutolinking.mm +16 -0
  157. package/nitrogen/generated/ios/NitroPlayerAutolinking.swift +30 -0
  158. package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.cpp +11 -0
  159. package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.hpp +386 -0
  160. package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.cpp +11 -0
  161. package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +223 -0
  162. package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +1 -0
  163. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +46 -6
  164. package/nitrogen/generated/ios/swift/CurrentPlayingType.swift +48 -0
  165. package/nitrogen/generated/ios/swift/DownloadConfig.swift +270 -0
  166. package/nitrogen/generated/ios/swift/DownloadError.swift +69 -0
  167. package/nitrogen/generated/ios/swift/DownloadErrorReason.swift +60 -0
  168. package/nitrogen/generated/ios/swift/DownloadProgress.swift +91 -0
  169. package/nitrogen/generated/ios/swift/DownloadQueueStatus.swift +102 -0
  170. package/nitrogen/generated/ios/swift/DownloadState.swift +56 -0
  171. package/nitrogen/generated/ios/swift/DownloadStorageInfo.swift +80 -0
  172. package/nitrogen/generated/ios/swift/DownloadTask.swift +315 -0
  173. package/nitrogen/generated/ios/swift/DownloadedPlaylist.swift +103 -0
  174. package/nitrogen/generated/ios/swift/DownloadedTrack.swift +147 -0
  175. package/nitrogen/generated/ios/swift/EqualizerBand.swift +69 -0
  176. package/nitrogen/generated/ios/swift/EqualizerPreset.swift +70 -0
  177. package/nitrogen/generated/ios/swift/EqualizerState.swift +115 -0
  178. package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  179. package/nitrogen/generated/ios/swift/Func_void_DownloadProgress.swift +47 -0
  180. package/nitrogen/generated/ios/swift/Func_void_DownloadStorageInfo.swift +47 -0
  181. package/nitrogen/generated/ios/swift/Func_void_DownloadedTrack.swift +47 -0
  182. package/nitrogen/generated/ios/swift/Func_void_PlayerState.swift +47 -0
  183. package/nitrogen/generated/ios/swift/Func_void_bool.swift +5 -5
  184. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  185. package/nitrogen/generated/ios/swift/Func_void_std__optional_std__variant_nitro__NullType__std__string__.swift +66 -0
  186. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
  187. package/nitrogen/generated/ios/swift/Func_void_std__string_std__string_DownloadState_std__optional_DownloadError_.swift +47 -0
  188. package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +47 -0
  189. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem_.swift +47 -0
  190. package/nitrogen/generated/ios/swift/Func_void_std__vector_std__string_.swift +47 -0
  191. package/nitrogen/generated/ios/swift/GainRange.swift +47 -0
  192. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec.swift +90 -0
  193. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec_cxx.swift +705 -0
  194. package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +73 -0
  195. package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +396 -0
  196. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +6 -2
  197. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +105 -8
  198. package/nitrogen/generated/ios/swift/PlaybackSource.swift +44 -0
  199. package/nitrogen/generated/ios/swift/PlayerState.swift +13 -2
  200. package/nitrogen/generated/ios/swift/PresetType.swift +40 -0
  201. package/nitrogen/generated/ios/swift/StorageLocation.swift +40 -0
  202. package/nitrogen/generated/ios/swift/TrackItem.swift +31 -1
  203. package/nitrogen/generated/ios/swift/TrackPlayerState.swift +4 -4
  204. package/nitrogen/generated/ios/swift/Variant_NullType_Double.swift +18 -0
  205. package/nitrogen/generated/ios/swift/Variant_NullType_DownloadError.swift +18 -0
  206. package/nitrogen/generated/ios/swift/Variant_NullType_DownloadTask.swift +18 -0
  207. package/nitrogen/generated/ios/swift/Variant_NullType_DownloadedPlaylist.swift +18 -0
  208. package/nitrogen/generated/ios/swift/Variant_NullType_DownloadedTrack.swift +18 -0
  209. package/nitrogen/generated/shared/c++/CurrentPlayingType.hpp +84 -0
  210. package/nitrogen/generated/shared/c++/DownloadConfig.hpp +108 -0
  211. package/nitrogen/generated/shared/c++/DownloadError.hpp +89 -0
  212. package/nitrogen/generated/shared/c++/DownloadErrorReason.hpp +96 -0
  213. package/nitrogen/generated/shared/c++/DownloadProgress.hpp +97 -0
  214. package/nitrogen/generated/shared/c++/DownloadQueueStatus.hpp +99 -0
  215. package/nitrogen/generated/shared/c++/DownloadState.hpp +92 -0
  216. package/nitrogen/generated/shared/c++/DownloadStorageInfo.hpp +91 -0
  217. package/nitrogen/generated/shared/c++/DownloadTask.hpp +122 -0
  218. package/nitrogen/generated/shared/c++/DownloadedPlaylist.hpp +101 -0
  219. package/nitrogen/generated/shared/c++/DownloadedTrack.hpp +107 -0
  220. package/nitrogen/generated/shared/c++/EqualizerBand.hpp +87 -0
  221. package/nitrogen/generated/shared/c++/EqualizerPreset.hpp +86 -0
  222. package/nitrogen/generated/shared/c++/EqualizerState.hpp +89 -0
  223. package/nitrogen/generated/shared/c++/GainRange.hpp +79 -0
  224. package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.cpp +55 -0
  225. package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.hpp +134 -0
  226. package/nitrogen/generated/shared/c++/HybridEqualizerSpec.cpp +38 -0
  227. package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +95 -0
  228. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +4 -0
  229. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +11 -5
  230. package/nitrogen/generated/shared/c++/PlaybackSource.hpp +80 -0
  231. package/nitrogen/generated/shared/c++/PlayerState.hpp +9 -2
  232. package/nitrogen/generated/shared/c++/PresetType.hpp +76 -0
  233. package/nitrogen/generated/shared/c++/StorageLocation.hpp +76 -0
  234. package/nitrogen/generated/shared/c++/TrackItem.hpp +7 -2
  235. package/nitrogen/generated/shared/c++/TrackPlayerState.hpp +5 -5
  236. package/package.json +1 -1
  237. package/src/hooks/downloadCallbackManager.ts +149 -0
  238. package/src/hooks/equalizerCallbackManager.ts +138 -0
  239. package/src/hooks/index.ts +23 -0
  240. package/src/hooks/useActualQueue.ts +116 -0
  241. package/src/hooks/useDownloadActions.ts +179 -0
  242. package/src/hooks/useDownloadProgress.ts +126 -0
  243. package/src/hooks/useDownloadStorage.ts +84 -0
  244. package/src/hooks/useDownloadedTracks.ts +138 -0
  245. package/src/hooks/useEqualizer.ts +173 -0
  246. package/src/hooks/useEqualizerPresets.ts +140 -0
  247. package/src/hooks/useNowPlaying.ts +3 -2
  248. package/src/hooks/useOnChangeTrack.ts +15 -11
  249. package/src/hooks/useOnPlaybackStateChange.ts +19 -15
  250. package/src/hooks/usePlaylist.ts +161 -0
  251. package/src/index.ts +12 -0
  252. package/src/specs/DownloadManager.nitro.ts +203 -0
  253. package/src/specs/Equalizer.nitro.ts +69 -0
  254. package/src/specs/TrackPlayer.nitro.ts +6 -2
  255. package/src/types/DownloadTypes.ts +135 -0
  256. package/src/types/EqualizerTypes.ts +72 -0
  257. package/src/types/PlayerQueue.ts +9 -0
@@ -0,0 +1,489 @@
1
+ package com.margelo.nitro.nitroplayer.download
2
+
3
+ import android.content.Context
4
+ import android.os.Handler
5
+ import android.os.Looper
6
+ import android.util.Log
7
+ import androidx.work.*
8
+ import com.margelo.nitro.core.NullType
9
+ import com.margelo.nitro.nitroplayer.*
10
+ import java.util.*
11
+ import java.util.concurrent.ConcurrentHashMap
12
+ import java.util.concurrent.CopyOnWriteArrayList
13
+
14
+ /**
15
+ * Core download manager using WorkManager for background downloads
16
+ */
17
+ class DownloadManagerCore private constructor(
18
+ private val context: Context,
19
+ ) {
20
+ companion object {
21
+ private const val TAG = "DownloadManagerCore"
22
+
23
+ @Volatile
24
+ private var instance: DownloadManagerCore? = null
25
+
26
+ fun getInstance(context: Context): DownloadManagerCore =
27
+ instance ?: synchronized(this) {
28
+ instance ?: DownloadManagerCore(context.applicationContext).also { instance = it }
29
+ }
30
+ }
31
+
32
+ // Configuration
33
+ private var config: DownloadConfig =
34
+ DownloadConfig(
35
+ storageLocation = StorageLocation.PRIVATE,
36
+ maxConcurrentDownloads = 3.0,
37
+ autoRetry = true,
38
+ maxRetryAttempts = 3.0,
39
+ backgroundDownloadsEnabled = true,
40
+ downloadArtwork = true,
41
+ customDownloadPath = null,
42
+ wifiOnlyDownloads = false,
43
+ )
44
+
45
+ private var playbackSourcePreference: PlaybackSource = PlaybackSource.AUTO
46
+
47
+ // Download tracking
48
+ private val activeTasks = ConcurrentHashMap<String, DownloadTaskMetadata>()
49
+ private val trackMetadata = ConcurrentHashMap<String, TrackItem>()
50
+ private val playlistAssociations = ConcurrentHashMap<String, String>()
51
+
52
+ // Callbacks
53
+ private val progressCallbacks = CopyOnWriteArrayList<(DownloadProgress) -> Unit>()
54
+ private val stateChangeCallbacks = CopyOnWriteArrayList<(String, String, DownloadState, DownloadError?) -> Unit>()
55
+ private val completeCallbacks = CopyOnWriteArrayList<(DownloadedTrack) -> Unit>()
56
+
57
+ private val mainHandler = Handler(Looper.getMainLooper())
58
+ private val database = DownloadDatabase.getInstance(context)
59
+ private val fileManager = DownloadFileManager.getInstance(context)
60
+
61
+ // WorkManager
62
+ private val workManager = WorkManager.getInstance(context)
63
+
64
+ // Configuration
65
+ fun configure(config: DownloadConfig) {
66
+ this.config = config
67
+ }
68
+
69
+ fun getConfig(): DownloadConfig = config
70
+
71
+ // Download Operations
72
+ fun downloadTrack(
73
+ track: TrackItem,
74
+ playlistId: String?,
75
+ ): String {
76
+ val downloadId = UUID.randomUUID().toString()
77
+
78
+ // Store track metadata
79
+ trackMetadata[track.id] = track
80
+
81
+ // Store playlist association if provided
82
+ playlistId?.let {
83
+ playlistAssociations[downloadId] = it
84
+ }
85
+
86
+ // Create download task metadata
87
+ val metadata =
88
+ DownloadTaskMetadata(
89
+ downloadId = downloadId,
90
+ trackId = track.id,
91
+ playlistId = playlistId,
92
+ state = DownloadState.PENDING,
93
+ createdAt = System.currentTimeMillis().toDouble(),
94
+ retryCount = 0,
95
+ )
96
+ activeTasks[downloadId] = metadata
97
+
98
+ // Create WorkManager request
99
+ val constraints =
100
+ Constraints
101
+ .Builder()
102
+ .setRequiredNetworkType(
103
+ if (config.wifiOnlyDownloads == true) NetworkType.UNMETERED else NetworkType.CONNECTED,
104
+ ).setRequiresStorageNotLow(true)
105
+ .build()
106
+
107
+ val inputData =
108
+ workDataOf(
109
+ DownloadWorker.KEY_DOWNLOAD_ID to downloadId,
110
+ DownloadWorker.KEY_TRACK_ID to track.id,
111
+ DownloadWorker.KEY_URL to track.url,
112
+ DownloadWorker.KEY_PLAYLIST_ID to (playlistId ?: ""),
113
+ DownloadWorker.KEY_STORAGE_LOCATION to (config.storageLocation?.name ?: StorageLocation.PRIVATE.name),
114
+ )
115
+
116
+ val downloadRequest =
117
+ OneTimeWorkRequestBuilder<DownloadWorker>()
118
+ .setConstraints(constraints)
119
+ .setInputData(inputData)
120
+ .addTag("download_$downloadId")
121
+ .addTag("track_${track.id}")
122
+ .build()
123
+
124
+ workManager.enqueue(downloadRequest)
125
+
126
+ // Update state
127
+ activeTasks[downloadId]?.state = DownloadState.PENDING
128
+ notifyStateChange(downloadId, track.id, DownloadState.PENDING, null)
129
+
130
+ return downloadId
131
+ }
132
+
133
+ fun downloadPlaylist(
134
+ playlistId: String,
135
+ tracks: Array<TrackItem>,
136
+ ): Array<String> =
137
+ tracks
138
+ .map { track ->
139
+ downloadTrack(track, playlistId)
140
+ }.toTypedArray()
141
+
142
+ // Download Control
143
+ fun pauseDownload(downloadId: String) {
144
+ workManager.cancelAllWorkByTag("download_$downloadId")
145
+ activeTasks[downloadId]?.let { metadata ->
146
+ metadata.state = DownloadState.PAUSED
147
+ notifyStateChange(downloadId, metadata.trackId, DownloadState.PAUSED, null)
148
+ }
149
+ }
150
+
151
+ fun resumeDownload(downloadId: String) {
152
+ activeTasks[downloadId]?.let { metadata ->
153
+ val track = trackMetadata[metadata.trackId] ?: return
154
+ val playlistId = playlistAssociations[downloadId]
155
+
156
+ // Re-create work request
157
+ val constraints =
158
+ Constraints
159
+ .Builder()
160
+ .setRequiredNetworkType(
161
+ if (config.wifiOnlyDownloads == true) NetworkType.UNMETERED else NetworkType.CONNECTED,
162
+ ).setRequiresStorageNotLow(true)
163
+ .build()
164
+
165
+ val inputData =
166
+ workDataOf(
167
+ DownloadWorker.KEY_DOWNLOAD_ID to downloadId,
168
+ DownloadWorker.KEY_TRACK_ID to track.id,
169
+ DownloadWorker.KEY_URL to track.url,
170
+ DownloadWorker.KEY_PLAYLIST_ID to (playlistId ?: ""),
171
+ DownloadWorker.KEY_STORAGE_LOCATION to (config.storageLocation?.name ?: StorageLocation.PRIVATE.name),
172
+ )
173
+
174
+ val downloadRequest =
175
+ OneTimeWorkRequestBuilder<DownloadWorker>()
176
+ .setConstraints(constraints)
177
+ .setInputData(inputData)
178
+ .addTag("download_$downloadId")
179
+ .addTag("track_${track.id}")
180
+ .build()
181
+
182
+ workManager.enqueue(downloadRequest)
183
+
184
+ metadata.state = DownloadState.DOWNLOADING
185
+ notifyStateChange(downloadId, metadata.trackId, DownloadState.DOWNLOADING, null)
186
+ }
187
+ }
188
+
189
+ fun cancelDownload(downloadId: String) {
190
+ workManager.cancelAllWorkByTag("download_$downloadId")
191
+ activeTasks[downloadId]?.let { metadata ->
192
+ metadata.state = DownloadState.CANCELLED
193
+ notifyStateChange(downloadId, metadata.trackId, DownloadState.CANCELLED, null)
194
+ activeTasks.remove(downloadId)
195
+ }
196
+ }
197
+
198
+ fun retryDownload(downloadId: String) {
199
+ activeTasks[downloadId]?.let { metadata ->
200
+ metadata.retryCount++
201
+ metadata.error = null
202
+ resumeDownload(downloadId)
203
+ }
204
+ }
205
+
206
+ fun pauseAllDownloads() {
207
+ activeTasks.keys.forEach { pauseDownload(it) }
208
+ }
209
+
210
+ fun resumeAllDownloads() {
211
+ activeTasks.filter { it.value.state == DownloadState.PAUSED }.keys.forEach { resumeDownload(it) }
212
+ }
213
+
214
+ fun cancelAllDownloads() {
215
+ activeTasks.keys.forEach { cancelDownload(it) }
216
+ }
217
+
218
+ // Download Status
219
+ fun getDownloadTask(downloadId: String): DownloadTask? = activeTasks[downloadId]?.toDownloadTask()
220
+
221
+ fun getActiveDownloads(): Array<DownloadTask> =
222
+ activeTasks.values
223
+ .filter { it.state in listOf(DownloadState.DOWNLOADING, DownloadState.PENDING, DownloadState.PAUSED) }
224
+ .map { it.toDownloadTask() }
225
+ .toTypedArray()
226
+
227
+ fun getQueueStatus(): DownloadQueueStatus {
228
+ val metadata = activeTasks.values.toList()
229
+
230
+ val pendingCount = metadata.count { it.state == DownloadState.PENDING }
231
+ val activeCount = metadata.count { it.state == DownloadState.DOWNLOADING }
232
+ val completedCount = database.getAllDownloadedTracks().size
233
+ val failedCount = metadata.count { it.state == DownloadState.FAILED }
234
+
235
+ val totalBytes = metadata.sumOf { it.totalBytes ?: 0.0 }
236
+ val downloadedBytes = metadata.sumOf { it.bytesDownloaded }
237
+
238
+ return DownloadQueueStatus(
239
+ pendingCount = pendingCount.toDouble(),
240
+ activeCount = activeCount.toDouble(),
241
+ completedCount = completedCount.toDouble(),
242
+ failedCount = failedCount.toDouble(),
243
+ totalBytesToDownload = totalBytes,
244
+ totalBytesDownloaded = downloadedBytes,
245
+ overallProgress = if (totalBytes > 0) downloadedBytes / totalBytes else 0.0,
246
+ )
247
+ }
248
+
249
+ fun isDownloading(trackId: String): Boolean = activeTasks.values.any { it.trackId == trackId && it.state == DownloadState.DOWNLOADING }
250
+
251
+ fun getDownloadState(trackId: String): DownloadState {
252
+ activeTasks.values.find { it.trackId == trackId }?.let {
253
+ return it.state
254
+ }
255
+ if (database.isTrackDownloaded(trackId)) {
256
+ return DownloadState.COMPLETED
257
+ }
258
+ return DownloadState.PENDING
259
+ }
260
+
261
+ // Downloaded Content Queries
262
+ fun isTrackDownloaded(trackId: String): Boolean = database.isTrackDownloaded(trackId)
263
+
264
+ fun isPlaylistDownloaded(playlistId: String): Boolean = database.isPlaylistDownloaded(playlistId)
265
+
266
+ fun isPlaylistPartiallyDownloaded(playlistId: String): Boolean = database.isPlaylistPartiallyDownloaded(playlistId)
267
+
268
+ fun getDownloadedTrack(trackId: String): DownloadedTrack? = database.getDownloadedTrack(trackId)
269
+
270
+ fun getAllDownloadedTracks(): Array<DownloadedTrack> = database.getAllDownloadedTracks().toTypedArray()
271
+
272
+ fun getDownloadedPlaylist(playlistId: String): DownloadedPlaylist? = database.getDownloadedPlaylist(playlistId)
273
+
274
+ fun getAllDownloadedPlaylists(): Array<DownloadedPlaylist> = database.getAllDownloadedPlaylists().toTypedArray()
275
+
276
+ fun getLocalPath(trackId: String): String? = database.getDownloadedTrack(trackId)?.localPath
277
+
278
+ // Deletion
279
+ fun deleteDownloadedTrack(trackId: String) = database.deleteDownloadedTrack(trackId)
280
+
281
+ fun deleteDownloadedPlaylist(playlistId: String) = database.deleteDownloadedPlaylist(playlistId)
282
+
283
+ fun deleteAllDownloads() = database.deleteAllDownloads()
284
+
285
+ // Storage
286
+ fun getStorageInfo(): DownloadStorageInfo = fileManager.getStorageInfo()
287
+
288
+ /** Validates all downloads and cleans up orphaned records */
289
+ fun syncDownloads(): Int {
290
+ val removedCount = database.syncDownloads()
291
+ val bytesFreed = fileManager.cleanupOrphanedFiles(database.getAllDownloadedTracks().map { it.trackId }.toSet())
292
+ Log.d(TAG, "syncDownloads: removed $removedCount orphaned records, freed $bytesFreed bytes")
293
+ return removedCount
294
+ }
295
+
296
+ // Playback Source Preference
297
+ fun setPlaybackSourcePreference(preference: PlaybackSource) {
298
+ playbackSourcePreference = preference
299
+ }
300
+
301
+ fun getPlaybackSourcePreference(): PlaybackSource = playbackSourcePreference
302
+
303
+ fun getEffectiveUrl(track: TrackItem): String =
304
+ when (playbackSourcePreference) {
305
+ PlaybackSource.NETWORK -> track.url
306
+ PlaybackSource.DOWNLOAD -> getLocalPath(track.id) ?: track.url
307
+ PlaybackSource.AUTO -> getLocalPath(track.id) ?: track.url
308
+ }
309
+
310
+ // Callbacks
311
+ fun addProgressCallback(callback: (DownloadProgress) -> Unit) {
312
+ progressCallbacks.add(callback)
313
+ }
314
+
315
+ fun addStateChangeCallback(callback: (String, String, DownloadState, DownloadError?) -> Unit) {
316
+ stateChangeCallbacks.add(callback)
317
+ }
318
+
319
+ fun addCompleteCallback(callback: (DownloadedTrack) -> Unit) {
320
+ completeCallbacks.add(callback)
321
+ }
322
+
323
+ // Internal callbacks from DownloadWorker
324
+ internal fun onProgress(
325
+ downloadId: String,
326
+ trackId: String,
327
+ bytesDownloaded: Long,
328
+ totalBytes: Long,
329
+ ) {
330
+ activeTasks[downloadId]?.let { metadata ->
331
+ metadata.bytesDownloaded = bytesDownloaded.toDouble()
332
+ metadata.totalBytes = totalBytes.toDouble()
333
+ metadata.state = DownloadState.DOWNLOADING
334
+ }
335
+
336
+ val progress =
337
+ DownloadProgress(
338
+ trackId = trackId,
339
+ downloadId = downloadId,
340
+ bytesDownloaded = bytesDownloaded.toDouble(),
341
+ totalBytes = totalBytes.toDouble(),
342
+ progress = if (totalBytes > 0) bytesDownloaded.toDouble() / totalBytes.toDouble() else 0.0,
343
+ state = DownloadState.DOWNLOADING,
344
+ )
345
+
346
+ mainHandler.post {
347
+ progressCallbacks.forEach { it(progress) }
348
+ }
349
+ }
350
+
351
+ internal fun onComplete(
352
+ downloadId: String,
353
+ trackId: String,
354
+ localPath: String,
355
+ ) {
356
+ val track = trackMetadata[trackId] ?: return
357
+ val playlistId = playlistAssociations[downloadId]
358
+ val storageLocation = config.storageLocation ?: StorageLocation.PRIVATE
359
+ val fileSize = fileManager.getFileSize(localPath)
360
+
361
+ val downloadedTrack =
362
+ DownloadedTrack(
363
+ trackId = trackId,
364
+ originalTrack = track,
365
+ localPath = localPath,
366
+ localArtworkPath = null,
367
+ downloadedAt = System.currentTimeMillis().toDouble(),
368
+ fileSize = fileSize.toDouble(),
369
+ storageLocation = storageLocation,
370
+ )
371
+
372
+ database.saveDownloadedTrack(downloadedTrack, playlistId)
373
+
374
+ activeTasks[downloadId]?.let { metadata ->
375
+ metadata.state = DownloadState.COMPLETED
376
+ metadata.completedAt = System.currentTimeMillis().toDouble()
377
+ }
378
+ activeTasks.remove(downloadId)
379
+
380
+ notifyStateChange(downloadId, trackId, DownloadState.COMPLETED, null)
381
+
382
+ mainHandler.post {
383
+ completeCallbacks.forEach { it(downloadedTrack) }
384
+ }
385
+ }
386
+
387
+ internal fun onError(
388
+ downloadId: String,
389
+ trackId: String,
390
+ error: DownloadError,
391
+ ) {
392
+ activeTasks[downloadId]?.let { metadata ->
393
+ metadata.state = DownloadState.FAILED
394
+ metadata.error = error
395
+
396
+ // Auto-retry if enabled
397
+ if (config.autoRetry == true && error.isRetryable && metadata.retryCount < (config.maxRetryAttempts?.toInt() ?: 3)) {
398
+ mainHandler.postDelayed({
399
+ retryDownload(downloadId)
400
+ }, 2000)
401
+ } else {
402
+ notifyStateChange(downloadId, trackId, DownloadState.FAILED, error)
403
+ }
404
+ }
405
+ }
406
+
407
+ private fun notifyStateChange(
408
+ downloadId: String,
409
+ trackId: String,
410
+ state: DownloadState,
411
+ error: DownloadError?,
412
+ ) {
413
+ mainHandler.post {
414
+ stateChangeCallbacks.forEach { it(downloadId, trackId, state, error) }
415
+ }
416
+ }
417
+ }
418
+
419
+ // Internal metadata class
420
+ internal data class DownloadTaskMetadata(
421
+ val downloadId: String,
422
+ val trackId: String,
423
+ val playlistId: String?,
424
+ var state: DownloadState,
425
+ val createdAt: Double,
426
+ var startedAt: Double? = null,
427
+ var completedAt: Double? = null,
428
+ var retryCount: Int = 0,
429
+ var bytesDownloaded: Double = 0.0,
430
+ var totalBytes: Double? = null,
431
+ var error: DownloadError? = null,
432
+ ) {
433
+ fun toDownloadTask(): DownloadTask {
434
+ val progress =
435
+ DownloadProgress(
436
+ trackId = trackId,
437
+ downloadId = downloadId,
438
+ bytesDownloaded = bytesDownloaded,
439
+ totalBytes = totalBytes ?: 0.0,
440
+ progress = if (totalBytes != null && totalBytes!! > 0) bytesDownloaded / totalBytes!! else 0.0,
441
+ state = state,
442
+ )
443
+
444
+ val playlistIdVariant =
445
+ if (playlistId != null) {
446
+ Variant_NullType_String.create(playlistId)
447
+ } else {
448
+ null
449
+ }
450
+
451
+ // Copy to local val to avoid smart cast issues
452
+ val localStartedAt = startedAt
453
+ val startedAtVariant =
454
+ if (localStartedAt != null) {
455
+ Variant_NullType_Double.create(localStartedAt)
456
+ } else {
457
+ null
458
+ }
459
+
460
+ val localCompletedAt = completedAt
461
+ val completedAtVariant =
462
+ if (localCompletedAt != null) {
463
+ Variant_NullType_Double.create(localCompletedAt)
464
+ } else {
465
+ null
466
+ }
467
+
468
+ val localError = error
469
+ val errorVariant =
470
+ if (localError != null) {
471
+ Variant_NullType_DownloadError.create(localError)
472
+ } else {
473
+ null
474
+ }
475
+
476
+ return DownloadTask(
477
+ downloadId = downloadId,
478
+ trackId = trackId,
479
+ playlistId = playlistIdVariant,
480
+ state = state,
481
+ progress = progress,
482
+ createdAt = createdAt,
483
+ startedAt = startedAtVariant,
484
+ completedAt = completedAtVariant,
485
+ error = errorVariant,
486
+ retryCount = retryCount.toDouble(),
487
+ )
488
+ }
489
+ }
@@ -0,0 +1,209 @@
1
+ package com.margelo.nitro.nitroplayer.download
2
+
3
+ import android.app.NotificationChannel
4
+ import android.app.NotificationManager
5
+ import android.content.Context
6
+ import android.os.Build
7
+ import androidx.core.app.NotificationCompat
8
+ import androidx.work.CoroutineWorker
9
+ import androidx.work.ForegroundInfo
10
+ import androidx.work.WorkerParameters
11
+ import com.margelo.nitro.nitroplayer.*
12
+ import kotlinx.coroutines.Dispatchers
13
+ import kotlinx.coroutines.withContext
14
+ import java.io.BufferedInputStream
15
+ import java.io.File
16
+ import java.io.FileOutputStream
17
+ import java.net.HttpURLConnection
18
+ import java.net.URL
19
+
20
+ /**
21
+ * WorkManager worker for background downloads
22
+ */
23
+ class DownloadWorker(
24
+ private val context: Context,
25
+ workerParams: WorkerParameters,
26
+ ) : CoroutineWorker(context, workerParams) {
27
+ companion object {
28
+ const val KEY_DOWNLOAD_ID = "download_id"
29
+ const val KEY_TRACK_ID = "track_id"
30
+ const val KEY_URL = "url"
31
+ const val KEY_PLAYLIST_ID = "playlist_id"
32
+ const val KEY_STORAGE_LOCATION = "storage_location"
33
+
34
+ private const val NOTIFICATION_CHANNEL_ID = "nitro_player_downloads"
35
+ private const val NOTIFICATION_ID = 2001
36
+ private const val BUFFER_SIZE = 8192
37
+ }
38
+
39
+ private val downloadManager = DownloadManagerCore.getInstance(context)
40
+ private val fileManager = DownloadFileManager.getInstance(context)
41
+
42
+ override suspend fun doWork(): Result =
43
+ withContext(Dispatchers.IO) {
44
+ val downloadId = inputData.getString(KEY_DOWNLOAD_ID) ?: return@withContext Result.failure()
45
+ val trackId = inputData.getString(KEY_TRACK_ID) ?: return@withContext Result.failure()
46
+ val urlString = inputData.getString(KEY_URL) ?: return@withContext Result.failure()
47
+ val storageLocationStr = inputData.getString(KEY_STORAGE_LOCATION) ?: StorageLocation.PRIVATE.name
48
+
49
+ val storageLocation =
50
+ try {
51
+ StorageLocation.valueOf(storageLocationStr)
52
+ } catch (e: Exception) {
53
+ StorageLocation.PRIVATE
54
+ }
55
+
56
+ try {
57
+ // Set foreground notification
58
+ setForeground(createForegroundInfo(trackId))
59
+
60
+ // Perform download
61
+ val localPath = downloadFile(downloadId, trackId, urlString, storageLocation)
62
+
63
+ if (localPath != null) {
64
+ downloadManager.onComplete(downloadId, trackId, localPath)
65
+ Result.success()
66
+ } else {
67
+ val error =
68
+ DownloadError(
69
+ code = "DOWNLOAD_FAILED",
70
+ message = "Failed to download file",
71
+ reason = DownloadErrorReason.UNKNOWN,
72
+ isRetryable = true,
73
+ )
74
+ downloadManager.onError(downloadId, trackId, error)
75
+ Result.retry()
76
+ }
77
+ } catch (e: Exception) {
78
+ val errorReason =
79
+ when {
80
+ e.message?.contains("network", ignoreCase = true) == true -> DownloadErrorReason.NETWORK_ERROR
81
+ e.message?.contains("timeout", ignoreCase = true) == true -> DownloadErrorReason.TIMEOUT
82
+ e.message?.contains("space", ignoreCase = true) == true -> DownloadErrorReason.STORAGE_FULL
83
+ else -> DownloadErrorReason.UNKNOWN
84
+ }
85
+
86
+ val error =
87
+ DownloadError(
88
+ code = e.javaClass.simpleName,
89
+ message = e.message ?: "Unknown error",
90
+ reason = errorReason,
91
+ isRetryable = errorReason in listOf(DownloadErrorReason.NETWORK_ERROR, DownloadErrorReason.TIMEOUT),
92
+ )
93
+ downloadManager.onError(downloadId, trackId, error)
94
+
95
+ if (error.isRetryable) {
96
+ Result.retry()
97
+ } else {
98
+ Result.failure()
99
+ }
100
+ }
101
+ }
102
+
103
+ private suspend fun downloadFile(
104
+ downloadId: String,
105
+ trackId: String,
106
+ urlString: String,
107
+ storageLocation: StorageLocation,
108
+ ): String? =
109
+ withContext(Dispatchers.IO) {
110
+ var connection: HttpURLConnection? = null
111
+ var inputStream: BufferedInputStream? = null
112
+ var outputStream: FileOutputStream? = null
113
+
114
+ try {
115
+ val url = URL(urlString)
116
+ connection = url.openConnection() as HttpURLConnection
117
+ connection.connectTimeout = 30000
118
+ connection.readTimeout = 30000
119
+ connection.connect()
120
+
121
+ val responseCode = connection.responseCode
122
+ if (responseCode != HttpURLConnection.HTTP_OK) {
123
+ throw Exception("Server returned HTTP $responseCode")
124
+ }
125
+
126
+ val totalBytes = connection.contentLengthLong
127
+ var bytesDownloaded: Long = 0
128
+
129
+ // Create destination file
130
+ val destinationFile = fileManager.createDownloadFile(trackId, storageLocation)
131
+
132
+ inputStream = BufferedInputStream(connection.inputStream)
133
+ outputStream = FileOutputStream(destinationFile)
134
+
135
+ val buffer = ByteArray(BUFFER_SIZE)
136
+ var bytesRead: Int
137
+ var lastProgressUpdate = System.currentTimeMillis()
138
+
139
+ while (inputStream.read(buffer).also { bytesRead = it } != -1) {
140
+ outputStream.write(buffer, 0, bytesRead)
141
+ bytesDownloaded += bytesRead
142
+
143
+ // Update progress every 250ms
144
+ val now = System.currentTimeMillis()
145
+ if (now - lastProgressUpdate >= 250) {
146
+ downloadManager.onProgress(downloadId, trackId, bytesDownloaded, totalBytes)
147
+ lastProgressUpdate = now
148
+ }
149
+ }
150
+
151
+ outputStream.flush()
152
+
153
+ // Final progress update
154
+ downloadManager.onProgress(downloadId, trackId, bytesDownloaded, totalBytes)
155
+
156
+ destinationFile.absolutePath
157
+ } catch (e: Exception) {
158
+ throw e
159
+ } finally {
160
+ try {
161
+ inputStream?.close()
162
+ outputStream?.close()
163
+ connection?.disconnect()
164
+ } catch (e: Exception) {
165
+ // Ignore cleanup errors
166
+ }
167
+ }
168
+ }
169
+
170
+ private fun createForegroundInfo(trackId: String): ForegroundInfo {
171
+ createNotificationChannel()
172
+
173
+ val notification =
174
+ NotificationCompat
175
+ .Builder(context, NOTIFICATION_CHANNEL_ID)
176
+ .setContentTitle("Downloading")
177
+ .setContentText("Downloading track...")
178
+ .setSmallIcon(android.R.drawable.stat_sys_download)
179
+ .setOngoing(true)
180
+ .setProgress(100, 0, true)
181
+ .build()
182
+
183
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
184
+ ForegroundInfo(
185
+ NOTIFICATION_ID,
186
+ notification,
187
+ android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
188
+ )
189
+ } else {
190
+ ForegroundInfo(NOTIFICATION_ID, notification)
191
+ }
192
+ }
193
+
194
+ private fun createNotificationChannel() {
195
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
196
+ val channel =
197
+ NotificationChannel(
198
+ NOTIFICATION_CHANNEL_ID,
199
+ "Downloads",
200
+ NotificationManager.IMPORTANCE_LOW,
201
+ ).apply {
202
+ description = "Download progress notifications"
203
+ }
204
+
205
+ val notificationManager = context.getSystemService(NotificationManager::class.java)
206
+ notificationManager?.createNotificationChannel(channel)
207
+ }
208
+ }
209
+ }