mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-28 16:00:31 +05:30
Fix notification action, download fragment and resource leak
- Bind service when service started using notification resume action. - Use `HttpURLConnection` to download file. - Use progress bar to determine overall progress.
This commit is contained in:
parent
8af9e20748
commit
4f0f9b7560
@ -77,7 +77,7 @@ class LibreTubeApp : Application() {
|
|||||||
private fun initializeNotificationChannels() {
|
private fun initializeNotificationChannels() {
|
||||||
val downloadChannel = NotificationChannelCompat.Builder(
|
val downloadChannel = NotificationChannelCompat.Builder(
|
||||||
DOWNLOAD_CHANNEL_ID,
|
DOWNLOAD_CHANNEL_ID,
|
||||||
NotificationManagerCompat.IMPORTANCE_DEFAULT
|
NotificationManagerCompat.IMPORTANCE_LOW
|
||||||
)
|
)
|
||||||
.setName(getString(R.string.download_channel_name))
|
.setName(getString(R.string.download_channel_name))
|
||||||
.setDescription(getString(R.string.download_channel_description))
|
.setDescription(getString(R.string.download_channel_description))
|
||||||
|
@ -15,4 +15,5 @@ object IntentData {
|
|||||||
const val audioFormat = "audioFormate"
|
const val audioFormat = "audioFormate"
|
||||||
const val audioQuality = "audioQuality"
|
const val audioQuality = "audioQuality"
|
||||||
const val subtitleCode = "subtitleCode"
|
const val subtitleCode = "subtitleCode"
|
||||||
|
const val downloading = "downloading"
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ interface DownloadDao {
|
|||||||
@Query("SELECT * FROM downloadItem WHERE path = :path")
|
@Query("SELECT * FROM downloadItem WHERE path = :path")
|
||||||
fun findDownloadItemByFilePath(path: String): DownloadItem
|
fun findDownloadItemByFilePath(path: String): DownloadItem
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
fun insertDownload(download: Download)
|
fun insertDownload(download: Download)
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
@ -8,13 +8,14 @@ import java.net.URL
|
|||||||
suspend fun URL.getContentLength(def: Long = -1): Long {
|
suspend fun URL.getContentLength(def: Long = -1): Long {
|
||||||
try {
|
try {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
val con = openConnection() as HttpURLConnection
|
val connection = openConnection() as HttpURLConnection
|
||||||
con.setRequestProperty("Range", "bytes=0-")
|
connection.setRequestProperty("Range", "bytes=0-")
|
||||||
|
|
||||||
val value = con.getHeaderField("content-length")
|
val value = connection.getHeaderField("content-length")
|
||||||
// If connection accepts range header, try to get total bytes
|
// If connection accepts range header, try to get total bytes
|
||||||
?: con.getHeaderField("content-range").split("/")[1]
|
?: connection.getHeaderField("content-range").split("/")[1]
|
||||||
|
|
||||||
|
connection.disconnect()
|
||||||
value.toLong()
|
value.toLong()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) { e.printStackTrace() }
|
} catch (e: Exception) { e.printStackTrace() }
|
||||||
|
@ -23,7 +23,9 @@ fun Streams.toDownloadItems(
|
|||||||
videoId = videoId,
|
videoId = videoId,
|
||||||
fileName = fileName + "." + stream?.mimeType?.split("/")?.last(),
|
fileName = fileName + "." + stream?.mimeType?.split("/")?.last(),
|
||||||
path = "",
|
path = "",
|
||||||
url = stream?.url
|
url = stream?.url,
|
||||||
|
format = videoFormat,
|
||||||
|
quality = videoQuality
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -36,7 +38,9 @@ fun Streams.toDownloadItems(
|
|||||||
videoId = videoId,
|
videoId = videoId,
|
||||||
fileName = fileName + "." + stream?.mimeType?.split("/")?.last(),
|
fileName = fileName + "." + stream?.mimeType?.split("/")?.last(),
|
||||||
path = "",
|
path = "",
|
||||||
url = stream?.url
|
url = stream?.url,
|
||||||
|
format = audioFormat,
|
||||||
|
quality = audioQuality
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,11 @@ package com.github.libretube.obj
|
|||||||
|
|
||||||
sealed class DownloadStatus {
|
sealed class DownloadStatus {
|
||||||
|
|
||||||
object Unknown : DownloadStatus()
|
|
||||||
|
|
||||||
object Completed : DownloadStatus()
|
object Completed : DownloadStatus()
|
||||||
|
|
||||||
object Paused : DownloadStatus()
|
object Paused : DownloadStatus()
|
||||||
|
|
||||||
data class Progress(val downloaded: Long, val total: Long) : DownloadStatus()
|
data class Progress(val progress: Long, val downloaded: Long, val total: Long) : DownloadStatus()
|
||||||
|
|
||||||
data class Error(val message: String, val cause: Throwable? = null) : DownloadStatus()
|
data class Error(val message: String, val cause: Throwable? = null) : DownloadStatus()
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
package com.github.libretube.receivers
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import com.github.libretube.constants.IntentData
|
||||||
|
import com.github.libretube.services.DownloadService
|
||||||
|
import com.github.libretube.ui.activities.MainActivity
|
||||||
|
|
||||||
|
class DownloadReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
val activityIntent = Intent(context, MainActivity::class.java)
|
||||||
|
|
||||||
|
when (intent?.action) {
|
||||||
|
DownloadService.ACTION_SERVICE_STARTED -> {
|
||||||
|
activityIntent.putExtra(IntentData.downloading, true)
|
||||||
|
}
|
||||||
|
DownloadService.ACTION_SERVICE_STOPPED -> {
|
||||||
|
activityIntent.putExtra(IntentData.downloading, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context?.startActivity(activityIntent)
|
||||||
|
}
|
||||||
|
}
|
@ -23,4 +23,9 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
context?.startService(serviceIntent)
|
context?.startService(serviceIntent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ACTION_DOWNLOAD_RESUME = "com.github.libretube.receivers.NotificationReceiver.ACTION_DOWNLOAD_RESUME"
|
||||||
|
const val ACTION_DOWNLOAD_PAUSE = "com.github.libretube.receivers.NotificationReceiver.ACTION_DOWNLOAD_PAUSE"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import com.github.libretube.api.RetrofitInstance
|
|||||||
import com.github.libretube.constants.DOWNLOAD_CHANNEL_ID
|
import com.github.libretube.constants.DOWNLOAD_CHANNEL_ID
|
||||||
import com.github.libretube.constants.DOWNLOAD_PROGRESS_NOTIFICATION_ID
|
import com.github.libretube.constants.DOWNLOAD_PROGRESS_NOTIFICATION_ID
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.db.DatabaseHolder
|
import com.github.libretube.db.DatabaseHolder.Companion.Database
|
||||||
import com.github.libretube.db.obj.Download
|
import com.github.libretube.db.obj.Download
|
||||||
import com.github.libretube.db.obj.DownloadItem
|
import com.github.libretube.db.obj.DownloadItem
|
||||||
import com.github.libretube.enums.FileType
|
import com.github.libretube.enums.FileType
|
||||||
@ -25,29 +25,29 @@ import com.github.libretube.extensions.toDownloadItems
|
|||||||
import com.github.libretube.extensions.toastFromMainThread
|
import com.github.libretube.extensions.toastFromMainThread
|
||||||
import com.github.libretube.obj.DownloadStatus
|
import com.github.libretube.obj.DownloadStatus
|
||||||
import com.github.libretube.receivers.NotificationReceiver
|
import com.github.libretube.receivers.NotificationReceiver
|
||||||
|
import com.github.libretube.receivers.NotificationReceiver.Companion.ACTION_DOWNLOAD_PAUSE
|
||||||
|
import com.github.libretube.receivers.NotificationReceiver.Companion.ACTION_DOWNLOAD_RESUME
|
||||||
import com.github.libretube.ui.activities.MainActivity
|
import com.github.libretube.ui.activities.MainActivity
|
||||||
import com.github.libretube.util.DownloadHelper
|
import com.github.libretube.util.DownloadHelper
|
||||||
import com.github.libretube.util.ImageHelper
|
import com.github.libretube.util.ImageHelper
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okio.BufferedSink
|
import okio.BufferedSink
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
|
import okio.source
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import kotlin.coroutines.coroutineContext
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download service with custom implementation of downloading using [OkHttpClient].
|
* Download service with custom implementation of downloading using [HttpURLConnection].
|
||||||
*/
|
*/
|
||||||
class DownloadService : Service() {
|
class DownloadService : Service() {
|
||||||
|
|
||||||
@ -59,6 +59,7 @@ class DownloadService : Service() {
|
|||||||
private lateinit var summaryNotificationBuilder: NotificationCompat.Builder
|
private lateinit var summaryNotificationBuilder: NotificationCompat.Builder
|
||||||
|
|
||||||
private val jobs = mutableMapOf<Int, Job>()
|
private val jobs = mutableMapOf<Int, Job>()
|
||||||
|
private val downloadQueue = mutableMapOf<Int, Boolean>()
|
||||||
private val _downloadFlow = MutableSharedFlow<Pair<Int, DownloadStatus>>()
|
private val _downloadFlow = MutableSharedFlow<Pair<Int, DownloadStatus>>()
|
||||||
val downloadFlow: SharedFlow<Pair<Int, DownloadStatus>> = _downloadFlow
|
val downloadFlow: SharedFlow<Pair<Int, DownloadStatus>> = _downloadFlow
|
||||||
|
|
||||||
@ -66,12 +67,13 @@ class DownloadService : Service() {
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
IS_DOWNLOAD_RUNNING = true
|
IS_DOWNLOAD_RUNNING = true
|
||||||
notifyForeground()
|
notifyForeground()
|
||||||
|
sendBroadcast(Intent(ACTION_SERVICE_STARTED))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
when (intent?.action) {
|
when (intent?.action) {
|
||||||
ACTION_RESUME -> resume(intent.getIntExtra("id", -1))
|
ACTION_DOWNLOAD_RESUME -> resume(intent.getIntExtra("id", -1))
|
||||||
ACTION_PAUSE -> pause(intent.getIntExtra("id", -1))
|
ACTION_DOWNLOAD_PAUSE -> pause(intent.getIntExtra("id", -1))
|
||||||
}
|
}
|
||||||
|
|
||||||
val videoId = intent?.getStringExtra(IntentData.videoId) ?: return START_NOT_STICKY
|
val videoId = intent?.getStringExtra(IntentData.videoId) ?: return START_NOT_STICKY
|
||||||
@ -87,7 +89,7 @@ class DownloadService : Service() {
|
|||||||
val streams = RetrofitInstance.api.getStreams(videoId)
|
val streams = RetrofitInstance.api.getStreams(videoId)
|
||||||
|
|
||||||
awaitQuery {
|
awaitQuery {
|
||||||
DatabaseHolder.Database.downloadDao().insertDownload(
|
Database.downloadDao().insertDownload(
|
||||||
Download(
|
Download(
|
||||||
videoId = videoId,
|
videoId = videoId,
|
||||||
title = streams.title ?: "",
|
title = streams.title ?: "",
|
||||||
@ -101,7 +103,17 @@ class DownloadService : Service() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
streams.thumbnailUrl?.let { url ->
|
streams.thumbnailUrl?.let { url ->
|
||||||
ImageHelper.downloadImage(this@DownloadService, url, fileName)
|
ImageHelper.downloadImage(
|
||||||
|
this@DownloadService,
|
||||||
|
url,
|
||||||
|
File(
|
||||||
|
DownloadHelper.getDownloadDir(
|
||||||
|
this@DownloadService,
|
||||||
|
DownloadHelper.THUMBNAIL_DIR
|
||||||
|
),
|
||||||
|
fileName
|
||||||
|
).absolutePath
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val downloadItems = streams.toDownloadItems(
|
val downloadItems = streams.toDownloadItems(
|
||||||
@ -154,7 +166,7 @@ class DownloadService : Service() {
|
|||||||
item.path = file.absolutePath
|
item.path = file.absolutePath
|
||||||
|
|
||||||
item.id = awaitQuery {
|
item.id = awaitQuery {
|
||||||
DatabaseHolder.Database.downloadDao().insertDownloadItem(item)
|
Database.downloadDao().insertDownloadItem(item)
|
||||||
}.toInt()
|
}.toInt()
|
||||||
|
|
||||||
jobs[item.id] = scope.launch {
|
jobs[item.id] = scope.launch {
|
||||||
@ -167,14 +179,9 @@ class DownloadService : Service() {
|
|||||||
* and notification.
|
* and notification.
|
||||||
*/
|
*/
|
||||||
private suspend fun downloadFile(item: DownloadItem) {
|
private suspend fun downloadFile(item: DownloadItem) {
|
||||||
|
downloadQueue[item.id] = true
|
||||||
val notificationBuilder = getNotificationBuilder(item)
|
val notificationBuilder = getNotificationBuilder(item)
|
||||||
setResumeNotification(notificationBuilder, item)
|
setResumeNotification(notificationBuilder, item)
|
||||||
|
|
||||||
val okHttpClient = OkHttpClient.Builder()
|
|
||||||
.connectTimeout(DownloadHelper.DEFAULT_TIMEOUT, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(DownloadHelper.DEFAULT_TIMEOUT, TimeUnit.SECONDS)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val file = File(item.path)
|
val file = File(item.path)
|
||||||
var totalRead = file.length()
|
var totalRead = file.length()
|
||||||
val url = URL(item.url ?: return)
|
val url = URL(item.url ?: return)
|
||||||
@ -183,50 +190,69 @@ class DownloadService : Service() {
|
|||||||
if (size > 0 && size != item.downloadSize) {
|
if (size > 0 && size != item.downloadSize) {
|
||||||
item.downloadSize = size
|
item.downloadSize = size
|
||||||
query {
|
query {
|
||||||
DatabaseHolder.Database.downloadDao().updateDownloadItem(item)
|
Database.downloadDao().updateDownloadItem(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set start range where last downloading was held.
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(url)
|
|
||||||
.addHeader("Range", "bytes=$totalRead-").build()
|
|
||||||
var lastTime = System.currentTimeMillis() / 1000
|
|
||||||
var lastRead: Long = 0
|
|
||||||
val sink: BufferedSink = file.sink(true).buffer()
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val response = okHttpClient.newCall(request).execute()
|
// Set start range where last downloading was held.
|
||||||
val sourceBytes = response.body!!.source()
|
val con = url.openConnection() as HttpURLConnection
|
||||||
|
con.requestMethod = "GET"
|
||||||
|
con.setRequestProperty("Range", "bytes=$totalRead-")
|
||||||
|
con.connectTimeout = DownloadHelper.DEFAULT_TIMEOUT
|
||||||
|
con.readTimeout = DownloadHelper.DEFAULT_TIMEOUT
|
||||||
|
con.connect()
|
||||||
|
|
||||||
// Check if job is still active and read next bytes.
|
if (con.responseCode !in 200..299) {
|
||||||
while (coroutineContext.isActive &&
|
val message = getString(R.string.downloadfailed) + ": " + con.responseMessage
|
||||||
sourceBytes
|
_downloadFlow.emit(item.id to DownloadStatus.Error(message))
|
||||||
.read(sink.buffer, DownloadHelper.DOWNLOAD_CHUNK_SIZE)
|
toastFromMainThread(message)
|
||||||
.also { lastRead = it } != -1L
|
|
||||||
) {
|
|
||||||
sink.emit()
|
|
||||||
totalRead += lastRead
|
|
||||||
_downloadFlow.emit(item.id to DownloadStatus.Progress(totalRead, item.downloadSize))
|
|
||||||
|
|
||||||
if (item.downloadSize != -1L &&
|
|
||||||
System.currentTimeMillis() / 1000 > lastTime
|
|
||||||
) {
|
|
||||||
notificationBuilder
|
|
||||||
.setContentText("${totalRead.formatAsFileSize()} / ${item.downloadSize.formatAsFileSize()}")
|
|
||||||
.setProgress(item.downloadSize.toInt(), totalRead.toInt(), false)
|
|
||||||
notificationManager.notify(item.id, notificationBuilder.build())
|
|
||||||
lastTime = System.currentTimeMillis() / 1000
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
toastFromMainThread("${getString(R.string.download)}: ${e.message.toString()}")
|
val sink: BufferedSink = file.sink(true).buffer()
|
||||||
_downloadFlow.emit(item.id to DownloadStatus.Error(e.message.toString(), e))
|
val sourceByte = con.inputStream.source()
|
||||||
} finally {
|
|
||||||
|
var lastTime = System.currentTimeMillis() / 1000
|
||||||
|
var lastRead: Long = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if downloading is still active and read next bytes.
|
||||||
|
while (downloadQueue[item.id] == true &&
|
||||||
|
sourceByte
|
||||||
|
.read(sink.buffer, DownloadHelper.DOWNLOAD_CHUNK_SIZE)
|
||||||
|
.also { lastRead = it } != -1L
|
||||||
|
) {
|
||||||
|
sink.emit()
|
||||||
|
totalRead += lastRead
|
||||||
|
_downloadFlow.emit(
|
||||||
|
item.id to DownloadStatus.Progress(
|
||||||
|
lastRead,
|
||||||
|
totalRead,
|
||||||
|
item.downloadSize
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (item.downloadSize != -1L &&
|
||||||
|
System.currentTimeMillis() / 1000 > lastTime
|
||||||
|
) {
|
||||||
|
notificationBuilder
|
||||||
|
.setContentText("${totalRead.formatAsFileSize()} / ${item.downloadSize.formatAsFileSize()}")
|
||||||
|
.setProgress(item.downloadSize.toInt(), totalRead.toInt(), false)
|
||||||
|
notificationManager.notify(item.id, notificationBuilder.build())
|
||||||
|
lastTime = System.currentTimeMillis() / 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: CancellationException) {
|
||||||
|
} catch (e: Exception) {
|
||||||
|
toastFromMainThread("${getString(R.string.download)}: ${e.message}")
|
||||||
|
_downloadFlow.emit(item.id to DownloadStatus.Error(e.message.toString(), e))
|
||||||
|
}
|
||||||
|
|
||||||
sink.flush()
|
sink.flush()
|
||||||
sink.close()
|
sink.close()
|
||||||
}
|
sourceByte.close()
|
||||||
|
con.disconnect()
|
||||||
|
} catch (_: Exception) { }
|
||||||
|
|
||||||
val completed = when (totalRead) {
|
val completed = when (totalRead) {
|
||||||
item.downloadSize -> {
|
item.downloadSize -> {
|
||||||
@ -238,45 +264,46 @@ class DownloadService : Service() {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pause(item.id)
|
|
||||||
setPauseNotification(notificationBuilder, item, completed)
|
setPauseNotification(notificationBuilder, item, completed)
|
||||||
|
pause(item.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resume download which may have been paused.
|
* Resume download which may have been paused.
|
||||||
*/
|
*/
|
||||||
fun resume(id: Int) {
|
fun resume(id: Int) {
|
||||||
// Cancel last job if it is still active to avoid multiple
|
// If file is already downloading then avoid new download job.
|
||||||
// jobs for same file.
|
if (downloadQueue[id] == true) return
|
||||||
jobs[id]?.cancel()
|
|
||||||
val downloadItem = awaitQuery {
|
val downloadItem = awaitQuery {
|
||||||
DatabaseHolder.Database.downloadDao().findDownloadItemById(id)
|
Database.downloadDao().findDownloadItemById(id)
|
||||||
}
|
}
|
||||||
jobs[id] = scope.launch {
|
scope.launch {
|
||||||
downloadFile(downloadItem)
|
downloadFile(downloadItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pause downloading job for given [id]. If no [jobs] are active, stop the service.
|
* Pause downloading job for given [id]. If no downloads are active, stop the service.
|
||||||
*/
|
*/
|
||||||
fun pause(id: Int) {
|
fun pause(id: Int) {
|
||||||
jobs[id]?.cancel()
|
downloadQueue[id] = false
|
||||||
|
|
||||||
// Stop the service if no downloads are active.
|
// Stop the service if no downloads are active.
|
||||||
if (jobs.values.none { it.isActive }) {
|
if (downloadQueue.none { it.value }) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
stopForeground(STOP_FOREGROUND_DETACH)
|
stopForeground(STOP_FOREGROUND_DETACH)
|
||||||
}
|
}
|
||||||
|
sendBroadcast(Intent(ACTION_SERVICE_STOPPED))
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether the file downloading job is active or not.
|
* Check whether the file downloading or not.
|
||||||
*/
|
*/
|
||||||
fun isDownloading(id: Int): Boolean {
|
fun isDownloading(id: Int): Boolean {
|
||||||
return jobs[id]?.isActive ?: false
|
return downloadQueue[id] ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyForeground() {
|
private fun notifyForeground() {
|
||||||
@ -314,7 +341,7 @@ class DownloadService : Service() {
|
|||||||
|
|
||||||
return NotificationCompat
|
return NotificationCompat
|
||||||
.Builder(this, DOWNLOAD_CHANNEL_ID)
|
.Builder(this, DOWNLOAD_CHANNEL_ID)
|
||||||
.setContentTitle(getString(R.string.downloading))
|
.setContentTitle("[${item.type}] ${item.fileName}")
|
||||||
.setProgress(0, 0, true)
|
.setProgress(0, 0, true)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setContentIntent(activityIntent)
|
.setContentIntent(activityIntent)
|
||||||
@ -325,7 +352,6 @@ class DownloadService : Service() {
|
|||||||
|
|
||||||
private fun setResumeNotification(notificationBuilder: NotificationCompat.Builder, item: DownloadItem) {
|
private fun setResumeNotification(notificationBuilder: NotificationCompat.Builder, item: DownloadItem) {
|
||||||
notificationBuilder
|
notificationBuilder
|
||||||
.setContentTitle(item.fileName)
|
|
||||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
.setWhen(System.currentTimeMillis())
|
.setWhen(System.currentTimeMillis())
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
@ -335,7 +361,11 @@ class DownloadService : Service() {
|
|||||||
notificationManager.notify(item.id, notificationBuilder.build())
|
notificationManager.notify(item.id, notificationBuilder.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setPauseNotification(notificationBuilder: NotificationCompat.Builder, item: DownloadItem, isCompleted: Boolean = false) {
|
private fun setPauseNotification(
|
||||||
|
notificationBuilder: NotificationCompat.Builder,
|
||||||
|
item: DownloadItem,
|
||||||
|
isCompleted: Boolean = false
|
||||||
|
) {
|
||||||
notificationBuilder
|
notificationBuilder
|
||||||
.setProgress(0, 0, false)
|
.setProgress(0, 0, false)
|
||||||
.setOngoing(false)
|
.setOngoing(false)
|
||||||
@ -357,7 +387,7 @@ class DownloadService : Service() {
|
|||||||
private fun getResumeAction(id: Int): NotificationCompat.Action {
|
private fun getResumeAction(id: Int): NotificationCompat.Action {
|
||||||
val intent = Intent(this, NotificationReceiver::class.java)
|
val intent = Intent(this, NotificationReceiver::class.java)
|
||||||
|
|
||||||
intent.action = ACTION_RESUME
|
intent.action = ACTION_DOWNLOAD_RESUME
|
||||||
intent.putExtra("id", id)
|
intent.putExtra("id", id)
|
||||||
|
|
||||||
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
@ -376,7 +406,7 @@ class DownloadService : Service() {
|
|||||||
private fun getPauseAction(id: Int): NotificationCompat.Action {
|
private fun getPauseAction(id: Int): NotificationCompat.Action {
|
||||||
val intent = Intent(this, NotificationReceiver::class.java)
|
val intent = Intent(this, NotificationReceiver::class.java)
|
||||||
|
|
||||||
intent.action = ACTION_PAUSE
|
intent.action = ACTION_DOWNLOAD_PAUSE
|
||||||
intent.putExtra("id", id)
|
intent.putExtra("id", id)
|
||||||
|
|
||||||
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
@ -393,12 +423,15 @@ class DownloadService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
jobMain.cancel()
|
downloadQueue.clear()
|
||||||
IS_DOWNLOAD_RUNNING = false
|
IS_DOWNLOAD_RUNNING = false
|
||||||
|
sendBroadcast(Intent(ACTION_SERVICE_STOPPED))
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder {
|
override fun onBind(intent: Intent?): IBinder {
|
||||||
|
val ids = intent?.getIntArrayExtra("ids")
|
||||||
|
ids?.forEach { id -> resume(id) }
|
||||||
return binder
|
return binder
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,8 +441,10 @@ class DownloadService : Service() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val DOWNLOAD_NOTIFICATION_GROUP = "download_notification_group"
|
private const val DOWNLOAD_NOTIFICATION_GROUP = "download_notification_group"
|
||||||
const val ACTION_RESUME = "com.github.libretube.services.DownloadService.ACTION_RESUME"
|
const val ACTION_SERVICE_STARTED =
|
||||||
const val ACTION_PAUSE = "com.github.libretube.services.DownloadService.ACTION_PAUSE"
|
"com.github.libretube.services.DownloadService.ACTION_SERVICE_STARTED"
|
||||||
|
const val ACTION_SERVICE_STOPPED =
|
||||||
|
"com.github.libretube.services.DownloadService.ACTION_SERVICE_STOPPED"
|
||||||
var IS_DOWNLOAD_RUNNING = false
|
var IS_DOWNLOAD_RUNNING = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import androidx.core.view.children
|
|||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import androidx.navigation.ui.setupWithNavController
|
import androidx.navigation.ui.setupWithNavController
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
@ -30,6 +31,7 @@ import com.github.libretube.extensions.toID
|
|||||||
import com.github.libretube.services.ClosingService
|
import com.github.libretube.services.ClosingService
|
||||||
import com.github.libretube.ui.base.BaseActivity
|
import com.github.libretube.ui.base.BaseActivity
|
||||||
import com.github.libretube.ui.dialogs.ErrorDialog
|
import com.github.libretube.ui.dialogs.ErrorDialog
|
||||||
|
import com.github.libretube.ui.fragments.DownloadsFragment
|
||||||
import com.github.libretube.ui.fragments.PlayerFragment
|
import com.github.libretube.ui.fragments.PlayerFragment
|
||||||
import com.github.libretube.ui.models.PlayerViewModel
|
import com.github.libretube.ui.models.PlayerViewModel
|
||||||
import com.github.libretube.ui.models.SearchViewModel
|
import com.github.libretube.ui.models.SearchViewModel
|
||||||
@ -382,11 +384,19 @@ class MainActivity : BaseActivity() {
|
|||||||
navController.navigate(R.id.subscriptionsFragment)
|
navController.navigate(R.id.subscriptionsFragment)
|
||||||
"library" ->
|
"library" ->
|
||||||
navController.navigate(R.id.libraryFragment)
|
navController.navigate(R.id.libraryFragment)
|
||||||
|
"downloads" ->
|
||||||
|
navController.navigate(R.id.downloadsFragment)
|
||||||
}
|
}
|
||||||
if (intent?.getBooleanExtra(IntentData.openQueueOnce, false) == true) {
|
if (intent?.getBooleanExtra(IntentData.openQueueOnce, false) == true) {
|
||||||
PlayingQueueSheet()
|
PlayingQueueSheet()
|
||||||
.show(supportFragmentManager)
|
.show(supportFragmentManager)
|
||||||
}
|
}
|
||||||
|
if (intent?.getBooleanExtra(IntentData.downloading, false) == true) {
|
||||||
|
(supportFragmentManager.fragments.find { it is NavHostFragment })
|
||||||
|
?.childFragmentManager?.fragments?.forEach { fragment ->
|
||||||
|
(fragment as? DownloadsFragment)?.bindDownloadService()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadVideo(videoId: String, timeStamp: Long?) {
|
private fun loadVideo(videoId: String, timeStamp: Long?) {
|
||||||
|
@ -33,7 +33,6 @@ import java.io.File
|
|||||||
|
|
||||||
class OfflinePlayerActivity : BaseActivity() {
|
class OfflinePlayerActivity : BaseActivity() {
|
||||||
private lateinit var binding: ActivityOfflinePlayerBinding
|
private lateinit var binding: ActivityOfflinePlayerBinding
|
||||||
private lateinit var fileName: String
|
|
||||||
private lateinit var videoId: String
|
private lateinit var videoId: String
|
||||||
private lateinit var player: ExoPlayer
|
private lateinit var player: ExoPlayer
|
||||||
private lateinit var playerView: StyledPlayerView
|
private lateinit var playerView: StyledPlayerView
|
||||||
|
@ -1,24 +1,29 @@
|
|||||||
package com.github.libretube.ui.adapters
|
package com.github.libretube.ui.adapters
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.databinding.DownloadedMediaRowBinding
|
import com.github.libretube.databinding.DownloadedMediaRowBinding
|
||||||
import com.github.libretube.extensions.formatShort
|
import com.github.libretube.db.DatabaseHolder
|
||||||
import com.github.libretube.obj.DownloadedFile
|
import com.github.libretube.db.obj.DownloadWithItems
|
||||||
|
import com.github.libretube.extensions.formatAsFileSize
|
||||||
|
import com.github.libretube.extensions.query
|
||||||
import com.github.libretube.ui.activities.OfflinePlayerActivity
|
import com.github.libretube.ui.activities.OfflinePlayerActivity
|
||||||
import com.github.libretube.ui.viewholders.DownloadsViewHolder
|
import com.github.libretube.ui.viewholders.DownloadsViewHolder
|
||||||
import com.github.libretube.util.DownloadHelper
|
import com.github.libretube.util.ImageHelper
|
||||||
import com.github.libretube.util.TextUtils
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class DownloadsAdapter(
|
class DownloadsAdapter(
|
||||||
private val files: MutableList<DownloadedFile>
|
private val context: Context,
|
||||||
|
private val downloads: MutableList<DownloadWithItems>,
|
||||||
|
private val toogleDownload: (DownloadWithItems) -> Boolean
|
||||||
) : RecyclerView.Adapter<DownloadsViewHolder>() {
|
) : RecyclerView.Adapter<DownloadsViewHolder>() {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadsViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadsViewHolder {
|
||||||
val binding = DownloadedMediaRowBinding.inflate(
|
val binding = DownloadedMediaRowBinding.inflate(
|
||||||
@ -31,24 +36,51 @@ class DownloadsAdapter(
|
|||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onBindViewHolder(holder: DownloadsViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: DownloadsViewHolder, position: Int) {
|
||||||
val file = files[position]
|
val download = downloads[position].download
|
||||||
|
val items = downloads[position].downloadItems
|
||||||
holder.binding.apply {
|
holder.binding.apply {
|
||||||
fileName.text = file.name
|
title.text = download.title
|
||||||
fileSize.text = "${file.size / (1024 * 1024)} MiB"
|
uploaderName.text = download.uploader
|
||||||
|
videoInfo.text = download.uploadDate
|
||||||
|
|
||||||
file.metadata?.let {
|
val downloadSize = items.sumOf { it.downloadSize }
|
||||||
uploaderName.text = it.uploader
|
val currentSize = items.sumOf { File(it.path).length() }
|
||||||
videoInfo.text = it.views.formatShort() + " " +
|
|
||||||
root.context.getString(R.string.views_placeholder) +
|
if (downloadSize == -1L) {
|
||||||
TextUtils.SEPARATOR + it.uploadDate
|
progressBar.isIndeterminate = true
|
||||||
|
} else {
|
||||||
|
progressBar.max = downloadSize.toInt()
|
||||||
|
progressBar.progress = currentSize.toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbnailImage.setImageBitmap(file.thumbnail)
|
if (downloadSize > currentSize) {
|
||||||
|
downloadOverlay.visibility = View.VISIBLE
|
||||||
|
resumePauseBtn.setImageResource(R.drawable.ic_download)
|
||||||
|
fileSize.text = "${currentSize.formatAsFileSize()} / ${downloadSize.formatAsFileSize()}"
|
||||||
|
} else {
|
||||||
|
downloadOverlay.visibility = View.GONE
|
||||||
|
fileSize.text = downloadSize.formatAsFileSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
download.thumbnailPath?.let { path ->
|
||||||
|
thumbnailImage.setImageBitmap(ImageHelper.getDownloadedImage(context, path))
|
||||||
|
}
|
||||||
|
|
||||||
|
progressBar.setOnClickListener {
|
||||||
|
val isDownloading = toogleDownload(downloads[position])
|
||||||
|
|
||||||
|
resumePauseBtn.setImageResource(
|
||||||
|
if (isDownloading) {
|
||||||
|
R.drawable.ic_pause
|
||||||
|
} else {
|
||||||
|
R.drawable.ic_download
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
root.setOnClickListener {
|
root.setOnClickListener {
|
||||||
val intent = Intent(root.context, OfflinePlayerActivity::class.java).also {
|
val intent = Intent(root.context, OfflinePlayerActivity::class.java)
|
||||||
it.putExtra(IntentData.fileName, file.name)
|
intent.putExtra(IntentData.videoId, download.videoId)
|
||||||
}
|
|
||||||
root.context.startActivity(intent)
|
root.context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,27 +93,18 @@ class DownloadsAdapter(
|
|||||||
) { _, index ->
|
) { _, index ->
|
||||||
when (index) {
|
when (index) {
|
||||||
0 -> {
|
0 -> {
|
||||||
val audioDir = DownloadHelper.getDownloadDir(
|
items.map { File(it.path) }.forEach { file ->
|
||||||
root.context,
|
if (file.exists()) {
|
||||||
DownloadHelper.AUDIO_DIR
|
|
||||||
)
|
|
||||||
val videoDir = DownloadHelper.getDownloadDir(
|
|
||||||
root.context,
|
|
||||||
DownloadHelper.VIDEO_DIR
|
|
||||||
)
|
|
||||||
|
|
||||||
listOf(audioDir, videoDir).forEach {
|
|
||||||
val f = File(it, file.name)
|
|
||||||
if (f.exists()) {
|
|
||||||
try {
|
try {
|
||||||
f.delete()
|
file.delete()
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) { }
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
files.removeAt(position)
|
query {
|
||||||
|
DatabaseHolder.Database.downloadDao().deleteDownload(download)
|
||||||
|
}
|
||||||
|
downloads.removeAt(position)
|
||||||
notifyItemRemoved(position)
|
notifyItemRemoved(position)
|
||||||
notifyItemRangeChanged(position, itemCount)
|
notifyItemRangeChanged(position, itemCount)
|
||||||
}
|
}
|
||||||
@ -95,6 +118,6 @@ class DownloadsAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
return files.size
|
return downloads.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,62 @@
|
|||||||
package com.github.libretube.ui.fragments
|
package com.github.libretube.ui.fragments
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.ServiceConnection
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.IBinder
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.size
|
import androidx.core.view.size
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.github.libretube.R
|
||||||
import com.github.libretube.databinding.FragmentDownloadsBinding
|
import com.github.libretube.databinding.FragmentDownloadsBinding
|
||||||
|
import com.github.libretube.db.DatabaseHolder.Companion.Database
|
||||||
|
import com.github.libretube.db.obj.DownloadWithItems
|
||||||
|
import com.github.libretube.extensions.awaitQuery
|
||||||
|
import com.github.libretube.extensions.formatAsFileSize
|
||||||
|
import com.github.libretube.obj.DownloadStatus
|
||||||
|
import com.github.libretube.receivers.DownloadReceiver
|
||||||
|
import com.github.libretube.services.DownloadService
|
||||||
import com.github.libretube.ui.adapters.DownloadsAdapter
|
import com.github.libretube.ui.adapters.DownloadsAdapter
|
||||||
import com.github.libretube.ui.base.BaseFragment
|
import com.github.libretube.ui.base.BaseFragment
|
||||||
|
import com.github.libretube.ui.viewholders.DownloadsViewHolder
|
||||||
import com.github.libretube.util.DownloadHelper
|
import com.github.libretube.util.DownloadHelper
|
||||||
import com.github.libretube.util.ImageHelper
|
import kotlinx.coroutines.Job
|
||||||
import com.github.libretube.util.MetadataHelper
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class DownloadsFragment : BaseFragment() {
|
class DownloadsFragment : BaseFragment() {
|
||||||
private lateinit var binding: FragmentDownloadsBinding
|
private lateinit var binding: FragmentDownloadsBinding
|
||||||
|
private var binder: DownloadService.LocalBinder? = null
|
||||||
|
private val downloads = mutableListOf<DownloadWithItems>()
|
||||||
|
private val downloadReceiver = DownloadReceiver()
|
||||||
|
|
||||||
|
private val serviceConnection = object : ServiceConnection {
|
||||||
|
var isBound = false
|
||||||
|
var job: Job? = null
|
||||||
|
|
||||||
|
override fun onServiceConnected(name: ComponentName?, iBinder: IBinder?) {
|
||||||
|
binder = iBinder as DownloadService.LocalBinder
|
||||||
|
isBound = true
|
||||||
|
job?.cancel()
|
||||||
|
job = lifecycleScope.launch {
|
||||||
|
binder?.getService()?.downloadFlow?.collectLatest {
|
||||||
|
updateProgress(it.first, it.second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
binder = null
|
||||||
|
isBound = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@ -29,36 +70,118 @@ class DownloadsFragment : BaseFragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
val files = DownloadHelper.getDownloadedFiles(requireContext())
|
awaitQuery {
|
||||||
|
downloads.addAll(Database.downloadDao().getAll())
|
||||||
if (files.isEmpty()) return
|
|
||||||
|
|
||||||
val metadataHelper = MetadataHelper(requireContext())
|
|
||||||
files.forEach {
|
|
||||||
metadataHelper.getMetadata(it.name)?.let { streams ->
|
|
||||||
it.metadata = streams
|
|
||||||
}
|
|
||||||
ImageHelper.getDownloadedImage(requireContext(), it.name)?.let { bitmap ->
|
|
||||||
it.thumbnail = bitmap
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (downloads.isEmpty()) return
|
||||||
|
|
||||||
binding.downloadsEmpty.visibility = View.GONE
|
binding.downloadsEmpty.visibility = View.GONE
|
||||||
binding.downloads.visibility = View.VISIBLE
|
binding.downloads.visibility = View.VISIBLE
|
||||||
|
|
||||||
binding.downloads.layoutManager = LinearLayoutManager(context)
|
binding.downloads.layoutManager = LinearLayoutManager(context)
|
||||||
binding.downloads.adapter = DownloadsAdapter(files)
|
|
||||||
|
binding.downloads.adapter = DownloadsAdapter(requireContext(), downloads) {
|
||||||
|
var isDownloading = false
|
||||||
|
val ids = it.downloadItems
|
||||||
|
.filter { item -> File(item.path).length() < item.downloadSize }
|
||||||
|
.map { item -> item.id }
|
||||||
|
|
||||||
|
if (!serviceConnection.isBound) {
|
||||||
|
DownloadHelper.startDownloadService(requireContext())
|
||||||
|
bindDownloadService(ids.toIntArray())
|
||||||
|
return@DownloadsAdapter true
|
||||||
|
}
|
||||||
|
|
||||||
|
binder?.getService()?.let { service ->
|
||||||
|
isDownloading = ids.any { id -> service.isDownloading(id) }
|
||||||
|
|
||||||
|
ids.forEach { id ->
|
||||||
|
if (isDownloading) {
|
||||||
|
service.pause(id)
|
||||||
|
} else {
|
||||||
|
service.resume(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@DownloadsAdapter isDownloading.not()
|
||||||
|
}
|
||||||
|
|
||||||
binding.downloads.adapter?.registerAdapterDataObserver(
|
binding.downloads.adapter?.registerAdapterDataObserver(
|
||||||
object : RecyclerView.AdapterDataObserver() {
|
object : RecyclerView.AdapterDataObserver() {
|
||||||
override fun onChanged() {
|
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
||||||
if (binding.downloads.size == 0) {
|
if (binding.downloads.size == 0) {
|
||||||
binding.downloads.visibility = View.GONE
|
binding.downloads.visibility = View.GONE
|
||||||
binding.downloadsEmpty.visibility = View.VISIBLE
|
binding.downloadsEmpty.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
super.onChanged()
|
super.onItemRangeRemoved(positionStart, itemCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
if (DownloadService.IS_DOWNLOAD_RUNNING) {
|
||||||
|
val intent = Intent(requireContext(), DownloadService::class.java)
|
||||||
|
context?.bindService(intent, serviceConnection, 0)
|
||||||
|
}
|
||||||
|
super.onStart()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
val filter = IntentFilter()
|
||||||
|
filter.addAction(DownloadService.ACTION_SERVICE_STARTED)
|
||||||
|
filter.addAction(DownloadService.ACTION_SERVICE_STOPPED)
|
||||||
|
context?.registerReceiver(downloadReceiver, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bindDownloadService(ids: IntArray? = null) {
|
||||||
|
if (serviceConnection.isBound) return
|
||||||
|
|
||||||
|
val intent = Intent(context, DownloadService::class.java)
|
||||||
|
intent.putExtra("ids", ids)
|
||||||
|
context?.bindService(intent, serviceConnection, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateProgress(id: Int, status: DownloadStatus) {
|
||||||
|
val index = downloads.indexOfFirst {
|
||||||
|
it.downloadItems.any { item -> item.id == id }
|
||||||
|
}
|
||||||
|
val view = binding.downloads.findViewHolderForAdapterPosition(index) as? DownloadsViewHolder
|
||||||
|
|
||||||
|
view?.binding?.apply {
|
||||||
|
when (status) {
|
||||||
|
DownloadStatus.Paused -> {
|
||||||
|
resumePauseBtn.setImageResource(R.drawable.ic_download)
|
||||||
|
}
|
||||||
|
DownloadStatus.Completed -> {
|
||||||
|
downloadOverlay.visibility = View.GONE
|
||||||
|
}
|
||||||
|
is DownloadStatus.Progress -> {
|
||||||
|
downloadOverlay.visibility = View.VISIBLE
|
||||||
|
resumePauseBtn.setImageResource(R.drawable.ic_pause)
|
||||||
|
if (progressBar.isIndeterminate) return
|
||||||
|
progressBar.incrementProgressBy(status.progress.toInt())
|
||||||
|
val progressInfo = progressBar.progress.formatAsFileSize() +
|
||||||
|
" / " + progressBar.max.formatAsFileSize()
|
||||||
|
fileSize.text = progressInfo
|
||||||
|
}
|
||||||
|
is DownloadStatus.Error -> {
|
||||||
|
resumePauseBtn.setImageResource(R.drawable.ic_restart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
context?.unregisterReceiver(downloadReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
if (serviceConnection.isBound) {
|
||||||
|
context?.unbindService(serviceConnection)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.github.libretube.constants.IntentData
|
import com.github.libretube.constants.IntentData
|
||||||
import com.github.libretube.obj.DownloadedFile
|
import com.github.libretube.db.obj.DownloadItem
|
||||||
import com.github.libretube.services.DownloadService
|
import com.github.libretube.services.DownloadService
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ object DownloadHelper {
|
|||||||
const val METADATA_DIR = "metadata"
|
const val METADATA_DIR = "metadata"
|
||||||
const val THUMBNAIL_DIR = "thumbnail"
|
const val THUMBNAIL_DIR = "thumbnail"
|
||||||
const val DOWNLOAD_CHUNK_SIZE = 8L * 1024
|
const val DOWNLOAD_CHUNK_SIZE = 8L * 1024
|
||||||
const val DEFAULT_TIMEOUT = 30L
|
const val DEFAULT_TIMEOUT = 15 * 1000
|
||||||
|
|
||||||
fun getOfflineStorageDir(context: Context): File {
|
fun getOfflineStorageDir(context: Context): File {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return context.filesDir
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return context.filesDir
|
||||||
@ -36,31 +36,6 @@ object DownloadHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun File.toDownloadedFile(): DownloadedFile {
|
|
||||||
return DownloadedFile(
|
|
||||||
name = this.name,
|
|
||||||
size = this.length()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDownloadedFiles(context: Context): MutableList<DownloadedFile> {
|
|
||||||
val videoFiles = getDownloadDir(context, VIDEO_DIR).listFiles().orEmpty()
|
|
||||||
val audioFiles = getDownloadDir(context, AUDIO_DIR).listFiles().orEmpty().toMutableList()
|
|
||||||
|
|
||||||
val files = mutableListOf<DownloadedFile>()
|
|
||||||
|
|
||||||
videoFiles.forEach {
|
|
||||||
audioFiles.removeIf { audioFile -> audioFile.name == it.name }
|
|
||||||
files.add(it.toDownloadedFile())
|
|
||||||
}
|
|
||||||
|
|
||||||
audioFiles.forEach {
|
|
||||||
files.add(it.toDownloadedFile())
|
|
||||||
}
|
|
||||||
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startDownloadService(
|
fun startDownloadService(
|
||||||
context: Context,
|
context: Context,
|
||||||
videoId: String? = null,
|
videoId: String? = null,
|
||||||
|
@ -59,15 +59,12 @@ object ImageHelper {
|
|||||||
if (!dataSaverModeEnabled) target.load(url, imageLoader)
|
if (!dataSaverModeEnabled) target.load(url, imageLoader)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadImage(context: Context, url: String, fileName: String) {
|
fun downloadImage(context: Context, url: String, path: String) {
|
||||||
val request = ImageRequest.Builder(context)
|
val request = ImageRequest.Builder(context)
|
||||||
.data(url)
|
.data(url)
|
||||||
.target { result ->
|
.target { result ->
|
||||||
val bitmap = (result as BitmapDrawable).bitmap
|
val bitmap = (result as BitmapDrawable).bitmap
|
||||||
val file = File(
|
val file = File(path)
|
||||||
DownloadHelper.getDownloadDir(context, DownloadHelper.THUMBNAIL_DIR),
|
|
||||||
fileName
|
|
||||||
)
|
|
||||||
saveImage(context, bitmap, Uri.fromFile(file))
|
saveImage(context, bitmap, Uri.fromFile(file))
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
@ -75,11 +72,8 @@ object ImageHelper {
|
|||||||
imageLoader.enqueue(request)
|
imageLoader.enqueue(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDownloadedImage(context: Context, fileName: String): Bitmap? {
|
fun getDownloadedImage(context: Context, path: String): Bitmap? {
|
||||||
val file = File(
|
val file = File(path)
|
||||||
DownloadHelper.getDownloadDir(context, DownloadHelper.THUMBNAIL_DIR),
|
|
||||||
fileName
|
|
||||||
)
|
|
||||||
if (!file.exists()) return null
|
if (!file.exists()) return null
|
||||||
return getImage(context, Uri.fromFile(file))
|
return getImage(context, Uri.fromFile(file))
|
||||||
}
|
}
|
||||||
|
26
app/src/main/res/drawable/circular_progress.xml
Normal file
26
app/src/main/res/drawable/circular_progress.xml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<item android:id="@android:id/background">
|
||||||
|
<shape
|
||||||
|
android:shape="ring"
|
||||||
|
android:thickness="2.5dp"
|
||||||
|
android:useLevel="false">
|
||||||
|
<solid android:color="#4e4e4e" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item android:id="@android:id/progress">
|
||||||
|
<rotate android:fromDegrees="270"
|
||||||
|
android:toDegrees="270">
|
||||||
|
<shape
|
||||||
|
android:shape="ring"
|
||||||
|
android:thickness="2.5dp"
|
||||||
|
android:useLevel="true">
|
||||||
|
<solid android:color="?android:colorAccent" />
|
||||||
|
<corners android:radius="20dp" />
|
||||||
|
</shape>
|
||||||
|
</rotate>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
</layer-list>
|
@ -23,6 +23,34 @@
|
|||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
tools:src="@tools:sample/backgrounds/scenic" />
|
tools:src="@tools:sample/backgrounds/scenic" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/downloadOverlay"
|
||||||
|
android:layout_width="140dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:background="#BF000000">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="52dp"
|
||||||
|
android:layout_height="52dp"
|
||||||
|
android:indeterminateOnly="false"
|
||||||
|
android:progressDrawable="@drawable/circular_progress"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/resumePauseBtn"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:src="@drawable/ic_download"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/progressBar"
|
||||||
|
app:layout_constraintLeft_toLeftOf="@id/progressBar"
|
||||||
|
app:layout_constraintRight_toRightOf="@id/progressBar"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/progressBar" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@ -33,7 +61,7 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/fileName"
|
android:id="@+id/title"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginVertical="2dp"
|
android:layout_marginVertical="2dp"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user