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 f39520f8..e4d6325d 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 @@ -26,7 +26,7 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE status='Active'") fun getActiveDownloads() : Flow> - @Query("SELECT * FROM downloads WHERE status='Active' or status = 'Paused' ORDER BY CASE WHEN status = 'Active' THEN 0 ELSE 1 END") + @Query("SELECT * FROM downloads WHERE status='Active' or status = 'Paused' ORDER BY id") fun getActiveAndPausedDownloads() : Flow> @Query("SELECT * FROM downloads WHERE status='Paused'") 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 5894c125..3b16b9f9 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 @@ -22,7 +22,7 @@ import com.deniscerri.ytdl.database.models.DownloadItemSimple import com.deniscerri.ytdl.util.Extensions.toListString import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.work.AlarmScheduler -import com.deniscerri.ytdl.work.DownloadWorker +import com.deniscerri.ytdl.work.downloader.DownloadWorker import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import java.io.File diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt index ce65ae50..864396af 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt @@ -49,6 +49,7 @@ import com.deniscerri.ytdl.util.extractors.YTDLPUtil import com.deniscerri.ytdl.work.AlarmScheduler import com.deniscerri.ytdl.work.UpdateMultipleDownloadsDataWorker import com.deniscerri.ytdl.work.UpdateMultipleDownloadsFormatsWorker +import com.deniscerri.ytdl.work.downloader.DownloadManager import com.google.gson.Gson import com.yausername.youtubedl_android.YoutubeDL import kotlinx.coroutines.CancellationException @@ -99,6 +100,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel val savedDownloadsCount : Flow val scheduledDownloadsCount : Flow + val downloadManager: DownloadManager + val pausedAllDownloads = MediatorLiveData(PausedAllDownloadsState.HIDDEN) private val pausedAllDownloadsFlow : Flow private var isPausingResuming = false @@ -145,6 +148,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel resultRepository = ResultRepository(dbManager.resultDao, commandTemplateDao, application) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) ytdlpUtil = YTDLPUtil(application, commandTemplateDao) + downloadManager = DownloadManager.getInstance() activeDownloadsCount = repository.activeDownloadsCount activePausedDownloadsCount = repository.activePausedDownloadsCount @@ -1291,28 +1295,45 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel return dao.getProcessingAsIncognitoCount() > 0 } + fun cancelDownloadOnly(id : Long) { + downloadManager.cancelDownload(id) + } - fun pauseAllDownloads() = viewModelScope.launch { + suspend fun cancelDownload(id: Long) { + downloadManager.cancelDownload(id) + val item = getItemByID(id) + item.status = DownloadRepository.Status.Cancelled.toString() + updateDownload(item) + } + + suspend fun pauseDownload(id: Long) { + downloadManager.cancelDownload(id) + updateToStatus(id, DownloadRepository.Status.Paused) + } + + suspend fun pauseAllDownloads() { pausedAllDownloads.value = PausedAllDownloadsState.PROCESSING isPausingResuming = true WorkManager.getInstance(application).cancelAllWorkByTag("download") + downloadManager.cancelAll() val activeDownloadsList = withContext(Dispatchers.IO){ getActiveDownloads() } - activeDownloadsList.forEach { - YoutubeDL.getInstance().destroyProcessById(it.id.toString()) - notificationUtil.cancelDownloadNotification(it.id.toInt()) - } delay(1000) isPausingResuming = false - repository.setDownloadStatusMultiple(activeDownloadsList.map { it.id }, DownloadRepository.Status.Paused) - pausedAllDownloads.value = PausedAllDownloadsState.RESUME + withContext(Dispatchers.IO) { + repository.setDownloadStatusMultiple(activeDownloadsList.map { it.id }, DownloadRepository.Status.Paused) + } + withContext(Dispatchers.Main) { + pausedAllDownloads.value = PausedAllDownloadsState.RESUME + } } - fun resumeAllDownloads() = viewModelScope.launch { + suspend fun resumeAllDownloads() { pausedAllDownloads.value = PausedAllDownloadsState.PROCESSING isPausingResuming = true WorkManager.getInstance(application).cancelAllWorkByTag("download") + downloadManager.cancelAll() val paused = withContext(Dispatchers.IO) { dao.getPausedDownloadsList() } diff --git a/app/src/main/java/com/deniscerri/ytdl/di/AppModule.kt b/app/src/main/java/com/deniscerri/ytdl/di/AppModule.kt new file mode 100644 index 00000000..0e1f5381 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/di/AppModule.kt @@ -0,0 +1,5 @@ +package com.deniscerri.ytdl.di + +class AppModule { + +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/receiver/ScheduleAlarmReceiver.kt b/app/src/main/java/com/deniscerri/ytdl/receiver/ScheduleAlarmReceiver.kt index 01e45b4f..7cc6d8b4 100644 --- a/app/src/main/java/com/deniscerri/ytdl/receiver/ScheduleAlarmReceiver.kt +++ b/app/src/main/java/com/deniscerri/ytdl/receiver/ScheduleAlarmReceiver.kt @@ -9,7 +9,7 @@ import androidx.work.ExistingWorkPolicy import androidx.work.NetworkType import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager -import com.deniscerri.ytdl.work.DownloadWorker +import com.deniscerri.ytdl.work.downloader.DownloadWorker import java.util.concurrent.TimeUnit class ScheduleAlarmReceiver : 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 2aecdb96..12258e64 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 @@ -48,7 +48,7 @@ import com.deniscerri.ytdl.util.Extensions.setFullScreen import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.VideoPlayerUtil -import com.deniscerri.ytdl.work.DownloadWorker +import com.deniscerri.ytdl.work.downloader.DownloadWorker import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment @@ -474,25 +474,16 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap override fun onCancelClick(itemID: Long) { lifecycleScope.launch { - YoutubeDL.getInstance().destroyProcessById(itemID.toString()) - notificationUtil.cancelDownloadNotification(itemID.toInt()) - - val item = withContext(Dispatchers.IO){ - downloadViewModel.getItemByID(itemID) - } - item.status = DownloadRepository.Status.Cancelled.toString() withContext(Dispatchers.IO){ - downloadViewModel.updateDownload(item) + downloadViewModel.cancelDownload(itemID) } } } override fun onPauseClick(itemID: Long, position: Int) { lifecycleScope.launch { - YoutubeDL.getInstance().destroyProcessById(itemID.toString()) - notificationUtil.cancelDownloadNotification(itemID.toInt()) withContext(Dispatchers.IO){ - downloadViewModel.updateToStatus(itemID, DownloadRepository.Status.Paused) + downloadViewModel.pauseDownload(itemID) } activeAdapter.notifyItemChanged(position) } @@ -511,7 +502,7 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap //dont remove @Subscribe(threadMode = ThreadMode.MAIN) - fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) { + fun onDownloadProgressEvent(event: com.deniscerri.ytdl.work.downloader.DownloadManager.DownloadProgress) { val progressBar = requireView().findViewWithTag("${event.downloadItemID}##progress") val outputText = requireView().findViewWithTag("${event.downloadItemID}##output") 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 dd50f196..74021e2b 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 @@ -24,16 +24,14 @@ import androidx.recyclerview.widget.RecyclerView import androidx.work.WorkManager 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.util.Extensions.forceFastScrollMode import com.deniscerri.ytdl.util.NotificationUtil -import com.deniscerri.ytdl.work.DownloadWorker +import com.deniscerri.ytdl.work.downloader.DownloadManager import com.google.android.material.badge.ExperimentalBadgeUtils import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.progressindicator.LinearProgressIndicator -import com.yausername.youtubedl_android.YoutubeDL import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -151,7 +149,7 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis //dont remove @Subscribe(threadMode = ThreadMode.MAIN) - fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) { + fun onDownloadProgressEvent(event: DownloadManager.DownloadProgress) { val progressBar = requireView().findViewWithTag("${event.downloadItemID}##progress") val outputText = requireView().findViewWithTag("${event.downloadItemID}##output") @@ -176,15 +174,8 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis override fun onCancelClick(itemID: Long) { lifecycleScope.launch { - YoutubeDL.getInstance().destroyProcessById(itemID.toString()) - notificationUtil.cancelDownloadNotification(itemID.toInt()) - - val item = withContext(Dispatchers.IO){ - downloadViewModel.getItemByID(itemID) - } - item.status = DownloadRepository.Status.Cancelled.toString() withContext(Dispatchers.IO){ - downloadViewModel.updateDownload(item) + downloadViewModel.cancelDownload(itemID) } } } @@ -202,10 +193,8 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis override fun onPauseClick(itemID: Long) { lifecycleScope.launch { - YoutubeDL.getInstance().destroyProcessById(itemID.toString()) - notificationUtil.cancelDownloadNotification(itemID.toInt()) withContext(Dispatchers.IO){ - downloadViewModel.updateToStatus(itemID, DownloadRepository.Status.Paused) + downloadViewModel.pauseDownload(itemID) } } } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/QueuedDownloadsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/QueuedDownloadsFragment.kt index 604322fb..eadbf697 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/QueuedDownloadsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/QueuedDownloadsFragment.kt @@ -216,8 +216,7 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis val selectedObjects = getSelectedIDs() adapter.clearCheckedItems() for (id in selectedObjects){ - YoutubeDL.getInstance().destroyProcessById(id.toInt().toString()) - notificationUtil.cancelDownloadNotification(id.toInt()) + downloadViewModel.cancelDownloadOnly(id) } downloadViewModel.deleteAllWithID(selectedObjects) actionMode?.finish() @@ -524,20 +523,10 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis private fun cancelDownload(itemID: Long){ lifecycleScope.launch { - cancelItem(itemID.toInt()) withContext(Dispatchers.IO){ - downloadViewModel.getItemByID(itemID) - }.let { - it.status = DownloadRepository.Status.Cancelled.toString() - withContext(Dispatchers.IO){ - downloadViewModel.updateDownload(it) - } + downloadViewModel.cancelDownload(itemID) } } } - private fun cancelItem(id: Int){ - YoutubeDL.getInstance().destroyProcessById(id.toString()) - notificationUtil.cancelDownloadNotification(id) - } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/MoreFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/MoreFragment.kt index 62162b40..512d30bd 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/MoreFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/MoreFragment.kt @@ -123,36 +123,14 @@ class MoreFragment : Fragment() { val checkbox = dialogView.findViewById(R.id.doNotShowAgain) terminateDialog.setView(dialogView) - - checkbox.setOnCheckedChangeListener { compoundButton, b -> doNotShowAgain = compoundButton.isChecked } - val workManager = WorkManager.getInstance(requireContext()) - val notificationUtil = NotificationUtil(requireContext()) - terminateDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } terminateDialog.setPositiveButton(getString(R.string.ok)) { diag: DialogInterface?, _: Int -> lifecycleScope.launch { - val activeDownloads = withContext(Dispatchers.IO){ - downloadViewModel.getActiveDownloadsCount() - } - if (activeDownloads > 0) { - workManager.cancelAllWorkByTag("download") - val activeDownloadsList = withContext(Dispatchers.IO){ - downloadViewModel.getActiveDownloads() - } - - activeDownloadsList.forEach { - YoutubeDL.getInstance().destroyProcessById(it.id.toString()) - notificationUtil.cancelDownloadNotification(it.id.toInt()) - it.status = DownloadRepository.Status.Paused.toString() - withContext(Dispatchers.IO) { - downloadViewModel.updateDownload(it) - } - } - } + downloadViewModel.pauseAllDownloads() if (doNotShowAgain){ mainSharedPreferencesEditor.putBoolean("ask_terminate_app", false).apply() diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/downloadLogs/DownloadLogFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/downloadLogs/DownloadLogFragment.kt index 3c4432da..b8b47f61 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/downloadLogs/DownloadLogFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/downloadLogs/DownloadLogFragment.kt @@ -30,7 +30,8 @@ import com.deniscerri.ytdl.util.Extensions.enableFastScroll import com.deniscerri.ytdl.util.Extensions.enableTextHighlight import com.deniscerri.ytdl.util.Extensions.setCustomTextSize import com.deniscerri.ytdl.util.FileUtil -import com.deniscerri.ytdl.work.DownloadWorker +import com.deniscerri.ytdl.work.downloader.DownloadManager +import com.deniscerri.ytdl.work.downloader.DownloadWorker import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton @@ -265,7 +266,7 @@ class DownloadLogFragment : Fragment() { //dont remove @Subscribe(threadMode = ThreadMode.MAIN) - fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) { + fun onDownloadProgressEvent(event: DownloadManager.DownloadProgress) { val progressBar = requireView().findViewById(R.id.progress) if (event.logItemID == logID) { progressBar.isVisible = event.progress < 100 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 7adccfab..17b5e607 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 @@ -22,7 +22,7 @@ import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.work.AlarmScheduler import com.deniscerri.ytdl.work.CleanUpLeftoverDownloads -import com.deniscerri.ytdl.work.DownloadWorker +import com.deniscerri.ytdl.work.downloader.DownloadWorker import java.util.Calendar import java.util.concurrent.TimeUnit 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 d759960b..0faa0102 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 @@ -20,7 +20,8 @@ import com.deniscerri.ytdl.database.models.TerminalItem import com.deniscerri.ytdl.database.viewmodel.TerminalViewModel import com.deniscerri.ytdl.ui.adapter.TerminalDownloadsAdapter import com.deniscerri.ytdl.util.Extensions.enableFastScroll -import com.deniscerri.ytdl.work.DownloadWorker +import com.deniscerri.ytdl.work.downloader.DownloadManager +import com.deniscerri.ytdl.work.downloader.DownloadWorker import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.progressindicator.LinearProgressIndicator import kotlinx.coroutines.flow.collectLatest @@ -84,7 +85,7 @@ class TerminalDownloadsListFragment : Fragment(), TerminalDownloadsAdapter.OnIte } @Subscribe(threadMode = ThreadMode.MAIN) - fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) { + fun onDownloadProgressEvent(event: DownloadManager.DownloadProgress) { val progressBar = requireView().findViewWithTag("${event.downloadItemID}##progress") val outputText = requireView().findViewWithTag("${event.downloadItemID}##output") 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 1fe1b8a2..16b7f536 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/TerminalDownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/TerminalDownloadWorker.kt @@ -22,6 +22,8 @@ import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.ui.more.terminal.TerminalActivity import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.NotificationUtil +import com.deniscerri.ytdl.work.downloader.DownloadManager +import com.deniscerri.ytdl.work.downloader.DownloadWorker import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDLRequest import kotlinx.coroutines.CoroutineScope @@ -122,7 +124,7 @@ class TerminalDownloadWorker( YoutubeDL.getInstance().execute(request, itemId.toString()){ progress, _, line -> runBlocking { - eventBus.post(DownloadWorker.WorkerProgress(progress.toInt(), line, itemId.toLong(), logItem.id)) + eventBus.post(DownloadManager.DownloadProgress(progress.toInt(), line, itemId.toLong(), logItem.id)) } val title: String = command.take(65) @@ -142,7 +144,7 @@ class TerminalDownloadWorker( //move file from internal to set download directory try { FileUtil.moveFile(File(FileUtil.getCachePath(context) + "/TERMINAL/" + itemId),context, downloadLocation!!, false){ p -> - eventBus.post(DownloadWorker.WorkerProgress(p, "", itemId.toLong(), logItem.id)) + eventBus.post(DownloadManager.DownloadProgress(p, "", itemId.toLong(), logItem.id)) } }catch (e: Exception){ e.printStackTrace() diff --git a/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/downloader/DownloadManager.kt similarity index 78% rename from app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt rename to app/src/main/java/com/deniscerri/ytdl/work/downloader/DownloadManager.kt index 746db6be..ae016888 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/downloader/DownloadManager.kt @@ -1,11 +1,9 @@ -package com.deniscerri.ytdl.work +package com.deniscerri.ytdl.work.downloader -import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.SharedPreferences -import android.content.pm.ServiceInfo import android.content.res.Configuration import android.content.res.Resources import android.os.Build @@ -15,10 +13,6 @@ import android.util.DisplayMetrics import android.util.Log import android.widget.Toast import androidx.preference.PreferenceManager -import androidx.work.CoroutineWorker -import androidx.work.ForegroundInfo -import androidx.work.WorkManager -import androidx.work.WorkerParameters import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.deniscerri.ytdl.App import com.deniscerri.ytdl.MainActivity @@ -38,17 +32,17 @@ import com.deniscerri.ytdl.util.Extensions.toStringDuration import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.extractors.YTDLPUtil +import com.deniscerri.ytdl.work.AlarmScheduler 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.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.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.supervisorScope @@ -58,56 +52,72 @@ import java.io.File import java.security.MessageDigest import java.util.Locale +class DownloadManager private constructor() { + private var notificationUtil: NotificationUtil = NotificationUtil(App.instance) + private var sharedPreferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(App.instance) -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 dao : DownloadDao 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 resources: Resources + + private lateinit var openDownloadQueue: PendingIntent private lateinit var queueState : Flow> - lateinit var observeJob : Job - - + private var observeJob : Job? = null 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 - ) + val isRunning get() = observeJob?.isActive == true - 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 getActiveDownloadsStore() : Set { + return sharedPreferences.getStringSet("ytdlp_active_downloads", setOf())!! } - @SuppressLint("RestrictedApi") - override suspend fun doWork(): Result { + private fun addIDToActiveDownloadsStore(id: Long) { + val new = mutableSetOf() + new.addAll(getActiveDownloadsStore()) + new.add(id.toString()) + + sharedPreferences.edit().putStringSet("ytdlp_active_downloads", new).apply() + } + + private fun removeIDFromActiveDownloadsStore(id: Long) { + val new = mutableSetOf() + new.addAll(getActiveDownloadsStore()) + new.remove(id.toString()) + + sharedPreferences.edit().putStringSet("ytdlp_active_downloads", new).apply() + } + + fun cancelDownload(id: Long) { + YoutubeDL.getInstance().destroyProcessById(id.toString()) + notificationUtil.cancelDownloadNotification(id.toInt()) + removeIDFromActiveDownloadsStore(id) + } + + fun cancelAll() { + getActiveDownloadsStore().map { it.toLong() }.forEach { + cancelDownload(it) + } + observeJob?.cancel() + } + + //only call from download worker + fun startDownload( + context: Context, //worker context + priorityItems: List = listOf(), + continueAfterPriorityIds : Boolean = true + ) { + val priorityItemIDs = priorityItems.toMutableList() + //init - notificationUtil = NotificationUtil(App.instance) + notificationUtil = NotificationUtil(context) dbManager = DBManager.getInstance(context) dao = dbManager.downloadDao historyDao = dbManager.historyDao @@ -119,6 +129,8 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) eventBus = EventBus.getDefault() + sharedPreferences.edit().putStringSet("ytdlp_active_downloads", setOf()).apply() + val confTmp = Configuration(context.resources.configuration) val locale = if (Build.VERSION.SDK_INT < 33) { sharedPreferences.getString("app_language", "")!!.ifEmpty { Locale.getDefault().language } @@ -133,13 +145,16 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters val metrics = DisplayMetrics() resources = Resources(context.assets, metrics, confTmp) - workManager = WorkManager.getInstance(context) - if (workManager.isRunning("download")) return Result.Failure() + openDownloadQueue = PendingIntent.getActivity( + context, + 1000000000, + Intent(context, MainActivity::class.java).run { + action = Intent.ACTION_VIEW + putExtra("destination", "Queue") + }, + PendingIntent.FLAG_IMMUTABLE + ) - 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) @@ -147,10 +162,8 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters dao.getActiveQueuedScheduledDownloadsUntilWithPriority(time, priorityItemIDs) } - val downloadJobs = mutableMapOf() - - observeJob = CoroutineScope(SupervisorJob()).launch { - queueState.collectLatest { queue -> + observeJob = CoroutineScope(SupervisorJob() + Dispatchers.IO).launch { + queueState.distinctUntilChanged().collectLatest { queue -> val runningCount = queue.asSequence() .filter { it.status == DownloadRepository.Status.Active.toString() } .count() @@ -160,66 +173,53 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters val queueCount = queueItems.count() if (queueCount == 0 && runningCount == 0) { - observeJob.cancel() + observeJob?.cancel() return@collectLatest } val useScheduler = sharedPreferences.getBoolean("use_scheduler", false) if (useScheduler){ if (queueItems.none{ it.downloadStartTime > 0L } && runningCount == 0 && !alarmScheduler.isDuringTheScheduledTime()) { - observeJob.cancel() + observeJob?.cancel() return@collectLatest } } + if (priorityItemIDs.isEmpty() && !continueAfterPriorityIds) { + observeJob?.cancel() + 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 - observeJob.cancel() - return@collectLatest - } - } - // Use supervisorScope to cancel child jobs when the downloader job is cancelled supervisorScope { val queuedDownloads = queueItems .toList() .take(concurrentDownloads) - val queuedIDs = queuedDownloads.map { it.id } - val downloadJobsToStop = downloadJobs.filter { it.key !in queuedIDs } - downloadJobsToStop.forEach { (download, job) -> - job.cancel() - downloadJobs.remove(download) - } - - val downloadsToStart = queuedDownloads.filter { it.id !in downloadJobs } + priorityItemIDs.removeAll(queuedDownloads.map { it.id }) + val downloadsToStart = queuedDownloads.filter { it.id.toString() !in getActiveDownloadsStore() } downloadsToStart.forEach { downloadItem -> val notification = notificationUtil.createDownloadServiceNotification(openDownloadQueue, downloadItem.title.ifEmpty { downloadItem.url }) notificationUtil.notify(downloadItem.id.toInt(), notification) - downloadJobs[downloadItem.id] = launchDownloadJob(downloadItem) + launchDownloadJob(context, downloadItem) + addIDToActiveDownloadsStore(downloadItem.id) } } } } } - - while (observeJob.isActive) { - //keep alive - } - - return Result.Success() } @OptIn(ExperimentalStdlibApi::class) - private fun launchDownloadJob(downloadItem: DownloadItem) = CoroutineScope(Dispatchers.IO).launch { + private fun launchDownloadJob(context: Context, downloadItem: DownloadItem) = 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 noCache = writtenPath || (!sharedPreferences.getBoolean("cache_downloads", true) && File( + FileUtil.formatPath(downloadItem.downloadPath)).canWrite()) val request = ytdlpUtil.buildYoutubeDLRequest(downloadItem) @@ -270,7 +270,7 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters 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)) + eventBus.post(DownloadProgress(progress.toInt(), line, downloadItem.id, downloadItem.logID)) val title: String = downloadItem.title.ifEmpty { downloadItem.url } notificationUtil.updateDownloadNotification( downloadItem.id.toInt(), @@ -288,12 +288,13 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters resultRepo.updateDownloadItem(downloadItem)?.apply { dao.updateWithoutUpsert(this) } + removeIDFromActiveDownloadsStore(downloadItem.id) //val wasQuickDownloaded = resultDao.getCountInt() == 0 runBlocking { var finalPaths = mutableListOf() if (noCache){ - eventBus.post(WorkerProgress(100, "Scanning Files", downloadItem.id, downloadItem.logID)) + eventBus.post(DownloadProgress(100, "Scanning Files", downloadItem.id, downloadItem.logID)) val outputSequence = it.out.split("\n") finalPaths = outputSequence.asSequence() @@ -315,17 +316,17 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters 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)) + eventBus.post(DownloadProgress(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)) + eventBus.post(DownloadProgress(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)) + eventBus.post(DownloadProgress(100, "Moved file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id, downloadItem.logID)) } }catch (e: Exception){ e.printStackTrace() @@ -413,10 +414,11 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters }.onFailure { FileUtil.deleteConfigFiles(request) + removeIDFromActiveDownloadsStore(downloadItem.id) withContext(Dispatchers.Main){ notificationUtil.cancelDownloadNotification(downloadItem.id.toInt()) } - if (isStopped) return@onFailure + if (!this.isActive) return@onFailure if (it is YoutubeDL.CanceledException) return@onFailure if (it.message?.contains("JSONDecodeError") == true) { val cachePath = "${FileUtil.getCachePath(context)}infojsons" @@ -456,19 +458,28 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters resources ) - eventBus.post(WorkerProgress(100, it.toString(), downloadItem.id, downloadItem.logID)) + eventBus.post(DownloadProgress(100, it.toString(), downloadItem.id, downloadItem.logID)) } } companion object { - const val TAG = "DownloadWorker" + const val TAG = "DownloadManager" + @Volatile + private var instance: DownloadManager? = null + + fun getInstance() : DownloadManager { + return instance ?: synchronized(this) { + instance ?: DownloadManager().also { + instance = it + } + } + } } - class WorkerProgress( + class DownloadProgress( val progress: Int, val output: String, val downloadItemID: Long, val logItemID: Long? ) - } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/work/downloader/DownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/downloader/DownloadWorker.kt new file mode 100644 index 00000000..e7cab134 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/work/downloader/DownloadWorker.kt @@ -0,0 +1,104 @@ +package com.deniscerri.ytdl.work.downloader + +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.content.pm.ServiceInfo +import android.content.res.Configuration +import android.content.res.Resources +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.util.DisplayMetrics +import android.util.Log +import android.widget.Toast +import androidx.preference.PreferenceManager +import androidx.work.CoroutineWorker +import androidx.work.ForegroundInfo +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import com.afollestad.materialdialogs.utils.MDUtil.getStringArray +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 +import com.deniscerri.ytdl.database.repository.LogRepository +import com.deniscerri.ytdl.database.repository.ResultRepository +import com.deniscerri.ytdl.util.Extensions.getMediaDuration +import com.deniscerri.ytdl.util.Extensions.toStringDuration +import com.deniscerri.ytdl.util.FileUtil +import com.deniscerri.ytdl.util.NotificationUtil +import com.deniscerri.ytdl.util.extractors.YTDLPUtil +import com.deniscerri.ytdl.work.AlarmScheduler +import com.deniscerri.ytdl.work.isRunning +import com.deniscerri.ytdl.work.setForegroundSafely +import com.yausername.youtubedl_android.YoutubeDL +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +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.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.withContext +import org.greenrobot.eventbus.EventBus +import java.io.File +import java.security.MessageDigest +import java.util.Locale + + +class DownloadWorker(private val context: Context,workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) { + private lateinit var workManager: WorkManager + private lateinit var downloadManager: DownloadManager + + 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 + }, + ) + } + + @SuppressLint("RestrictedApi") + override suspend fun doWork(): Result { + workManager = WorkManager.getInstance(context) + if (workManager.isRunning("download")) return Result.Failure() + + setForegroundSafely() + downloadManager = DownloadManager.getInstance() + + val priorityItemIDs = (inputData.getLongArray("priority_item_ids") ?: longArrayOf()).toMutableList() + val continueAfterPriorityIds = inputData.getBoolean("continue_after_priority_ids", true) + downloadManager.startDownload(context, priorityItemIDs, continueAfterPriorityIds) + + var active = downloadManager.isRunning + while (active) { + active = !isStopped && downloadManager.isRunning + } + + return Result.Success() + } + + companion object { + const val TAG = "DownloadWorker" + } + +} \ No newline at end of file