diff --git a/app/src/main/java/com/github/libretube/api/obj/Streams.kt b/app/src/main/java/com/github/libretube/api/obj/Streams.kt index 1aabe0752..154338b1d 100644 --- a/app/src/main/java/com/github/libretube/api/obj/Streams.kt +++ b/app/src/main/java/com/github/libretube/api/obj/Streams.kt @@ -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 = 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 ) ) diff --git a/app/src/main/java/com/github/libretube/db/Converters.kt b/app/src/main/java/com/github/libretube/db/Converters.kt index be094b0b4..e6e9cf59a 100644 --- a/app/src/main/java/com/github/libretube/db/Converters.kt +++ b/app/src/main/java/com/github/libretube/db/Converters.kt @@ -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) } } diff --git a/app/src/main/java/com/github/libretube/db/obj/Download.kt b/app/src/main/java/com/github/libretube/db/obj/Download.kt index c3994bfc9..a30953dc2 100644 --- a/app/src/main/java/com/github/libretube/db/obj/Download.kt +++ b/app/src/main/java/com/github/libretube/db/obj/Download.kt @@ -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 ) diff --git a/app/src/main/java/com/github/libretube/db/obj/DownloadItem.kt b/app/src/main/java/com/github/libretube/db/obj/DownloadItem.kt index aadc27e1b..c309657e5 100644 --- a/app/src/main/java/com/github/libretube/db/obj/DownloadItem.kt +++ b/app/src/main/java/com/github/libretube/db/obj/DownloadItem.kt @@ -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, diff --git a/app/src/main/java/com/github/libretube/extensions/Path.kt b/app/src/main/java/com/github/libretube/extensions/Path.kt new file mode 100644 index 000000000..6547935ea --- /dev/null +++ b/app/src/main/java/com/github/libretube/extensions/Path.kt @@ -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 +} diff --git a/app/src/main/java/com/github/libretube/helpers/DownloadHelper.kt b/app/src/main/java/com/github/libretube/helpers/DownloadHelper.kt index fcf315785..211455a86 100644 --- a/app/src/main/java/com/github/libretube/helpers/DownloadHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/DownloadHelper.kt @@ -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 { diff --git a/app/src/main/java/com/github/libretube/helpers/ImageHelper.kt b/app/src/main/java/com/github/libretube/helpers/ImageHelper.kt index 7e2325504..2bfc90d3a 100644 --- a/app/src/main/java/com/github/libretube/helpers/ImageHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/ImageHelper.kt @@ -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) { diff --git a/app/src/main/java/com/github/libretube/services/DownloadService.kt b/app/src/main/java/com/github/libretube/services/DownloadService.kt index afc54f696..144a4b04e 100644 --- a/app/src/main/java/com/github/libretube/services/DownloadService.kt +++ b/app/src/main/java/com/github/libretube/services/DownloadService.kt @@ -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() { diff --git a/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt b/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt index 58801d97b..66080a88b 100644 --- a/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt +++ b/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt @@ -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 -> { diff --git a/app/src/main/java/com/github/libretube/ui/adapters/DownloadsAdapter.kt b/app/src/main/java/com/github/libretube/ui/adapters/DownloadsAdapter.kt index fe44a3ca9..c6d5dd321 100644 --- a/app/src/main/java/com/github/libretube/ui/adapters/DownloadsAdapter.kt +++ b/app/src/main/java/com/github/libretube/ui/adapters/DownloadsAdapter.kt @@ -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) diff --git a/app/src/main/java/com/github/libretube/ui/fragments/DownloadsFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/DownloadsFragment.kt index 15923b9b6..f5f889fc1 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/DownloadsFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/DownloadsFragment.kt @@ -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) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 36bb336de..868ee6c76 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }