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.
- package/README.md +444 -4
- package/android/build.gradle +4 -1
- package/android/src/main/AndroidManifest.xml +16 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +2 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +8 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +225 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +105 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +6 -6
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +37 -12
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +970 -213
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +475 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadFileManager.kt +159 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +489 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +209 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +486 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaBrowserService.kt +3 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +14 -6
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +27 -0
- package/ios/HybridDownloadManager.swift +226 -0
- package/ios/HybridEqualizer.swift +111 -0
- package/ios/HybridTrackPlayer.swift +36 -8
- package/ios/core/TrackPlayerCore.swift +998 -276
- package/ios/download/DownloadDatabase.swift +493 -0
- package/ios/download/DownloadFileManager.swift +241 -0
- package/ios/download/DownloadManagerCore.swift +923 -0
- package/ios/equalizer/EqualizerCore.swift +685 -0
- package/ios/media/MediaSessionManager.swift +40 -28
- package/ios/playlist/PlaylistManager.swift +40 -9
- package/ios/queue/HybridPlayerQueue.swift +33 -13
- package/lib/hooks/downloadCallbackManager.d.ts +36 -0
- package/lib/hooks/downloadCallbackManager.js +108 -0
- package/lib/hooks/equalizerCallbackManager.d.ts +37 -0
- package/lib/hooks/equalizerCallbackManager.js +109 -0
- package/lib/hooks/index.d.ts +16 -0
- package/lib/hooks/index.js +10 -0
- package/lib/hooks/useActualQueue.d.ts +48 -0
- package/lib/hooks/useActualQueue.js +98 -0
- package/lib/hooks/useDownloadActions.d.ts +26 -0
- package/lib/hooks/useDownloadActions.js +117 -0
- package/lib/hooks/useDownloadProgress.d.ts +25 -0
- package/lib/hooks/useDownloadProgress.js +79 -0
- package/lib/hooks/useDownloadStorage.d.ts +19 -0
- package/lib/hooks/useDownloadStorage.js +60 -0
- package/lib/hooks/useDownloadedTracks.d.ts +25 -0
- package/lib/hooks/useDownloadedTracks.js +69 -0
- package/lib/hooks/useEqualizer.d.ts +25 -0
- package/lib/hooks/useEqualizer.js +124 -0
- package/lib/hooks/useEqualizerPresets.d.ts +22 -0
- package/lib/hooks/useEqualizerPresets.js +96 -0
- package/lib/hooks/useNowPlaying.js +3 -2
- package/lib/hooks/useOnChangeTrack.js +15 -12
- package/lib/hooks/useOnPlaybackStateChange.js +16 -13
- package/lib/hooks/usePlaylist.d.ts +48 -0
- package/lib/hooks/usePlaylist.js +136 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +6 -0
- package/lib/specs/DownloadManager.nitro.d.ts +152 -0
- package/lib/specs/DownloadManager.nitro.js +1 -0
- package/lib/specs/Equalizer.nitro.d.ts +43 -0
- package/lib/specs/Equalizer.nitro.js +1 -0
- package/lib/specs/TrackPlayer.nitro.d.ts +6 -2
- package/lib/types/DownloadTypes.d.ts +110 -0
- package/lib/types/DownloadTypes.js +1 -0
- package/lib/types/EqualizerTypes.d.ts +52 -0
- package/lib/types/EqualizerTypes.js +1 -0
- package/lib/types/PlayerQueue.d.ts +4 -0
- package/nitro.json +8 -0
- package/nitrogen/generated/android/NitroPlayer+autolinking.cmake +10 -1
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +32 -2
- package/nitrogen/generated/android/c++/JCurrentPlayingType.hpp +65 -0
- package/nitrogen/generated/android/c++/JDownloadConfig.hpp +92 -0
- package/nitrogen/generated/android/c++/JDownloadError.hpp +71 -0
- package/nitrogen/generated/android/c++/JDownloadErrorReason.hpp +74 -0
- package/nitrogen/generated/android/c++/JDownloadProgress.hpp +79 -0
- package/nitrogen/generated/android/c++/JDownloadQueueStatus.hpp +81 -0
- package/nitrogen/generated/android/c++/JDownloadState.hpp +71 -0
- package/nitrogen/generated/android/c++/JDownloadStorageInfo.hpp +73 -0
- package/nitrogen/generated/android/c++/JDownloadTask.hpp +108 -0
- package/nitrogen/generated/android/c++/JDownloadedPlaylist.hpp +111 -0
- package/nitrogen/generated/android/c++/JDownloadedTrack.hpp +92 -0
- package/nitrogen/generated/android/c++/JEqualizerBand.hpp +69 -0
- package/nitrogen/generated/android/c++/JEqualizerPreset.hpp +78 -0
- package/nitrogen/generated/android/c++/JEqualizerState.hpp +91 -0
- package/nitrogen/generated/android/c++/JFunc_void_DownloadProgress.hpp +80 -0
- package/nitrogen/generated/android/c++/JFunc_void_DownloadedTrack.hpp +89 -0
- package/nitrogen/generated/android/c++/JFunc_void_TrackItem_std__optional_Reason_.hpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__std__string__.hpp +81 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string_Playlist_std__optional_QueueOperation_.hpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string_std__string_DownloadState_std__optional_DownloadError_.hpp +83 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_EqualizerBand_.hpp +97 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Playlist__std__optional_QueueOperation_.hpp +2 -0
- package/nitrogen/generated/android/c++/JGainRange.hpp +61 -0
- package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +470 -0
- package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +99 -0
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +204 -0
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +82 -0
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +2 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +117 -15
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +6 -2
- package/nitrogen/generated/android/c++/JPlaybackSource.hpp +62 -0
- package/nitrogen/generated/android/c++/JPlayerState.hpp +11 -3
- package/nitrogen/generated/android/c++/JPlaylist.hpp +2 -0
- package/nitrogen/generated/android/c++/JPresetType.hpp +59 -0
- package/nitrogen/generated/android/c++/JStorageLocation.hpp +59 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +9 -3
- package/nitrogen/generated/android/c++/JTrackPlayerState.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_Double.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +69 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.hpp +74 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.hpp +84 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.hpp +85 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.hpp +80 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.hpp +2 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/CurrentPlayingType.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadConfig.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadError.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadErrorReason.kt +26 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadProgress.kt +53 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadQueueStatus.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadState.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadStorageInfo.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadTask.kt +65 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadedPlaylist.kt +53 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadedTrack.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerBand.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerPreset.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerState.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_DownloadProgress.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_DownloadedTrack.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__optional_std__variant_nitro__NullType__std__string__.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__string_std__string_DownloadState_std__optional_DownloadError_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_EqualizerBand_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/GainRange.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +210 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +141 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +19 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlaybackSource.kt +22 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerState.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PresetType.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/StorageLocation.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackItem.kt +7 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackPlayerState.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Double.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadError.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadTask.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedPlaylist.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedTrack.kt +59 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +138 -8
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +1046 -121
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +66 -0
- package/nitrogen/generated/ios/NitroPlayerAutolinking.mm +16 -0
- package/nitrogen/generated/ios/NitroPlayerAutolinking.swift +30 -0
- package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.hpp +386 -0
- package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +223 -0
- package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +1 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +46 -6
- package/nitrogen/generated/ios/swift/CurrentPlayingType.swift +48 -0
- package/nitrogen/generated/ios/swift/DownloadConfig.swift +270 -0
- package/nitrogen/generated/ios/swift/DownloadError.swift +69 -0
- package/nitrogen/generated/ios/swift/DownloadErrorReason.swift +60 -0
- package/nitrogen/generated/ios/swift/DownloadProgress.swift +91 -0
- package/nitrogen/generated/ios/swift/DownloadQueueStatus.swift +102 -0
- package/nitrogen/generated/ios/swift/DownloadState.swift +56 -0
- package/nitrogen/generated/ios/swift/DownloadStorageInfo.swift +80 -0
- package/nitrogen/generated/ios/swift/DownloadTask.swift +315 -0
- package/nitrogen/generated/ios/swift/DownloadedPlaylist.swift +103 -0
- package/nitrogen/generated/ios/swift/DownloadedTrack.swift +147 -0
- package/nitrogen/generated/ios/swift/EqualizerBand.swift +69 -0
- package/nitrogen/generated/ios/swift/EqualizerPreset.swift +70 -0
- package/nitrogen/generated/ios/swift/EqualizerState.swift +115 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_DownloadProgress.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_DownloadStorageInfo.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_DownloadedTrack.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_PlayerState.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__optional_std__variant_nitro__NullType__std__string__.swift +66 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string_std__string_DownloadState_std__optional_DownloadError_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_std__string_.swift +47 -0
- package/nitrogen/generated/ios/swift/GainRange.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec.swift +90 -0
- package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec_cxx.swift +705 -0
- package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +73 -0
- package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +396 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +6 -2
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +105 -8
- package/nitrogen/generated/ios/swift/PlaybackSource.swift +44 -0
- package/nitrogen/generated/ios/swift/PlayerState.swift +13 -2
- package/nitrogen/generated/ios/swift/PresetType.swift +40 -0
- package/nitrogen/generated/ios/swift/StorageLocation.swift +40 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +31 -1
- package/nitrogen/generated/ios/swift/TrackPlayerState.swift +4 -4
- package/nitrogen/generated/ios/swift/Variant_NullType_Double.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_DownloadError.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_DownloadTask.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_DownloadedPlaylist.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_DownloadedTrack.swift +18 -0
- package/nitrogen/generated/shared/c++/CurrentPlayingType.hpp +84 -0
- package/nitrogen/generated/shared/c++/DownloadConfig.hpp +108 -0
- package/nitrogen/generated/shared/c++/DownloadError.hpp +89 -0
- package/nitrogen/generated/shared/c++/DownloadErrorReason.hpp +96 -0
- package/nitrogen/generated/shared/c++/DownloadProgress.hpp +97 -0
- package/nitrogen/generated/shared/c++/DownloadQueueStatus.hpp +99 -0
- package/nitrogen/generated/shared/c++/DownloadState.hpp +92 -0
- package/nitrogen/generated/shared/c++/DownloadStorageInfo.hpp +91 -0
- package/nitrogen/generated/shared/c++/DownloadTask.hpp +122 -0
- package/nitrogen/generated/shared/c++/DownloadedPlaylist.hpp +101 -0
- package/nitrogen/generated/shared/c++/DownloadedTrack.hpp +107 -0
- package/nitrogen/generated/shared/c++/EqualizerBand.hpp +87 -0
- package/nitrogen/generated/shared/c++/EqualizerPreset.hpp +86 -0
- package/nitrogen/generated/shared/c++/EqualizerState.hpp +89 -0
- package/nitrogen/generated/shared/c++/GainRange.hpp +79 -0
- package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.cpp +55 -0
- package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.hpp +134 -0
- package/nitrogen/generated/shared/c++/HybridEqualizerSpec.cpp +38 -0
- package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +95 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +11 -5
- package/nitrogen/generated/shared/c++/PlaybackSource.hpp +80 -0
- package/nitrogen/generated/shared/c++/PlayerState.hpp +9 -2
- package/nitrogen/generated/shared/c++/PresetType.hpp +76 -0
- package/nitrogen/generated/shared/c++/StorageLocation.hpp +76 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +7 -2
- package/nitrogen/generated/shared/c++/TrackPlayerState.hpp +5 -5
- package/package.json +1 -1
- package/src/hooks/downloadCallbackManager.ts +149 -0
- package/src/hooks/equalizerCallbackManager.ts +138 -0
- package/src/hooks/index.ts +23 -0
- package/src/hooks/useActualQueue.ts +116 -0
- package/src/hooks/useDownloadActions.ts +179 -0
- package/src/hooks/useDownloadProgress.ts +126 -0
- package/src/hooks/useDownloadStorage.ts +84 -0
- package/src/hooks/useDownloadedTracks.ts +138 -0
- package/src/hooks/useEqualizer.ts +173 -0
- package/src/hooks/useEqualizerPresets.ts +140 -0
- package/src/hooks/useNowPlaying.ts +3 -2
- package/src/hooks/useOnChangeTrack.ts +15 -11
- package/src/hooks/useOnPlaybackStateChange.ts +19 -15
- package/src/hooks/usePlaylist.ts +161 -0
- package/src/index.ts +12 -0
- package/src/specs/DownloadManager.nitro.ts +203 -0
- package/src/specs/Equalizer.nitro.ts +69 -0
- package/src/specs/TrackPlayer.nitro.ts +6 -2
- package/src/types/DownloadTypes.ts +135 -0
- package/src/types/EqualizerTypes.ts +72 -0
- 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
|
+
}
|