LibreTube/app/src/main/java/com/github/libretube/services/DownloadService.kt

453 lines
16 KiB
Kotlin
Raw Normal View History

2022-06-28 20:02:26 +05:30
package com.github.libretube.services
2022-03-03 12:08:36 +05:30
import android.app.NotificationManager
import android.app.PendingIntent
2022-05-21 13:32:04 +05:30
import android.app.Service
2022-03-03 12:08:36 +05:30
import android.content.Intent
import android.os.Binder
import android.os.Build
2022-03-03 12:08:36 +05:30
import android.os.IBinder
import androidx.core.app.NotificationCompat
2022-09-08 22:12:52 +05:30
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
2022-09-08 22:11:57 +05:30
import com.github.libretube.constants.DOWNLOAD_CHANNEL_ID
import com.github.libretube.constants.DOWNLOAD_PROGRESS_NOTIFICATION_ID
import com.github.libretube.constants.IntentData
import com.github.libretube.db.DatabaseHolder.Companion.Database
import com.github.libretube.db.obj.Download
import com.github.libretube.db.obj.DownloadItem
import com.github.libretube.enums.FileType
import com.github.libretube.extensions.awaitQuery
import com.github.libretube.extensions.formatAsFileSize
import com.github.libretube.extensions.getContentLength
import com.github.libretube.extensions.query
import com.github.libretube.extensions.toDownloadItems
import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.obj.DownloadStatus
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
2022-09-10 15:21:50 +05:30
import com.github.libretube.util.DownloadHelper
import com.github.libretube.util.DownloadHelper.getNotificationId
import com.github.libretube.util.ImageHelper
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
import okio.BufferedSink
import okio.buffer
import okio.sink
import okio.source
2022-03-03 12:08:36 +05:30
import java.io.File
import java.net.HttpURLConnection
import java.net.URL
2022-03-04 23:30:50 +05:30
/**
* Download service with custom implementation of downloading using [HttpURLConnection].
*/
2022-05-20 03:52:10 +05:30
class DownloadService : Service() {
2022-07-12 01:30:56 +05:30
private val binder = LocalBinder()
private val jobMain = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + jobMain)
2022-10-23 23:12:33 +05:30
private lateinit var notificationManager: NotificationManager
private lateinit var summaryNotificationBuilder: NotificationCompat.Builder
private val jobs = mutableMapOf<Int, Job>()
private val downloadQueue = mutableMapOf<Int, Boolean>()
private val _downloadFlow = MutableSharedFlow<Pair<Int, DownloadStatus>>()
val downloadFlow: SharedFlow<Pair<Int, DownloadStatus>> = _downloadFlow
2022-09-09 16:41:54 +05:30
2022-03-04 21:27:10 +05:30
override fun onCreate() {
super.onCreate()
2022-09-19 23:37:55 +05:30
IS_DOWNLOAD_RUNNING = true
notifyForeground()
sendBroadcast(Intent(ACTION_SERVICE_STARTED))
2022-03-04 21:27:10 +05:30
}
2022-03-03 12:08:36 +05:30
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_DOWNLOAD_RESUME -> resume(intent.getIntExtra("id", -1))
ACTION_DOWNLOAD_PAUSE -> pause(intent.getIntExtra("id", -1))
2022-08-24 21:26:57 +05:30
}
2022-09-09 21:09:49 +05:30
val videoId = intent?.getStringExtra(IntentData.videoId) ?: return START_NOT_STICKY
val fileName = intent.getStringExtra(IntentData.fileName) ?: videoId
val videoFormat = intent.getStringExtra(IntentData.videoFormat)
val videoQuality = intent.getStringExtra(IntentData.videoQuality)
val audioFormat = intent.getStringExtra(IntentData.audioFormat)
val audioQuality = intent.getStringExtra(IntentData.audioQuality)
val subtitleCode = intent.getStringExtra(IntentData.subtitleCode)
scope.launch {
try {
val streams = RetrofitInstance.api.getStreams(videoId)
awaitQuery {
Database.downloadDao().insertDownload(
Download(
videoId = videoId,
title = streams.title ?: "",
thumbnailPath = File(
DownloadHelper.getDownloadDir(this@DownloadService, DownloadHelper.THUMBNAIL_DIR),
fileName
).absolutePath,
description = streams.description ?: "",
uploadDate = streams.uploadDate,
uploader = streams.uploader ?: ""
)
)
}
streams.thumbnailUrl?.let { url ->
ImageHelper.downloadImage(
this@DownloadService,
url,
File(
DownloadHelper.getDownloadDir(
this@DownloadService,
DownloadHelper.THUMBNAIL_DIR
),
fileName
).absolutePath
)
}
val downloadItems = streams.toDownloadItems(
videoId,
fileName,
videoFormat,
videoQuality,
audioFormat,
audioQuality,
subtitleCode
)
downloadItems.forEach { start(it) }
} catch (e: Exception) {
return@launch
}
2022-06-05 23:10:16 +05:30
}
2022-03-03 12:08:36 +05:30
return START_NOT_STICKY
2022-03-03 12:08:36 +05:30
}
2022-05-21 13:32:04 +05:30
/**
* Initiate download [Job] using [DownloadItem] by creating file according to [FileType]
* for the requested file.
*/
private fun start(item: DownloadItem) {
val file: File = when (item.type) {
FileType.AUDIO -> {
val audioDownloadDir = DownloadHelper.getDownloadDir(
this,
DownloadHelper.AUDIO_DIR
)
File(audioDownloadDir, item.fileName)
}
FileType.VIDEO -> {
val videoDownloadDir = DownloadHelper.getDownloadDir(
this,
DownloadHelper.VIDEO_DIR
)
File(videoDownloadDir, item.fileName)
}
FileType.SUBTITLE -> {
val subtitleDownloadDir = DownloadHelper.getDownloadDir(
this,
DownloadHelper.SUBTITLE_DIR
)
File(subtitleDownloadDir, item.fileName)
}
}
file.createNewFile()
item.path = file.absolutePath
item.id = awaitQuery {
Database.downloadDao().insertDownloadItem(item)
}.toInt()
jobs[item.id] = scope.launch {
downloadFile(item)
}
2022-03-03 12:08:36 +05:30
}
/**
* Download file and emit [DownloadStatus] to the collectors of [downloadFlow]
* and notification.
*/
private suspend fun downloadFile(item: DownloadItem) {
downloadQueue[item.id] = true
val notificationBuilder = getNotificationBuilder(item)
setResumeNotification(notificationBuilder, item)
val file = File(item.path)
var totalRead = file.length()
val url = URL(item.url ?: return)
url.getContentLength().let { size ->
if (size > 0 && size != item.downloadSize) {
item.downloadSize = size
query {
Database.downloadDao().updateDownloadItem(item)
}
2022-05-21 13:32:04 +05:30
}
2022-03-03 12:08:36 +05:30
}
try {
// Set start range where last downloading was held.
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()
if (con.responseCode !in 200..299) {
val message = getString(R.string.downloadfailed) + ": " + con.responseMessage
_downloadFlow.emit(item.id to DownloadStatus.Error(message))
toastFromMainThread(message)
}
val sink: BufferedSink = file.sink(true).buffer()
val sourceByte = con.inputStream.source()
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.getNotificationId(), 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.close()
sourceByte.close()
con.disconnect()
} catch (_: Exception) { }
2022-09-09 21:09:49 +05:30
val completed = when (totalRead) {
item.downloadSize -> {
_downloadFlow.emit(item.id to DownloadStatus.Completed)
true
}
else -> {
_downloadFlow.emit(item.id to DownloadStatus.Paused)
false
}
}
setPauseNotification(notificationBuilder, item, completed)
pause(item.id)
}
2022-09-09 16:41:54 +05:30
/**
* Resume download which may have been paused.
*/
fun resume(id: Int) {
// If file is already downloading then avoid new download job.
if (downloadQueue[id] == true) return
val downloadItem = awaitQuery {
Database.downloadDao().findDownloadItemById(id)
2022-03-03 12:08:36 +05:30
}
scope.launch {
downloadFile(downloadItem)
}
}
/**
* Pause downloading job for given [id]. If no downloads are active, stop the service.
*/
fun pause(id: Int) {
downloadQueue[id] = false
// Stop the service if no downloads are active.
if (downloadQueue.none { it.value }) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_DETACH)
}
sendBroadcast(Intent(ACTION_SERVICE_STOPPED))
stopSelf()
}
2022-03-03 12:08:36 +05:30
}
/**
* Check whether the file downloading or not.
*/
fun isDownloading(id: Int): Boolean {
return downloadQueue[id] ?: false
2022-06-05 21:38:47 +05:30
}
private fun notifyForeground() {
notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
2022-09-09 21:09:49 +05:30
summaryNotificationBuilder = NotificationCompat
.Builder(this, DOWNLOAD_CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(getString(R.string.downloading))
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.setGroup(DOWNLOAD_NOTIFICATION_GROUP)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOnlyAlertOnce(true)
.setGroupSummary(true)
startForeground(DOWNLOAD_PROGRESS_NOTIFICATION_ID, summaryNotificationBuilder.build())
}
private fun getNotificationBuilder(item: DownloadItem): NotificationCompat.Builder {
val flag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
2022-06-05 21:38:47 +05:30
}
val activityIntent =
PendingIntent.getActivity(
this@DownloadService,
0,
Intent(this@DownloadService, MainActivity::class.java).apply {
putExtra("fragmentToOpen", "downloads")
},
flag
)
return NotificationCompat
.Builder(this, DOWNLOAD_CHANNEL_ID)
.setContentTitle("[${item.type}] ${item.fileName}")
.setProgress(0, 0, true)
.setOngoing(true)
.setContentIntent(activityIntent)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
.setGroup(DOWNLOAD_NOTIFICATION_GROUP)
}
private fun setResumeNotification(notificationBuilder: NotificationCompat.Builder, item: DownloadItem) {
notificationBuilder
.setSmallIcon(android.R.drawable.stat_sys_download)
.setWhen(System.currentTimeMillis())
.setOngoing(true)
.clearActions()
.addAction(getPauseAction(item.id))
notificationManager.notify(item.getNotificationId(), notificationBuilder.build())
}
private fun setPauseNotification(
notificationBuilder: NotificationCompat.Builder,
item: DownloadItem,
isCompleted: Boolean = false
) {
notificationBuilder
.setProgress(0, 0, false)
.setOngoing(false)
.clearActions()
if (isCompleted) {
notificationBuilder
.setSmallIcon(R.drawable.ic_done)
.setContentText(getString(R.string.download_completed))
} else {
notificationBuilder
.setSmallIcon(R.drawable.ic_pause)
.setContentText(getString(R.string.download_paused))
.addAction(getResumeAction(item.id))
}
notificationManager.notify(item.getNotificationId(), notificationBuilder.build())
2022-06-05 21:38:47 +05:30
}
private fun getResumeAction(id: Int): NotificationCompat.Action {
val intent = Intent(this, NotificationReceiver::class.java)
intent.action = ACTION_DOWNLOAD_RESUME
intent.putExtra("id", id)
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
(PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
} else {
PendingIntent.FLAG_UPDATE_CURRENT
2022-06-05 21:38:47 +05:30
}
return NotificationCompat.Action.Builder(
R.drawable.ic_play,
getString(R.string.resume),
PendingIntent.getBroadcast(this, id, intent, flags)
).build()
2022-06-05 23:10:16 +05:30
}
private fun getPauseAction(id: Int): NotificationCompat.Action {
val intent = Intent(this, NotificationReceiver::class.java)
intent.action = ACTION_DOWNLOAD_PAUSE
intent.putExtra("id", id)
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
(PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
} else {
PendingIntent.FLAG_UPDATE_CURRENT
2022-06-07 12:22:11 +05:30
}
2022-06-05 23:10:16 +05:30
return NotificationCompat.Action.Builder(
R.drawable.ic_pause,
getString(R.string.pause),
PendingIntent.getBroadcast(this, id, intent, flags)
).build()
}
2022-09-08 22:11:57 +05:30
override fun onDestroy() {
downloadQueue.clear()
IS_DOWNLOAD_RUNNING = false
sendBroadcast(Intent(ACTION_SERVICE_STOPPED))
2022-03-03 12:08:36 +05:30
super.onDestroy()
}
2022-09-19 23:37:55 +05:30
override fun onBind(intent: Intent?): IBinder {
val ids = intent?.getIntArrayExtra("ids")
ids?.forEach { id -> resume(id) }
return binder
}
inner class LocalBinder : Binder() {
fun getService(): DownloadService = this@DownloadService
}
2022-09-19 23:37:55 +05:30
companion object {
private const val DOWNLOAD_NOTIFICATION_GROUP = "download_notification_group"
const val ACTION_SERVICE_STARTED =
"com.github.libretube.services.DownloadService.ACTION_SERVICE_STARTED"
const val ACTION_SERVICE_STOPPED =
"com.github.libretube.services.DownloadService.ACTION_SERVICE_STOPPED"
2022-09-19 23:37:55 +05:30
var IS_DOWNLOAD_RUNNING = false
}
2022-03-03 12:08:36 +05:30
}