This commit is contained in:
zaednasr 2024-06-11 22:57:59 +02:00
parent f724b14dfa
commit 3c5167f6eb
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
15 changed files with 193 additions and 477 deletions

View file

@ -10,7 +10,7 @@ plugins {
def properties = new Properties() def properties = new Properties()
def versionMajor = 1 def versionMajor = 1
def versionMinor = 7 def versionMinor = 7
def versionPatch = 6 def versionPatch = 7
def versionBuild = 0 // bump for dogfood builds, public betas, etc. def versionBuild = 0 // bump for dogfood builds, public betas, etc.
def isBeta = false def isBeta = false
def versionExt = isBeta ? ".${versionBuild}-beta" : "" def versionExt = isBeta ? ".${versionBuild}-beta" : ""

View file

@ -18,10 +18,6 @@
<uses-permission android:name="android.permission.ACTION_OPEN_DOCUMENT" /> <uses-permission android:name="android.permission.ACTION_OPEN_DOCUMENT" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- for display over apps-->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- alarm scheduler--> <!-- alarm scheduler-->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
@ -434,8 +430,6 @@
</intent-filter> </intent-filter>
</activity> </activity>
<service android:name=".services.ProcessDownloadsInBackgroundService" android:exported="false" />
<service <service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService" android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false" android:enabled="false"

View file

@ -38,9 +38,9 @@ interface DownloadDao {
@Query(""" @Query("""
SELECT DISTINCT type from downloads where status = 'Processing' and id in (:ids) SELECT DISTINCT type from downloads where status = 'Processing'
""") """)
fun getProcessingDownloadTypes(ids: List<Long>) : List<String> fun getProcessingDownloadTypes() : List<String>
@Query("UPDATE downloads set status = 'Processing' WHERE id in (:ids)") @Query("UPDATE downloads set status = 'Processing' WHERE id in (:ids)")
@ -56,6 +56,12 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status = 'Processing'") @Query("SELECT * FROM downloads WHERE status = 'Processing'")
fun getProcessingDownloadsList() : List<DownloadItem> fun getProcessingDownloadsList() : List<DownloadItem>
@Query("UPDATE downloads set downloadStartTime=:time, status='Scheduled' WHERE status ='Processing'")
suspend fun updateProcessingDownloadTime(time: Long)
@Query("UPDATE downloads set downloadPath=:path WHERE status ='Processing'")
suspend fun updateProcessingDownloadPath(path: String)
@Query("SELECT * FROM downloads WHERE status='Active'") @Query("SELECT * FROM downloads WHERE status='Active'")
suspend fun getActiveDownloadsList() : List<DownloadItem> suspend fun getActiveDownloadsList() : List<DownloadItem>
@ -126,12 +132,6 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE id IN (:ids)") @Query("SELECT * FROM downloads WHERE id IN (:ids)")
fun getDownloadsByIds(ids: List<Long>) : List<DownloadItem> fun getDownloadsByIds(ids: List<Long>) : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Processing' AND id >=:id")
fun getProcessingItemsFromID(id: Long) : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Processing' AND id >=:id and id<=:id2")
fun getProcessingItemsBetweenIDs(id: Long, id2: Long) : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE id IN (:ids)") @Query("SELECT * FROM downloads WHERE id IN (:ids)")
fun getDownloadsByIdsFlow(ids: List<Long>) : Flow<List<DownloadItem>> fun getDownloadsByIdsFlow(ids: List<Long>) : Flow<List<DownloadItem>>
@ -231,8 +231,8 @@ interface DownloadDao {
@Query("Update downloads SET status='Queued', downloadStartTime = 0 WHERE id in (:list)") @Query("Update downloads SET status='Queued', downloadStartTime = 0 WHERE id in (:list)")
suspend fun reQueueDownloadItems(list: List<Long>) suspend fun reQueueDownloadItems(list: List<Long>)
@Query("Update downloads SET status='Saved' WHERE status='Processing' and id in (:ids)") @Query("Update downloads SET status='Saved' WHERE status='Processing'")
fun updateProcessingtoSavedStatus(ids: List<Long>) suspend fun updateProcessingtoSavedStatus()
@Transaction @Transaction
suspend fun putAtTopOfTheQueue(existingIDs: List<Long>){ suspend fun putAtTopOfTheQueue(existingIDs: List<Long>){

View file

@ -3,8 +3,11 @@ package com.deniscerri.ytdl.database.repository
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.os.Handler
import android.os.Looper import android.os.Looper
import android.text.format.DateFormat
import android.widget.Toast import android.widget.Toast
import androidx.core.os.postDelayed
import androidx.paging.Pager import androidx.paging.Pager
import androidx.paging.PagingConfig import androidx.paging.PagingConfig
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@ -28,6 +31,8 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import java.io.File import java.io.File
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -109,14 +114,6 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.getDownloadById(id) return downloadDao.getDownloadById(id)
} }
fun getProcessingItemsFromID(id: Long) : Flow<List<DownloadItem>> {
return downloadDao.getProcessingItemsFromID(id)
}
fun getProcessingItemsBetweenIDs(first: Long, last:Long) : List<DownloadItem> {
return downloadDao.getProcessingItemsBetweenIDs(first, last)
}
fun getAllItemsByIDs(ids : List<Long>) : List<DownloadItem>{ fun getAllItemsByIDs(ids : List<Long>) : List<DownloadItem>{
return downloadDao.getDownloadsByIds(ids) return downloadDao.getDownloadsByIds(ids)
} }
@ -137,6 +134,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.getActiveAndQueuedDownloadsList() return downloadDao.getActiveAndQueuedDownloadsList()
} }
suspend fun updateProcessingDownloadTime(time: Long) {
downloadDao.updateProcessingDownloadTime(time)
}
fun getActiveAndQueuedDownloadIDs() : List<Long> { fun getActiveAndQueuedDownloadIDs() : List<Long> {
return downloadDao.getActiveAndQueuedDownloadIDs() return downloadDao.getActiveAndQueuedDownloadIDs()
} }
@ -258,12 +259,23 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
} }
val handler = Handler(Looper.getMainLooper())
val isCurrentNetworkMetered = (context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered val isCurrentNetworkMetered = (context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered
if (!allowMeteredNetworks && isCurrentNetworkMetered){ if (!allowMeteredNetworks && isCurrentNetworkMetered){
Looper.prepare().run { handler.postDelayed({
Toast.makeText(context, context.getString(R.string.metered_network_download_start_info), Toast.LENGTH_LONG).show() Toast.makeText(context, context.getString(R.string.metered_network_download_start_info), Toast.LENGTH_LONG).show()
} }, 0)
} }
val first = queuedItems.first()
if (first.downloadStartTime > 0L) {
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(queuedItems.first().downloadStartTime)
handler.postDelayed({
Toast.makeText(context, context.getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
}, 0)
}
} }
} }

View file

@ -9,11 +9,8 @@ import android.util.DisplayMetrics
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.media3.exoplayer.offline.Download
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.Data import androidx.work.Data
@ -46,21 +43,9 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.last
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.util.Locale import java.util.Locale
@ -75,8 +60,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val allDownloads : Flow<PagingData<DownloadItem>> val allDownloads : Flow<PagingData<DownloadItem>>
val queuedDownloads : Flow<PagingData<DownloadItemSimple>> val queuedDownloads : Flow<PagingData<DownloadItemSimple>>
val activeDownloads : Flow<List<DownloadItem>> val activeDownloads : Flow<List<DownloadItem>>
val allProcessingDownloads : Flow<List<DownloadItem>> val processingDownloads : Flow<List<DownloadItem>>
var mostRecentProcessingDownloads: Flow<List<DownloadItem>>
val cancelledDownloads : Flow<PagingData<DownloadItemSimple>> val cancelledDownloads : Flow<PagingData<DownloadItemSimple>>
val erroredDownloads : Flow<PagingData<DownloadItemSimple>> val erroredDownloads : Flow<PagingData<DownloadItemSimple>>
val savedDownloads : Flow<PagingData<DownloadItemSimple>> val savedDownloads : Flow<PagingData<DownloadItemSimple>>
@ -115,8 +99,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
auto, audio, video, command auto, audio, video, command
} }
val processingDownloads = MediatorLiveData<List<DownloadItem>>(listOf())
var processingItemsFlow : ProcessingItemsJob? = null var processingItemsFlow : ProcessingItemsJob? = null
var processingItems = MutableStateFlow(false) var processingItems = MutableStateFlow(false)
@ -143,8 +125,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
allDownloads = repository.allDownloads.flow allDownloads = repository.allDownloads.flow
queuedDownloads = repository.queuedDownloads.flow queuedDownloads = repository.queuedDownloads.flow
activeDownloads = repository.activeDownloads activeDownloads = repository.activeDownloads
allProcessingDownloads = repository.processingDownloads processingDownloads = repository.processingDownloads
mostRecentProcessingDownloads = repository.getProcessingItemsFromID(0)
savedDownloads = repository.savedDownloads.flow savedDownloads = repository.savedDownloads.flow
scheduledDownloads = repository.scheduledDownloads.flow scheduledDownloads = repository.scheduledDownloads.flow
cancelledDownloads = repository.cancelledDownloads.flow cancelledDownloads = repository.cancelledDownloads.flow
@ -385,6 +366,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
fun turnDownloadItemsToProcessingDownloads(itemIDs: List<Long>) = viewModelScope.launch(Dispatchers.IO){ fun turnDownloadItemsToProcessingDownloads(itemIDs: List<Long>) = viewModelScope.launch(Dispatchers.IO){
updateProcessingJobData(ProcessingItemsJob(null, DownloadItem::class.java.toString(), itemIDs)) updateProcessingJobData(ProcessingItemsJob(null, DownloadItem::class.java.toString(), itemIDs))
val job = viewModelScope.launch(Dispatchers.IO) { val job = viewModelScope.launch(Dispatchers.IO) {
repository.deleteProcessing()
processingItems.emit(true) processingItems.emit(true)
try { try {
itemIDs.forEachIndexed { idx, it -> itemIDs.forEachIndexed { idx, it ->
@ -395,7 +377,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
item.status = DownloadRepository.Status.Processing.toString() item.status = DownloadRepository.Status.Processing.toString()
processingItemsFlow?.apply { processingItemsFlow?.apply {
val id = repository.insert(item) val id = repository.insert(item)
if (idx == 0) mostRecentProcessingDownloads = repository.getProcessingItemsFromID(id)
processingDownloadItemIDs.add(id) processingDownloadItemIDs.add(id)
updateProcessingJobData(this) updateProcessingJobData(this)
} }
@ -499,7 +480,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.deleteProcessing() repository.deleteProcessing()
} }
fun deleteAllWithID(ids: List<Long>) = viewModelScope.launch(Dispatchers.IO){ suspend fun deleteAllWithID(ids: List<Long>) {
repository.deleteAllWithIDs(ids) repository.deleteAllWithIDs(ids)
} }
@ -618,6 +599,11 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.startDownloadWorker(emptyList(), application) repository.startDownloadWorker(emptyList(), application)
} }
suspend fun queueProcessingDownloads(){
val processingItems = repository.getProcessingDownloads()
queueDownloads(processingItems)
}
suspend fun queueDownloads(items: List<DownloadItem>, ign : Boolean = false) : List<SharedDownloadViewModel.AlreadyExistsIDs> { suspend fun queueDownloads(items: List<DownloadItem>, ign : Boolean = false) : List<SharedDownloadViewModel.AlreadyExistsIDs> {
val ids = sharedDownloadViewModel.queueDownloads(items, ign) val ids = sharedDownloadViewModel.queueDownloads(items, ign)
return ids return ids
@ -639,43 +625,33 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return dbManager.downloadDao.getDownloadIDsNotPresentInList(items.ifEmpty { listOf(-1L) }, status.map { it.toString() }) return dbManager.downloadDao.getDownloadIDsNotPresentInList(items.ifEmpty { listOf(-1L) }, status.map { it.toString() })
} }
fun moveProcessingToSavedCategory(ids: List<Long>){ suspend fun moveProcessingToSavedCategory(){
ids.chunked(100).forEach { dao.updateProcessingtoSavedStatus()
dao.updateProcessingtoSavedStatus(it) }
suspend fun updateProcessingFormat(selectedFormats: List<FormatTuple>): List<Long> {
val items = repository.getProcessingDownloads()
items.forEachIndexed { index, i ->
selectedFormats[index].format?.apply {
i.format = this
}
if (i.type == Type.video) selectedFormats[index].audioFormats?.map { it.format_id }?.let { i.videoPreferences.audioFormatIDs.addAll(it) }
repository.update(i)
}
return items.map { itm -> itm.format.filesize }
}
suspend fun updateProcessingCommandFormat(format: Format){
val items = repository.getProcessingDownloads()
items.forEach {
it.format = format
repository.update(it)
} }
} }
suspend fun updateProcessingFormat(ids: List<Long>, selectedFormats: List<FormatTuple>): List<Long> { suspend fun updateProcessingDownloadPath(path: String){
return ids.chunked(100).map { dao.updateProcessingDownloadPath(path)
val items = repository.getAllItemsByIDs(it)
items.forEachIndexed { index, i ->
selectedFormats[index].format?.apply {
i.format = this
}
if (i.type == Type.video) selectedFormats[index].audioFormats?.map { it.format_id }?.let { i.videoPreferences.audioFormatIDs.addAll(it) }
repository.update(i)
}
items.map { itm -> itm.format.filesize }
}.flatten()
}
suspend fun updateProcessingCommandFormat(ids: List<Long>, format: Format){
ids.chunked(100).forEach {
repository.getAllItemsByIDs(it).forEach { i ->
i.format = format
repository.update(i)
}
}
}
suspend fun updateProcessingDownloadPath(ids: List<Long>, path: String){
ids.chunked(100).forEach {
repository.getAllItemsByIDs(it).forEach { i ->
i.downloadPath = path
repository.update(i)
}
}
} }
fun getProcessingDownloadsCount() : Int { fun getProcessingDownloadsCount() : Int {
@ -687,42 +663,35 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
suspend fun updateProcessingAllFormats(ids: List<Long>, formatCollection: List<List<Format>>) { suspend fun updateProcessingAllFormats(formatCollection: List<List<Format>>) {
ids.chunked(100).forEach { val items = repository.getProcessingDownloads()
val items = repository.getAllItemsByIDs(it) items.forEachIndexed { index, i ->
items.forEachIndexed { index, i -> i.allFormats.clear()
i.allFormats.clear() if (formatCollection.size == items.size && formatCollection[index].isNotEmpty()) {
if (formatCollection.size == items.size && formatCollection[index].isNotEmpty()) { runCatching {
runCatching { i.allFormats.addAll(formatCollection[index])
i.allFormats.addAll(formatCollection[index])
}
} }
i.format = getFormat(i.allFormats, i.type)
kotlin.runCatching {
dbManager.resultDao.getResultByURL(i.url)?.apply {
this.formats = formatCollection[index].toMutableList()
dbManager.resultDao.update(this)
}
}
repository.update(i)
} }
i.format = getFormat(i.allFormats, i.type)
kotlin.runCatching {
dbManager.resultDao.getResultByURL(i.url)?.apply {
this.formats = formatCollection[index].toMutableList()
dbManager.resultDao.update(this)
}
}
repository.update(i)
} }
} }
suspend fun continueUpdatingFormatsOnBackground(itemIDs: List<Long>){ suspend fun continueUpdatingFormatsOnBackground(){
itemIDs.chunked(100).forEach {list -> val ids = dao.getProcessingDownloadsList().map { it.id }
repository.getAllItemsByIDs(list).onEach { dao.updateProcessingtoSavedStatus()
it.status = DownloadRepository.Status.Saved.toString()
repository.update(it)
}
}
val id = System.currentTimeMillis().toInt() val id = System.currentTimeMillis().toInt()
val workRequest = OneTimeWorkRequestBuilder<UpdatePlaylistFormatsWorker>() val workRequest = OneTimeWorkRequestBuilder<UpdatePlaylistFormatsWorker>()
.setInputData( .setInputData(
Data.Builder() Data.Builder()
.putLongArray("ids", itemIDs.toLongArray()) .putLongArray("ids", ids.toLongArray())
.putInt("id", id) .putInt("id", id)
.build()) .build())
.addTag("updateFormats") .addTag("updateFormats")
@ -736,23 +705,22 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
suspend fun updateProcessingType(ids: List<Long>, newType: Type) { suspend fun updateProcessingType(newType: Type) {
ids.chunked(100).forEach { _ -> val processing = repository.getProcessingDownloads()
repository.getAllItemsByIDs(ids).apply { processing.apply {
val new = switchDownloadType(this, newType) val new = switchDownloadType(this, newType)
new.forEach { new.forEach {
repository.update(it) repository.update(it)
}
} }
} }
} }
fun checkIfAllProcessingItemsHaveSameType(ids: List<Long>) : Pair<Boolean, Type> { suspend fun updateProcessingDownloadTime(time: Long) {
val types = mutableSetOf<String>() repository.updateProcessingDownloadTime(time)
ids.chunked(100).forEach { }
types.addAll(dao.getProcessingDownloadTypes(it))
}
fun checkIfAllProcessingItemsHaveSameType() : Pair<Boolean, Type> {
val types = dao.getProcessingDownloadTypes()
return Pair(types.size == 1, Type.valueOf(types.first())) return Pair(types.size == 1, Type.valueOf(types.first()))
} }

View file

@ -321,7 +321,7 @@ class SharedDownloadViewModel(private val context: Context) {
val prefVideo = sharedPreferences.getString("format_importance_video", itemValues.joinToString(","))!! val prefVideo = sharedPreferences.getString("format_importance_video", itemValues.joinToString(","))!!
prefVideo.split(",").forEachIndexed { idx, s -> prefVideo.split(",").forEachIndexed { idx, s ->
val importance = (itemValues.size - idx) * 10 var importance = (itemValues.size - idx) * 10
when(s) { when(s) {
"id" -> { "id" -> {
@ -345,8 +345,9 @@ class SharedDownloadViewModel(private val context: Context) {
for(i in 0..preferenceIndex){ for(i in 0..preferenceIndex){
removeAt(0) removeAt(0)
} }
add(0, preference) requirements.add { it: Format -> if (it.format_note.contains(preference, ignoreCase = true)) importance else 0 }
forEachIndexed { index, res -> forEachIndexed { index, res ->
if (index >= 2) importance = 0
requirements.add { it: Format -> if (it.format_note.contains(res, ignoreCase = true)) (importance - index - 1) else 0 } requirements.add { it: Format -> if (it.format_note.contains(res, ignoreCase = true)) (importance - index - 1) else 0 }
} }
} }

View file

@ -1,165 +0,0 @@
package com.deniscerri.ytdl.services
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.Build
import android.os.IBinder
import androidx.core.content.IntentCompat
import androidx.lifecycle.lifecycleScope
import com.deniscerri.ytdl.database.DBManager
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.SharedDownloadViewModel
import com.deniscerri.ytdl.util.NotificationUtil
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ProcessDownloadsInBackgroundService : Service() {
private val binder: IBinder = LocalBinder()
private val queueProcessingDownloadsJobList = mutableListOf<Job>()
private lateinit var repository: DownloadRepository
private lateinit var resultRepository: ResultRepository
private lateinit var downloadViewModel: SharedDownloadViewModel
inner class LocalBinder: Binder() {
val service: ProcessDownloadsInBackgroundService
get() = this@ProcessDownloadsInBackgroundService
}
override fun onCreate() {
super.onCreate()
val dbManager = DBManager.getInstance(this)
repository = DownloadRepository(dbManager.downloadDao)
resultRepository = ResultRepository(dbManager.resultDao, this)
downloadViewModel = SharedDownloadViewModel(this)
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val binding = intent.getBooleanExtra("binding", false)
if (binding) {
val cancel = intent.getBooleanExtra("cancel", false)
if (cancel) {
cancelAllProcessingJobs()
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO){
repository.deleteProcessing()
withContext(Dispatchers.Main){
if (Build.VERSION.SDK_INT > 23){
stopForeground(STOP_FOREGROUND_REMOVE)
}else{
stopForeground(true)
}
stopSelf()
}
}
}else{
return super.onStartCommand(intent, flags, startId)
}
}
val notificationUtil = NotificationUtil(this)
startForeground(System.currentTimeMillis().toInt(), notificationUtil.createProcessingDownloads())
val itemType = intent.getStringExtra("itemType")!!
val itemIDs = intent.getLongArrayExtra("itemIDs")!!.toList()
val processingItemIDs = intent.getLongArrayExtra("processingItemIDs")!!.toList()
val timeInMillis = intent.getLongExtra("timeInMillis", 0)
val processingFinished = intent.getBooleanExtra("processingFinished", true)
val job = CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
if (!processingFinished) {
when(itemType) {
ResultItem::class.java.toString() -> {
itemIDs.chunked(100).map { ids ->
resultRepository.getAllByIDs(ids).map {
downloadViewModel.createDownloadItemFromResult(
result = it, givenType = DownloadViewModel.Type.valueOf(
downloadViewModel.getDownloadType(url = it.url).toString()
)
)
}.apply {
if (timeInMillis > 0) {
this.forEach {
it.setAsScheduling(timeInMillis)
}
}
if (isActive){
downloadViewModel.queueDownloads(this)
}
}
}
}
DownloadItem::class.java.toString() -> {
itemIDs.chunked(100).map { ids ->
repository.getAllItemsByIDs(ids).apply {
if (timeInMillis > 0) {
this.forEach {
it.setAsScheduling(timeInMillis)
}
}
if (isActive){
downloadViewModel.queueDownloads(this)
}
}
}
}
}
}else {
repository.getProcessingItemsBetweenIDs(processingItemIDs.first(), processingItemIDs.last()).apply {
this.chunked(100).map {
if (timeInMillis > 0){
this.forEach { d ->
d.setAsScheduling(timeInMillis)
}
}
if (isActive){
downloadViewModel.queueDownloads(it)
}
}
}
}
}
job.invokeOnCompletion {
queueProcessingDownloadsJobList.remove(job)
if (queueProcessingDownloadsJobList.isEmpty()){
stopForeground(true)
stopSelf()
}
}
queueProcessingDownloadsJobList.add(job)
return super.onStartCommand(intent, flags, startId)
}
override fun onBind(intent: Intent): IBinder {
return binder
}
private fun DownloadItem.setAsScheduling(timeInMillis: Long) {
status = DownloadRepository.Status.Scheduled.toString()
downloadStartTime = timeInMillis
}
fun cancelAllProcessingJobs(){
queueProcessingDownloadsJobList.onEach { it.cancel(CancellationException()) }
}
}

View file

@ -3,32 +3,23 @@ package com.deniscerri.ytdl.ui.downloadcard
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.content.ComponentName
import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.os.IBinder
import android.text.format.DateFormat
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.widget.Button
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.forEach
import androidx.core.view.get import androidx.core.view.get
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@ -45,13 +36,11 @@ import com.deniscerri.ytdl.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.services.ProcessDownloadsInBackgroundService
import com.deniscerri.ytdl.ui.adapter.ConfigureMultipleDownloadsAdapter import com.deniscerri.ytdl.ui.adapter.ConfigureMultipleDownloadsAdapter
import com.deniscerri.ytdl.util.Extensions.enableFastScroll import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.facebook.shimmer.Shimmer
import com.facebook.shimmer.ShimmerFrameLayout import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -61,22 +50,14 @@ import com.google.android.material.button.MaterialButton
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.SurfaceColors import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.function.Predicate
class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), ConfigureMultipleDownloadsAdapter.OnItemClickListener, View.OnClickListener, class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), ConfigureMultipleDownloadsAdapter.OnItemClickListener, View.OnClickListener,
ConfigureDownloadBottomSheetDialog.OnDownloadItemUpdateListener { ConfigureDownloadBottomSheetDialog.OnDownloadItemUpdateListener {
@ -91,6 +72,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
private lateinit var bottomAppBar: BottomAppBar private lateinit var bottomAppBar: BottomAppBar
private lateinit var filesize : TextView private lateinit var filesize : TextView
private lateinit var count : TextView private lateinit var count : TextView
private lateinit var downloadBtn : MaterialButton
private lateinit var scheduleBtn : MaterialButton
private lateinit var title: TextView private lateinit var title: TextView
private lateinit var subtitle: TextView private lateinit var subtitle: TextView
private lateinit var shimmerTitle: ShimmerFrameLayout private lateinit var shimmerTitle: ShimmerFrameLayout
@ -154,19 +137,14 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
itemTouchHelper.attachToRecyclerView(recyclerView) itemTouchHelper.attachToRecyclerView(recyclerView)
} }
val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button) scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button)
val download = view.findViewById<Button>(R.id.bottomsheet_download_button) downloadBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_download_button)
bottomAppBar = view.findViewById(R.id.bottomAppBar) bottomAppBar = view.findViewById(R.id.bottomAppBar)
val preferredDownloadType = bottomAppBar.menu.findItem(R.id.preferred_download_type) val preferredDownloadType = bottomAppBar.menu.findItem(R.id.preferred_download_type)
filesize = view.findViewById(R.id.filesize) filesize = view.findViewById(R.id.filesize)
count = view.findViewById(R.id.count) count = view.findViewById(R.id.count)
val continueInBackgroundSnackBar = Snackbar.make(recyclerView, R.string.process_downloads_background, Snackbar.LENGTH_LONG)
val snackbarView: View = continueInBackgroundSnackBar.view
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
snackTextView.maxLines = 9999999
title = view.findViewById(R.id.bottom_sheet_title) title = view.findViewById(R.id.bottom_sheet_title)
subtitle = view.findViewById(R.id.bottom_sheet_subtitle) subtitle = view.findViewById(R.id.bottom_sheet_subtitle)
shimmerTitle = view.findViewById(R.id.shimmer_loading_title) shimmerTitle = view.findViewById(R.id.shimmer_loading_title)
@ -174,17 +152,12 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.processingItems.collectLatest { downloadViewModel.processingItems.collectLatest {
val loading = it toggleLoading(it)
toggleLoadingShimmerTitle(loading)
bottomAppBar.menu.children.forEach { m -> m.isEnabled = !loading }
if (!loading){
continueInBackgroundSnackBar.dismiss()
}
} }
} }
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.mostRecentProcessingDownloads.collectLatest { items -> downloadViewModel.processingDownloads.collectLatest { items ->
processingItemsCount = items.size processingItemsCount = items.size
count.text = "${processingItemsCount} ${getString(R.string.selected)}" count.text = "${processingItemsCount} ${getString(R.string.selected)}"
listAdapter.submitList(items) listAdapter.submitList(items)
@ -224,71 +197,31 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
scheduleBtn.setOnClickListener{ scheduleBtn.setOnClickListener{
fun initSchedule(processingFinished: Boolean) { UiUtil.showDatePicker(parentFragmentManager) { cal ->
UiUtil.showDatePicker(parentFragmentManager) { cal -> toggleLoading(true)
scheduleBtn.isEnabled = false
download.isEnabled = false
lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.deleteAllWithID(currentDownloadIDs)
}
getProcessingItemsData()?.apply {
this.job?.cancel(CancellationException())
this.job = null
downloadProcessDownloadsInBackground(this, processingFinished)
}
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(cal.timeInMillis)
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
dismiss()
}
}
}
if (shimmerTitle.isVisible){
continueInBackgroundSnackBar.setAction(R.string.ok) {
initSchedule(false)
}
continueInBackgroundSnackBar.show()
}else{
initSchedule(true)
}
}
download!!.setOnClickListener {
fun initDownload(processingFinished: Boolean) {
scheduleBtn.isEnabled = false
download.isEnabled = false
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.updateProcessingDownloadTime(cal.timeInMillis)
downloadViewModel.deleteAllWithID(currentDownloadIDs) downloadViewModel.deleteAllWithID(currentDownloadIDs)
} downloadViewModel.queueProcessingDownloads()
getProcessingItemsData()?.apply {
this.job?.cancel(CancellationException())
this.job = null
downloadProcessDownloadsInBackground(this, processingFinished)
} }
dismiss() dismiss()
} }
} }
if (shimmerTitle.isVisible){
continueInBackgroundSnackBar.setAction(R.string.ok) {
initDownload(false)
}
continueInBackgroundSnackBar.show()
}else{
initDownload(true)
}
} }
download.setOnLongClickListener { downloadBtn.setOnClickListener {
toggleLoading(true)
lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.deleteAllWithID(currentDownloadIDs)
downloadViewModel.queueProcessingDownloads()
}
dismiss()
}
}
downloadBtn.setOnLongClickListener {
val dd = MaterialAlertDialogBuilder(requireContext()) val dd = MaterialAlertDialogBuilder(requireContext())
dd.setTitle(getString(R.string.save_for_later)) dd.setTitle(getString(R.string.save_for_later))
dd.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } dd.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
@ -296,7 +229,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
lifecycleScope.launch{ lifecycleScope.launch{
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.deleteAllWithID(currentDownloadIDs) downloadViewModel.deleteAllWithID(currentDownloadIDs)
downloadViewModel.moveProcessingToSavedCategory(getProcessingDownloadItemIDs()) downloadViewModel.moveProcessingToSavedCategory()
} }
getProcessingItemsData()?.apply { getProcessingItemsData()?.apply {
this.job?.cancel(CancellationException()) this.job?.cancel(CancellationException())
@ -312,13 +245,13 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
val formatListener = object : OnFormatClickListener { val formatListener = object : OnFormatClickListener {
override fun onFormatClick(selectedFormats: List<FormatTuple>) { override fun onFormatClick(selectedFormats: List<FormatTuple>) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingFormat(getProcessingDownloadItemIDs(), selectedFormats) downloadViewModel.updateProcessingFormat(selectedFormats)
} }
} }
override fun onFormatsUpdated(allFormats: List<List<Format>>) { override fun onFormatsUpdated(allFormats: List<List<Format>>) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingAllFormats(getProcessingDownloadItemIDs(), allFormats) downloadViewModel.updateProcessingAllFormats(allFormats)
} }
} }
@ -326,7 +259,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
override fun onContinueOnBackground() { override fun onContinueOnBackground() {
requireActivity().lifecycleScope.launch { requireActivity().lifecycleScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.continueUpdatingFormatsOnBackground(getProcessingDownloadItemIDs()) downloadViewModel.continueUpdatingFormatsOnBackground()
} }
getProcessingItemsData()?.apply { getProcessingItemsData()?.apply {
this.job?.cancel(CancellationException()) this.job?.cancel(CancellationException())
@ -369,7 +302,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
audio!!.setOnClickListener { audio!!.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingType(getProcessingDownloadItemIDs(), DownloadViewModel.Type.audio) downloadViewModel.updateProcessingType(DownloadViewModel.Type.audio)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24) preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24)
bottomAppBar.menu[1].icon?.alpha = 255 bottomAppBar.menu[1].icon?.alpha = 255
@ -381,7 +314,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
video!!.setOnClickListener { video!!.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch{ CoroutineScope(Dispatchers.IO).launch{
downloadViewModel.updateProcessingType(getProcessingDownloadItemIDs(), DownloadViewModel.Type.video) downloadViewModel.updateProcessingType(DownloadViewModel.Type.video)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_video_file_24) preferredDownloadType.setIcon(R.drawable.baseline_video_file_24)
bottomAppBar.menu[1].icon?.alpha = 255 bottomAppBar.menu[1].icon?.alpha = 255
@ -393,7 +326,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
command!!.setOnClickListener { command!!.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch{ CoroutineScope(Dispatchers.IO).launch{
downloadViewModel.updateProcessingType(getProcessingDownloadItemIDs(), DownloadViewModel.Type.command) downloadViewModel.updateProcessingType(DownloadViewModel.Type.command)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24) preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24)
bottomAppBar.menu[1].icon?.alpha = 255 bottomAppBar.menu[1].icon?.alpha = 255
@ -417,7 +350,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
R.id.format -> { R.id.format -> {
lifecycleScope.launch { lifecycleScope.launch {
val res = withContext(Dispatchers.IO){ val res = withContext(Dispatchers.IO){
downloadViewModel.checkIfAllProcessingItemsHaveSameType(getProcessingDownloadItemIDs()) downloadViewModel.checkIfAllProcessingItemsHaveSameType()
} }
if (!res.first){ if (!res.first){
Toast.makeText(requireContext(), getString(R.string.format_filtering_hint), Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), getString(R.string.format_filtering_hint), Toast.LENGTH_SHORT).show()
@ -435,13 +368,15 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
it.joinToString(" ") { c -> c.content } it.joinToString(" ") { c -> c.content }
) )
CoroutineScope(Dispatchers.IO).launch { lifecycleScope.launch {
downloadViewModel.updateProcessingCommandFormat(getProcessingDownloadItemIDs(), format) withContext(Dispatchers.IO){
downloadViewModel.updateProcessingCommandFormat(format)
}
} }
} }
}else{ }else{
val items = withContext(Dispatchers.IO){ val items = withContext(Dispatchers.IO){
downloadViewModel.getAllByIDs(getProcessingDownloadItemIDs()) downloadViewModel.getProcessingDownloads()
} }
val flatFormatCollection = items.map { it.allFormats }.flatten() val flatFormatCollection = items.map { it.allFormats }.flatten()
val commonFormats = withContext(Dispatchers.IO){ val commonFormats = withContext(Dispatchers.IO){
@ -465,7 +400,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
R.id.more -> { R.id.more -> {
lifecycleScope.launch { lifecycleScope.launch {
val res = withContext(Dispatchers.IO){ val res = withContext(Dispatchers.IO){
downloadViewModel.checkIfAllProcessingItemsHaveSameType(getProcessingDownloadItemIDs()) downloadViewModel.checkIfAllProcessingItemsHaveSameType()
} }
if (!res.first) { if (!res.first) {
Toast.makeText( Toast.makeText(
@ -486,7 +421,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
sheetView.findViewById<View>(R.id.adjust).setPadding(padding,padding,padding,padding) sheetView.findViewById<View>(R.id.adjust).setPadding(padding,padding,padding,padding)
val items = withContext(Dispatchers.IO){ val items = withContext(Dispatchers.IO){
downloadViewModel.getAllByIDs(getProcessingDownloadItemIDs()) downloadViewModel.getProcessingDownloads()
} }
UiUtil.configureAudio( UiUtil.configureAudio(
@ -567,7 +502,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
sheetView.findViewById<View>(R.id.adjust).setPadding(padding,padding,padding,padding) sheetView.findViewById<View>(R.id.adjust).setPadding(padding,padding,padding,padding)
val items = withContext(Dispatchers.IO){ val items = withContext(Dispatchers.IO){
downloadViewModel.getAllByIDs(getProcessingDownloadItemIDs()) downloadViewModel.getProcessingDownloads()
} }
UiUtil.configureVideo( UiUtil.configureVideo(
@ -664,19 +599,27 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
private fun toggleLoadingShimmerTitle(show: Boolean) { private fun toggleLoading(loading: Boolean){
shimmerTitle.isVisible = show shimmerTitle.isVisible = loading
title.isVisible = !show title.isVisible = !loading
shimmerSubtitle.isVisible = show shimmerSubtitle.isVisible = loading
subtitle.isVisible = !show subtitle.isVisible = !loading
if (show){ if (loading){
shimmerTitle.startShimmer() shimmerTitle.startShimmer()
shimmerSubtitle.startShimmer() shimmerSubtitle.startShimmer()
}else{ }else{
shimmerTitle.stopShimmer() shimmerTitle.stopShimmer()
shimmerSubtitle.stopShimmer() shimmerSubtitle.stopShimmer()
} }
scheduleBtn.isEnabled = !loading
downloadBtn.isEnabled = !loading
bottomAppBar.menu.children.forEach { m -> m.isEnabled = !loading }
}
private fun toggleLoadingShimmerTitle(show: Boolean) {
} }
private fun updateFileSize(items: List<Long>){ private fun updateFileSize(items: List<Long>){
@ -705,7 +648,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingDownloadPath(getProcessingDownloadItemIDs(), result.data?.data.toString()) downloadViewModel.updateProcessingDownloadPath(result.data?.data.toString())
} }
val path = FileUtil.formatPath(result.data!!.data.toString()) val path = FileUtil.formatPath(result.data!!.data.toString())
@ -837,12 +780,18 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
override fun onDismiss(dialog: DialogInterface) { override fun onDismiss(dialog: DialogInterface) {
getProcessingItemsData()?.apply { lifecycleScope.launch {
if (this.job != null){ withContext(Dispatchers.IO){
this.job?.cancel(CancellationException()) getProcessingItemsData()?.apply {
downloadViewModel.deleteAllWithID(this.processingDownloadItemIDs) if (this.job?.isActive == true){
this.job?.cancel(CancellationException())
downloadViewModel.deleteAllWithID(this.processingDownloadItemIDs)
}
}
downloadViewModel.deleteProcessing()
} }
} }
super.onDismiss(dialog) super.onDismiss(dialog)
} }
@ -923,24 +872,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
} }
private fun getProcessingDownloadItemIDs() : List<Long> {
return downloadViewModel.processingItemsFlow?.processingDownloadItemIDs ?: listOf()
}
private fun getProcessingItemsData() : DownloadViewModel.ProcessingItemsJob? { private fun getProcessingItemsData() : DownloadViewModel.ProcessingItemsJob? {
return downloadViewModel.processingItemsFlow return downloadViewModel.processingItemsFlow
} }
private fun downloadProcessDownloadsInBackground(jobData: DownloadViewModel.ProcessingItemsJob, processingFinished: Boolean, timeInMillis: Long = 0){
Intent(requireActivity(), ProcessDownloadsInBackgroundService::class.java).also { intent ->
intent.putExtra("itemType", jobData.originItemType)
intent.putExtra("itemIDs", jobData.originItemIDs.toLongArray())
intent.putExtra("processingItemIDs", jobData.processingDownloadItemIDs.toLongArray())
intent.putExtra("timeInMillis", timeInMillis)
intent.putExtra("processingFinished", processingFinished)
requireActivity().startService(intent)
}
}
} }

View file

@ -257,7 +257,24 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
private fun checkRanges(start: String, end: String) : Boolean { private fun checkRanges(start: String, end: String) : Boolean {
return start.isNotBlank() && end.isNotBlank() fromTextInput.error = ""
toTextInput.error = ""
if (start.isBlank() || end.isBlank()) return false
val startValid = start.toInt() >= 0
val endValid = end.toInt() <= resultItemIDs.size
if (!startValid) {
fromTextInput.editText?.setText("")
fromTextInput.error = "Invalid Number"
}
if (!endValid) {
toTextInput.editText?.setText("")
toTextInput.error = "Invalid Number"
}
return startValid && endValid
} }
private fun reset(){ private fun reset(){

View file

@ -29,7 +29,6 @@ import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.services.ProcessDownloadsInBackgroundService
import com.deniscerri.ytdl.util.Extensions.createBadge import com.deniscerri.ytdl.util.Extensions.createBadge
import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
@ -266,7 +265,6 @@ class DownloadQueueMainFragment : Fragment(){
} }
private fun cancelAllDownloads() { private fun cancelAllDownloads() {
cancelBackgroundProcessingDownloads()
workManager.cancelAllWorkByTag("download") workManager.cancelAllWorkByTag("download")
lifecycleScope.launch { lifecycleScope.launch {
val notificationUtil = NotificationUtil(requireContext()) val notificationUtil = NotificationUtil(requireContext())
@ -280,26 +278,4 @@ class DownloadQueueMainFragment : Fragment(){
downloadViewModel.cancelActiveQueued() downloadViewModel.cancelActiveQueued()
} }
} }
private fun cancelBackgroundProcessingDownloads(){
val connection = object : ServiceConnection {
private lateinit var mService: ProcessDownloadsInBackgroundService
private var mBound: Boolean = false
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = service as ProcessDownloadsInBackgroundService.LocalBinder
mService = binder.service
mBound = true
}
override fun onServiceDisconnected(arg0: ComponentName) {
mBound = false
}
}
Intent(requireActivity(), ProcessDownloadsInBackgroundService::class.java).also { intent ->
intent.putExtra("binding", true)
intent.putExtra("cancel", true)
requireActivity().bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
}
} }

View file

@ -26,7 +26,6 @@ import com.deniscerri.ytdl.receiver.CancelDownloadNotificationReceiver
import com.deniscerri.ytdl.receiver.CancelWorkReceiver import com.deniscerri.ytdl.receiver.CancelWorkReceiver
import com.deniscerri.ytdl.receiver.PauseDownloadNotificationReceiver import com.deniscerri.ytdl.receiver.PauseDownloadNotificationReceiver
import com.deniscerri.ytdl.receiver.ResumeActivity import com.deniscerri.ytdl.receiver.ResumeActivity
import com.deniscerri.ytdl.services.ProcessDownloadsInBackgroundService
import com.deniscerri.ytdl.util.Extensions.toBitmap import com.deniscerri.ytdl.util.Extensions.toBitmap
import java.io.File import java.io.File
@ -643,36 +642,6 @@ class NotificationUtil(var context: Context) {
notificationManager.notify(QUERY_PROCESS_FINISHED_NOTIFICATION_ID, notificationBuilder.build()) notificationManager.notify(QUERY_PROCESS_FINISHED_NOTIFICATION_ID, notificationBuilder.build())
} }
fun createProcessingDownloads() : Notification{
val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID)
val intent = Intent(context, ProcessDownloadsInBackgroundService::class.java)
intent.putExtra("binding", true)
intent.putExtra("cancel", true)
val cancelNotificationPendingIntent = PendingIntent.getService(
context,
System.currentTimeMillis().toInt(),
intent,
PendingIntent.FLAG_IMMUTABLE
)
notificationBuilder
.setContentTitle(resources.getString(R.string.processing))
.setSmallIcon(R.drawable.ic_app_icon)
.setLargeIcon(
BitmapFactory.decodeResource(
resources,
R.drawable.ic_app_icon
)
)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.clearActions()
.addAction(0, resources.getString(R.string.cancel), cancelNotificationPendingIntent)
return notificationBuilder.build()
}
companion object { companion object {
const val DOWNLOAD_SERVICE_CHANNEL_ID = "1" const val DOWNLOAD_SERVICE_CHANNEL_ID = "1"
const val COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID = "2" const val COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID = "2"

View file

@ -25,6 +25,7 @@
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_marginStart="50dp" android:layout_marginStart="50dp"
android:layout_marginTop="10dp"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
@ -32,6 +33,7 @@
style="@style/Widget.Material3.TextInputLayout.FilledBox.Dense" style="@style/Widget.Material3.TextInputLayout.FilledBox.Dense"
android:layout_width="70dp" android:layout_width="70dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:errorEnabled="true"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:hint="@string/start"> android:hint="@string/start">
@ -48,6 +50,7 @@
android:layout_width="70dp" android:layout_width="70dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/end" android:hint="@string/end"
app:errorEnabled="true"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/colon"> app:layout_constraintStart_toEndOf="@+id/colon">

View file

@ -391,8 +391,7 @@
<string name="minute">Phút</string> <string name="minute">Phút</string>
<string name="second">Giây</string> <string name="second">Giây</string>
<string name="milliseconds">Mili-giây</string> <string name="milliseconds">Mili-giây</string>
<string name="format_importance_video">Định dạng theo thứ tự quan trọng (Video)</string> <string name="format_importance">Định dạng theo thứ tự quan trọng</string>
<string name="format_importance_audio">Định dạng theo thứ tự quan trọng (Âm thanh)</string>
<string name="format_importance_note">Điều chỉnh định dạng theo tầm quan trọng của phần tử. Thứ tự này sẽ được sử dụng khi ứng dụng tìm nạp các định dạng trong thẻ tải xuống và tự động chọn định dạng!</string> <string name="format_importance_note">Điều chỉnh định dạng theo tầm quan trọng của phần tử. Thứ tự này sẽ được sử dụng khi ứng dụng tìm nạp các định dạng trong thẻ tải xuống và tự động chọn định dạng!</string>
<string name="no_audio">Video không âm thanh</string> <string name="no_audio">Video không âm thanh</string>
<string name="enable_alarm_permission">Bạn cần cho phép quyền SCHEDULE_EXACT_ALARM trong cài đặt ứng dụng.</string> <string name="enable_alarm_permission">Bạn cần cho phép quyền SCHEDULE_EXACT_ALARM trong cài đặt ứng dụng.</string>

View file

@ -62,6 +62,7 @@
app:defaultValue="false" app:defaultValue="false"
android:icon="@drawable/baseline_layers_24" android:icon="@drawable/baseline_layers_24"
android:key="display_over_apps" android:key="display_over_apps"
app:isPreferenceVisible="false"
app:summary="@string/display_over_apps_summary" app:summary="@string/display_over_apps_summary"
app:title="@string/display_over_apps" /> app:title="@string/display_over_apps" />

View file

@ -0,0 +1,8 @@
## Shortest changelog ever
- Fixed multiple download card issues. Processing them in the background was not a good idea for most users as it was confusing and users thought it wasnt doing anything. It led to duplicate downloads
Usually playlist downloads dont really have 4000 items in them so background. If you have one, just enable quick download and consider the whole playlist as a single item. :)
- Some users had issues with the display over apps perm. Since that perm ended up not doing anything to the revanced issue, i removed it
Had to push this out since multiple download card was almost broken