From 3b40ba7e02a2c19add28f1475e112a9a7854f914 Mon Sep 17 00:00:00 2001 From: deniscerri <64997243+deniscerri@users.noreply.github.com> Date: Sat, 22 Mar 2025 21:37:08 +0100 Subject: [PATCH] fix downloads not continuing in the queue --- app/src/main/AndroidManifest.xml | 2 +- .../ytdl/database/dao/DownloadDao.kt | 19 +- .../database/repository/DownloadRepository.kt | 7 - .../receiver/CancelScheduleAlarmReceiver.kt | 3 - .../downloadcard/ResultCardDetailsDialog.kt | 3 - .../ui/downloads/ActiveDownloadsFragment.kt | 38 - .../more/settings/DownloadSettingsFragment.kt | 2 - .../terminal/TerminalDownloadsListFragment.kt | 1 - .../ytdl/ui/more/terminal/TerminalFragment.kt | 5 - .../ytdl/util/extractors/YTDLPUtil.kt | 58 +- .../ytdl/work/AutoGenerateWebPoTokenWorker.kt | 2 +- .../ytdl/work/CleanUpLeftoverDownloads.kt | 4 +- .../deniscerri/ytdl/work/DownloadWorker.kt | 663 ++++++++++-------- .../ytdl/work/MoveCacheFilesWorker.kt | 4 +- .../ytdl/work/ObserveSourceWorker.kt | 4 +- .../ytdl/work/TerminalDownloadWorker.kt | 4 +- .../work/UpdateMultipleDownloadsDataWorker.kt | 4 +- .../UpdateMultipleDownloadsFormatsWorker.kt | 4 +- .../ytdl/work/WorkManagerExtensions.kt | 34 + 19 files changed, 446 insertions(+), 415 deletions(-) create mode 100644 app/src/main/java/com/deniscerri/ytdl/work/WorkManagerExtensions.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3efd5cc8..270dd72f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -503,7 +503,7 @@ + android:foregroundServiceType="dataSync"/> \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt b/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt index 181b91f6..f39520f8 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt @@ -107,19 +107,24 @@ interface DownloadDao { @Query(""" SELECT * FROM downloads - WHERE status in ('Queued', 'Scheduled') AND downloadStartTime <= :currentTime + WHERE status in ('Active', 'Queued', 'Scheduled') AND downloadStartTime <= :currentTime ORDER BY downloadStartTime, id - LIMIT 20 + LIMIT 10 """) - fun getQueuedScheduledDownloadsUntil(currentTime: Long) : Flow> + fun getActiveQueuedScheduledDownloadsUntil(currentTime: Long) : Flow> @Query(""" SELECT * FROM downloads - WHERE id in (:priorityItems) AND status in ('Queued', 'Scheduled') AND downloadStartTime <= :currentTime - ORDER BY downloadStartTime, id - LIMIT 20 + WHERE status in ('Active','Queued', 'Scheduled') AND downloadStartTime <= :currentTime + ORDER BY + CASE + WHEN id in (:priorityItems) THEN 0 + ELSE 1 + END, + downloadStartTime, id + LIMIT 10 """) - fun getQueuedScheduledDownloadsUntilWithPriority(currentTime: Long, priorityItems: List) : Flow> + fun getActiveQueuedScheduledDownloadsUntilWithPriority(currentTime: Long, priorityItems: List) : Flow> @Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY downloadStartTime, id") fun getQueuedDownloadsList() : List diff --git a/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt b/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt index 5d89e3c3..5894c125 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt @@ -3,11 +3,7 @@ package com.deniscerri.ytdl.database.repository import android.annotation.SuppressLint import android.content.Context import android.net.ConnectivityManager -import android.os.Handler -import android.os.Looper import android.text.format.DateFormat -import android.widget.Toast -import androidx.core.os.postDelayed import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.preference.PreferenceManager @@ -16,9 +12,7 @@ import androidx.work.Data import androidx.work.ExistingWorkPolicy import androidx.work.NetworkType import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkInfo import androidx.work.WorkManager -import androidx.work.await import com.deniscerri.ytdl.App import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.dao.DownloadDao @@ -31,7 +25,6 @@ import com.deniscerri.ytdl.work.AlarmScheduler import com.deniscerri.ytdl.work.DownloadWorker import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map import java.io.File import java.text.SimpleDateFormat import java.util.Locale diff --git a/app/src/main/java/com/deniscerri/ytdl/receiver/CancelScheduleAlarmReceiver.kt b/app/src/main/java/com/deniscerri/ytdl/receiver/CancelScheduleAlarmReceiver.kt index 590bf6eb..36d00d42 100644 --- a/app/src/main/java/com/deniscerri/ytdl/receiver/CancelScheduleAlarmReceiver.kt +++ b/app/src/main/java/com/deniscerri/ytdl/receiver/CancelScheduleAlarmReceiver.kt @@ -3,14 +3,11 @@ package com.deniscerri.ytdl.receiver import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import androidx.preference.PreferenceManager import androidx.work.Constraints import androidx.work.ExistingWorkPolicy -import androidx.work.NetworkType import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import com.deniscerri.ytdl.work.CancelScheduledDownloadWorker -import com.deniscerri.ytdl.work.DownloadWorker import java.util.concurrent.TimeUnit class CancelScheduleAlarmReceiver : BroadcastReceiver() { diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ResultCardDetailsDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ResultCardDetailsDialog.kt index 333d0334..2aecdb96 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ResultCardDetailsDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ResultCardDetailsDialog.kt @@ -12,7 +12,6 @@ import android.graphics.Color import android.net.Uri import android.os.Build import android.os.Bundle -import android.os.Environment import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.View @@ -20,8 +19,6 @@ import android.view.ViewGroup import android.view.Window import android.widget.Button import android.widget.TextView -import android.widget.Toast -import androidx.core.net.toUri import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/ActiveDownloadsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/ActiveDownloadsFragment.kt index b16c20e7..dd50f196 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/ActiveDownloadsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/ActiveDownloadsFragment.kt @@ -1,80 +1,42 @@ package com.deniscerri.ytdl.ui.downloads -import android.animation.AnimatorSet import android.annotation.SuppressLint import android.app.Activity -import android.content.DialogInterface import android.content.SharedPreferences import android.content.res.ColorStateList -import android.graphics.Canvas -import android.graphics.Color import android.os.Bundle import android.util.TypedValue import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.view.animation.AccelerateDecelerateInterpolator -import android.widget.LinearLayout -import android.widget.ListView import android.widget.RelativeLayout import android.widget.TextView -import android.widget.Toast import androidx.annotation.OptIn -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ActionMode -import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat -import androidx.core.content.res.ResourcesCompat -import androidx.core.os.bundleOf import androidx.core.view.isVisible -import androidx.core.view.marginBottom -import androidx.core.view.setMargins -import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView -import androidx.room.util.getColumnIndexOrThrow import androidx.work.WorkManager -import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.ui.adapter.ActiveDownloadAdapter -import com.deniscerri.ytdl.ui.adapter.QueuedDownloadAdapter -import com.deniscerri.ytdl.util.Extensions.enableFastScroll import com.deniscerri.ytdl.util.Extensions.forceFastScrollMode -import com.deniscerri.ytdl.util.Extensions.toListString -import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.NotificationUtil -import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.work.DownloadWorker -import com.google.android.material.badge.BadgeDrawable -import com.google.android.material.badge.BadgeUtils import com.google.android.material.badge.ExperimentalBadgeUtils -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.button.MaterialButton -import com.google.android.material.card.MaterialCardView -import com.google.android.material.color.MaterialColors -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.progressindicator.LinearProgressIndicator -import com.google.android.material.snackbar.Snackbar import com.yausername.youtubedl_android.YoutubeDL -import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/DownloadSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/DownloadSettingsFragment.kt index 6f7f3ad5..7adccfab 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/DownloadSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/DownloadSettingsFragment.kt @@ -6,8 +6,6 @@ import android.os.Build import android.os.Bundle import android.provider.Settings import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.interaction.DragInteraction -import androidx.core.content.edit import androidx.navigation.fragment.findNavController import androidx.preference.EditTextPreference import androidx.preference.ListPreference diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/terminal/TerminalDownloadsListFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/terminal/TerminalDownloadsListFragment.kt index 75bfcff7..d759960b 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/terminal/TerminalDownloadsListFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/terminal/TerminalDownloadsListFragment.kt @@ -15,7 +15,6 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView -import androidx.work.WorkManager import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.models.TerminalItem import com.deniscerri.ytdl.database.viewmodel.TerminalViewModel diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/terminal/TerminalFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/terminal/TerminalFragment.kt index 7f643383..dc157150 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/terminal/TerminalFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/terminal/TerminalFragment.kt @@ -40,20 +40,15 @@ import com.deniscerri.ytdl.util.Extensions.setCustomTextSize import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.UiUtil -import com.deniscerri.ytdl.work.DownloadWorker import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton -import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.slider.Slider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode import kotlin.properties.Delegates diff --git a/app/src/main/java/com/deniscerri/ytdl/util/extractors/YTDLPUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/extractors/YTDLPUtil.kt index ad992f1e..569d3c0e 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/extractors/YTDLPUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/extractors/YTDLPUtil.kt @@ -1280,22 +1280,24 @@ class YTDLPUtil(private val context: Context, private val commandTemplateDao: Co val poTokens = mutableListOf() val configuredPlayerClientsRaw = sharedPreferences.getString("youtube_player_clients", "[]")!! - val configuredPlayerClients = Gson().fromJson(configuredPlayerClientsRaw, Array::class.java).toMutableList() + kotlin.runCatching { + val configuredPlayerClients = Gson().fromJson(configuredPlayerClientsRaw, Array::class.java).toMutableList() - for (value in configuredPlayerClients) { - if (value.enabled) { - if (!value.useOnlyPoToken) { - playerClients.add(value.playerClient) - } + for (value in configuredPlayerClients) { + if (value.enabled) { + if (!value.useOnlyPoToken) { + playerClients.add(value.playerClient) + } - var canUsePoToken = true - if (value.urlRegex.isNotEmpty() && url != null) { - canUsePoToken = value.urlRegex.any { url.matches(it.toRegex()) } - } + var canUsePoToken = true + if (value.urlRegex.isNotEmpty() && url != null) { + canUsePoToken = value.urlRegex.any { url.matches(it.toRegex()) } + } - if (canUsePoToken) { - value.poTokens.forEach { pt -> - poTokens.add("${value.playerClient}.${pt.context}+${pt.token}") + if (canUsePoToken) { + value.poTokens.forEach { pt -> + poTokens.add("${value.playerClient}.${pt.context}+${pt.token}") + } } } } @@ -1308,24 +1310,26 @@ class YTDLPUtil(private val context: Context, private val commandTemplateDao: Co } val generatedPoTokensRaw = sharedPreferences.getString("youtube_generated_po_tokens", "[]") - val generatedPoTokens = Gson().fromJson(generatedPoTokensRaw,Array::class.java).toMutableList() - if (generatedPoTokens.isNotEmpty()) { - for (value in generatedPoTokens) { - if (value.enabled) { - for (cl in value.clients) { - playerClients.add(cl) - for (pt in value.poTokens) { - if (pt.token.isNotBlank()) { - poTokens.add("${cl}.${pt.context}+${pt.token}") + kotlin.runCatching { + val generatedPoTokens = Gson().fromJson(generatedPoTokensRaw,Array::class.java).toMutableList() + if (generatedPoTokens.isNotEmpty()) { + for (value in generatedPoTokens) { + if (value.enabled) { + for (cl in value.clients) { + playerClients.add(cl) + for (pt in value.poTokens) { + if (pt.token.isNotBlank()) { + poTokens.add("${cl}.${pt.context}+${pt.token}") + } } } - } - if (dataSyncID.isBlank() && value.useVisitorData) { - extractorArgs.add("player_skip=webpage,configs") - extractorArgs.add("visitor_data=${value.visitorData}") - } + if (dataSyncID.isBlank() && value.useVisitorData) { + extractorArgs.add("player_skip=webpage,configs") + extractorArgs.add("visitor_data=${value.visitorData}") + } + } } } } diff --git a/app/src/main/java/com/deniscerri/ytdl/work/AutoGenerateWebPoTokenWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/AutoGenerateWebPoTokenWorker.kt index 3019f431..724e84bf 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/AutoGenerateWebPoTokenWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/AutoGenerateWebPoTokenWorker.kt @@ -3,7 +3,7 @@ package com.deniscerri.ytdl.work import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC import android.os.Build import android.os.Environment import android.os.Handler diff --git a/app/src/main/java/com/deniscerri/ytdl/work/CleanUpLeftoverDownloads.kt b/app/src/main/java/com/deniscerri/ytdl/work/CleanUpLeftoverDownloads.kt index 5834a096..9ca46769 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/CleanUpLeftoverDownloads.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/CleanUpLeftoverDownloads.kt @@ -1,7 +1,7 @@ package com.deniscerri.ytdl.work import android.content.Context -import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC import android.os.Build import androidx.work.CoroutineWorker import androidx.work.ForegroundInfo @@ -26,7 +26,7 @@ class CleanUpLeftoverDownloads( val notification = notificationUtil.createDeletingLeftoverDownloadsNotification() if (Build.VERSION.SDK_INT > 33) { - setForegroundAsync(ForegroundInfo(id, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)) + setForegroundAsync(ForegroundInfo(id, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)) }else{ setForegroundAsync(ForegroundInfo(id, notification)) } diff --git a/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt index 7f5833d1..091b534e 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt @@ -4,7 +4,8 @@ import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE +import android.content.SharedPreferences +import android.content.pm.ServiceInfo import android.content.res.Configuration import android.content.res.Resources import android.os.Build @@ -16,7 +17,6 @@ import android.widget.Toast import androidx.preference.PreferenceManager import androidx.work.CoroutineWorker import androidx.work.ForegroundInfo -import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.WorkerParameters import com.afollestad.materialdialogs.utils.MDUtil.getStringArray @@ -24,6 +24,10 @@ import com.deniscerri.ytdl.App import com.deniscerri.ytdl.MainActivity import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.DBManager +import com.deniscerri.ytdl.database.dao.CommandTemplateDao +import com.deniscerri.ytdl.database.dao.DownloadDao +import com.deniscerri.ytdl.database.dao.HistoryDao +import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.HistoryItem import com.deniscerri.ytdl.database.models.LogItem import com.deniscerri.ytdl.database.repository.DownloadRepository @@ -36,11 +40,23 @@ import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.extractors.YTDLPUtil import com.yausername.youtubedl_android.YoutubeDL import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus import java.io.File @@ -48,45 +64,68 @@ import java.security.MessageDigest import java.util.Locale -class DownloadWorker( - private val context: Context, - workerParams: WorkerParameters -) : CoroutineWorker(context, workerParams) { - @OptIn(ExperimentalStdlibApi::class) +class DownloadWorker(private val context: Context,workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) { + private lateinit var dao : DownloadDao + private lateinit var notificationUtil: NotificationUtil + private lateinit var dbManager: DBManager + private lateinit var historyDao: HistoryDao + private lateinit var commandTemplateDao: CommandTemplateDao + private lateinit var logRepo: LogRepository + private lateinit var resultRepo: ResultRepository + private lateinit var ytdlpUtil: YTDLPUtil + private lateinit var alarmScheduler : AlarmScheduler + private lateinit var sharedPreferences : SharedPreferences + private lateinit var resources: Resources + private lateinit var workManager: WorkManager + private lateinit var eventBus: EventBus + + private lateinit var queueState : Flow> + + private val handler = Handler(Looper.getMainLooper()) + + private val openDownloadQueue: PendingIntent = PendingIntent.getActivity( + context, + 1000000000, + Intent(context, MainActivity::class.java).run { + action = Intent.ACTION_VIEW + putExtra("destination", "Queue") + }, + PendingIntent.FLAG_IMMUTABLE + ) + + override suspend fun getForegroundInfo(): ForegroundInfo { + val workNotif = NotificationUtil(App.instance).createDefaultWorkerNotification() + + return ForegroundInfo( + 1000000000, + workNotif, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + } else { + 0 + }, + ) + } + + private fun cancelSelf() { + workManager.cancelWorkById(this@DownloadWorker.id) + } + + @SuppressLint("RestrictedApi") override suspend fun doWork(): Result { - if (isStopped) return Result.success() - val workManager = WorkManager.getInstance(context) - val currentWork = withContext(Dispatchers.IO) { - workManager.getWorkInfosByTag("download").get() - } - if (currentWork.count{it.state == WorkInfo.State.RUNNING} > 1) { - return Result.success() - } - - val notificationUtil = NotificationUtil(App.instance) - val dbManager = DBManager.getInstance(context) - val dao = dbManager.downloadDao - val historyDao = dbManager.historyDao - val commandTemplateDao = dbManager.commandTemplateDao - val logRepo = LogRepository(dbManager.logDao) - val resultRepo = ResultRepository(dbManager.resultDao, commandTemplateDao, context) - val ytdlpUtil = YTDLPUtil(context, commandTemplateDao) - val handler = Handler(Looper.getMainLooper()) - val alarmScheduler = AlarmScheduler(context) - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) - val time = System.currentTimeMillis() + 6000 - val priorityItemIDs = (inputData.getLongArray("priority_item_ids") ?: longArrayOf()).toMutableList() - val continueAfterPriorityIds = inputData.getBoolean("continue_after_priority_ids", true) - val queuedItems = if (priorityItemIDs.isEmpty()) { - dao.getQueuedScheduledDownloadsUntil(time) - }else { - dao.getQueuedScheduledDownloadsUntilWithPriority(time, priorityItemIDs) - } - - // this is needed for observe sources call, so it wont create result items - // [removed] - //val createResultItem = inputData.getBoolean("createResultItem", true) + //init + notificationUtil = NotificationUtil(App.instance) + dbManager = DBManager.getInstance(context) + dao = dbManager.downloadDao + historyDao = dbManager.historyDao + commandTemplateDao = dbManager.commandTemplateDao + logRepo = LogRepository(dbManager.logDao) + resultRepo = ResultRepository(dbManager.resultDao, commandTemplateDao, context) + ytdlpUtil = YTDLPUtil(context, commandTemplateDao) + alarmScheduler = AlarmScheduler(context) + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + eventBus = EventBus.getDefault() val confTmp = Configuration(context.resources.configuration) val locale = if (Build.VERSION.SDK_INT < 33) { @@ -100,240 +139,265 @@ class DownloadWorker( } confTmp.setLocale(locale) val metrics = DisplayMetrics() - val resources = Resources(context.assets, metrics, confTmp) + resources = Resources(context.assets, metrics, confTmp) - val openQueueIntent = Intent(context, MainActivity::class.java) - openQueueIntent.setAction(Intent.ACTION_VIEW) - openQueueIntent.putExtra("destination", "Queue") - val openDownloadQueue = PendingIntent.getActivity( - context, - 1000000000, - openQueueIntent, - PendingIntent.FLAG_IMMUTABLE - ) + workManager = WorkManager.getInstance(context) + if (workManager.isRunning("download")) return Result.Failure() - val workNotif = notificationUtil.createDefaultWorkerNotification() - val notificationID = 1000000000 - if (Build.VERSION.SDK_INT > 33) { - setForegroundAsync(ForegroundInfo(notificationID, workNotif, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)) - }else{ - setForegroundAsync(ForegroundInfo(notificationID, workNotif)) + setForegroundSafely() + + val priorityItemIDs = (inputData.getLongArray("priority_item_ids") ?: longArrayOf()).toMutableList() + val continueAfterPriorityIds = inputData.getBoolean("continue_after_priority_ids", true) + val time = System.currentTimeMillis() + 6000 + queueState = if (priorityItemIDs.isEmpty()) { + dao.getActiveQueuedScheduledDownloadsUntil(time) + }else { + dao.getActiveQueuedScheduledDownloadsUntilWithPriority(time, priorityItemIDs) } - queuedItems.collectLatest { items -> - if (this@DownloadWorker.isStopped) return@collectLatest + val downloadJobs = mutableMapOf() - runningYTDLInstances.clear() - val activeDownloads = dao.getActiveDownloadsList() - activeDownloads.forEach { - runningYTDLInstances.add(it.id) - } + queueState.collectLatest { queue -> + val runningCount = queue.asSequence() + .filter { it.status == DownloadRepository.Status.Active.toString() } + .count() - val running = ArrayList(runningYTDLInstances) - val useScheduler = sharedPreferences.getBoolean("use_scheduler", false) - if (items.isEmpty() && running.isEmpty()) { - WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id) + val queueItems = queue.asSequence() + .filter { it.status != DownloadRepository.Status.Active.toString() } + + val queueCount = queueItems.count() + if (queueCount == 0 && runningCount == 0) { + cancelSelf() return@collectLatest } + val useScheduler = sharedPreferences.getBoolean("use_scheduler", false) if (useScheduler){ - if (items.none{it.downloadStartTime > 0L} && running.isEmpty() && !alarmScheduler.isDuringTheScheduledTime()) { - WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id) + if (queueItems.none{ it.downloadStartTime > 0L } && runningCount == 0 && !alarmScheduler.isDuringTheScheduledTime()) { + cancelSelf() return@collectLatest } } - if (priorityItemIDs.isEmpty() && !continueAfterPriorityIds) { - WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id) - return@collectLatest - } + val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - runningCount + if (concurrentDownloads > 0) { + //free spots are open + if (priorityItemIDs.isNotEmpty()) { + if (!continueAfterPriorityIds && queueItems.none { priorityItemIDs.contains(it.id) }) { + //dont queue any more items if only required priority items + cancelSelf() + return@collectLatest + } + } - val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - running.size - val eligibleDownloads = if (priorityItemIDs.isNotEmpty()) { - val tmp = priorityItemIDs.take(concurrentDownloads) - items.filter { it.id !in running && tmp.contains(it.id) } - }else{ - items.take(concurrentDownloads).filter { it.id !in running } - } + // Use supervisorScope to cancel child jobs when the downloader job is cancelled + supervisorScope { + val queuedDownloads = queueItems + .toList() + .take(concurrentDownloads) - eligibleDownloads.forEach{downloadItem -> - val notification = notificationUtil.createDownloadServiceNotification(openDownloadQueue, downloadItem.title.ifEmpty { downloadItem.url }) - notificationUtil.notify(downloadItem.id.toInt(), notification) - - CoroutineScope(Dispatchers.IO).launch { - val writtenPath = downloadItem.format.format_note.contains("-P ") - val noCache = writtenPath || (!sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()) - - val request = ytdlpUtil.buildYoutubeDLRequest(downloadItem) - downloadItem.status = DownloadRepository.Status.Active.toString() - CoroutineScope(Dispatchers.IO).launch { - delay(1500) - //update item if its incomplete - resultRepo.updateDownloadItem(downloadItem)?.apply { - val status = dao.checkStatus(this.id) - if (status == DownloadRepository.Status.Active){ - dao.updateWithoutUpsert(this) - } - } + val queuedIDs = queuedDownloads.map { it.id } + val downloadJobsToStop = downloadJobs.filter { it.key !in queuedIDs } + downloadJobsToStop.forEach { (download, job) -> + job.cancel() + downloadJobs.remove(download) } - val cacheDir = FileUtil.getCachePath(context) - val tempFileDir = File(cacheDir, downloadItem.id.toString()) - tempFileDir.delete() - tempFileDir.mkdirs() + val downloadsToStart = queuedDownloads.filter { it.id !in downloadJobs } + downloadsToStart.forEach { download -> + downloadJobs[download.id] = launchDownloadJob(download) + } + } + } - val downloadLocation = downloadItem.downloadPath - val keepCache = sharedPreferences.getBoolean("keep_cache", false) - val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !downloadItem.incognito + } + + return Result.Success() + } - val commandString = ytdlpUtil.parseYTDLRequestString(request) - val initialLogDetails = "Downloading:\n" + - "Title: ${downloadItem.title}\n" + - "URL: ${downloadItem.url}\n" + - "Type: ${downloadItem.type}\n" + - "Command:\n$commandString \n\n" - val logString = StringBuilder(initialLogDetails) - val logItem = LogItem( - 0, - downloadItem.title.ifBlank { downloadItem.url }, - logString.toString(), - downloadItem.format, - downloadItem.type, - System.currentTimeMillis(), + + @OptIn(DelicateCoroutinesApi::class) + private fun launchIO(block: suspend CoroutineScope.() -> Unit): Job = + GlobalScope.launch(Dispatchers.IO, CoroutineStart.DEFAULT, block) + + @OptIn(ExperimentalStdlibApi::class) + private fun launchDownloadJob(downloadItem: DownloadItem) = launchIO { + val notification = notificationUtil.createDownloadServiceNotification(openDownloadQueue, downloadItem.title.ifEmpty { downloadItem.url }) + notificationUtil.notify(downloadItem.id.toInt(), notification) + + val writtenPath = downloadItem.format.format_note.contains("-P ") + val noCache = writtenPath || (!sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()) + + val request = ytdlpUtil.buildYoutubeDLRequest(downloadItem) + + val cacheDir = FileUtil.getCachePath(context) + val tempFileDir = File(cacheDir, downloadItem.id.toString()) + tempFileDir.delete() + tempFileDir.mkdirs() + + val downloadLocation = downloadItem.downloadPath + val keepCache = sharedPreferences.getBoolean("keep_cache", false) + val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !downloadItem.incognito + + val commandString = ytdlpUtil.parseYTDLRequestString(request) + val initialLogDetails = "Downloading:\n" + + "Title: ${downloadItem.title}\n" + + "URL: ${downloadItem.url}\n" + + "Type: ${downloadItem.type}\n" + + "Command:\n$commandString \n\n" + val logString = StringBuilder(initialLogDetails) + val logItem = LogItem( + 0, + downloadItem.title.ifBlank { downloadItem.url }, + logString.toString(), + downloadItem.format, + downloadItem.type, + System.currentTimeMillis(), + ) + + + runBlocking { + if (logDownloads) logItem.id = logRepo.insert(logItem) + downloadItem.logID = logItem.id + downloadItem.status = DownloadRepository.Status.Active.toString() + dao.update(downloadItem) + + CoroutineScope(Dispatchers.IO).launch { + delay(1500) + //update item if its incomplete + resultRepo.updateDownloadItem(downloadItem)?.apply { + val status = dao.checkStatus(id) + if (status == DownloadRepository.Status.Active){ + dao.updateWithoutUpsert(this) + } + } + } + } + + runCatching { + YoutubeDL.getInstance().destroyProcessById(downloadItem.id.toString()) + YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line -> + eventBus.post(WorkerProgress(progress.toInt(), line, downloadItem.id, downloadItem.logID)) + val title: String = downloadItem.title.ifEmpty { downloadItem.url } + notificationUtil.updateDownloadNotification( + downloadItem.id.toInt(), + line, progress.toInt(), 0, title, + NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID + ) + CoroutineScope(Dispatchers.IO).launch { + if (logDownloads) { + logRepo.update(line, logItem.id) + logString.append("$line\n") + } + } + } + }.onSuccess { + resultRepo.updateDownloadItem(downloadItem)?.apply { + dao.updateWithoutUpsert(this) + } + //val wasQuickDownloaded = resultDao.getCountInt() == 0 + runBlocking { + var finalPaths = mutableListOf() + + if (noCache){ + eventBus.post(WorkerProgress(100, "Scanning Files", downloadItem.id, downloadItem.logID)) + val outputSequence = it.out.split("\n") + finalPaths = + outputSequence.asSequence() + .filter { it.startsWith("'/storage") } + .map { it.removeSuffix("\n") } + .map { it.removeSurrounding("'", "'") } + .toMutableList() + + finalPaths.addAll( + outputSequence.asSequence() + .filter { it.startsWith("[SplitChapters]") && it.contains("Destination: ") } + .map { it.split("Destination: ")[1] } + .map { it.removeSuffix("\n") } + .toList() ) + finalPaths.sortBy { File(it).lastModified() } + finalPaths = finalPaths.distinct().toMutableList() + FileUtil.scanMedia(finalPaths, context) + }else{ + //move file from internal to set download directory + eventBus.post(WorkerProgress(100, "Moving file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id, downloadItem.logID)) + try { + finalPaths = withContext(Dispatchers.IO){ + FileUtil.moveFile(tempFileDir.absoluteFile, + context, downloadLocation, keepCache){ p -> + eventBus.post(WorkerProgress(p, "Moving file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id, downloadItem.logID)) + } + }.filter { !it.matches("\\.(description)|(txt)\$".toRegex()) }.toMutableList() - runBlocking { - if (logDownloads) logItem.id = logRepo.insert(logItem) - downloadItem.logID = logItem.id - dao.update(downloadItem) + if (finalPaths.isNotEmpty()){ + eventBus.post(WorkerProgress(100, "Moved file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id, downloadItem.logID)) + } + }catch (e: Exception){ + e.printStackTrace() + handler.postDelayed({ + Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() + }, 1000) } + } - val eventBus = EventBus.getDefault() - runCatching { - YoutubeDL.getInstance().destroyProcessById(downloadItem.id.toString()) - YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line -> - eventBus.post(WorkerProgress(progress.toInt(), line, downloadItem.id, downloadItem.logID)) - val title: String = downloadItem.title.ifEmpty { downloadItem.url } - notificationUtil.updateDownloadNotification( - downloadItem.id.toInt(), - line, progress.toInt(), 0, title, - NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID - ) - CoroutineScope(Dispatchers.IO).launch { - if (logDownloads) { - logRepo.update(line, logItem.id) - logString.append("$line\n") - } + val nonMediaExtensions = mutableListOf().apply { + addAll(context.getStringArray(R.array.thumbnail_containers_values)) + addAll(context.getStringArray(R.array.sub_formats_values).filter { it.isNotBlank() }) + add("description") + add("txt") + } + finalPaths = finalPaths.filter { path -> !nonMediaExtensions.any { path.endsWith(it) } }.toMutableList() + FileUtil.deleteConfigFiles(request) + + //put download in history + if (!downloadItem.incognito) { + if (request.hasOption("--download-archive") && finalPaths.isEmpty()) { + handler.postDelayed({ + Toast.makeText(context, resources.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show() + }, 100) + }else{ + if (finalPaths.isNotEmpty()) { + val unixTime = System.currentTimeMillis() / 1000 + finalPaths.first().apply { + val file = File(this) + var duration = downloadItem.duration + val d = file.getMediaDuration(context) + if (d > 0) duration = d.toStringDuration(Locale.US) + + downloadItem.format.filesize = file.length() + downloadItem.format.container = file.extension + downloadItem.duration = duration } + + val historyItem = HistoryItem(0, + downloadItem.url, + downloadItem.title, + downloadItem.author, + downloadItem.duration, + downloadItem.thumb, + downloadItem.type, + unixTime, + finalPaths, + downloadItem.website, + downloadItem.format, + downloadItem.format.filesize, + downloadItem.id, + commandString) + historyDao.insert(historyItem) } - }.onSuccess { - resultRepo.updateDownloadItem(downloadItem)?.apply { - dao.updateWithoutUpsert(this) - } - //val wasQuickDownloaded = resultDao.getCountInt() == 0 - runBlocking { - var finalPaths = mutableListOf() + } + } - if (noCache){ - eventBus.post(WorkerProgress(100, "Scanning Files", downloadItem.id, downloadItem.logID)) - val outputSequence = it.out.split("\n") - finalPaths = - outputSequence.asSequence() - .filter { it.startsWith("'/storage") } - .map { it.removeSuffix("\n") } - .map { it.removeSurrounding("'", "'") } - .toMutableList() - - finalPaths.addAll( - outputSequence.asSequence() - .filter { it.startsWith("[SplitChapters]") && it.contains("Destination: ") } - .map { it.split("Destination: ")[1] } - .map { it.removeSuffix("\n") } - .toList() - ) - - finalPaths.sortBy { File(it).lastModified() } - finalPaths = finalPaths.distinct().toMutableList() - FileUtil.scanMedia(finalPaths, context) - }else{ - //move file from internal to set download directory - eventBus.post(WorkerProgress(100, "Moving file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id, downloadItem.logID)) - try { - finalPaths = withContext(Dispatchers.IO){ - FileUtil.moveFile(tempFileDir.absoluteFile,context, downloadLocation, keepCache){ p -> - eventBus.post(WorkerProgress(p, "Moving file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id, downloadItem.logID)) - } - }.filter { !it.matches("\\.(description)|(txt)\$".toRegex()) }.toMutableList() - - if (finalPaths.isNotEmpty()){ - eventBus.post(WorkerProgress(100, "Moved file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id, downloadItem.logID)) - } - }catch (e: Exception){ - e.printStackTrace() - handler.postDelayed({ - Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() - }, 1000) - } - } - - - val nonMediaExtensions = mutableListOf().apply { - addAll(context.getStringArray(R.array.thumbnail_containers_values)) - addAll(context.getStringArray(R.array.sub_formats_values).filter { it.isNotBlank() }) - add("description") - add("txt") - } - finalPaths = finalPaths.filter { path -> !nonMediaExtensions.any { path.endsWith(it) } }.toMutableList() - FileUtil.deleteConfigFiles(request) - - //put download in history - if (!downloadItem.incognito) { - if (request.hasOption("--download-archive") && finalPaths.isEmpty()) { - handler.postDelayed({ - Toast.makeText(context, resources.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show() - }, 100) - }else{ - if (finalPaths.isNotEmpty()) { - val unixTime = System.currentTimeMillis() / 1000 - finalPaths.first().apply { - val file = File(this) - var duration = downloadItem.duration - val d = file.getMediaDuration(context) - if (d > 0) duration = d.toStringDuration(Locale.US) - - downloadItem.format.filesize = file.length() - downloadItem.format.container = file.extension - downloadItem.duration = duration - } - - val historyItem = HistoryItem(0, - downloadItem.url, - downloadItem.title, - downloadItem.author, - downloadItem.duration, - downloadItem.thumb, - downloadItem.type, - unixTime, - finalPaths, - downloadItem.website, - downloadItem.format, - downloadItem.format.filesize, - downloadItem.id, - commandString) - historyDao.insert(historyItem) - } - } - } - - withContext(Dispatchers.Main) { - notificationUtil.cancelDownloadNotification(downloadItem.id.toInt()) - notificationUtil.createDownloadFinished( - downloadItem.id, downloadItem.title, downloadItem.type, if (finalPaths.isEmpty()) null else finalPaths, resources - ) - } + withContext(Dispatchers.Main) { + notificationUtil.cancelDownloadNotification(downloadItem.id.toInt()) + notificationUtil.createDownloadFinished( + downloadItem.id, downloadItem.title, downloadItem.type, if (finalPaths.isEmpty()) null else finalPaths, resources + ) + } // if (wasQuickDownloaded && createResultItem){ // runCatching { @@ -348,80 +412,63 @@ class DownloadWorker( // } // } - dao.delete(downloadItem.id) + dao.delete(downloadItem.id) - if (logDownloads){ - logRepo.update(initialLogDetails + it.out, logItem.id, true) - } - } - - }.onFailure { - FileUtil.deleteConfigFiles(request) - withContext(Dispatchers.Main){ - notificationUtil.cancelDownloadNotification(downloadItem.id.toInt()) - } - if (this@DownloadWorker.isStopped) return@onFailure - if (it is YoutubeDL.CanceledException) return@onFailure - if (it.message?.contains("JSONDecodeError") == true) { - val cachePath = "${FileUtil.getCachePath(context)}infojsons" - val infoJsonName = MessageDigest.getInstance("MD5").digest(downloadItem.url.toByteArray()).toHexString() - FileUtil.deleteFile("${cachePath}/${infoJsonName}.info.json") - } - - if(it.message != null){ - if (logDownloads){ - logRepo.update(it.message!!, logItem.id) - }else{ - logString.append("${it.message}\n") - logItem.content = logString.toString() - val logID = logRepo.insert(logItem) - downloadItem.logID = logID - } - } - - tempFileDir.delete() - handler.postDelayed({ - Toast.makeText(context, it.message, Toast.LENGTH_LONG).show() - }, 1000) - - Log.e(TAG, context.getString(R.string.failed_download), it) - notificationUtil.cancelDownloadNotification(downloadItem.id.toInt()) - - downloadItem.status = DownloadRepository.Status.Error.toString() - runBlocking { - dao.update(downloadItem) - } - - notificationUtil.createDownloadErrored( - downloadItem.id, - downloadItem.title.ifEmpty { downloadItem.url }, - it.message, - downloadItem.logID, - resources - ) - - eventBus.post(WorkerProgress(100, it.toString(), downloadItem.id, downloadItem.logID)) - } + if (logDownloads){ + logRepo.update(initialLogDetails + it.out, logItem.id, true) } } - if (eligibleDownloads.isNotEmpty()){ - eligibleDownloads.forEach { - it.status = DownloadRepository.Status.Active.toString() - priorityItemIDs.remove(it.id) - } - dao.updateMultiple(eligibleDownloads) + }.onFailure { + FileUtil.deleteConfigFiles(request) + withContext(Dispatchers.Main){ + notificationUtil.cancelDownloadNotification(downloadItem.id.toInt()) } + if (isStopped) return@onFailure + if (it is YoutubeDL.CanceledException) return@onFailure + if (it.message?.contains("JSONDecodeError") == true) { + val cachePath = "${FileUtil.getCachePath(context)}infojsons" + val infoJsonName = MessageDigest.getInstance("MD5").digest(downloadItem.url.toByteArray()).toHexString() + FileUtil.deleteFile("${cachePath}/${infoJsonName}.info.json") + } + + if(it.message != null){ + if (logDownloads){ + logRepo.update(it.message!!, logItem.id) + }else{ + logString.append("${it.message}\n") + logItem.content = logString.toString() + val logID = logRepo.insert(logItem) + downloadItem.logID = logID + } + } + + tempFileDir.delete() + handler.postDelayed({ + Toast.makeText(context, it.message, Toast.LENGTH_LONG).show() + }, 1000) + + Log.e(TAG, context.getString(R.string.failed_download), it) + notificationUtil.cancelDownloadNotification(downloadItem.id.toInt()) + + downloadItem.status = DownloadRepository.Status.Error.toString() + runBlocking { + dao.update(downloadItem) + } + + notificationUtil.createDownloadErrored( + downloadItem.id, + downloadItem.title.ifEmpty { downloadItem.url }, + it.message, + downloadItem.logID, + resources + ) + + eventBus.post(WorkerProgress(100, it.toString(), downloadItem.id, downloadItem.logID)) } - - - return Result.success() } - - companion object { - val runningYTDLInstances: MutableList = mutableListOf() const val TAG = "DownloadWorker" } diff --git a/app/src/main/java/com/deniscerri/ytdl/work/MoveCacheFilesWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/MoveCacheFilesWorker.kt index c14add11..fd2400c6 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/MoveCacheFilesWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/MoveCacheFilesWorker.kt @@ -3,7 +3,7 @@ package com.deniscerri.ytdl.work import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC import android.os.Build import android.os.Environment import android.os.Handler @@ -42,7 +42,7 @@ class MoveCacheFilesWorker( val notification = notificationUtil.createMoveCacheFilesNotification(pendingIntent, NotificationUtil.DOWNLOAD_MISC_CHANNEL_ID) if (Build.VERSION.SDK_INT > 33) { - setForegroundAsync(ForegroundInfo(id, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)) + setForegroundAsync(ForegroundInfo(id, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)) }else{ setForegroundAsync(ForegroundInfo(id, notification)) } diff --git a/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt index a071fba1..57725142 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt @@ -1,7 +1,7 @@ package com.deniscerri.ytdl.work import android.content.Context -import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC import android.os.Build import android.util.Log import androidx.preference.PreferenceManager @@ -57,7 +57,7 @@ class ObserveSourceWorker( val workerID = System.currentTimeMillis().toInt() val notification = notificationUtil.createObserveSourcesNotification(item.name) if (Build.VERSION.SDK_INT > 33) { - setForegroundAsync(ForegroundInfo(workerID, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)) + setForegroundAsync(ForegroundInfo(workerID, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)) }else{ setForegroundAsync(ForegroundInfo(workerID, notification)) } diff --git a/app/src/main/java/com/deniscerri/ytdl/work/TerminalDownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/TerminalDownloadWorker.kt index 28528e59..1fe1b8a2 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/TerminalDownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/TerminalDownloadWorker.kt @@ -3,7 +3,7 @@ package com.deniscerri.ytdl.work import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC import android.os.Build import android.os.Handler import android.os.Looper @@ -53,7 +53,7 @@ class TerminalDownloadWorker( val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, command.take(65), NotificationUtil.DOWNLOAD_TERMINAL_RUNNING_NOTIFICATION_ID) if (Build.VERSION.SDK_INT > 33) { - setForegroundAsync(ForegroundInfo(itemId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)) + setForegroundAsync(ForegroundInfo(itemId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)) }else{ setForegroundAsync(ForegroundInfo(itemId, notification)) } diff --git a/app/src/main/java/com/deniscerri/ytdl/work/UpdateMultipleDownloadsDataWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/UpdateMultipleDownloadsDataWorker.kt index 39ced447..07d0764c 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/UpdateMultipleDownloadsDataWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/UpdateMultipleDownloadsDataWorker.kt @@ -1,7 +1,7 @@ package com.deniscerri.ytdl.work import android.content.Context -import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC import android.os.Build import androidx.work.ForegroundInfo import androidx.work.Worker @@ -32,7 +32,7 @@ class UpdateMultipleDownloadsDataWorker( val notification = notificationUtil.createDataUpdateNotification() if (Build.VERSION.SDK_INT > 33) { - setForegroundAsync(ForegroundInfo(workID, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)) + setForegroundAsync(ForegroundInfo(workID, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)) }else{ setForegroundAsync(ForegroundInfo(workID, notification)) } diff --git a/app/src/main/java/com/deniscerri/ytdl/work/UpdateMultipleDownloadsFormatsWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/UpdateMultipleDownloadsFormatsWorker.kt index 9b8adc15..cd06064d 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/UpdateMultipleDownloadsFormatsWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/UpdateMultipleDownloadsFormatsWorker.kt @@ -1,7 +1,7 @@ package com.deniscerri.ytdl.work import android.content.Context -import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC import android.os.Build import androidx.work.ForegroundInfo import androidx.work.Worker @@ -33,7 +33,7 @@ class UpdateMultipleDownloadsFormatsWorker( val notification = notificationUtil.createFormatsUpdateNotification() if (Build.VERSION.SDK_INT > 33) { - setForegroundAsync(ForegroundInfo(workID, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)) + setForegroundAsync(ForegroundInfo(workID, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC)) }else{ setForegroundAsync(ForegroundInfo(workID, notification)) } diff --git a/app/src/main/java/com/deniscerri/ytdl/work/WorkManagerExtensions.kt b/app/src/main/java/com/deniscerri/ytdl/work/WorkManagerExtensions.kt new file mode 100644 index 00000000..68f4a128 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/work/WorkManagerExtensions.kt @@ -0,0 +1,34 @@ +package com.deniscerri.ytdl.work + +import android.content.Context +import android.util.Log +import androidx.work.CoroutineWorker +import androidx.work.WorkInfo +import androidx.work.WorkManager +import kotlinx.coroutines.delay + +val Context.workManager: WorkManager + get() = WorkManager.getInstance(this) + +fun WorkManager.isRunning(tag: String): Boolean { + val list = this.getWorkInfosByTag(tag).get() + return list.count { it.state == WorkInfo.State.RUNNING } > 1 +} + +/** + * Makes this worker run in the context of a foreground service. + * + * Note that this function is a no-op if the process is subject to foreground + * service restrictions. + * + * Moving to foreground service context requires the worker to run a bit longer, + * allowing Service.startForeground() to be called and avoiding system crash. + */ +suspend fun CoroutineWorker.setForegroundSafely() { + try { + setForeground(getForegroundInfo()) + delay(500) + } catch (e: IllegalStateException) { + Log.e("ERROR", "Not allowed to set foreground job") + } +} \ No newline at end of file