From 0dfe756da42ae371e60762e1b655f0cde2b8ed25 Mon Sep 17 00:00:00 2001 From: SkyD666 Date: Wed, 21 Feb 2024 22:39:21 +0800 Subject: [PATCH] [feature|optimize|fix] Support opening non-media files via other apps; optimize torrent download code; fix the problem that the About page Badge is not displayed --- README.md | 18 +- app/build.gradle.kts | 12 +- .../java/com/skyd/anivu/ext/ContextExt.kt | 18 - app/src/main/java/com/skyd/anivu/ext/IOExt.kt | 40 ++ .../main/java/com/skyd/anivu/ext/NumberExt.kt | 4 +- .../main/java/com/skyd/anivu/ext/ViewExt.kt | 18 + .../anivu/model/db/dao/DownloadInfoDao.kt | 12 +- .../model/repository/DownloadRepository.kt | 2 +- .../worker/download/DownloadTorrentWorker.kt | 384 +++++++----------- .../worker/download/DownloadWorkerUtil.kt | 24 -- .../skyd/anivu/model/worker/download/Util.kt | 242 +++++++++++ .../adapter/variety/proxy/Download1Proxy.kt | 58 +-- .../ui/adapter/variety/proxy/License1Proxy.kt | 2 +- .../ui/adapter/variety/proxy/Media1Proxy.kt | 4 + .../adapter/variety/proxy/OtherWorks1Proxy.kt | 2 +- .../anivu/ui/fragment/about/AboutFragment.kt | 24 +- .../anivu/ui/fragment/read/ReadFragment.kt | 2 +- .../java/com/skyd/anivu/util/NumberUtil.kt | 2 - .../java/com/skyd/anivu/util/torrent/Util.kt | 40 -- app/src/main/res/values-zh-rCN/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + doc/readme/README-zh-rCN.md | 18 +- 22 files changed, 536 insertions(+), 398 deletions(-) delete mode 100644 app/src/main/java/com/skyd/anivu/model/worker/download/DownloadWorkerUtil.kt create mode 100644 app/src/main/java/com/skyd/anivu/model/worker/download/Util.kt delete mode 100644 app/src/main/java/com/skyd/anivu/util/torrent/Util.kt diff --git a/README.md b/README.md index 7bb0de22..402b2775 100644 --- a/README.md +++ b/README.md @@ -43,13 +43,15 @@ 1. **Subscribe to RSS**, Update RSS, **Read** RSS 2. **Download enclosures** (enclosure tags) of **torrent or magnet** links in RSS articles -3. **Play downloaded videos** -4. Support variable playback **speed**, **long press** to speed up playback -5. **Double-finger** gesture to **rotate and zoom** video -6. **Swipe** on the video to **control volume**, **brightness**, and **playback position** -7. Support **searching** existing **RSS subscription content** -8. Support **dark mode** -9. ...... +2. **Seeding** downloaded files +4. **Play downloaded videos** +5. Support variable playback **speed**, **long press** to speed up playback +6. **Double-finger** gesture to **rotate and zoom** video +7. **Swipe** on the video to **control volume**, **brightness**, and **playback position** +8. **Searching** existing **RSS subscription content** +9. **Play other videos on the phone** +10. Support **dark mode** +11. ...... ## 🚧 Todo @@ -57,8 +59,6 @@ 2. **Customize player settings**, such as default screen scale, surface type used by the player, and more 3. **Float** video playback **window** 4. **Automatically** play the **next video** -5. **Seeding** downloaded files -6. **Play other videos on the phone** ## 🤩 Screenshots diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 68c55a3a..f2ce7339 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -19,8 +19,8 @@ android { applicationId = "com.skyd.anivu" minSdk = 24 targetSdk = 34 - versionCode = 3 - versionName = "1.0-beta05" + versionCode = 4 + versionName = "1.0-beta06" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -49,6 +49,14 @@ android { } } + applicationVariants.all { + outputs + .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } + .forEach { + it.outputFileName = "AniVu_${versionName}_${buildType.name}_${flavorName}.apk" + } + } + buildTypes { debug { isMinifyEnabled = false diff --git a/app/src/main/java/com/skyd/anivu/ext/ContextExt.kt b/app/src/main/java/com/skyd/anivu/ext/ContextExt.kt index d8dc3485..856559f0 100644 --- a/app/src/main/java/com/skyd/anivu/ext/ContextExt.kt +++ b/app/src/main/java/com/skyd/anivu/ext/ContextExt.kt @@ -1,22 +1,16 @@ package com.skyd.anivu.ext import android.app.Activity -import android.content.ActivityNotFoundException import android.content.Context import android.content.ContextWrapper -import android.content.Intent import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.res.Configuration import android.content.res.TypedArray import android.graphics.Point -import android.net.Uri import android.os.Build import android.util.TypedValue import android.view.WindowManager -import android.widget.Toast -import com.skyd.anivu.R -import com.skyd.anivu.ui.component.showToast val Context.activity: Activity get() { @@ -91,16 +85,4 @@ fun Context.getAppName(): String? { e.printStackTrace() null } -} - -fun Context.openBrowser(url: String) { - try { - val uri: Uri = Uri.parse(url) - val intent = Intent(Intent.ACTION_VIEW, uri) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - startActivity(intent) - } catch (e: ActivityNotFoundException) { - e.printStackTrace() - getString(R.string.no_browser_found, url).showToast(Toast.LENGTH_LONG) - } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ext/IOExt.kt b/app/src/main/java/com/skyd/anivu/ext/IOExt.kt index cd1f78e2..afd6bdb8 100644 --- a/app/src/main/java/com/skyd/anivu/ext/IOExt.kt +++ b/app/src/main/java/com/skyd/anivu/ext/IOExt.kt @@ -1,8 +1,15 @@ package com.skyd.anivu.ext +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent import android.net.Uri import android.provider.OpenableColumns +import android.widget.Toast +import androidx.core.content.ContextCompat +import com.skyd.anivu.R import com.skyd.anivu.appContext +import com.skyd.anivu.ui.component.showToast import java.io.File import java.io.FileInputStream import java.io.FileOutputStream @@ -36,6 +43,39 @@ fun Uri.fileName(): String? { return name ?: path?.substringAfterLast("/")?.toDecodedUrl() } +fun String.openBrowser(context: Context) { + Uri.parse(this).openBrowser(context) +} + +fun Uri.openBrowser(context: Context) { + try { + val intent = Intent(Intent.ACTION_VIEW, this) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + } catch (e: ActivityNotFoundException) { + e.printStackTrace() + context.getString(R.string.no_browser_found, path).showToast(Toast.LENGTH_LONG) + } +} + +fun Uri.openWith(context: Context) { + try { + val mimeType = context.contentResolver.getType(this) + val intent = Intent.createChooser( + Intent().apply { + action = Intent.ACTION_VIEW + setDataAndType(this@openWith, mimeType) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + }, + context.getString(R.string.open_with) + ) + ContextCompat.startActivity(context, intent, null) + } catch (e: Exception) { + e.printStackTrace() + context.getString(R.string.failed_msg, e.message).showToast(Toast.LENGTH_LONG) + } +} + fun InputStream.saveTo(target: File): File { val parentFile = target.parentFile if (parentFile?.exists() == false) { diff --git a/app/src/main/java/com/skyd/anivu/ext/NumberExt.kt b/app/src/main/java/com/skyd/anivu/ext/NumberExt.kt index d8bc2d90..0be81e4e 100644 --- a/app/src/main/java/com/skyd/anivu/ext/NumberExt.kt +++ b/app/src/main/java/com/skyd/anivu/ext/NumberExt.kt @@ -34,4 +34,6 @@ val Int.sp: Int TypedValue.COMPLEX_UNIT_SP, this.toFloat(), Resources.getSystem().displayMetrics - ).toInt() \ No newline at end of file + ).toInt() + +fun Float.toPercentage(): String = "%.2f%%".format(this * 100) \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ext/ViewExt.kt b/app/src/main/java/com/skyd/anivu/ext/ViewExt.kt index cf33bba3..344cc8d1 100644 --- a/app/src/main/java/com/skyd/anivu/ext/ViewExt.kt +++ b/app/src/main/java/com/skyd/anivu/ext/ViewExt.kt @@ -7,9 +7,11 @@ import android.graphics.Rect import android.view.DisplayCutout import android.view.View import android.view.ViewGroup +import android.view.ViewTreeObserver import android.view.Window import android.view.animation.AlphaAnimation import android.view.inputmethod.InputMethodManager +import androidx.annotation.OptIn import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat @@ -21,6 +23,9 @@ import androidx.core.view.marginLeft import androidx.core.view.marginRight import androidx.core.view.marginTop import androidx.core.view.updatePadding +import com.google.android.material.badge.BadgeDrawable +import com.google.android.material.badge.BadgeUtils +import com.google.android.material.badge.ExperimentalBadgeUtils import com.skyd.anivu.R import com.skyd.anivu.appContext @@ -292,4 +297,17 @@ fun View.inSafeInset(displayCutout: DisplayCutout): Boolean { if (overlapConsiderPaddingMargin(it)) return false } return true +} + +@OptIn(ExperimentalBadgeUtils::class) +fun View.addBadge(init: BadgeDrawable.() -> Unit) { + viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + BadgeDrawable.create(context).apply { + this.init() + BadgeUtils.attachBadgeDrawable(this, this@addBadge) + } + viewTreeObserver.removeOnGlobalLayoutListener(this) + } + }) } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/db/dao/DownloadInfoDao.kt b/app/src/main/java/com/skyd/anivu/model/db/dao/DownloadInfoDao.kt index d4b37c16..980a171d 100644 --- a/app/src/main/java/com/skyd/anivu/model/db/dao/DownloadInfoDao.kt +++ b/app/src/main/java/com/skyd/anivu/model/db/dao/DownloadInfoDao.kt @@ -33,7 +33,7 @@ interface DownloadInfoDao { fun updateDownloadInfoRequestId( link: String, downloadRequestId: String, - ) + ): Int @Transaction @Query( @@ -78,7 +78,7 @@ interface DownloadInfoDao { fun updateDownloadState( link: String, downloadState: DownloadInfoBean.DownloadState, - ) + ): Int @Transaction @Query( @@ -124,7 +124,7 @@ interface DownloadInfoDao { fun updateDownloadDescription( link: String, description: String?, - ) + ): Int @Transaction @Query( @@ -148,7 +148,7 @@ interface DownloadInfoDao { fun updateDownloadSize( link: String, size: Long, - ) + ): Int @Transaction @Query( @@ -172,7 +172,7 @@ interface DownloadInfoDao { fun updateDownloadProgress( link: String, progress: Float, - ) + ): Int @Transaction @Query( @@ -207,7 +207,7 @@ interface DownloadInfoDao { fun updateDownloadName( link: String, name: String, - ) + ): Int @Transaction @Query( diff --git a/app/src/main/java/com/skyd/anivu/model/repository/DownloadRepository.kt b/app/src/main/java/com/skyd/anivu/model/repository/DownloadRepository.kt index 0947c9a7..b3cad317 100644 --- a/app/src/main/java/com/skyd/anivu/model/repository/DownloadRepository.kt +++ b/app/src/main/java/com/skyd/anivu/model/repository/DownloadRepository.kt @@ -28,7 +28,7 @@ class DownloadRepository @Inject constructor( list.map { downloadInfoBean -> downloadInfoBean.copy().apply { peerInfoList = peerInfoMap.getOrDefault(downloadRequestId, emptyList()).toList() - val torrentStatus = uploadPayloadRateMap.get(downloadRequestId) + val torrentStatus = uploadPayloadRateMap[downloadRequestId] if (torrentStatus != null) { uploadPayloadRate = torrentStatus.uploadPayloadRate() downloadPayloadRate = torrentStatus.downloadPayloadRate() diff --git a/app/src/main/java/com/skyd/anivu/model/worker/download/DownloadTorrentWorker.kt b/app/src/main/java/com/skyd/anivu/model/worker/download/DownloadTorrentWorker.kt index 12a381f3..2efdb3b2 100644 --- a/app/src/main/java/com/skyd/anivu/model/worker/download/DownloadTorrentWorker.kt +++ b/app/src/main/java/com/skyd/anivu/model/worker/download/DownloadTorrentWorker.kt @@ -4,9 +4,7 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE -import android.database.sqlite.SQLiteConstraintException import android.os.Build -import android.util.Log import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.navigation.NavDeepLinkBuilder @@ -27,18 +25,15 @@ import com.skyd.anivu.ext.getAppVersionName import com.skyd.anivu.ext.ifNullOfBlank import com.skyd.anivu.ext.saveTo import com.skyd.anivu.ext.toDecodedUrl +import com.skyd.anivu.ext.toPercentage import com.skyd.anivu.ext.validateFileName import com.skyd.anivu.model.bean.DownloadInfoBean import com.skyd.anivu.model.bean.DownloadLinkUuidMapBean import com.skyd.anivu.model.bean.PeerInfoBean -import com.skyd.anivu.model.bean.SessionParamsBean import com.skyd.anivu.model.db.dao.DownloadInfoDao import com.skyd.anivu.model.db.dao.SessionParamsDao import com.skyd.anivu.model.repository.DownloadRepository import com.skyd.anivu.model.service.HttpService -import com.skyd.anivu.util.floatToPercentage -import com.skyd.anivu.util.torrent.readResumeData -import com.skyd.anivu.util.torrent.serializeResumeData import com.skyd.anivu.util.uniqueInt import dagger.hilt.EntryPoint import dagger.hilt.InstallIn @@ -48,11 +43,12 @@ import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.take import kotlinx.coroutines.job import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine @@ -91,15 +87,9 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : private lateinit var torrentLinkUuid: String private lateinit var torrentLink: String private var progress: Float = 0f - set(value) { - field = value - notificationContentText = floatToPercentage(value) - } - private var notificationContentText: String = "Starting Download" private var name: String? = null private var tempDownloadingDirName: String? = null private var description: String? = null - private var peerInfoList: MutableList = mutableListOf() private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -107,32 +97,42 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : private val sessionManager = SessionManager(BuildConfig.DEBUG) + private fun initData(): Boolean { + torrentLinkUuid = inputData.getString(TORRENT_LINK_UUID) ?: return false + hiltEntryPoint.downloadInfoDao.apply { + torrentLink = getDownloadLinkByUuid(torrentLinkUuid) ?: return false + name = getDownloadName(link = torrentLink) + tempDownloadingDirName = getDownloadingDirName(link = torrentLink) + progress = getDownloadProgress(link = torrentLink) ?: 0f + } + return true + } + override suspend fun doWork(): Result { withContext(Dispatchers.IO) { coroutineContext.job.invokeOnCompletion { if (it is CancellationException) { - this@DownloadTorrentWorker.pause(handle = null) - removeWorkerFromFlow(id.toString()) + pauseWorker(handle = null) } } - torrentLinkUuid = - inputData.getString(TORRENT_LINK_UUID) ?: return@withContext Result.failure() - val downloadInfoDao = hiltEntryPoint.downloadInfoDao - torrentLink = downloadInfoDao.getDownloadLinkByUuid(torrentLinkUuid) - ?: return@withContext Result.failure() - name = downloadInfoDao.getDownloadName(link = torrentLink) - tempDownloadingDirName = downloadInfoDao.getDownloadingDirName(link = torrentLink) - progress = downloadInfoDao.getDownloadProgress(link = torrentLink) ?: 0f + if (!initData()) return@withContext Result.failure() updateNotification() - updateAllDownloadVideoInfoToDb() + // 如果数据库中没有下载信息,就添加新的下载信息(为新下载任务添加信息) + addNewDownloadInfoToDbIfNotExists( + link = torrentLink, + name = name, + progress = progress, + size = sessionManager.stats().totalDownload(), + downloadingDirName = tempDownloadingDirName.orEmpty(), + downloadRequestId = id.toString(), + ) workerDownload() } - hiltEntryPoint.downloadInfoDao.removeDownloadLinkByUuid(torrentLinkUuid) removeWorkerFromFlow(id.toString()) return Result.success( workDataOf( - STATE to (hiltEntryPoint.downloadInfoDao - .getDownloadState(link = torrentLink)?.ordinal ?: 0), + STATE to (hiltEntryPoint.downloadInfoDao.getDownloadState(link = torrentLink) + ?.ordinal ?: 0), TORRENT_LINK_UUID to torrentLinkUuid, ) ) @@ -143,36 +143,36 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : saveDir: File = if (tempDownloadingDirName.isNullOrBlank()) Const.DOWNLOADING_VIDEO_DIR else File(Const.DOWNLOADING_VIDEO_DIR, tempDownloadingDirName!!) ) = suspendCancellableCoroutine { continuation -> + if (!saveDir.exists() && !saveDir.mkdirs()) { continuation.resumeWithException(RuntimeException("Mkdirs failed: $saveDir")) } - sessionManager.apply { - addListener(object : AlertListener { - override fun types(): IntArray? = null // 监听所有类型的警报 - override fun alert(alert: Alert<*>?) { - if (alert == null) return - onAlert(continuation, alert) + sessionManager.addListener(object : AlertListener { + override fun types(): IntArray? = null // 监听所有类型的警报 + override fun alert(alert: Alert<*>?) { + if (alert == null) return - if (isStopped && !sessionIsStopping) { - val handle = (alert as? TorrentAlert)?.handle() ?: return - this@DownloadTorrentWorker.pause(handle = handle) - continuation.resume(Unit, null) - } - } - }) + onAlert(continuation, alert) - // 这里不是挂起函数,因此外面的job.invokeOnCompletion不能捕获到异常,需要手动runCatching - runCatching { - howToDownload(continuation = continuation, saveDir = saveDir) - }.onFailure { - this@DownloadTorrentWorker.pause(handle = null) - continuation.resumeWithException(it) + if (isStopped && !sessionIsStopping) { + val handle = (alert as? TorrentAlert)?.handle() ?: return + pauseWorker(handle = handle) + continuation.resume(Unit, null) + } } + }) + + // 这里不是挂起函数,因此外面的job.invokeOnCompletion不能捕获到异常,需要手动runCatching + runCatching { + howToDownload(saveDir = saveDir) + }.onFailure { + pauseWorker(handle = null) + continuation.resumeWithException(it) } } - private fun howToDownload(continuation: CancellableContinuation, saveDir: File) { + private fun howToDownload(saveDir: File) { sessionManager.apply { val lastSessionParams = hiltEntryPoint.sessionParamsDao .getSessionParams(link = torrentLink) @@ -193,6 +193,7 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : downloadRequestId = id.toString(), ) } + var newDownloadState: DownloadInfoBean.DownloadState? = null when (hiltEntryPoint.downloadInfoDao.getDownloadState(link = torrentLink)) { null, // 重新下载 @@ -203,12 +204,12 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : if (resumeData != null) { swig().async_add_torrent(resumeData) } - updateDownloadStateAndSessionParams(DownloadInfoBean.DownloadState.Seeding) + newDownloadState = DownloadInfoBean.DownloadState.Seeding } DownloadInfoBean.DownloadState.Init -> { downloadByMagnetOrTorrent(torrentLink, saveDir) - updateDownloadStateAndSessionParams(DownloadInfoBean.DownloadState.Downloading) + newDownloadState = DownloadInfoBean.DownloadState.Downloading } DownloadInfoBean.DownloadState.Downloading, @@ -216,9 +217,13 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : DownloadInfoBean.DownloadState.StorageMovedFailed, DownloadInfoBean.DownloadState.Paused -> { downloadByMagnetOrTorrent(torrentLink, saveDir) - updateDownloadStateAndSessionParams(DownloadInfoBean.DownloadState.Downloading) + newDownloadState = DownloadInfoBean.DownloadState.Downloading } } + updateDownloadState( + link = torrentLink, + downloadState = newDownloadState, + ) } } @@ -266,9 +271,7 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : // Creates an instance of ForegroundInfo which can be used to update the ongoing notification. private fun createForegroundInfo(): ForegroundInfo { - val title = name.ifNullOfBlank { - applicationContext.getString(R.string.downloading) - } + val title = name.ifNullOfBlank { applicationContext.getString(R.string.downloading) } // This PendingIntent can be used to cancel the worker val cancelIntent = WorkManager.getInstance(applicationContext) .createCancelPendingIntent(id) @@ -285,7 +288,7 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID) .setContentTitle(title) .setTicker(title) - .setContentText(notificationContentText) + .setContentText(progress.toPercentage()) .setSmallIcon(R.drawable.ic_icon_24) .setOngoing(true) .setProgress(100, (progress * 100).toInt(), false) @@ -324,8 +327,8 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : } is TorrentErrorAlert -> { - // 下载错误更新 - this@DownloadTorrentWorker.pause( + // 下载错误 + pauseWorker( handle = alert.handle(), state = DownloadInfoBean.DownloadState.ErrorPaused, ) @@ -335,7 +338,7 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : // this alert is generated and the torrent is paused. is FileErrorAlert -> { // 文件错误,例如存储空间已满 - this@DownloadTorrentWorker.pause( + pauseWorker( handle = alert.handle(), state = DownloadInfoBean.DownloadState.ErrorPaused, ) @@ -345,13 +348,17 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : is StorageMovedAlert -> { alert.handle().saveResumeData() updateNotificationAsync() // 更新Notification - updateDownloadStateAndSessionParams(DownloadInfoBean.DownloadState.Seeding) + updateDownloadStateAndSessionParams( + link = torrentLink, + sessionStateData = sessionManager.saveState() ?: byteArrayOf(), + downloadState = DownloadInfoBean.DownloadState.Seeding, + ) } is StorageMovedFailedAlert -> { // 文件移动,例如存储空间已满 alert.handle().saveResumeData() - this@DownloadTorrentWorker.pause( + pauseWorker( handle = alert.handle(), state = DownloadInfoBean.DownloadState.StorageMovedFailed, ) @@ -365,7 +372,11 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : handle.saveResumeData() updateNotificationAsync() // 更新Notification moveFromDownloadingDirToVideoDir(handle = handle) - updateDownloadStateAndSessionParams(DownloadInfoBean.DownloadState.Completed) + updateDownloadStateAndSessionParams( + link = torrentLink, + sessionStateData = sessionManager.saveState() ?: byteArrayOf(), + downloadState = DownloadInfoBean.DownloadState.Completed, + ) } is MetadataReceivedAlert -> { @@ -373,21 +384,25 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : val handle = alert.handle() name = handle.name updateNotificationAsync() // 更新Notification - updateNameInfoToDb() + updateNameInfoToDb(link = torrentLink, name = name) } is StateChangedAlert -> { // 下载状态更新 if (alert.state == TorrentStatus.State.SEEDING) { - updateDownloadStateAndSessionParams(DownloadInfoBean.DownloadState.Seeding) + updateDownloadStateAndSessionParams( + link = torrentLink, + sessionStateData = sessionManager.saveState() ?: byteArrayOf(), + downloadState = DownloadInfoBean.DownloadState.Seeding, + ) } description = alert.state.toDisplayString(context = applicationContext) - updateDescriptionInfoToDb() + updateDescriptionInfoToDb(link = torrentLink, description = description!!) val handle = alert.handle() if (handle.isValid) { progress = handle.status().progress() updateNotificationAsync() // 更新Notification - updateProgressInfoToDb() + updateProgressInfoToDb(link = torrentLink, progress = progress) } } @@ -396,11 +411,9 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : is PeerInfoAlert -> { val peerInfo = (alert as? TorrentAlert<*>) ?.handle()?.peerInfo()?.map { PeerInfoBean.from(it) } - Log.e("TAG", "onAlert: ${peerInfo?.size}") +// Log.e("TAG", "onAlert: ${peerInfo?.size}") if (!peerInfo.isNullOrEmpty()) { - peerInfoMapFlow.tryEmit(peerInfoMapFlow.value.toMutableMap().apply { - put(id.toString(), peerInfo) - }) + updatePeerInfoMapFlow(id.toString(), peerInfo) } } @@ -409,37 +422,36 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : // 下载进度更新 val handle = alert.handle() if (handle.isValid) { - torrentStatusMapFlow.tryEmit(torrentStatusMapFlow.value.toMutableMap().apply { - put(id.toString(), handle.status()) - }) + updateTorrentStatusMapFlow(id.toString(), handle.status()) if (progress != handle.status().progress()) { progress = handle.status().progress() updateNotificationAsync() // 更新Notification - updateProgressInfoToDb() - updateSizeInfoToDb() + updateProgressInfoToDb(link = torrentLink, progress = progress) + updateSizeInfoToDb( + link = torrentLink, + size = sessionManager.stats().totalDownload() + ) } } } } } - private fun pause( + private fun pauseWorker( handle: TorrentHandle?, - state: DownloadInfoBean.DownloadState = hiltEntryPoint.downloadInfoDao - .getDownloadState(link = torrentLink).let { - if (it == DownloadInfoBean.DownloadState.Seeding || - it == DownloadInfoBean.DownloadState.Completed || - it == DownloadInfoBean.DownloadState.SeedingPaused - ) DownloadInfoBean.DownloadState.Completed - else DownloadInfoBean.DownloadState.Paused - } + state: DownloadInfoBean.DownloadState = getWhatPausedState( + hiltEntryPoint.downloadInfoDao.getDownloadState(link = torrentLink) + ) ) { if (!sessionManager.isRunning || sessionIsStopping) { return } sessionIsStopping = true - updateSizeInfoToDb() - updateDownloadStateAndSessionParams(state) + updateDownloadStateAndSessionParams( + link = torrentLink, + sessionStateData = sessionManager.saveState() ?: byteArrayOf(), + downloadState = state + ) if (handle != null) { handle.saveResumeData() sessionManager.remove(handle) @@ -448,6 +460,8 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : sessionManager.pause() sessionManager.stopDht() sessionManager.stop() + + removeWorkerFromFlow(id.toString()) } private fun moveFromDownloadingDirToVideoDir(handle: TorrentHandle) { @@ -456,126 +470,8 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : } } - private fun updateDownloadStateAndSessionParams(downloadState: DownloadInfoBean.DownloadState) { - try { - hiltEntryPoint.sessionParamsDao.updateSessionParams( - SessionParamsBean( - link = torrentLink, - data = sessionManager.saveState() ?: byteArrayOf() - ) - ) - hiltEntryPoint.downloadInfoDao.updateDownloadState( - link = torrentLink, - downloadState = downloadState, - ) - } catch (e: SQLiteConstraintException) { - // 捕获link外键约束异常 - e.printStackTrace() - } - } - - private fun updateNameInfoToDb() { - hiltEntryPoint.downloadInfoDao.apply { - val lastName = getDownloadName(link = torrentLink) - if (lastName == null) { - updateAllDownloadVideoInfoToDb() - } else { - if (lastName != name) { - updateDownloadName( - link = torrentLink, - name = name.ifNullOfBlank { lastName }, - ).apply { setProgressAsync(workDataOf("data" to progress)) } - } - } - } - } - - private fun updateSizeInfoToDb() { - hiltEntryPoint.downloadInfoDao.apply { - val lastSize = getDownloadSize(link = torrentLink) - if (lastSize == null) { - updateAllDownloadVideoInfoToDb() - } else { - val size = sessionManager.stats().totalDownload() - if (size != lastSize) { - updateDownloadSize( - link = torrentLink, - size = size, - ) - } - } - } - } - - private fun updateProgressInfoToDb() { - hiltEntryPoint.downloadInfoDao.apply { - val lastProgress = getDownloadProgress(link = torrentLink) - if (lastProgress == null) { - updateAllDownloadVideoInfoToDb() - } else { - if (lastProgress != progress) { - updateDownloadProgress( - link = torrentLink, - progress = progress, - ).apply { setProgressAsync(workDataOf("data" to progress)) } - } - } - } - } - - private fun updateDescriptionInfoToDb() { - hiltEntryPoint.downloadInfoDao.apply { - val lastDescription = getDownloadDescription(link = torrentLink) - if (lastDescription == null && - getDownloadInfo(link = torrentLink) == null - ) { - updateAllDownloadVideoInfoToDb() - } else { - if (lastDescription != description) { - updateDownloadDescription( - link = torrentLink, - description = description, - ) - } - } - } - } - - private fun updateAllDownloadVideoInfoToDb() { - hiltEntryPoint.downloadInfoDao.apply { - val video = getDownloadInfo(link = torrentLink) - if (video != null) { - updateDownloadInfo( - link = torrentLink, - name = name.ifNullOfBlank { - torrentLink.substringAfterLast('/') - .toDecodedUrl() - .validateFileName() - }, - size = sessionManager.stats().totalDownload(), - progress = progress, - ).apply { setProgressAsync(workDataOf("data" to progress)) } - } else { - updateDownloadInfo( - DownloadInfoBean( - link = torrentLink, - name = name.ifNullOfBlank { - torrentLink.substringAfterLast('/') - .toDecodedUrl() - .validateFileName() - }, - downloadingDirName = tempDownloadingDirName.orEmpty(), - downloadDate = System.currentTimeMillis(), - size = sessionManager.stats().totalDownload(), - progress = progress, - downloadRequestId = id.toString(), - ).apply { setProgressAsync(workDataOf("data" to progress)) } - ) - } - } - } - companion object { + const val TAG = "DownloadTorrentWorker" const val STATE = "state" const val TORRENT_LINK_UUID = "torrentLinkUuid" const val CHANNEL_ID = "downloadTorrent" @@ -586,6 +482,18 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : val peerInfoMapFlow = MutableStateFlow(mutableMapOf>()) val torrentStatusMapFlow = MutableStateFlow(mutableMapOf()) + private fun updatePeerInfoMapFlow(requestId: String, list: List) { + peerInfoMapFlow.tryEmit(peerInfoMapFlow.value.toMutableMap().apply { + put(requestId, list) + }) + } + + private fun updateTorrentStatusMapFlow(requestId: String, status: TorrentStatus) { + torrentStatusMapFlow.tryEmit(torrentStatusMapFlow.value.toMutableMap().apply { + put(requestId, status) + }) + } + private fun removeWorkerFromFlow(requestId: String) { peerInfoMapFlow.tryEmit( peerInfoMapFlow.value.toMutableMap().apply { remove(requestId) } @@ -604,7 +512,7 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : val downloadRepository: DownloadRepository } - private val hiltEntryPoint = EntryPointAccessors.fromApplication( + internal val hiltEntryPoint = EntryPointAccessors.fromApplication( appContext, WorkerEntryPoint::class.java ) @@ -622,21 +530,21 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : ) } - val sendLogsWorkRequest = OneTimeWorkRequestBuilder() + val workRequest = OneTimeWorkRequestBuilder() .run { if (requestId != null) setId(UUID.fromString(requestId)) else this } .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .setInputData(workDataOf(TORRENT_LINK_UUID to torrentLinkUuid)) .build() - WorkManager.getInstance(context).enqueueUniqueWork( - torrentLinkUuid, - ExistingWorkPolicy.KEEP, - sendLogsWorkRequest - ) - val flow = WorkManager.getInstance(context) - .getWorkInfoByIdFlow(sendLogsWorkRequest.id) - coroutineScope.launch { - flow.filter { it == null || it.state.isFinished } + WorkManager.getInstance(context).apply { + enqueueUniqueWork( + torrentLinkUuid, + ExistingWorkPolicy.KEEP, + workRequest + ) + + getWorkInfoByIdFlow(workRequest.id) + .filter { it == null || it.state.isFinished } .onEach { removeWorkerFromFlow(it.id.toString()) }.collect { @@ -647,35 +555,22 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : } fun pause( - context: Context, requestId: String, + context: Context, + requestId: String, link: String, ) { - val uuid = UUID.fromString(requestId) + val requestUuid = UUID.fromString(requestId) WorkManager.getInstance(context).apply { - if (getWorkInfoById(uuid).get().state.isFinished) { + if (getWorkInfoById(requestUuid).get().state.isFinished) { coroutineScope.launch { - try { - val state = hiltEntryPoint.downloadInfoDao.getDownloadState(link) - val newState: DownloadInfoBean.DownloadState = - if (state == DownloadInfoBean.DownloadState.Seeding || - state == DownloadInfoBean.DownloadState.Completed || - state == DownloadInfoBean.DownloadState.SeedingPaused - ) { - DownloadInfoBean.DownloadState.SeedingPaused - } else { - DownloadInfoBean.DownloadState.Paused - } - hiltEntryPoint.downloadInfoDao.updateDownloadState( - link = link, - downloadState = newState, - ) - } catch (e: SQLiteConstraintException) { - // 捕获link外键约束异常 - e.printStackTrace() - } + val state = hiltEntryPoint.downloadInfoDao.getDownloadState(link) + updateDownloadState( + link = link, + downloadState = getWhatPausedState(state), + ) } } else { - cancelWorkById(uuid) + cancelWorkById(requestUuid) } } } @@ -686,22 +581,21 @@ class DownloadTorrentWorker(context: Context, parameters: WorkerParameters) : link: String, downloadingDirName: String ) { - val flow = WorkManager.getInstance(context) - .getWorkInfoByIdFlow(UUID.fromString(requestId)) + val requestUuid = UUID.fromString(requestId) + val worker = WorkManager.getInstance(context) // 在worker结束后删除数据库中的下载任务信息 coroutineScope.launch { - flow.filter { it == null || it.state.isFinished } + worker.cancelWorkById(requestUuid) + worker.getWorkInfoByIdFlow(requestUuid) + .filter { it == null || it.state.isFinished } .flatMapConcat { - delay(2000) hiltEntryPoint.downloadRepository.deleteDownloadTaskInfo( link = link, downloadingDirName = downloadingDirName, ) - }.collect { - cancel() - } + }.take(1) + .collect() } - pause(context, requestId, link) } } } \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/worker/download/DownloadWorkerUtil.kt b/app/src/main/java/com/skyd/anivu/model/worker/download/DownloadWorkerUtil.kt deleted file mode 100644 index 51427141..00000000 --- a/app/src/main/java/com/skyd/anivu/model/worker/download/DownloadWorkerUtil.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.skyd.anivu.model.worker.download - -import android.content.Context -import com.skyd.anivu.R -import org.libtorrent4j.TorrentStatus -import org.libtorrent4j.TorrentStatus.State.CHECKING_FILES -import org.libtorrent4j.TorrentStatus.State.CHECKING_RESUME_DATA -import org.libtorrent4j.TorrentStatus.State.DOWNLOADING -import org.libtorrent4j.TorrentStatus.State.DOWNLOADING_METADATA -import org.libtorrent4j.TorrentStatus.State.FINISHED -import org.libtorrent4j.TorrentStatus.State.SEEDING -import org.libtorrent4j.TorrentStatus.State.UNKNOWN - -fun TorrentStatus.State.toDisplayString(context: Context): String { - return when (this) { - CHECKING_FILES -> context.getString(R.string.torrent_status_checking_files) - DOWNLOADING_METADATA -> context.getString(R.string.torrent_status_downloading_metadata) - DOWNLOADING -> context.getString(R.string.torrent_status_downloading) - FINISHED -> context.getString(R.string.torrent_status_finished) - SEEDING -> context.getString(R.string.torrent_status_seeding) - CHECKING_RESUME_DATA -> context.getString(R.string.torrent_status_checking_resume_data) - UNKNOWN -> "" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/model/worker/download/Util.kt b/app/src/main/java/com/skyd/anivu/model/worker/download/Util.kt new file mode 100644 index 00000000..f8f89ee0 --- /dev/null +++ b/app/src/main/java/com/skyd/anivu/model/worker/download/Util.kt @@ -0,0 +1,242 @@ +package com.skyd.anivu.model.worker.download + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import android.util.Log +import com.skyd.anivu.R +import com.skyd.anivu.config.Const +import com.skyd.anivu.ext.ifNullOfBlank +import com.skyd.anivu.ext.toDecodedUrl +import com.skyd.anivu.ext.validateFileName +import com.skyd.anivu.model.bean.DownloadInfoBean +import com.skyd.anivu.model.bean.SessionParamsBean +import org.libtorrent4j.TorrentStatus +import org.libtorrent4j.Vectors +import org.libtorrent4j.alerts.SaveResumeDataAlert +import org.libtorrent4j.swig.add_torrent_params +import org.libtorrent4j.swig.error_code +import org.libtorrent4j.swig.libtorrent +import java.io.File +import java.io.FileOutputStream +import java.io.IOException + + +fun TorrentStatus.State.toDisplayString(context: Context): String { + return when (this) { + TorrentStatus.State.CHECKING_FILES -> context.getString(R.string.torrent_status_checking_files) + TorrentStatus.State.DOWNLOADING_METADATA -> context.getString(R.string.torrent_status_downloading_metadata) + TorrentStatus.State.DOWNLOADING -> context.getString(R.string.torrent_status_downloading) + TorrentStatus.State.FINISHED -> context.getString(R.string.torrent_status_finished) + TorrentStatus.State.SEEDING -> context.getString(R.string.torrent_status_seeding) + TorrentStatus.State.CHECKING_RESUME_DATA -> context.getString(R.string.torrent_status_checking_resume_data) + TorrentStatus.State.UNKNOWN -> "" + } +} + +internal fun getWhatPausedState(oldState: DownloadInfoBean.DownloadState?) = + when (oldState) { + DownloadInfoBean.DownloadState.Seeding, + DownloadInfoBean.DownloadState.Completed, + DownloadInfoBean.DownloadState.SeedingPaused -> { + DownloadInfoBean.DownloadState.SeedingPaused + } + + else -> { + DownloadInfoBean.DownloadState.Paused + } + } + +internal fun updateDownloadState( + link: String, + downloadState: DownloadInfoBean.DownloadState, +): Boolean { + try { + val result = DownloadTorrentWorker.hiltEntryPoint.downloadInfoDao.updateDownloadState( + link = link, + downloadState = downloadState, + ) + if (result == 0) { + Log.w( + DownloadTorrentWorker.TAG, + "updateDownloadState return 0. downloadState: $downloadState" + ) + } + return result != 0 + } catch (e: SQLiteConstraintException) { + // 捕获link外键约束异常 + e.printStackTrace() + } + return false +} + +internal fun updateDownloadStateAndSessionParams( + link: String, + sessionStateData: ByteArray, + downloadState: DownloadInfoBean.DownloadState, +) { + updateDownloadState(link, downloadState) + try { + DownloadTorrentWorker.hiltEntryPoint.sessionParamsDao.updateSessionParams( + SessionParamsBean( + link = link, + data = sessionStateData, + ) + ) + } catch (e: SQLiteConstraintException) { + // 捕获link外键约束异常 + e.printStackTrace() + } +} + +internal fun updateDescriptionInfoToDb(link: String, description: String): Boolean { + DownloadTorrentWorker.hiltEntryPoint.downloadInfoDao.apply { + val result = updateDownloadDescription( + link = link, + description = description, + ) + if (result == 0) { + Log.w( + DownloadTorrentWorker.TAG, + "updateDownloadDescription return 0. description: $description" + ) + } + return result != 0 + } +} + +internal fun updateNameInfoToDb(link: String, name: String?): Boolean { + if (name.isNullOrBlank()) return false + DownloadTorrentWorker.hiltEntryPoint.downloadInfoDao.apply { + val result = updateDownloadName( + link = link, + name = name, + ) + if (result == 0) { + Log.w(DownloadTorrentWorker.TAG, "updateDownloadName return 0. name: $name") + } + return result != 0 + } +} + +internal fun updateProgressInfoToDb(link: String, progress: Float): Boolean { + DownloadTorrentWorker.hiltEntryPoint.downloadInfoDao.apply { + val result = updateDownloadProgress( + link = link, + progress = progress, + ) + if (result == 0) { + Log.w(DownloadTorrentWorker.TAG, "updateDownloadProgress return 0. progress: $progress") + } + return result != 0 + } +} + +internal fun updateSizeInfoToDb(link: String, size: Long): Boolean { + DownloadTorrentWorker.hiltEntryPoint.downloadInfoDao.apply { + val result = updateDownloadSize( + link = link, + size = size, + ) + if (result == 0) { + Log.w(DownloadTorrentWorker.TAG, "updateDownloadSize return 0. size: $size") + } + return result != 0 + } +} + +/** + * 添加新的下载信息(之前没下载过的) + */ +internal fun addNewDownloadInfoToDbIfNotExists( + forceAdd: Boolean = false, + link: String, + name: String?, + progress: Float, + size: Long, + downloadingDirName: String, + downloadRequestId: String, +) { + DownloadTorrentWorker.hiltEntryPoint.downloadInfoDao.apply { + if (!forceAdd) { + val video = getDownloadInfo(link = link) + if (video != null) return + } + updateDownloadInfo( + DownloadInfoBean( + link = link, + name = name.ifNullOfBlank { + link.substringAfterLast('/') + .toDecodedUrl() + .validateFileName() + }, + downloadingDirName = downloadingDirName, + downloadDate = System.currentTimeMillis(), + size = size, + progress = progress, + downloadRequestId = downloadRequestId, + ) + ) + } +} + +internal fun updateAllDownloadVideoInfoToDb( + link: String, + name: String?, + progress: Float, + size: Long, + downloadingDirName: String, + downloadRequestId: String, +) { + DownloadTorrentWorker.hiltEntryPoint.downloadInfoDao.apply { + val video = getDownloadInfo(link = link) + if (video != null) { + updateDownloadInfo( + link = link, + name = name.ifNullOfBlank { + link.substringAfterLast('/') + .toDecodedUrl() + .validateFileName() + }, + size = size, + progress = progress, + ) + } else { + addNewDownloadInfoToDbIfNotExists( + forceAdd = true, + link = link, + name = name, + progress = progress, + size = size, + downloadingDirName = downloadingDirName, + downloadRequestId = downloadRequestId, + ) + } + } +} + +fun serializeResumeData(name: String, alert: SaveResumeDataAlert) { + val resume = File(Const.TORRENT_RESUME_DATA_DIR, name) + if (!resume.exists()) resume.createNewFile() + val data = libtorrent.write_resume_data(alert.params().swig()).bencode() + try { + FileOutputStream(resume).use { it.write(Vectors.byte_vector2bytes(data)) } + } catch (e: IOException) { + Log.e("serializeResumeData", "Error saving resume data") + } +} + +fun readResumeData(name: String): add_torrent_params? { + val resume = File(Const.TORRENT_RESUME_DATA_DIR, name) + if (!resume.exists()) return null + try { + val data = resume.readBytes() + val ec = error_code() + val p: add_torrent_params = + libtorrent.read_resume_data_ex(Vectors.bytes2byte_vector(data), ec) + require(ec.value() == 0) { "Unable to read the resume data: " + ec.message() } + return p + } catch (e: Throwable) { + Log.w("readResumeData", "Unable to set resume data: $e") + } + return null +} \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/Download1Proxy.kt b/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/Download1Proxy.kt index 83eb9825..f94c367f 100644 --- a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/Download1Proxy.kt +++ b/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/Download1Proxy.kt @@ -8,11 +8,10 @@ import com.skyd.anivu.databinding.ItemDownload1Binding import com.skyd.anivu.ext.disable import com.skyd.anivu.ext.enable import com.skyd.anivu.ext.fileSize -import com.skyd.anivu.ext.gone +import com.skyd.anivu.ext.toPercentage import com.skyd.anivu.model.bean.DownloadInfoBean import com.skyd.anivu.ui.adapter.variety.Download1ViewHolder import com.skyd.anivu.ui.adapter.variety.VarietyAdapter -import com.skyd.anivu.util.floatToPercentage class Download1Proxy( @@ -45,6 +44,7 @@ class Download1Proxy( else -> Unit } } + holder.binding.btnDownload1Cancel.enable() holder.binding.btnDownload1Cancel.setOnClickListener { val data = adapter.dataList.getOrNull(holder.bindingAdapterPosition) if (data !is DownloadInfoBean) return@setOnClickListener @@ -123,9 +123,6 @@ class Download1Proxy( DownloadInfoBean.PAYLOAD_DOWNLOADING_DIR_NAME -> { } - - DownloadInfoBean.PAYLOAD_SIZE -> { - } } } } @@ -137,7 +134,7 @@ class Download1Proxy( data: DownloadInfoBean, ) { holder.binding.apply { - tvDownload1Progress.text = floatToPercentage(data.progress) + tvDownload1Progress.text = data.progress.toPercentage() when (data.downloadState) { DownloadInfoBean.DownloadState.Seeding, DownloadInfoBean.DownloadState.SeedingPaused, @@ -182,9 +179,10 @@ class Download1Proxy( holder: Download1ViewHolder, data: DownloadInfoBean, ) { - holder.binding.tvDownload1UploadPayloadRate.text = holder.itemView.context.getString( + val context = holder.itemView.context + holder.binding.tvDownload1UploadPayloadRate.text = context.getString( R.string.download_upload_payload_rate, - data.uploadPayloadRate.toLong().fileSize(holder.itemView.context) + "/s" + data.uploadPayloadRate.toLong().fileSize(context) + "/s" ) } @@ -192,9 +190,10 @@ class Download1Proxy( holder: Download1ViewHolder, data: DownloadInfoBean, ) { - holder.binding.tvDownload1DownloadPayloadRate.text = holder.itemView.context.getString( + val context = holder.itemView.context + holder.binding.tvDownload1DownloadPayloadRate.text = context.getString( R.string.download_download_payload_rate, - data.downloadPayloadRate.toLong().fileSize(holder.itemView.context) + "/s" + data.downloadPayloadRate.toLong().fileSize(context) + "/s" ) } @@ -224,11 +223,13 @@ class Download1Proxy( data: DownloadInfoBean, ) { holder.binding.apply { + val context = holder.itemView.context when (data.downloadState) { DownloadInfoBean.DownloadState.Seeding -> { btnDownload1Pause.enable() btnDownload1Pause.setIconResource(R.drawable.ic_pause_24) - btnDownload1Cancel.enable() + btnDownload1Pause.contentDescription = + context.getString(R.string.download_pause) tvDownload1Description.text = data.description lpDownload1.isIndeterminate = false } @@ -236,7 +237,8 @@ class Download1Proxy( DownloadInfoBean.DownloadState.Downloading -> { btnDownload1Pause.enable() btnDownload1Pause.setIconResource(R.drawable.ic_pause_24) - btnDownload1Cancel.enable() + btnDownload1Pause.contentDescription = + context.getString(R.string.download_pause) tvDownload1Description.text = data.description lpDownload1.isIndeterminate = false } @@ -245,37 +247,43 @@ class Download1Proxy( DownloadInfoBean.DownloadState.ErrorPaused -> { btnDownload1Pause.enable() btnDownload1Pause.setIconResource(R.drawable.ic_refresh_24) - btnDownload1Cancel.enable() - tvDownload1Description.text = - holder.itemView.context.getString(R.string.download_error_paused) + btnDownload1Pause.contentDescription = + context.getString(R.string.download_retry) + tvDownload1Description.text = context.getString(R.string.download_error_paused) + lpDownload1.isIndeterminate = false + } + + DownloadInfoBean.DownloadState.SeedingPaused -> { + btnDownload1Pause.enable() + btnDownload1Pause.setIconResource(R.drawable.ic_cloud_upload_24) + btnDownload1Pause.contentDescription = + context.getString(R.string.download_click_to_seeding) + tvDownload1Description.text = context.getString(R.string.download_paused) lpDownload1.isIndeterminate = false } - DownloadInfoBean.DownloadState.SeedingPaused, DownloadInfoBean.DownloadState.Paused -> { btnDownload1Pause.enable() btnDownload1Pause.setIconResource(R.drawable.ic_play_arrow_24) - btnDownload1Cancel.enable() - tvDownload1Description.text = - holder.itemView.context.getString(R.string.download_paused) + btnDownload1Pause.contentDescription = context.getString(R.string.download) + tvDownload1Description.text = context.getString(R.string.download_paused) lpDownload1.isIndeterminate = false } DownloadInfoBean.DownloadState.Init -> { btnDownload1Pause.disable() btnDownload1Pause.setIconResource(R.drawable.ic_play_arrow_24) - btnDownload1Cancel.enable() - tvDownload1Description.text = - holder.itemView.context.getString(R.string.download_initializing) + btnDownload1Pause.contentDescription = context.getString(R.string.download) + tvDownload1Description.text = context.getString(R.string.download_initializing) lpDownload1.isIndeterminate = true } DownloadInfoBean.DownloadState.Completed -> { btnDownload1Pause.enable() btnDownload1Pause.setIconResource(R.drawable.ic_cloud_upload_24) - btnDownload1Cancel.enable() - tvDownload1Description.text = - holder.itemView.context.getString(R.string.download_completed) + btnDownload1Pause.contentDescription = + context.getString(R.string.download_click_to_seeding) + tvDownload1Description.text = context.getString(R.string.download_completed) lpDownload1.isIndeterminate = false } } diff --git a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/License1Proxy.kt b/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/License1Proxy.kt index 21a49622..19af30d1 100644 --- a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/License1Proxy.kt +++ b/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/License1Proxy.kt @@ -22,7 +22,7 @@ class License1Proxy( holder.itemView.setOnClickListener { val data = adapter.dataList.getOrNull(holder.bindingAdapterPosition) if (data !is LicenseBean) return@setOnClickListener - it.context.openBrowser(data.link) + data.link.openBrowser(it.context) } return holder } diff --git a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/Media1Proxy.kt b/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/Media1Proxy.kt index 87b1b05d..ff690d59 100644 --- a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/Media1Proxy.kt +++ b/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/Media1Proxy.kt @@ -11,7 +11,9 @@ import com.skyd.anivu.R import com.skyd.anivu.databinding.ItemMedia1Binding import com.skyd.anivu.ext.fileSize import com.skyd.anivu.ext.gone +import com.skyd.anivu.ext.openWith import com.skyd.anivu.ext.toDateTimeString +import com.skyd.anivu.ext.toUri import com.skyd.anivu.ext.tryAddIcon import com.skyd.anivu.ext.visible import com.skyd.anivu.model.bean.VideoBean @@ -65,6 +67,8 @@ class Media1Proxy( onOpenDir(data) } else if (data.isMedia(parent.context)) { onPlay(data) + } else { + data.file.toUri(it.context).openWith(it.context) } } return holder diff --git a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/OtherWorks1Proxy.kt b/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/OtherWorks1Proxy.kt index 85137729..688a192c 100644 --- a/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/OtherWorks1Proxy.kt +++ b/app/src/main/java/com/skyd/anivu/ui/adapter/variety/proxy/OtherWorks1Proxy.kt @@ -22,7 +22,7 @@ class OtherWorks1Proxy( holder.itemView.setOnClickListener { val data = adapter.dataList.getOrNull(holder.bindingAdapterPosition) if (data !is OtherWorksBean) return@setOnClickListener - it.context.openBrowser(data.url) + data.url.openBrowser(it.context) } return holder } diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/about/AboutFragment.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/about/AboutFragment.kt index 6c8f388f..fc026950 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/about/AboutFragment.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/about/AboutFragment.kt @@ -3,20 +3,17 @@ package com.skyd.anivu.ui.fragment.about import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup -import androidx.annotation.OptIn import androidx.appcompat.content.res.AppCompatResources import androidx.core.view.ViewCompat -import androidx.core.view.updatePadding import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.GridLayoutManager -import com.google.android.material.badge.BadgeDrawable -import com.google.android.material.badge.BadgeUtils -import com.google.android.material.badge.ExperimentalBadgeUtils import com.skyd.anivu.R import com.skyd.anivu.base.BaseFragment import com.skyd.anivu.config.Const import com.skyd.anivu.databinding.FragmentAboutBinding +import com.skyd.anivu.ext.addBadge import com.skyd.anivu.ext.addInsetsByPadding +import com.skyd.anivu.ext.dp import com.skyd.anivu.ext.getAppVersionName import com.skyd.anivu.ext.openBrowser import com.skyd.anivu.ext.popBackStackWithLifecycle @@ -63,7 +60,6 @@ class AboutFragment : BaseFragment() { ) } - @OptIn(ExperimentalBadgeUtils::class) override fun FragmentAboutBinding.initView() { topAppBar.setNavigationOnClickListener { findNavController().popBackStackWithLifecycle() } topAppBar.setOnMenuItemClickListener { @@ -77,19 +73,21 @@ class AboutFragment : BaseFragment() { } } - val badgeDrawable = BadgeDrawable.create(requireContext()) - badgeDrawable.isVisible = true - badgeDrawable.text = requireContext().getAppVersionName() - BadgeUtils.attachBadgeDrawable(badgeDrawable, tvAboutFragmentAppName) + tvAboutFragmentAppName.addBadge { + isVisible = true + verticalOffset = 7.dp + horizontalOffset = 10.dp + text = requireContext().getAppVersionName() + } btnAboutFragmentGithub.setOnClickListener { - it.context.openBrowser(Const.GITHUB_REPO) + Const.GITHUB_REPO.openBrowser(it.context) } btnAboutFragmentTelegram.setOnClickListener { - it.context.openBrowser(Const.TELEGRAM_GROUP) + Const.TELEGRAM_GROUP.openBrowser(it.context) } btnAboutFragmentDiscord.setOnClickListener { - it.context.openBrowser(Const.DISCORD_SERVER) + Const.DISCORD_SERVER.openBrowser(it.context) } rvAboutFragment.layoutManager = GridLayoutManager( diff --git a/app/src/main/java/com/skyd/anivu/ui/fragment/read/ReadFragment.kt b/app/src/main/java/com/skyd/anivu/ui/fragment/read/ReadFragment.kt index 5c40b268..d86af6dd 100644 --- a/app/src/main/java/com/skyd/anivu/ui/fragment/read/ReadFragment.kt +++ b/app/src/main/java/com/skyd/anivu/ui/fragment/read/ReadFragment.kt @@ -112,7 +112,7 @@ class ReadFragment : BaseFragment() { if (articleState is ArticleState.Success) { val link = articleState.article.article.link if (!link.isNullOrBlank()) { - requireContext().openBrowser(link) + link.openBrowser(requireContext()) } } true diff --git a/app/src/main/java/com/skyd/anivu/util/NumberUtil.kt b/app/src/main/java/com/skyd/anivu/util/NumberUtil.kt index ecc17621..495d29db 100644 --- a/app/src/main/java/com/skyd/anivu/util/NumberUtil.kt +++ b/app/src/main/java/com/skyd/anivu/util/NumberUtil.kt @@ -3,5 +3,3 @@ package com.skyd.anivu.util import android.os.SystemClock fun uniqueInt(): Int = (SystemClock.uptimeMillis() % 99999999).toInt() - -fun floatToPercentage(floatValue: Float): String = "%.2f%%".format(floatValue * 100) \ No newline at end of file diff --git a/app/src/main/java/com/skyd/anivu/util/torrent/Util.kt b/app/src/main/java/com/skyd/anivu/util/torrent/Util.kt deleted file mode 100644 index 5c900dad..00000000 --- a/app/src/main/java/com/skyd/anivu/util/torrent/Util.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.skyd.anivu.util.torrent - -import android.util.Log -import com.skyd.anivu.config.Const -import org.libtorrent4j.Vectors -import org.libtorrent4j.alerts.SaveResumeDataAlert -import org.libtorrent4j.swig.add_torrent_params -import org.libtorrent4j.swig.error_code -import org.libtorrent4j.swig.libtorrent -import java.io.File -import java.io.FileOutputStream -import java.io.IOException - - -fun serializeResumeData(name: String, alert: SaveResumeDataAlert) { - val resume = File(Const.TORRENT_RESUME_DATA_DIR, name) - if (!resume.exists()) resume.createNewFile() - val data = libtorrent.write_resume_data(alert.params().swig()).bencode() - try { - FileOutputStream(resume).use { it.write(Vectors.byte_vector2bytes(data)) } - } catch (e: IOException) { - Log.e("serializeResumeData", "Error saving resume data") - } -} - -fun readResumeData(name: String): add_torrent_params? { - val resume = File(Const.TORRENT_RESUME_DATA_DIR, name) - if (!resume.exists()) return null - try { - val data = resume.readBytes() - val ec = error_code() - val p: add_torrent_params = - libtorrent.read_resume_data_ex(Vectors.bytes2byte_vector(data), ec) - require(ec.value() == 0) { "Unable to read the resume data: " + ec.message() } - return p - } catch (e: Throwable) { - Log.w("readResumeData", "Unable to set resume data: $e") - } - return null -} \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index ed2d4c8a..50caead7 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -28,8 +28,10 @@ 下载中… 下载任务 暂停 + 重试 下载失败 已暂停 + 做种 初始化中… 已完成 用户:%d @@ -63,4 +65,6 @@ 搜索订阅 在浏览器中打开 播放 + 操作失败:%s + 打开方式 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1a58f75b..d8884b19 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -28,8 +28,10 @@ Downloading… Download task Pause + Retry Error Paused + Seed Initializing… Completed Peer(s): %d @@ -68,4 +70,6 @@ Search feeds Open in browser Play + Failed: %s + Open with \ No newline at end of file diff --git a/doc/readme/README-zh-rCN.md b/doc/readme/README-zh-rCN.md index 140c2117..64e0bae1 100644 --- a/doc/readme/README-zh-rCN.md +++ b/doc/readme/README-zh-rCN.md @@ -39,13 +39,15 @@ 1. **订阅** RSS、**更新** RSS、**阅读** RSS 2. **下载** RSS 文章中的 **BT 种子或磁力链接**附件(enclosure 标签) -3. **播放**已下载的**视频文件** -4. **更改播放速度**、**长按**视频**倍速播放** -5. **双指旋转缩放视频画面** -6. **滑动**调整**音量**、**屏幕亮度和播放位置** -7. 支持**搜索已获取的 RSS 订阅或文章** -8. 支持**深色模式** -9. ...... +3. 已下载**文件做种** +4. **播放**已下载的**视频文件** +5. **更改播放速度**、**长按**视频**倍速播放** +6. **双指旋转缩放视频画面** +7. **滑动**调整**音量**、**屏幕亮度和播放位置** +8. 支持**搜索已获取的 RSS 订阅或文章** +9. **播放**手机中的**其他视频** +10. 支持**深色模式** +11. ...... ## 🚧待实现 @@ -53,8 +55,6 @@ 2. **自定义播放器配置**,例如:默认的画面比例、播放器使用的 Surface type 等等 3. **悬浮窗播放视频** 4. **自动播放**下一个视频 -5. 已下载的**文件做种** -6. **播放**手机中的**其他视频** ## 🤩应用截图