Use java.nio.file APIs in download functionality.

This commit is contained in:
Isira Seneviratne 2023-03-07 07:14:09 +05:30
parent 9c237861c5
commit 13f8b3d49d
12 changed files with 82 additions and 71 deletions

View File

@ -2,6 +2,7 @@ package com.github.libretube.api.obj
import com.github.libretube.db.obj.DownloadItem
import com.github.libretube.enums.FileType
import java.nio.file.Paths
import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
@ -33,6 +34,7 @@ data class Streams(
val uploaderSubscriberCount: Long = 0,
val previewFrames: List<PreviewFrames> = emptyList()
) {
@Suppress("NewApi") // The Paths class is desugared.
fun toDownloadItems(
videoId: String,
fileName: String,
@ -53,7 +55,7 @@ data class Streams(
type = FileType.VIDEO,
videoId = videoId,
fileName = stream?.getQualityString(fileName).orEmpty(),
path = "",
path = Paths.get(""),
url = stream?.url,
format = videoFormat,
quality = videoQuality
@ -70,7 +72,7 @@ data class Streams(
type = FileType.AUDIO,
videoId = videoId,
fileName = stream?.getQualityString(fileName).orEmpty(),
path = "",
path = Paths.get(""),
url = stream?.url,
format = audioFormat,
quality = audioQuality
@ -84,7 +86,7 @@ data class Streams(
type = FileType.SUBTITLE,
videoId = videoId,
fileName = "${fileName}_$subtitleCode.srt",
path = "",
path = Paths.get(""),
url = subtitles.find { it.code == subtitleCode }?.url
)
)

View File

@ -1,6 +1,8 @@
package com.github.libretube.db
import androidx.room.TypeConverter
import java.nio.file.Path
import java.nio.file.Paths
import kotlinx.datetime.LocalDate
import kotlinx.datetime.toLocalDate
@ -10,4 +12,10 @@ object Converters {
@TypeConverter
fun stringToLocalDate(string: String?) = string?.toLocalDate()
@TypeConverter
fun pathToString(path: Path?) = path?.toString()
@TypeConverter
fun stringToPath(string: String?) = string?.let { Paths.get(it) }
}

View File

@ -2,6 +2,7 @@ package com.github.libretube.db.obj
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.nio.file.Path
import kotlinx.datetime.LocalDate
@Entity(tableName = "download")
@ -12,5 +13,5 @@ data class Download(
val description: String = "",
val uploader: String = "",
val uploadDate: LocalDate? = null,
val thumbnailPath: String? = null
val thumbnailPath: Path? = null
)

View File

@ -5,6 +5,7 @@ import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import com.github.libretube.enums.FileType
import java.nio.file.Path
@Entity(
tableName = "downloadItem",
@ -24,7 +25,7 @@ data class DownloadItem(
val type: FileType,
val videoId: String,
val fileName: String,
var path: String,
var path: Path,
var url: String? = null,
var format: String? = null,
var quality: String? = null,

View File

@ -0,0 +1,11 @@
package com.github.libretube.extensions
import android.net.Uri
import androidx.core.net.toUri
import java.nio.file.Path
import kotlin.io.path.exists
fun Path.toAndroidUri(): Uri? {
@Suppress("NewApi") // The Path class is desugared.
return if (exists()) toFile().toUri() else null
}

View File

@ -8,7 +8,8 @@ import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.db.obj.DownloadItem
import com.github.libretube.services.DownloadService
import java.io.File
import java.nio.file.Path
import kotlin.io.path.createDirectories
object DownloadHelper {
const val VIDEO_DIR = "video"
@ -20,23 +21,21 @@ object DownloadHelper {
const val DEFAULT_TIMEOUT = 15 * 1000
const val DEFAULT_RETRY = 3
fun getOfflineStorageDir(context: Context): File {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return context.filesDir
return try {
context.getExternalFilesDir(null)!!
} catch (e: Exception) {
private fun getOfflineStorageDir(context: Context): Path {
val file = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
context.filesDir
} else {
try {
context.getExternalFilesDir(null)!!
} catch (e: Exception) {
context.filesDir
}
}
return file.toPath()
}
fun getDownloadDir(context: Context, path: String): File {
return File(
getOfflineStorageDir(context),
path
).apply {
if (!this.exists()) this.mkdirs()
}
fun getDownloadDir(context: Context, path: String): Path {
return getOfflineStorageDir(context).resolve(path).createDirectories()
}
fun getMaxConcurrentDownloads(): Int {

View File

@ -6,6 +6,7 @@ import android.graphics.BitmapFactory
import android.net.Uri
import android.widget.ImageView
import androidx.core.graphics.drawable.toBitmap
import androidx.core.net.toUri
import coil.ImageLoader
import coil.disk.DiskCache
import coil.load
@ -13,8 +14,9 @@ import coil.request.CachePolicy
import coil.request.ImageRequest
import com.github.libretube.api.CronetHelper
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.extensions.toAndroidUri
import com.github.libretube.util.DataSaverMode
import java.io.File
import java.nio.file.Path
object ImageHelper {
lateinit var imageLoader: ImageLoader
@ -54,9 +56,10 @@ object ImageHelper {
if (!DataSaverMode.isEnabled(target.context)) target.load(url, imageLoader)
}
fun downloadImage(context: Context, url: String, path: String) {
fun downloadImage(context: Context, url: String, path: Path) {
getAsync(context, url) { bitmap ->
saveImage(context, bitmap, Uri.fromFile(File(path)))
@Suppress("NewApi") // The Path class is desugared.
saveImage(context, bitmap, path.toFile().toUri())
}
}
@ -69,10 +72,8 @@ object ImageHelper {
imageLoader.enqueue(request)
}
fun getDownloadedImage(context: Context, path: String): Bitmap? {
val file = File(path)
if (!file.exists()) return null
return getImage(context, Uri.fromFile(file))
fun getDownloadedImage(context: Context, path: Path): Bitmap? {
return path.toAndroidUri()?.let { getImage(context, it) }
}
private fun saveImage(context: Context, bitmapImage: Bitmap, imagePath: Uri) {

View File

@ -38,7 +38,12 @@ import java.io.File
import java.net.HttpURLConnection
import java.net.SocketTimeoutException
import java.net.URL
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import java.util.concurrent.Executors
import kotlin.io.path.absolute
import kotlin.io.path.createFile
import kotlin.io.path.fileSize
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -48,7 +53,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okio.BufferedSink
import okio.buffer
import okio.sink
import okio.source
@ -96,7 +100,8 @@ class DownloadService : LifecycleService() {
RetrofitInstance.api.getStreams(videoId)
}
val thumbnailTargetFile = getDownloadFile(DownloadHelper.THUMBNAIL_DIR, fileName)
val thumbnailTargetPath = getDownloadPath(DownloadHelper.THUMBNAIL_DIR, fileName)
.absolute()
val download = Download(
videoId,
@ -104,13 +109,13 @@ class DownloadService : LifecycleService() {
streams.description,
streams.uploader,
streams.uploadDate,
thumbnailTargetFile.absolutePath
thumbnailTargetPath
)
Database.downloadDao().insertDownload(download)
ImageHelper.downloadImage(
this@DownloadService,
streams.thumbnailUrl,
thumbnailTargetFile.absolutePath
thumbnailTargetPath
)
val downloadItems = streams.toDownloadItems(
@ -136,13 +141,11 @@ class DownloadService : LifecycleService() {
* for the requested file.
*/
private fun start(item: DownloadItem) {
val file = when (item.type) {
FileType.AUDIO -> getDownloadFile(DownloadHelper.AUDIO_DIR, item.fileName)
FileType.VIDEO -> getDownloadFile(DownloadHelper.VIDEO_DIR, item.fileName)
FileType.SUBTITLE -> getDownloadFile(DownloadHelper.SUBTITLE_DIR, item.fileName)
}
file.createNewFile()
item.path = file.absolutePath
item.path = when (item.type) {
FileType.AUDIO -> getDownloadPath(DownloadHelper.AUDIO_DIR, item.fileName)
FileType.VIDEO -> getDownloadPath(DownloadHelper.VIDEO_DIR, item.fileName)
FileType.SUBTITLE -> getDownloadPath(DownloadHelper.SUBTITLE_DIR, item.fileName)
}.createFile().absolute()
lifecycleScope.launch(coroutineContext) {
item.id = Database.downloadDao().insertDownloadItem(item).toInt()
@ -158,8 +161,8 @@ class DownloadService : LifecycleService() {
downloadQueue[item.id] = true
val notificationBuilder = getNotificationBuilder(item)
setResumeNotification(notificationBuilder, item)
val file = File(item.path)
var totalRead = file.length()
val path = item.path
var totalRead = path.fileSize()
val url = URL(item.url ?: return)
url.getContentLength().let { size ->
@ -206,7 +209,8 @@ class DownloadService : LifecycleService() {
return
}
val sink: BufferedSink = file.sink(true).buffer()
@Suppress("NewApi") // The StandardOpenOption enum is desugared.
val sink = path.sink(StandardOpenOption.APPEND).buffer()
val sourceByte = con.inputStream.source()
var lastTime = System.currentTimeMillis() / 1000
@ -437,14 +441,8 @@ class DownloadService : LifecycleService() {
/**
* Get a [File] from the corresponding download directory and the file name
*/
private fun getDownloadFile(directory: String, fileName: String): File {
return File(
DownloadHelper.getDownloadDir(
this@DownloadService,
directory
),
fileName
)
private fun getDownloadPath(directory: String, fileName: String): Path {
return DownloadHelper.getDownloadDir(this, directory).resolve(fileName)
}
override fun onDestroy() {

View File

@ -15,6 +15,7 @@ import com.github.libretube.databinding.ActivityOfflinePlayerBinding
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
import com.github.libretube.db.DatabaseHolder.Database
import com.github.libretube.enums.FileType
import com.github.libretube.extensions.toAndroidUri
import com.github.libretube.extensions.updateParameters
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.helpers.PlayerHelper.loadPlaybackParams
@ -33,7 +34,6 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.upstream.FileDataSource
import com.google.android.exoplayer2.util.MimeTypes
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -107,10 +107,6 @@ class OfflinePlayerActivity : BaseActivity() {
)
}
private fun File.toUri(): Uri? {
return if (this.exists()) Uri.fromFile(this) else null
}
private fun playVideo() {
lifecycleScope.launch {
val downloadFiles = withContext(Dispatchers.IO) {
@ -121,9 +117,9 @@ class OfflinePlayerActivity : BaseActivity() {
val audio = downloadFiles.firstOrNull { it.type == FileType.AUDIO }
val subtitle = downloadFiles.firstOrNull { it.type == FileType.SUBTITLE }
val videoUri = video?.path?.let { File(it).toUri() }
val audioUri = audio?.path?.let { File(it).toUri() }
val subtitleUri = subtitle?.path?.let { File(it).toUri() }
val videoUri = video?.path?.toAndroidUri()
val audioUri = audio?.path?.toAndroidUri()
val subtitleUri = subtitle?.path?.toAndroidUri()
setMediaSource(videoUri, audioUri, subtitleUri)
@ -143,7 +139,6 @@ class OfflinePlayerActivity : BaseActivity() {
.setMimeType(MimeTypes.APPLICATION_SUBRIP)
.build()
}
subtitle?.id
when {
videoUri != null && audioUri != null -> {

View File

@ -18,7 +18,8 @@ import com.github.libretube.ui.activities.OfflinePlayerActivity
import com.github.libretube.ui.viewholders.DownloadsViewHolder
import com.github.libretube.util.TextUtils
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.io.File
import kotlin.io.path.deleteIfExists
import kotlin.io.path.fileSize
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
@ -46,7 +47,7 @@ class DownloadsAdapter(
videoInfo.text = download.uploadDate?.let { TextUtils.localizeDate(it) }
val downloadSize = items.sumOf { it.downloadSize }
val currentSize = items.sumOf { File(it.path).length() }
val currentSize = items.sumOf { it.path.fileSize() }
if (downloadSize == -1L) {
progressBar.isIndeterminate = true
@ -96,16 +97,10 @@ class DownloadsAdapter(
.setTitle(R.string.delete)
.setMessage(R.string.irreversible)
.setPositiveButton(R.string.okay) { _, _ ->
items.map { File(it.path) }.forEach { file ->
runCatching {
if (file.exists()) file.delete()
}
}
runCatching {
download.thumbnailPath?.let {
File(it).delete()
}
items.forEach {
it.path.deleteIfExists()
}
download.thumbnailPath?.deleteIfExists()
runBlocking(Dispatchers.IO) {
DatabaseHolder.Database.downloadDao().deleteDownload(download)

View File

@ -24,7 +24,7 @@ import com.github.libretube.receivers.DownloadReceiver
import com.github.libretube.services.DownloadService
import com.github.libretube.ui.adapters.DownloadsAdapter
import com.github.libretube.ui.viewholders.DownloadsViewHolder
import java.io.File
import kotlin.io.path.fileSize
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
@ -84,7 +84,7 @@ class DownloadsFragment : Fragment() {
binding.downloads.adapter = DownloadsAdapter(requireContext(), downloads) {
var isDownloading = false
val ids = it.downloadItems
.filter { item -> File(item.path).length() < item.downloadSize }
.filter { item -> item.path.fileSize() < item.downloadSize }
.map { item -> item.id }
if (!serviceConnection.isBound) {

View File

@ -36,7 +36,7 @@ androidx-work-runtime = { group = "androidx.work", name="work-runtime-ktx", vers
exoplayer = { group = "com.google.android.exoplayer", name = "exoplayer", version.ref = "exoplayer" }
exoplayer-extension-mediasession = { group = "com.google.android.exoplayer", name = "extension-mediasession", version.ref = "exoplayer" }
square-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
desugaring = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "desugaring" }
desugaring = { group = "com.android.tools", name = "desugar_jdk_libs_nio", version.ref = "desugaring" }
exoplayer-extension-cronet = { group = "com.google.android.exoplayer", name = "extension-cronet", version.ref = "exoplayer" }
exoplayer-dash = { group = "com.google.android.exoplayer", name = "exoplayer-dash", version.ref = "exoplayer" }
cronet-embedded = { group = "org.chromium.net", name = "cronet-embedded", version.ref = "cronetEmbedded" }