diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4dda33a7e9..03c7357090 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -218,6 +218,12 @@
android:foregroundServiceType="dataSync"
android:exported="false" />
+
+
=
+ mutableListOf()
+
+ private val baseNotification by lazy {
+ val intent = Intent(this, MainActivity::class.java)
+ val pendingIntent =
+ PendingIntentCompat.getActivity(this, 0, intent, 0, false)
+
+ val activeDownloads = resources.getQuantityString(R.plurals.downloads_active, 0).format(0)
+ val activeQueue = resources.getQuantityString(R.plurals.downloads_queued, 0).format(0)
+
+ NotificationCompat.Builder(this, DOWNLOAD_QUEUE_CHANNEL_ID)
+ .setOngoing(true) // Make it persistent
+ .setAutoCancel(false)
+ .setColorized(false)
+ .setOnlyAlertOnce(true)
+ .setSilent(true)
+ .setShowWhen(false)
+ // If low priority then the notification might not show :(
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setColor(this.colorFromAttribute(R.attr.colorPrimary))
+ .setContentText(activeDownloads)
+ .setSubText(activeQueue)
+ .setContentIntent(pendingIntent)
+ .setSmallIcon(R.drawable.download_icon_load)
+ }
+
+
+ private fun updateNotification(context: Context, downloads: Int, queued: Int) {
+ val activeDownloads =
+ resources.getQuantityString(R.plurals.downloads_active, downloads).format(downloads)
+ val activeQueue =
+ resources.getQuantityString(R.plurals.downloads_queued, queued).format(queued)
+
+ val newNotification = baseNotification
+ .setContentText(activeDownloads)
+ .setSubText(activeQueue)
+ .build()
+
+ NotificationManagerCompat.from(context)
+ .notify(DOWNLOAD_QUEUE_NOTIFICATION_ID, newNotification)
+ }
+
+ override fun onCreate() {
+ isRunning = true
+ Log.d(TAG, "Download queue service started.")
+ this.createNotificationChannel(
+ DOWNLOAD_QUEUE_CHANNEL_ID,
+ DOWNLOAD_QUEUE_CHANNEL_NAME,
+ DOWNLOAD_QUEUE_CHANNEL_DESCRIPTION
+ )
+ if (SDK_INT >= 29) {
+ startForeground(
+ DOWNLOAD_QUEUE_NOTIFICATION_ID,
+ baseNotification.build(),
+ FOREGROUND_SERVICE_TYPE_DATA_SYNC
+ )
+ } else {
+ startForeground(DOWNLOAD_QUEUE_NOTIFICATION_ID, baseNotification.build())
+ }
+
+ val context = this.applicationContext
+
+ ioSafe {
+ while (isRunning && (DownloadQueueManager.queue.isNotEmpty() || downloadInstances.isNotEmpty())) {
+ // Remove any completed or failed works
+ downloadInstances =
+ downloadInstances.filterNot { it.isCompleted || it.isFailed }.toMutableList()
+
+ val maxDownloads = VideoDownloadManager.maxConcurrentDownloads(context)
+ val currentDownloads = downloadInstances.size
+
+ val newDownloads = minOf(
+ // Cannot exceed the max downloads
+ maxOf(0, maxDownloads - currentDownloads),
+ // Cannot start more downloads than the queue size
+ DownloadQueueManager.queue.size
+ )
+
+ repeat(newDownloads) {
+ val downloadInstance = DownloadQueueManager.popQueue(context) ?: return@repeat
+ downloadInstance.startDownload()
+ downloadInstances.add(downloadInstance)
+ }
+
+ // The downloads actually displayed to the user with a notification
+ val currentVisualDownloads =
+ VideoDownloadManager.currentDownloads.size + downloadInstances.count {
+ VideoDownloadManager.currentDownloads.contains(it.downloadQueueWrapper.id)
+ .not()
+ }
+ // Just the queue
+ val currentVisualQueue = DownloadQueueManager.queue.size
+
+ updateNotification(context, currentVisualDownloads, currentVisualQueue)
+
+ // Arbitrary delay to prevent hogging the CPU, decrease to make the queue feel slightly more responsive
+ delay(500)
+ }
+ stopSelf()
+ }
+ }
+
+ override fun onDestroy() {
+ Log.d(TAG, "Download queue service stopped.")
+ isRunning = false
+ super.onDestroy()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ return START_STICKY // We want the service restarted if its killed
+ }
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ override fun onTimeout(reason: Int) {
+ stopSelf()
+ Log.e(TAG, "Service stopped due to timeout: $reason")
+ }
+
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt
index fc31c1f3e0..242f081296 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt
@@ -22,7 +22,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
-import com.lagradost.cloudstream3.utils.VideoDownloadManager.getImageBitmapFromUrl
+import com.lagradost.cloudstream3.utils.downloader.DownloadUtils.getImageBitmapFromUrl
import kotlinx.coroutines.withTimeoutOrNull
import java.util.concurrent.TimeUnit
diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/VideoDownloadService.kt b/app/src/main/java/com/lagradost/cloudstream3/services/VideoDownloadService.kt
index 6151a0edd2..d63b18cdc9 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/services/VideoDownloadService.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/services/VideoDownloadService.kt
@@ -2,12 +2,13 @@ package com.lagradost.cloudstream3.services
import android.app.Service
import android.content.Intent
import android.os.IBinder
-import com.lagradost.cloudstream3.utils.VideoDownloadManager
+import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
+/** Handle notification actions such as pause/resume downloads */
class VideoDownloadService : Service() {
private val downloadScope = CoroutineScope(Dispatchers.Default)
@@ -42,19 +43,3 @@ class VideoDownloadService : Service() {
super.onDestroy()
}
}
-// override fun onHandleIntent(intent: Intent?) {
-// if (intent != null) {
-// val id = intent.getIntExtra("id", -1)
-// val type = intent.getStringExtra("type")
-// if (id != -1 && type != null) {
-// val state = when (type) {
-// "resume" -> VideoDownloadManager.DownloadActionType.Resume
-// "pause" -> VideoDownloadManager.DownloadActionType.Pause
-// "stop" -> VideoDownloadManager.DownloadActionType.Stop
-// else -> return
-// }
-// VideoDownloadManager.downloadEvent.invoke(Pair(id, state))
-// }
-// }
-// }
-//}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt
index a0e5cabc46..7321406e49 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt
@@ -19,7 +19,7 @@ import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull
import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.ImageLoader.loadImage
-import com.lagradost.cloudstream3.utils.VideoDownloadHelper
+import com.lagradost.cloudstream3.utils.downloader.DownloadObjects
const val DOWNLOAD_ACTION_PLAY_FILE = 0
const val DOWNLOAD_ACTION_DELETE_FILE = 1
@@ -34,22 +34,22 @@ const val DOWNLOAD_ACTION_LOAD_RESULT = 1
sealed class VisualDownloadCached {
abstract val currentBytes: Long
abstract val totalBytes: Long
- abstract val data: VideoDownloadHelper.DownloadCached
+ abstract val data: DownloadObjects.DownloadCached
abstract var isSelected: Boolean
data class Child(
override val currentBytes: Long,
override val totalBytes: Long,
- override val data: VideoDownloadHelper.DownloadEpisodeCached,
+ override val data: DownloadObjects.DownloadEpisodeCached,
override var isSelected: Boolean,
) : VisualDownloadCached()
data class Header(
override val currentBytes: Long,
override val totalBytes: Long,
- override val data: VideoDownloadHelper.DownloadHeaderCached,
+ override val data: DownloadObjects.DownloadHeaderCached,
override var isSelected: Boolean,
- val child: VideoDownloadHelper.DownloadEpisodeCached?,
+ val child: DownloadObjects.DownloadEpisodeCached?,
val currentOngoingDownloads: Int,
val totalDownloads: Int,
) : VisualDownloadCached()
@@ -57,12 +57,12 @@ sealed class VisualDownloadCached {
data class DownloadClickEvent(
val action: Int,
- val data: VideoDownloadHelper.DownloadEpisodeCached
+ val data: DownloadObjects.DownloadEpisodeCached
)
data class DownloadHeaderClickEvent(
val action: Int,
- val data: VideoDownloadHelper.DownloadHeaderCached
+ val data: DownloadObjects.DownloadHeaderCached
)
class DownloadAdapter(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt
index 83e0d01678..682a691e85 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt
@@ -14,12 +14,14 @@ import com.lagradost.cloudstream3.ui.player.ExtractorUri
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull
import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
+import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.SnackbarHelper.showSnackbar
import com.lagradost.cloudstream3.utils.UIHelper.navigate
-import com.lagradost.cloudstream3.utils.VideoDownloadHelper
-import com.lagradost.cloudstream3.utils.VideoDownloadManager
+import com.lagradost.cloudstream3.utils.downloader.DownloadObjects
+import com.lagradost.cloudstream3.utils.downloader.DownloadQueueManager
+import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager
import kotlinx.coroutines.MainScope
object DownloadButtonSetup {
@@ -82,7 +84,7 @@ object DownloadButtonSetup {
} else {
val pkg = VideoDownloadManager.getDownloadResumePackage(ctx, id)
if (pkg != null) {
- VideoDownloadManager.downloadFromResumeUsingWorker(ctx, pkg)
+ DownloadQueueManager.addToQueue(pkg.toWrapper())
} else {
VideoDownloadManager.downloadEvent.invoke(
Pair(click.data.id, VideoDownloadManager.DownloadActionType.Resume)
@@ -95,7 +97,7 @@ object DownloadButtonSetup {
DOWNLOAD_ACTION_LONG_CLICK -> {
activity?.let { act ->
val length =
- VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(
+ VideoDownloadManager.getDownloadFileInfo(
act,
click.data.id
)?.fileLength
@@ -112,22 +114,25 @@ object DownloadButtonSetup {
DOWNLOAD_ACTION_PLAY_FILE -> {
activity?.let { act ->
- val parent = getKey(
+ val parent = getKey(
DOWNLOAD_HEADER_CACHE,
click.data.parentId.toString()
) ?: return
val episodes = getKeys(DOWNLOAD_EPISODE_CACHE)
?.mapNotNull {
- getKey(it)
+ getKey(it)
}
?.filter { it.parentId == click.data.parentId }
val items = mutableListOf()
- val allRelevantEpisodes = episodes?.sortedWith(compareBy { it.season ?: 0 }.thenBy { it.episode })
+ val allRelevantEpisodes =
+ episodes?.sortedWith(compareBy {
+ it.season ?: 0
+ }.thenBy { it.episode })
allRelevantEpisodes?.forEach {
- val keyInfo = getKey(
+ val keyInfo = getKey(
VideoDownloadManager.KEY_DOWNLOAD_INFO,
it.id.toString()
) ?: return@forEach
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt
index 2010fe7e36..7438c63413 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt
@@ -24,6 +24,7 @@ import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding
@@ -222,6 +223,10 @@ class DownloadFragment : Fragment() {
setOnClickListener { showStreamInputDialog(it.context) }
}
+ downloadQueueButton.setOnClickListener {
+ findNavController().navigate(R.id.action_navigation_global_to_navigation_download_queue)
+ }
+
downloadStreamButtonTv.isFocusableInTouchMode = isLayout(TV)
downloadAppbar.isFocusableInTouchMode = isLayout(TV)
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt
index 137f1355e2..a1f8c75e8d 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt
@@ -20,9 +20,9 @@ import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.DataStore.getFolderName
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.getKeys
-import com.lagradost.cloudstream3.utils.VideoDownloadHelper
-import com.lagradost.cloudstream3.utils.VideoDownloadManager.deleteFilesAndUpdateSettings
-import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadFileInfoAndUpdateSettings
+import com.lagradost.cloudstream3.utils.downloader.DownloadObjects
+import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.deleteFilesAndUpdateSettings
+import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.getDownloadFileInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -119,14 +119,14 @@ class DownloadViewModel : ViewModel() {
fun updateHeaderList(context: Context) = viewModelScope.launchSafe {
val visual = withContext(Dispatchers.IO) {
val children = context.getKeys(DOWNLOAD_EPISODE_CACHE)
- .mapNotNull { context.getKey(it) }
+ .mapNotNull { context.getKey(it) }
.distinctBy { it.id } // Remove duplicates
val (totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads) =
calculateDownloadStats(context, children)
val cached = context.getKeys(DOWNLOAD_HEADER_CACHE)
- .mapNotNull { context.getKey(it) }
+ .mapNotNull { context.getKey(it) }
createVisualDownloadList(
context, cached, totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads
@@ -142,7 +142,7 @@ class DownloadViewModel : ViewModel() {
private fun calculateDownloadStats(
context: Context,
- children: List
+ children: List
): Triple