mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
bump + refactor
This commit is contained in:
parent
b38a9b1ff5
commit
8193e39b30
11 changed files with 40 additions and 779 deletions
|
@ -32,11 +32,12 @@ android {
|
||||||
enable = true
|
enable = true
|
||||||
}
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
// disable this for now
|
||||||
cmake {
|
//externalNativeBuild {
|
||||||
path("CMakeLists.txt")
|
// cmake {
|
||||||
}
|
// path("CMakeLists.txt")
|
||||||
}
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
create("prerelease") {
|
create("prerelease") {
|
||||||
|
@ -58,7 +59,7 @@ android {
|
||||||
targetSdk = 29
|
targetSdk = 29
|
||||||
|
|
||||||
versionCode = 59
|
versionCode = 59
|
||||||
versionName = "4.1.7"
|
versionName = "4.1.8"
|
||||||
|
|
||||||
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
||||||
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
||||||
|
@ -232,7 +233,7 @@ dependencies {
|
||||||
// To fix SSL fuckery on android 9
|
// To fix SSL fuckery on android 9
|
||||||
implementation("org.conscrypt:conscrypt-android:2.2.1")
|
implementation("org.conscrypt:conscrypt-android:2.2.1")
|
||||||
// Util to skip the URI file fuckery 🙏
|
// Util to skip the URI file fuckery 🙏
|
||||||
implementation("com.github.tachiyomiorg:unifile:17bec43")
|
implementation("com.github.LagradOst:SafeFile:0.0.2")
|
||||||
|
|
||||||
// API because cba maintaining it myself
|
// API because cba maintaining it myself
|
||||||
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0")
|
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0")
|
||||||
|
|
|
@ -144,6 +144,7 @@ import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
||||||
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
||||||
import com.lagradost.nicehttp.Requests
|
import com.lagradost.nicehttp.Requests
|
||||||
import com.lagradost.nicehttp.ResponseParser
|
import com.lagradost.nicehttp.ResponseParser
|
||||||
|
import com.lagradost.safefile.SafeFile
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -279,6 +280,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "MAINACT"
|
const val TAG = "MAINACT"
|
||||||
var lastError: String? = null
|
var lastError: String? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setting this will automatically enter the query in the search
|
* Setting this will automatically enter the query in the search
|
||||||
* next time the search fragment is opened.
|
* next time the search fragment is opened.
|
||||||
|
@ -1131,10 +1133,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
snackbar.show()
|
snackbar.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ioSafe { SafeFile.check(this@MainActivity) }
|
||||||
|
|
||||||
if (PluginManager.checkSafeModeFile()) {
|
if (PluginManager.checkSafeModeFile()) {
|
||||||
normalSafeApiCall {
|
normalSafeApiCall {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import kotlinx.coroutines.launch
|
||||||
|
|
||||||
object NativeCrashHandler {
|
object NativeCrashHandler {
|
||||||
// external fun triggerNativeCrash()
|
// external fun triggerNativeCrash()
|
||||||
private external fun initNativeCrashHandler()
|
/*private external fun initNativeCrashHandler()
|
||||||
private external fun getSignalStatus(): Int
|
private external fun getSignalStatus(): Int
|
||||||
|
|
||||||
private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch {
|
private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
@ -49,5 +49,5 @@ object NativeCrashHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
initSignalPolling()
|
initSignalPolling()
|
||||||
}
|
}*/
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package com.lagradost.cloudstream3.ui.player
|
package com.lagradost.cloudstream3.ui.player
|
||||||
|
|
||||||
|
import android.content.ContentUris
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -10,7 +11,7 @@ import com.lagradost.cloudstream3.CommonActivity
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
import com.lagradost.cloudstream3.utils.ExtractorUri
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||||
import com.lagradost.cloudstream3.utils.storage.SafeFile
|
import com.lagradost.safefile.SafeFile
|
||||||
|
|
||||||
const val DTAG = "PlayerActivity"
|
const val DTAG = "PlayerActivity"
|
||||||
|
|
||||||
|
@ -57,7 +58,10 @@ class DownloadedPlayerActivity : AppCompatActivity() {
|
||||||
listOf(
|
listOf(
|
||||||
ExtractorUri(
|
ExtractorUri(
|
||||||
uri = uri,
|
uri = uri,
|
||||||
name = name ?: getString(R.string.downloaded_file)
|
name = name ?: getString(R.string.downloaded_file),
|
||||||
|
// well not the same as a normal id, but we take it as users may want to
|
||||||
|
// play downloaded files and save the location
|
||||||
|
id = kotlin.runCatching { ContentUris.parseId(uri) }.getOrNull()?.hashCode()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -21,13 +21,11 @@ import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.media3.common.Format.NO_VALUE
|
import androidx.media3.common.Format.NO_VALUE
|
||||||
import androidx.media3.common.MimeTypes
|
import androidx.media3.common.MimeTypes
|
||||||
import com.hippo.unifile.UniFile
|
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding
|
import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding
|
||||||
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
|
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
|
||||||
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
|
|
||||||
import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding
|
import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding
|
||||||
import com.lagradost.cloudstream3.databinding.PlayerSelectTracksBinding
|
import com.lagradost.cloudstream3.databinding.PlayerSelectTracksBinding
|
||||||
import com.lagradost.cloudstream3.mvvm.*
|
import com.lagradost.cloudstream3.mvvm.*
|
||||||
|
@ -52,7 +50,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
|
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
import com.lagradost.cloudstream3.utils.storage.SafeFile
|
import com.lagradost.safefile.SafeFile
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
@ -136,7 +134,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
return durPos.position
|
return durPos.position
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentVerifyLink: Job? = null
|
private var currentVerifyLink: Job? = null
|
||||||
|
|
||||||
private fun loadExtractorJob(extractorLink: ExtractorLink?) {
|
private fun loadExtractorJob(extractorLink: ExtractorLink?) {
|
||||||
currentVerifyLink?.cancel()
|
currentVerifyLink?.cancel()
|
||||||
|
|
|
@ -38,7 +38,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||||
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath
|
||||||
import com.lagradost.cloudstream3.utils.storage.SafeFile
|
import com.lagradost.safefile.SafeFile
|
||||||
|
|
||||||
fun getCurrentLocale(context: Context): String {
|
fun getCurrentLocale(context: Context): String {
|
||||||
val res = context.resources
|
val res = context.resources
|
||||||
|
@ -335,7 +335,7 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
val currentDir =
|
val currentDir =
|
||||||
settingsManager.getString(getString(R.string.download_path_pref), null)
|
settingsManager.getString(getString(R.string.download_path_pref), null)
|
||||||
?: context?.let { ctx -> VideoDownloadManager.getDefaultDir(ctx).toString() }
|
?: context?.let { ctx -> VideoDownloadManager.getDefaultDir(ctx)?.filePath() }
|
||||||
|
|
||||||
activity?.showBottomDialog(
|
activity?.showBottomDialog(
|
||||||
dirs + listOf("Custom"),
|
dirs + listOf("Custom"),
|
||||||
|
|
|
@ -158,6 +158,7 @@ object BackupUtils {
|
||||||
val displayName = "CS3_Backup_${date}"
|
val displayName = "CS3_Backup_${date}"
|
||||||
val backupFile = getBackup()
|
val backupFile = getBackup()
|
||||||
val stream = setupStream(this, displayName, null, ext, false)
|
val stream = setupStream(this, displayName, null, ext, false)
|
||||||
|
|
||||||
fileStream = stream.openNew()
|
fileStream = stream.openNew()
|
||||||
printStream = PrintWriter(fileStream)
|
printStream = PrintWriter(fileStream)
|
||||||
printStream.print(mapper.writeValueAsString(backupFile))
|
printStream.print(mapper.writeValueAsString(backupFile))
|
||||||
|
|
|
@ -35,8 +35,8 @@ import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||||
import com.lagradost.cloudstream3.utils.storage.MediaFileContentType
|
import com.lagradost.safefile.MediaFileContentType
|
||||||
import com.lagradost.cloudstream3.utils.storage.SafeFile
|
import com.lagradost.safefile.SafeFile
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -554,9 +554,8 @@ object VideoDownloadManager {
|
||||||
extension: String,
|
extension: String,
|
||||||
tryResume: Boolean,
|
tryResume: Boolean,
|
||||||
): StreamData {
|
): StreamData {
|
||||||
val (base, _) = context.getBasePath()
|
|
||||||
return setupStream(
|
return setupStream(
|
||||||
base ?: throw IOException("Bad config"),
|
context.getBasePath().first ?: getDefaultDir(context) ?: throw IOException("Bad config"),
|
||||||
name,
|
name,
|
||||||
folder,
|
folder,
|
||||||
extension,
|
extension,
|
||||||
|
@ -1401,7 +1400,12 @@ object VideoDownloadManager {
|
||||||
metadata.type = DownloadType.IsFailed
|
metadata.type = DownloadType.IsFailed
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
try {
|
||||||
|
// may cause java.lang.IllegalStateException: Mutex is not locked because of cancelling
|
||||||
fileMutex.unlock()
|
fileMutex.unlock()
|
||||||
|
} catch (t : Throwable) {
|
||||||
|
logError(t)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,389 +0,0 @@
|
||||||
package com.lagradost.cloudstream3.utils.storage
|
|
||||||
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.content.ContentUris
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Environment
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import com.hippo.unifile.UniRandomAccessFile
|
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
|
||||||
import okhttp3.internal.closeQuietly
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
|
|
||||||
enum class MediaFileContentType {
|
|
||||||
Downloads,
|
|
||||||
Audio,
|
|
||||||
Video,
|
|
||||||
Images,
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://developer.android.com/training/data-storage/shared/media
|
|
||||||
fun MediaFileContentType.toPath(): String {
|
|
||||||
return when (this) {
|
|
||||||
MediaFileContentType.Downloads -> Environment.DIRECTORY_DOWNLOADS
|
|
||||||
MediaFileContentType.Audio -> Environment.DIRECTORY_MUSIC
|
|
||||||
MediaFileContentType.Video -> Environment.DIRECTORY_MOVIES
|
|
||||||
MediaFileContentType.Images -> Environment.DIRECTORY_DCIM
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MediaFileContentType.defaultPrefix(): String {
|
|
||||||
return Environment.getExternalStorageDirectory().absolutePath
|
|
||||||
}
|
|
||||||
|
|
||||||
fun MediaFileContentType.toAbsolutePath(): String {
|
|
||||||
return defaultPrefix() + File.separator +
|
|
||||||
this.toPath()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun replaceDuplicateFileSeparators(path: String): String {
|
|
||||||
return path.replace(Regex("${File.separator}+"), File.separator)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
|
||||||
fun MediaFileContentType.toUri(external: Boolean): Uri {
|
|
||||||
val volume = if (external) MediaStore.VOLUME_EXTERNAL_PRIMARY else MediaStore.VOLUME_INTERNAL
|
|
||||||
return when (this) {
|
|
||||||
MediaFileContentType.Downloads -> MediaStore.Downloads.getContentUri(volume)
|
|
||||||
MediaFileContentType.Audio -> MediaStore.Audio.Media.getContentUri(volume)
|
|
||||||
MediaFileContentType.Video -> MediaStore.Video.Media.getContentUri(volume)
|
|
||||||
MediaFileContentType.Images -> MediaStore.Images.Media.getContentUri(volume)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
|
||||||
class MediaFile(
|
|
||||||
private val context: Context,
|
|
||||||
private val folderType: MediaFileContentType,
|
|
||||||
private val external: Boolean = true,
|
|
||||||
absolutePath: String,
|
|
||||||
) : SafeFile {
|
|
||||||
override fun toString(): String {
|
|
||||||
return sanitizedAbsolutePath
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is the path relative to the download directory so "/hello/text.txt" = "hello/text.txt" is in fact "Download/hello/text.txt"
|
|
||||||
private val sanitizedAbsolutePath: String =
|
|
||||||
replaceDuplicateFileSeparators(absolutePath)
|
|
||||||
|
|
||||||
// this is only a directory if the filepath ends with a /
|
|
||||||
private val isDir: Boolean = sanitizedAbsolutePath.endsWith(File.separator)
|
|
||||||
private val isFile: Boolean = !isDir
|
|
||||||
|
|
||||||
// this is the relative path including the Download directory, so "/hello/text.txt" => "Download/hello"
|
|
||||||
private val relativePath: String =
|
|
||||||
replaceDuplicateFileSeparators(folderType.toPath() + File.separator + sanitizedAbsolutePath).substringBeforeLast(
|
|
||||||
File.separator
|
|
||||||
)
|
|
||||||
|
|
||||||
// "/hello/text.txt" => "text.txt"
|
|
||||||
private val namePath: String = sanitizedAbsolutePath.substringAfterLast(File.separator)
|
|
||||||
private val baseUri = folderType.toUri(external)
|
|
||||||
private val contentResolver: ContentResolver = context.contentResolver
|
|
||||||
|
|
||||||
init {
|
|
||||||
// some standard asserts that should always be hold or else this class wont work
|
|
||||||
assert(!relativePath.endsWith(File.separator))
|
|
||||||
assert(!(isDir && isFile))
|
|
||||||
assert(!relativePath.contains(File.separator + File.separator))
|
|
||||||
assert(!namePath.contains(File.separator))
|
|
||||||
|
|
||||||
if (isDir) {
|
|
||||||
assert(namePath.isBlank())
|
|
||||||
} else {
|
|
||||||
assert(namePath.isNotBlank())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private fun splitFilenameExt(name: String): Pair<String, String?> {
|
|
||||||
val split = name.indexOfLast { it == '.' }
|
|
||||||
if (split <= 0) return name to null
|
|
||||||
val ext = name.substring(split + 1 until name.length)
|
|
||||||
if (ext.isBlank()) return name to null
|
|
||||||
|
|
||||||
return name.substring(0 until split) to ext
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun splitFilenameMime(name: String): Pair<String, String?> {
|
|
||||||
val (display, ext) = splitFilenameExt(name)
|
|
||||||
val mimeType = when (ext) {
|
|
||||||
|
|
||||||
// Absolutely ridiculous, if text/vtt is used as mimetype scoped storage prevents
|
|
||||||
// downloading to /Downloads yet it works with null
|
|
||||||
|
|
||||||
"vtt" -> null // "text/vtt"
|
|
||||||
"mp4" -> "video/mp4"
|
|
||||||
"srt" -> null // "application/x-subrip"//"text/plain"
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
return display to mimeType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun appendRelativePath(path: String, folder: Boolean): MediaFile? {
|
|
||||||
if (isFile) return null
|
|
||||||
|
|
||||||
// VideoDownloadManager.sanitizeFilename(path.replace(File.separator, ""))
|
|
||||||
|
|
||||||
// in case of duplicate path, aka Download -> Download
|
|
||||||
if (relativePath == path) return this
|
|
||||||
|
|
||||||
val newPath =
|
|
||||||
sanitizedAbsolutePath + path + if (folder) File.separator else ""
|
|
||||||
|
|
||||||
return MediaFile(
|
|
||||||
context = context,
|
|
||||||
folderType = folderType,
|
|
||||||
external = external,
|
|
||||||
absolutePath = newPath
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createUri(displayName: String? = namePath): Uri? {
|
|
||||||
if (displayName == null) return null
|
|
||||||
if (isFile) return null
|
|
||||||
val (name, mime) = splitFilenameMime(displayName)
|
|
||||||
|
|
||||||
val newFile = ContentValues().apply {
|
|
||||||
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
|
|
||||||
put(MediaStore.MediaColumns.TITLE, name)
|
|
||||||
if (mime != null)
|
|
||||||
put(MediaStore.MediaColumns.MIME_TYPE, mime)
|
|
||||||
put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
|
|
||||||
}
|
|
||||||
return contentResolver.insert(baseUri, newFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createFile(displayName: String?): SafeFile? {
|
|
||||||
if (isFile || displayName == null) return null
|
|
||||||
query(displayName)?.uri ?: createUri(displayName) ?: return null
|
|
||||||
return appendRelativePath(displayName, false) //SafeFile.fromUri(context, ?: return null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createDirectory(directoryName: String?): SafeFile? {
|
|
||||||
if (directoryName == null) return null
|
|
||||||
// we don't create a dir here tbh, just fake create it
|
|
||||||
return appendRelativePath(directoryName, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class QueryResult(
|
|
||||||
val uri: Uri,
|
|
||||||
val lastModified: Long,
|
|
||||||
val length: Long,
|
|
||||||
)
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
|
||||||
private fun query(displayName: String = namePath): QueryResult? {
|
|
||||||
try {
|
|
||||||
//val (name, mime) = splitFilenameMime(fullName)
|
|
||||||
|
|
||||||
val projection = arrayOf(
|
|
||||||
MediaStore.MediaColumns._ID,
|
|
||||||
MediaStore.MediaColumns.DATE_MODIFIED,
|
|
||||||
MediaStore.MediaColumns.SIZE,
|
|
||||||
)
|
|
||||||
|
|
||||||
val selection =
|
|
||||||
"${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath${File.separator}' AND ${MediaStore.MediaColumns.DISPLAY_NAME}='$displayName'"
|
|
||||||
|
|
||||||
contentResolver.query(
|
|
||||||
baseUri,
|
|
||||||
projection, selection, null, null
|
|
||||||
)?.use { cursor ->
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
val id =
|
|
||||||
cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
|
|
||||||
|
|
||||||
return QueryResult(
|
|
||||||
uri = ContentUris.withAppendedId(
|
|
||||||
baseUri, id
|
|
||||||
),
|
|
||||||
lastModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)),
|
|
||||||
length = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
logError(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun uri(): Uri? {
|
|
||||||
return query()?.uri
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun name(): String? {
|
|
||||||
if (isDir) return null
|
|
||||||
return namePath
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun type(): String? {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun filePath(): String {
|
|
||||||
return replaceDuplicateFileSeparators(relativePath + File.separator + namePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isDirectory(): Boolean {
|
|
||||||
return isDir
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isFile(): Boolean {
|
|
||||||
return isFile
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun lastModified(): Long? {
|
|
||||||
if (isDir) return null
|
|
||||||
return query()?.lastModified
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun length(): Long? {
|
|
||||||
if (isDir) return null
|
|
||||||
val query = query()
|
|
||||||
val length = query?.length ?: return null
|
|
||||||
if (length <= 0) {
|
|
||||||
try {
|
|
||||||
contentResolver.openFileDescriptor(query.uri, "r")
|
|
||||||
.use {
|
|
||||||
it?.statSize
|
|
||||||
}?.let {
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val inputStream: InputStream = openInputStream() ?: return null
|
|
||||||
return try {
|
|
||||||
inputStream.available().toLong()
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
null
|
|
||||||
} finally {
|
|
||||||
inputStream.closeQuietly()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return length
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canRead(): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canWrite(): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun delete(uri: Uri): Boolean {
|
|
||||||
return contentResolver.delete(uri, null, null) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun delete(): Boolean {
|
|
||||||
return if (isDir) {
|
|
||||||
(listFiles() ?: return false).all {
|
|
||||||
it.delete()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delete(uri() ?: return false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun exists(): Boolean {
|
|
||||||
if (isDir) return true
|
|
||||||
return query() != null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun listFiles(): List<SafeFile>? {
|
|
||||||
if (isFile) return null
|
|
||||||
try {
|
|
||||||
val projection = arrayOf(
|
|
||||||
MediaStore.MediaColumns.DISPLAY_NAME
|
|
||||||
)
|
|
||||||
|
|
||||||
val selection =
|
|
||||||
"${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath${File.separator}'"
|
|
||||||
contentResolver.query(
|
|
||||||
baseUri,
|
|
||||||
projection, selection, null, null
|
|
||||||
)?.use { cursor ->
|
|
||||||
val out = ArrayList<SafeFile>(cursor.count)
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
val nameIdx = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)
|
|
||||||
if (nameIdx == -1) continue
|
|
||||||
val name = cursor.getString(nameIdx)
|
|
||||||
|
|
||||||
appendRelativePath(name, false)?.let { new ->
|
|
||||||
out.add(new)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
out
|
|
||||||
}
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
logError(t)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun findFile(displayName: String?, ignoreCase: Boolean): SafeFile? {
|
|
||||||
if (isFile || displayName == null) return null
|
|
||||||
|
|
||||||
val new = appendRelativePath(displayName, false) ?: return null
|
|
||||||
if (new.exists()) {
|
|
||||||
return new
|
|
||||||
}
|
|
||||||
|
|
||||||
return null//SafeFile.fromUri(context, query(displayName ?: return null)?.uri ?: return null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun renameTo(name: String?): Boolean {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun openOutputStream(append: Boolean): OutputStream? {
|
|
||||||
try {
|
|
||||||
// use current file
|
|
||||||
uri()?.let {
|
|
||||||
return contentResolver.openOutputStream(
|
|
||||||
it,
|
|
||||||
if (append) "wa" else "wt"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a new file if current is not found,
|
|
||||||
// as we know it is new only write access is needed
|
|
||||||
createUri()?.let {
|
|
||||||
return contentResolver.openOutputStream(
|
|
||||||
it,
|
|
||||||
"w"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun openInputStream(): InputStream? {
|
|
||||||
try {
|
|
||||||
return contentResolver.openInputStream(uri() ?: return null)
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createRandomAccessFile(mode: String?): UniRandomAccessFile? {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,244 +0,0 @@
|
||||||
package com.lagradost.cloudstream3.utils.storage
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Environment
|
|
||||||
import com.hippo.unifile.UniFile
|
|
||||||
import com.hippo.unifile.UniRandomAccessFile
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
interface SafeFile {
|
|
||||||
companion object {
|
|
||||||
fun fromUri(context: Context, uri: Uri): SafeFile? {
|
|
||||||
return UniFileWrapper(UniFile.fromUri(context, uri) ?: return null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fromFile(context: Context, file: File?): SafeFile? {
|
|
||||||
if (file == null) return null
|
|
||||||
// because UniFile sucks balls on Media we have to do this
|
|
||||||
val absPath = file.absolutePath.removePrefix(File.separator)
|
|
||||||
for (value in MediaFileContentType.values()) {
|
|
||||||
val prefixes = listOf(value.toAbsolutePath(), value.toPath()).map { it.removePrefix(File.separator) }
|
|
||||||
for (prefix in prefixes) {
|
|
||||||
if (!absPath.startsWith(prefix)) continue
|
|
||||||
return fromMedia(
|
|
||||||
context,
|
|
||||||
value,
|
|
||||||
absPath.removePrefix(prefix).ifBlank { File.separator }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return UniFileWrapper(UniFile.fromFile(file) ?: return null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fromAsset(
|
|
||||||
context: Context,
|
|
||||||
filename: String?
|
|
||||||
): SafeFile? {
|
|
||||||
return UniFileWrapper(
|
|
||||||
UniFile.fromAsset(context.assets, filename ?: return null) ?: return null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fromResource(
|
|
||||||
context: Context,
|
|
||||||
id: Int
|
|
||||||
): SafeFile? {
|
|
||||||
return UniFileWrapper(
|
|
||||||
UniFile.fromResource(context, id) ?: return null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fromMedia(
|
|
||||||
context: Context,
|
|
||||||
folderType: MediaFileContentType,
|
|
||||||
path: String = File.separator,
|
|
||||||
external: Boolean = true,
|
|
||||||
): SafeFile? {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
//fromUri(context, folderType.toUri(external))?.findFile(folderType.toPath())?.gotoDirectory(path)
|
|
||||||
|
|
||||||
return MediaFile(
|
|
||||||
context = context,
|
|
||||||
folderType = folderType,
|
|
||||||
external = external,
|
|
||||||
absolutePath = path
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
fromFile(
|
|
||||||
context,
|
|
||||||
File(
|
|
||||||
(Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
|
||||||
folderType.toPath() + File.separator + folderType).replace(
|
|
||||||
File.separator + File.separator,
|
|
||||||
File.separator
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*val uri: Uri? get() = getUri()
|
|
||||||
val name: String? get() = getName()
|
|
||||||
val type: String? get() = getType()
|
|
||||||
val filePath: String? get() = getFilePath()
|
|
||||||
val isFile: Boolean? get() = isFile()
|
|
||||||
val isDirectory: Boolean? get() = isDirectory()
|
|
||||||
val length: Long? get() = length()
|
|
||||||
val canRead: Boolean get() = canRead()
|
|
||||||
val canWrite: Boolean get() = canWrite()
|
|
||||||
val lastModified: Long? get() = lastModified()*/
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun isFileOrThrow(): Boolean {
|
|
||||||
return isFile() ?: throw IOException("Unable to get if file is a file or directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun lengthOrThrow(): Long {
|
|
||||||
return length() ?: throw IOException("Unable to get file length")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun isDirectoryOrThrow(): Boolean {
|
|
||||||
return isDirectory() ?: throw IOException("Unable to get if file is a directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun filePathOrThrow(): String {
|
|
||||||
return filePath() ?: throw IOException("Unable to get file path")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun uriOrThrow(): Uri {
|
|
||||||
return uri() ?: throw IOException("Unable to get uri")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun renameOrThrow(name: String?) {
|
|
||||||
if (!renameTo(name)) {
|
|
||||||
throw IOException("Unable to rename to $name")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun openOutputStreamOrThrow(append: Boolean = false): OutputStream {
|
|
||||||
return openOutputStream(append) ?: throw IOException("Unable to open output stream")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun openInputStreamOrThrow(): InputStream {
|
|
||||||
return openInputStream() ?: throw IOException("Unable to open input stream")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun existsOrThrow(): Boolean {
|
|
||||||
return exists() ?: throw IOException("Unable get if file exists")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun findFileOrThrow(displayName: String?, ignoreCase: Boolean = false): SafeFile {
|
|
||||||
return findFile(displayName, ignoreCase) ?: throw IOException("Unable find file")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun gotoDirectoryOrThrow(
|
|
||||||
directoryName: String?,
|
|
||||||
createMissingDirectories: Boolean = true
|
|
||||||
): SafeFile {
|
|
||||||
return gotoDirectory(directoryName, createMissingDirectories)
|
|
||||||
?: throw IOException("Unable to go to directory $directoryName")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun listFilesOrThrow(): List<SafeFile> {
|
|
||||||
return listFiles() ?: throw IOException("Unable to get files")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun createFileOrThrow(displayName: String?): SafeFile {
|
|
||||||
return createFile(displayName) ?: throw IOException("Unable to create file $displayName")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun createDirectoryOrThrow(directoryName: String?): SafeFile {
|
|
||||||
return createDirectory(
|
|
||||||
directoryName ?: throw IOException("Unable to create file with invalid name")
|
|
||||||
)
|
|
||||||
?: throw IOException("Unable to create directory $directoryName")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun deleteOrThrow() {
|
|
||||||
if (!delete()) {
|
|
||||||
throw IOException("Unable to delete file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** file.gotoDirectory("a/b/c") -> "file/a/b/c/" where a null or blank directoryName
|
|
||||||
* returns itself. createMissingDirectories specifies if the dirs should be created
|
|
||||||
* when travelling or break at a dir not found */
|
|
||||||
fun gotoDirectory(
|
|
||||||
directoryName: String?,
|
|
||||||
createMissingDirectories: Boolean = true
|
|
||||||
): SafeFile? {
|
|
||||||
if (directoryName == null) return this
|
|
||||||
|
|
||||||
return directoryName.split(File.separatorChar).filter { it.isNotBlank() }
|
|
||||||
.fold(this) { file: SafeFile?, directory ->
|
|
||||||
// as MediaFile does not actually create a directory we can do this
|
|
||||||
if (createMissingDirectories || this is MediaFile) {
|
|
||||||
file?.createDirectory(directory)
|
|
||||||
} else {
|
|
||||||
val next = file?.findFile(directory)
|
|
||||||
|
|
||||||
// we require the file to be a directory
|
|
||||||
if (next?.isDirectory() != true) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
next
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun createFile(displayName: String?): SafeFile?
|
|
||||||
fun createDirectory(directoryName: String?): SafeFile?
|
|
||||||
fun uri(): Uri?
|
|
||||||
fun name(): String?
|
|
||||||
fun type(): String?
|
|
||||||
fun filePath(): String?
|
|
||||||
fun isDirectory(): Boolean?
|
|
||||||
fun isFile(): Boolean?
|
|
||||||
fun lastModified(): Long?
|
|
||||||
fun length(): Long?
|
|
||||||
fun canRead(): Boolean
|
|
||||||
fun canWrite(): Boolean
|
|
||||||
fun delete(): Boolean
|
|
||||||
fun exists(): Boolean?
|
|
||||||
fun listFiles(): List<SafeFile>?
|
|
||||||
|
|
||||||
// fun listFiles(filter: FilenameFilter?): Array<File>?
|
|
||||||
fun findFile(displayName: String?, ignoreCase: Boolean = false): SafeFile?
|
|
||||||
|
|
||||||
fun renameTo(name: String?): Boolean
|
|
||||||
|
|
||||||
/** Open a stream on to the content associated with the file */
|
|
||||||
fun openOutputStream(append: Boolean = false): OutputStream?
|
|
||||||
|
|
||||||
/** Open a stream on to the content associated with the file */
|
|
||||||
fun openInputStream(): InputStream?
|
|
||||||
|
|
||||||
/** Get a random access stuff of the UniFile, "r" or "rw" */
|
|
||||||
fun createRandomAccessFile(mode: String?): UniRandomAccessFile?
|
|
||||||
}
|
|
|
@ -1,116 +0,0 @@
|
||||||
package com.lagradost.cloudstream3.utils.storage
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import com.hippo.unifile.UniFile
|
|
||||||
import com.hippo.unifile.UniRandomAccessFile
|
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
|
||||||
import okhttp3.internal.closeQuietly
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
private fun UniFile.toFile(): SafeFile {
|
|
||||||
return UniFileWrapper(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> safe(apiCall: () -> T): T? {
|
|
||||||
return try {
|
|
||||||
apiCall.invoke()
|
|
||||||
} catch (throwable: Throwable) {
|
|
||||||
logError(throwable)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UniFileWrapper(val file: UniFile) : SafeFile {
|
|
||||||
override fun createFile(displayName: String?): SafeFile? {
|
|
||||||
return file.createFile(displayName)?.toFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createDirectory(directoryName: String?): SafeFile? {
|
|
||||||
return file.createDirectory(directoryName)?.toFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun uri(): Uri? {
|
|
||||||
return safe { file.uri }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun name(): String? {
|
|
||||||
return safe { file.name }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun type(): String? {
|
|
||||||
return safe { file.type }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun filePath(): String? {
|
|
||||||
return safe { file.filePath }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isDirectory(): Boolean? {
|
|
||||||
return safe { file.isDirectory }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isFile(): Boolean? {
|
|
||||||
return safe { file.isFile }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun lastModified(): Long? {
|
|
||||||
return safe { file.lastModified() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun length(): Long? {
|
|
||||||
return safe {
|
|
||||||
val len = file.length()
|
|
||||||
if (len <= 1) {
|
|
||||||
val inputStream = this.openInputStream() ?: return@safe null
|
|
||||||
try {
|
|
||||||
inputStream.available().toLong()
|
|
||||||
} finally {
|
|
||||||
inputStream.closeQuietly()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
len
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canRead(): Boolean {
|
|
||||||
return safe { file.canRead() } ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canWrite(): Boolean {
|
|
||||||
return safe { file.canWrite() } ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun delete(): Boolean {
|
|
||||||
return safe { file.delete() } ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun exists(): Boolean? {
|
|
||||||
return safe { file.exists() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun listFiles(): List<SafeFile>? {
|
|
||||||
return safe { file.listFiles()?.mapNotNull { it?.toFile() } }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun findFile(displayName: String?, ignoreCase: Boolean): SafeFile? {
|
|
||||||
return safe { file.findFile(displayName, ignoreCase)?.toFile() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun renameTo(name: String?): Boolean {
|
|
||||||
return safe { file.renameTo(name) } ?: return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun openOutputStream(append: Boolean): OutputStream? {
|
|
||||||
return safe { file.openOutputStream(append) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun openInputStream(): InputStream? {
|
|
||||||
return safe { file.openInputStream() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createRandomAccessFile(mode: String?): UniRandomAccessFile? {
|
|
||||||
return safe { file.createRandomAccessFile(mode) }
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue