Files
QieKan-3.0/app/src/main/java/io/legado/app/service/DownloadService.kt
T
2025-01-10 17:29:13 +08:00

280 lines
9.5 KiB
Kotlin

package io.legado.app.service
import android.annotation.SuppressLint
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Environment
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import io.legado.app.R
import io.legado.app.base.BaseService
import io.legado.app.constant.AppConst
import io.legado.app.constant.AppLog
import io.legado.app.constant.IntentAction
import io.legado.app.constant.NotificationId
import io.legado.app.utils.IntentType
import io.legado.app.utils.openFileUri
import io.legado.app.utils.servicePendingIntent
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import splitties.init.appCtx
import splitties.systemservices.downloadManager
import splitties.systemservices.notificationManager
/**
* 下载文件
*/
class DownloadService : BaseService() {
private val groupKey = "${appCtx.packageName}.download"
private val downloads = hashMapOf<Long, DownloadInfo>()
private val completeDownloads = hashSetOf<Long>()
private var upStateJob: Job? = null
private val downloadReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
queryState()
}
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
override fun onCreate() {
super.onCreate()
ContextCompat.registerReceiver(
this,
downloadReceiver,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
ContextCompat.RECEIVER_EXPORTED
)
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(downloadReceiver)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
IntentAction.start -> startDownload(
intent.getStringExtra("url"),
intent.getStringExtra("fileName")
)
IntentAction.play -> {
val id = intent.getLongExtra("downloadId", 0)
if (completeDownloads.contains(id)) {
openDownload(id, downloads[id]?.fileName)
} else {
toastOnUi("未完成,下载的文件夹Download")
}
}
IntentAction.stop -> {
val downloadId = intent.getLongExtra("downloadId", 0)
removeDownload(downloadId)
}
}
return super.onStartCommand(intent, flags, startId)
}
/**
* 开始下载
*/
@Synchronized
private fun startDownload(url: String?, fileName: String?) {
if (url == null || fileName == null) {
if (downloads.isEmpty()) {
stopSelf()
}
return
}
if (downloads.values.any { it.url == url }) {
toastOnUi("已在下载列表")
return
}
kotlin.runCatching {
// 指定下载地址
val request = DownloadManager.Request(Uri.parse(url))
// 设置通知
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
// 设置下载文件保存的路径和文件名
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
// 添加一个下载任务
val downloadId = downloadManager.enqueue(request)
downloads[downloadId] =
DownloadInfo(url, fileName, NotificationId.Download + downloads.size)
queryState()
if (upStateJob == null) {
checkDownloadState()
}
}.onFailure {
it.printStackTrace()
val msg = when (it) {
is SecurityException -> "下载出错,没有存储权限"
else -> "下载出错,${it.localizedMessage}"
}
toastOnUi(msg)
AppLog.put(msg, it)
}
}
/**
* 取消下载
*/
@Synchronized
private fun removeDownload(downloadId: Long) {
if (!completeDownloads.contains(downloadId)) {
downloadManager.remove(downloadId)
}
downloads.remove(downloadId)
completeDownloads.remove(downloadId)
notificationManager.cancel(downloadId.toInt())
}
/**
* 下载成功
*/
@Synchronized
private fun successDownload(downloadId: Long) {
if (!completeDownloads.contains(downloadId)) {
completeDownloads.add(downloadId)
val fileName = downloads[downloadId]?.fileName
openDownload(downloadId, fileName)
}
}
private fun checkDownloadState() {
upStateJob?.cancel()
upStateJob = lifecycleScope.launch {
while (isActive) {
queryState()
delay(1000)
}
}
}
/**
* 查询下载进度
*/
@Synchronized
private fun queryState() {
if (downloads.isEmpty()) {
stopSelf()
return
}
val ids = downloads.keys
val query = DownloadManager.Query()
query.setFilterById(*ids.toLongArray())
downloadManager.query(query).use { cursor ->
if (cursor.moveToFirst()) {
val idIndex = cursor.getColumnIndex(DownloadManager.COLUMN_ID)
val progressIndex =
cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
val fileSizeIndex = cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
val statusIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
do {
val id = cursor.getLong(idIndex)
val progress = cursor.getInt(progressIndex)
val max = cursor.getInt(fileSizeIndex)
val status = when (cursor.getInt(statusIndex)) {
DownloadManager.STATUS_PAUSED -> getString(R.string.pause)
DownloadManager.STATUS_PENDING -> getString(R.string.wait_download)
DownloadManager.STATUS_RUNNING -> getString(R.string.downloading)
DownloadManager.STATUS_SUCCESSFUL -> {
successDownload(id)
getString(R.string.download_success)
}
DownloadManager.STATUS_FAILED -> getString(R.string.download_error)
else -> getString(R.string.unknown_state)
}
downloads[id]?.let { downloadInfo ->
upDownloadNotification(
id,
downloadInfo.notificationId,
"${downloadInfo.fileName} $status",
max,
progress,
downloadInfo.startTime
)
}
} while (cursor.moveToNext())
}
}
}
/**
* 打开下载文件
*/
private fun openDownload(downloadId: Long, fileName: String?) {
kotlin.runCatching {
downloadManager.getUriForDownloadedFile(downloadId)?.let { uri ->
val type = IntentType.from(fileName)
openFileUri(uri, type)
}
}.onFailure {
AppLog.put("打开下载文件${fileName}出错", it)
}
}
override fun startForegroundNotification() {
val notification = NotificationCompat.Builder(this, AppConst.channelIdDownload)
.setSmallIcon(R.drawable.ic_download)
.setSubText(getString(R.string.action_download))
.setGroup(groupKey)
.setGroupSummary(true)
.setOngoing(true)
.build()
startForeground(NotificationId.DownloadService, notification)
}
/**
* 更新通知
*/
private fun upDownloadNotification(
downloadId: Long,
notificationId: Int,
content: String,
max: Int,
progress: Int,
startTime: Long
) {
val notificationBuilder = NotificationCompat.Builder(this, AppConst.channelIdDownload)
.setSmallIcon(R.drawable.ic_download)
.setSubText(getString(R.string.action_download))
.setContentTitle(content)
.setOnlyAlertOnce(true)
.setContentIntent(
servicePendingIntent<DownloadService>(IntentAction.play, downloadId.toInt()) {
putExtra("downloadId", downloadId)
}
)
.setDeleteIntent(
servicePendingIntent<DownloadService>(IntentAction.stop, downloadId.toInt()) {
putExtra("downloadId", downloadId)
}
)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setGroup(groupKey)
.setWhen(startTime)
if (progress < max) {
notificationBuilder.setProgress(max, progress, false)
}
notificationManager.notify(notificationId, notificationBuilder.build())
}
private data class DownloadInfo(
val url: String,
val fileName: String,
val notificationId: Int,
val startTime: Long = System.currentTimeMillis()
)
}