bump + refactor

This commit is contained in:
LagradOst 2023-08-25 23:16:34 +02:00
parent b38a9b1ff5
commit 8193e39b30
11 changed files with 40 additions and 779 deletions

View file

@ -32,11 +32,12 @@ android {
enable = true
}
externalNativeBuild {
cmake {
path("CMakeLists.txt")
}
}
// disable this for now
//externalNativeBuild {
// cmake {
// path("CMakeLists.txt")
// }
//}
signingConfigs {
create("prerelease") {
@ -58,7 +59,7 @@ android {
targetSdk = 29
versionCode = 59
versionName = "4.1.7"
versionName = "4.1.8"
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
@ -232,7 +233,7 @@ dependencies {
// To fix SSL fuckery on android 9
implementation("org.conscrypt:conscrypt-android:2.2.1")
// 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
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0")

View file

@ -144,6 +144,7 @@ import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser
import com.lagradost.safefile.SafeFile
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.File
@ -279,6 +280,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
companion object {
const val TAG = "MAINACT"
var lastError: String? = null
/**
* Setting this will automatically enter the query in the search
* next time the search fragment is opened.
@ -366,7 +368,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
nextSearchQuery =
try {
URLDecoder.decode(query, "UTF-8")
} catch (t : Throwable) {
} catch (t: Throwable) {
logError(t)
query
}
@ -859,7 +861,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
RecyclerView::class.java.declaredMethods.firstOrNull {
it.name == "scrollStep"
}?.also { it.isAccessible = true }
} catch (t : Throwable) {
} catch (t: Throwable) {
null
}
}
@ -906,11 +908,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (dx > 0) dx else 0
}
if(!NO_MOVE_LIST) {
if (!NO_MOVE_LIST) {
parent.smoothScrollBy(rdx, 0)
}else {
} else {
val smoothScroll = reflectedScroll
if(smoothScroll == null) {
if (smoothScroll == null) {
parent.smoothScrollBy(rdx, 0)
} else {
try {
@ -920,12 +922,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val out = IntArray(2)
smoothScroll.invoke(parent, rdx, 0, out)
val scrolledX = out[0]
if(abs(scrolledX) <= 0) { // newFocus.measuredWidth*2
if (abs(scrolledX) <= 0) { // newFocus.measuredWidth*2
smoothScroll.invoke(parent, -rdx, 0, out)
parent.smoothScrollBy(scrolledX, 0)
if (NO_MOVE_LIST) targetDx = scrolledX
}
} catch (t : Throwable) {
} catch (t: Throwable) {
parent.smoothScrollBy(rdx, 0)
}
}
@ -1131,10 +1133,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
snackbar.show()
}
}
}
}
ioSafe { SafeFile.check(this@MainActivity) }
if (PluginManager.checkSafeModeFile()) {
normalSafeApiCall {

View file

@ -10,7 +10,7 @@ import kotlinx.coroutines.launch
object NativeCrashHandler {
// external fun triggerNativeCrash()
private external fun initNativeCrashHandler()
/*private external fun initNativeCrashHandler()
private external fun getSignalStatus(): Int
private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch {
@ -49,5 +49,5 @@ object NativeCrashHandler {
}
initSignalPolling()
}
}*/
}

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.ui.player
import android.content.ContentUris
import android.content.Intent
import android.net.Uri
import android.os.Bundle
@ -10,7 +11,7 @@ import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.utils.ExtractorUri
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.storage.SafeFile
import com.lagradost.safefile.SafeFile
const val DTAG = "PlayerActivity"
@ -57,7 +58,10 @@ class DownloadedPlayerActivity : AppCompatActivity() {
listOf(
ExtractorUri(
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()
)
)
)

View file

@ -21,13 +21,11 @@ import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import androidx.media3.common.Format.NO_VALUE
import androidx.media3.common.MimeTypes
import com.hippo.unifile.UniFile
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding
import com.lagradost.cloudstream3.databinding.PlayerSelectTracksBinding
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.popCurrentPage
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import com.lagradost.cloudstream3.utils.storage.SafeFile
import com.lagradost.safefile.SafeFile
import kotlinx.coroutines.Job
import java.util.*
import kotlin.math.abs
@ -136,7 +134,7 @@ class GeneratorPlayer : FullScreenPlayer() {
return durPos.position
}
var currentVerifyLink: Job? = null
private var currentVerifyLink: Job? = null
private fun loadExtractorJob(extractorLink: ExtractorLink?) {
currentVerifyLink?.cancel()

View file

@ -38,7 +38,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
import com.lagradost.cloudstream3.utils.VideoDownloadManager
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath
import com.lagradost.cloudstream3.utils.storage.SafeFile
import com.lagradost.safefile.SafeFile
fun getCurrentLocale(context: Context): String {
val res = context.resources
@ -335,7 +335,7 @@ class SettingsGeneral : PreferenceFragmentCompat() {
val currentDir =
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(
dirs + listOf("Custom"),

View file

@ -158,6 +158,7 @@ object BackupUtils {
val displayName = "CS3_Backup_${date}"
val backupFile = getBackup()
val stream = setupStream(this, displayName, null, ext, false)
fileStream = stream.openNew()
printStream = PrintWriter(fileStream)
printStream.print(mapper.writeValueAsString(backupFile))

View file

@ -35,8 +35,8 @@ import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.storage.MediaFileContentType
import com.lagradost.cloudstream3.utils.storage.SafeFile
import com.lagradost.safefile.MediaFileContentType
import com.lagradost.safefile.SafeFile
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -554,9 +554,8 @@ object VideoDownloadManager {
extension: String,
tryResume: Boolean,
): StreamData {
val (base, _) = context.getBasePath()
return setupStream(
base ?: throw IOException("Bad config"),
context.getBasePath().first ?: getDefaultDir(context) ?: throw IOException("Bad config"),
name,
folder,
extension,
@ -1401,7 +1400,12 @@ object VideoDownloadManager {
metadata.type = DownloadType.IsFailed
}
} finally {
fileMutex.unlock()
try {
// may cause java.lang.IllegalStateException: Mutex is not locked because of cancelling
fileMutex.unlock()
} catch (t : Throwable) {
logError(t)
}
}
}
}

View file

@ -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")
}
}

View file

@ -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?
}

View file

@ -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) }
}
}