Refactored to use DocumentFile instead of FSAF

This commit is contained in:
Nite 2021-12-10 21:28:46 +01:00
parent 90638e5fd7
commit 34c5ced32e
No known key found for this signature in database
GPG Key ID: 1D1AD59B1C6386C1
10 changed files with 174 additions and 214 deletions

View File

@ -24,7 +24,6 @@ ext.versions = [
kotlin : "1.5.31",
kotlinxCoroutines : "1.5.2-native-mt",
viewModelKtx : "2.3.0",
lifecycle : "2.3.1",
retrofit : "2.6.4",
jackson : "2.9.5",
@ -42,7 +41,6 @@ ext.versions = [
timber : "4.7.1",
fastScroll : "2.0.1",
colorPicker : "2.2.3",
fsaf : "1.1",
rxJava : "3.1.2",
rxAndroid : "3.0.0",
multiType : "4.3.0",
@ -67,7 +65,6 @@ ext.androidSupport = [
roomRuntime : "androidx.room:room-runtime:$versions.room",
roomKtx : "androidx.room:room-ktx:$versions.room",
viewModelKtx : "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.viewModelKtx",
lifecycle : "androidx.lifecycle:lifecycle-process:$versions.lifecycle",
navigationFragment : "androidx.navigation:navigation-fragment:$versions.navigation",
navigationUi : "androidx.navigation:navigation-ui:$versions.navigation",
navigationFragmentKtx : "androidx.navigation:navigation-fragment-ktx:$versions.navigation",
@ -93,7 +90,6 @@ ext.other = [
timber : "com.jakewharton.timber:timber:$versions.timber",
fastScroll : "com.simplecityapps:recyclerview-fastscroll:$versions.fastScroll",
colorPickerView : "com.github.skydoves:colorpickerview:$versions.colorPicker",
fsaf : "com.github.K1rakishou:Fuck-Storage-Access-Framework:$versions.fsaf",
rxJava : "io.reactivex.rxjava3:rxjava:$versions.rxJava",
rxAndroid : "io.reactivex.rxjava3:rxandroid:$versions.rxAndroid",
multiType : "com.drakeet.multitype:multitype:$versions.multiType",

View File

@ -98,7 +98,6 @@ dependencies {
implementation androidSupport.navigationFragmentKtx
implementation androidSupport.navigationUiKtx
implementation androidSupport.navigationFeature
implementation androidSupport.lifecycle
implementation other.kotlinStdlib
implementation other.kotlinxCoroutines
@ -106,7 +105,6 @@ dependencies {
implementation other.okhttpLogging
implementation other.fastScroll
implementation other.colorPickerView
implementation other.fsaf
implementation other.rxJava
implementation other.rxAndroid
implementation other.multiType

View File

@ -49,6 +49,7 @@ import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.ServerColor
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.StorageFile
import org.moire.ultrasonic.util.UncaughtExceptionHandler
import org.moire.ultrasonic.util.Util
import timber.log.Timber
@ -213,6 +214,7 @@ class NavigationActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
StorageFile.resetCaches()
setMenuForServerCapabilities()
// Lifecycle support's constructor registers some event receivers so it should be created early

View File

@ -211,8 +211,6 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
if (status == cachedStatus) return
cachedStatus = status
Timber.w("STATUS: %s", status)
when (status) {
DownloadStatus.DONE -> {
statusImage = imageHelper.downloadedImage

View File

@ -1,10 +1,6 @@
package org.moire.ultrasonic.app
import android.content.Context
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDexApplication
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
@ -19,7 +15,6 @@ import org.moire.ultrasonic.di.musicServiceModule
import org.moire.ultrasonic.log.FileLoggerTree
import org.moire.ultrasonic.log.TimberKoinLogger
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.StorageFile
import timber.log.Timber
import timber.log.Timber.DebugTree
@ -57,8 +52,6 @@ class UApp : MultiDexApplication() {
mediaPlayerModule
)
}
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleListener())
}
companion object {
@ -69,11 +62,3 @@ class UApp : MultiDexApplication() {
}
}
}
class AppLifecycleListener : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onMoveToForeground() {
StorageFile.resetCaches()
}
}

View File

@ -20,7 +20,6 @@ import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import com.github.k1rakishou.fsaf.FileChooser
import kotlin.math.ceil
import org.koin.core.component.KoinComponent
import org.koin.java.KoinJavaComponent.get

View File

@ -378,10 +378,12 @@ class LocalMediaPlayer : KoinComponent {
Timber.i("Preparing media player")
if (dataSource != null) mediaPlayer.setDataSource(dataSource)
else if (file!!.isRawFile) mediaPlayer.setDataSource(file.rawFilePath)
else {
val descriptor = file.getDocumentFileDescriptor("r")!!
if (dataSource != null) {
Timber.v("LocalMediaPlayer doPlay dataSource: %s", dataSource)
mediaPlayer.setDataSource(dataSource)
} else {
Timber.v("LocalMediaPlayer doPlay Path: %s", file!!.path)
val descriptor = file!!.getDocumentFileDescriptor("r")!!
mediaPlayer.setDataSource(descriptor.fileDescriptor)
descriptor.close()
}
@ -465,12 +467,11 @@ class LocalMediaPlayer : KoinComponent {
} catch (ignored: Throwable) {
}
if (file!!.isRawFile) nextMediaPlayer!!.setDataSource(file.rawFilePath)
else {
val descriptor = file.getDocumentFileDescriptor("r")!!
Timber.v("LocalMediaPlayer setupNext Path: %s", file!!.path)
val descriptor = file!!.getDocumentFileDescriptor("r")!!
nextMediaPlayer!!.setDataSource(descriptor.fileDescriptor)
descriptor.close()
}
setNextPlayerState(PlayerState.PREPARING)
nextMediaPlayer!!.setOnPreparedListener {
try {

View File

@ -41,6 +41,7 @@ import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Util.safeClose
import timber.log.Timber
import java.io.File
import java.io.FileReader
import java.io.FileWriter
@ -526,7 +527,7 @@ class OfflineMusicService : MusicService, KoinComponent {
title = name
val albumArt = FileUtil.getAlbumArtFile(this)
if (albumArt != null && StorageFile.isPathExists(albumArt)) {
if (albumArt != null && File(albumArt).exists()) {
coverArt = albumArt
}
}
@ -543,12 +544,9 @@ class OfflineMusicService : MusicService, KoinComponent {
try {
val mmr = MediaMetadataRetriever()
if (file.isRawFile) mmr.setDataSource(file.rawFilePath)
else {
val descriptor = file.getDocumentFileDescriptor("r")!!
mmr.setDataSource(descriptor.fileDescriptor)
descriptor.close()
}
meta.artist = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST)
meta.album = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM)

View File

@ -1,6 +1,5 @@
package org.moire.ultrasonic.util
import android.os.StatFs
import android.system.Os
import java.util.ArrayList
import java.util.HashSet
@ -174,13 +173,6 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
val bytesTotalFs: Long
val bytesAvailableFs: Long
if (files[0].isRawFile) {
val stat = StatFs(files[0].rawFilePath)
bytesTotalFs = stat.blockCountLong * stat.blockSizeLong
bytesAvailableFs = stat.availableBlocksLong * stat.blockSizeLong
bytesUsedFs = bytesTotalFs - bytesAvailableFs
minFsAvailability = bytesTotalFs - MIN_FREE_SPACE
} else {
val descriptor = files[0].getDocumentFileDescriptor("r")!!
val stat = Os.fstatvfs(descriptor.fileDescriptor)
bytesTotalFs = stat.f_blocks * stat.f_bsize
@ -188,7 +180,6 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
bytesUsedFs = bytesTotalFs - bytesAvailableFs
minFsAvailability = bytesTotalFs - MIN_FREE_SPACE
descriptor.close()
}
val bytesToDeleteCacheLimit = (bytesUsedBySubsonic - cacheSizeBytes).coerceAtLeast(0L)
val bytesToDeleteFsLimit = (bytesUsedFs - minFsAvailability).coerceAtLeast(0L)

View File

@ -9,17 +9,10 @@ package org.moire.ultrasonic.util
import android.content.res.AssetFileDescriptor
import android.net.Uri
import com.github.k1rakishou.fsaf.FileManager
import com.github.k1rakishou.fsaf.document_file.CachingDocumentFile
import com.github.k1rakishou.fsaf.file.AbstractFile
import com.github.k1rakishou.fsaf.file.DirectorySegment
import com.github.k1rakishou.fsaf.file.FileSegment
import com.github.k1rakishou.fsaf.file.RawFile
import com.github.k1rakishou.fsaf.manager.base_directory.BaseDirectory
import android.webkit.MimeTypeMap
import androidx.documentfile.provider.DocumentFile
import org.moire.ultrasonic.R
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
@ -33,8 +26,7 @@ import java.util.concurrent.ConcurrentHashMap
*/
class StorageFile private constructor(
private var parentStorageFile: StorageFile?,
private var abstractFile: AbstractFile,
private var fileManager: FileManager
private var documentFile: DocumentFile
): Comparable<StorageFile> {
override fun compareTo(other: StorageFile): Int {
@ -45,87 +37,64 @@ class StorageFile private constructor(
return name
}
var name: String = fileManager.getName(abstractFile)
var name: String = documentFile.name!!
var isDirectory: Boolean = fileManager.isDirectory(abstractFile)
var isDirectory: Boolean = documentFile.isDirectory
var isFile: Boolean = fileManager.isFile(abstractFile)
var isFile: Boolean = documentFile.isFile
val length: Long
get() = fileManager.getLength(abstractFile)
get() = documentFile.length()
val lastModified: Long
get() = fileManager.lastModified(abstractFile)
get() = documentFile.lastModified()
fun delete(): Boolean {
val deleted = fileManager.delete(abstractFile)
val deleted = documentFile.delete()
if (!deleted) return false
val path = normalizePath(path)
storageFilePathDictionary.remove(path)
notExistingPathDictionary.putIfAbsent(path, path)
listedPathDictionary.remove(path)
listedPathDictionary.remove(parent?.path)
return true
}
fun listFiles(): Array<StorageFile> {
val fileList = fileManager.listFiles(abstractFile)
return fileList.map { file -> StorageFile(this, file, fileManager) }.toTypedArray()
val fileList = documentFile.listFiles()
return fileList.map { file -> StorageFile(this, file) }.toTypedArray()
}
fun getFileOutputStream(append: Boolean): OutputStream {
if (isRawFile) return FileOutputStream(File(abstractFile.getFullPath()), append)
val mode = if (append) "wa" else "w"
val descriptor = UApp.applicationContext().contentResolver.openAssetFileDescriptor(
abstractFile.getFileRoot<CachingDocumentFile>().holder.uri(), mode)
documentFile.uri, mode)
return descriptor?.createOutputStream()
?: throw IOException("Couldn't retrieve OutputStream")
}
fun getFileInputStream(): InputStream {
if (isRawFile) return FileInputStream(abstractFile.getFullPath())
return fileManager.getInputStream(abstractFile)
return UApp.applicationContext().contentResolver.openInputStream(documentFile.uri)
?: throw IOException("Couldn't retrieve InputStream")
}
val path: String
get() {
if (isRawFile) return abstractFile.getFullPath()
// We can't assume that the file's Uri is related to its path,
// so we generate our own path by concatenating the names on the path.
if (parentStorageFile != null) return parentStorageFile!!.path + "/" + name
return Uri.parse(abstractFile.getFullPath()).toString()
return documentFile.uri.toString()
}
val parent: StorageFile?
get() {
if (isRawFile) {
return StorageFile(
null,
fileManager.fromRawFile(File(abstractFile.getFullPath()).parentFile!!),
fileManager
)
}
return parentStorageFile
}
val isRawFile: Boolean
get() {
return abstractFile is RawFile
}
val rawFilePath: String?
get() {
return if (abstractFile is RawFile) abstractFile.getFullPath()
else null
}
fun getDocumentFileDescriptor(openMode: String): AssetFileDescriptor? {
return if (abstractFile !is RawFile) {
UApp.applicationContext().contentResolver.openAssetFileDescriptor(
abstractFile.getFileRoot<CachingDocumentFile>().holder.uri(),
return UApp.applicationContext().contentResolver.openAssetFileDescriptor(
documentFile.uri,
openMode
)
} else null
}
companion object {
@ -135,62 +104,56 @@ class StorageFile private constructor(
// If this isn't good enough we can add locking.
private val storageFilePathDictionary = ConcurrentHashMap<String, StorageFile>()
private val notExistingPathDictionary = ConcurrentHashMap<String, String>()
private val fileManager: ResettableLazy<FileManager> = ResettableLazy {
val manager = FileManager(UApp.applicationContext())
manager.registerBaseDir<MusicCacheBaseDirectory>(MusicCacheBaseDirectory())
manager
}
private val listedPathDictionary = ConcurrentHashMap<String, String>()
val mediaRoot: ResettableLazy<StorageFile> = ResettableLazy {
StorageFile(
null,
fileManager.value.newBaseDirectoryFile<MusicCacheBaseDirectory>()!!,
fileManager.value
StorageFile(null, getRoot()!!)
}
private fun getRoot(): DocumentFile? {
return if (Settings.cacheLocation.isUri()) {
DocumentFile.fromTreeUri(
UApp.applicationContext(),
Uri.parse(Settings.cacheLocation)
)
} else {
DocumentFile.fromFile(File(Settings.cacheLocation))
}
}
fun resetCaches() {
storageFilePathDictionary.clear()
notExistingPathDictionary.clear()
fileManager.value.unregisterBaseDir<MusicCacheBaseDirectory>()
fileManager.reset()
listedPathDictionary.clear()
mediaRoot.reset()
Timber.v("StorageFile caches were reset")
if (!fileManager.value.baseDirectoryExists<MusicCacheBaseDirectory>()) {
Timber.i("StorageFile caches were reset")
val root = getRoot()
if (root == null || !root.exists()) {
Settings.cacheLocation = FileUtil.defaultMusicDirectory.path
Util.toast(UApp.applicationContext(), R.string.settings_cache_location_error)
}
}
@Synchronized
fun getOrCreateFileFromPath(path: String): StorageFile {
val normalizedPath = normalizePath(path)
if (!normalizedPath.isUri()) {
File(normalizedPath).createNewFile()
return StorageFile(
null,
fileManager.value.fromPath(normalizedPath),
fileManager.value
)
}
if (storageFilePathDictionary.containsKey(path))
return storageFilePathDictionary[path]!!
if (storageFilePathDictionary.containsKey(normalizedPath))
return storageFilePathDictionary[normalizedPath]!!
val parent = getStorageFileForParentDirectory(normalizedPath)
val parent = getStorageFileForParentDirectory(path)
?: throw IOException("Parent directory doesn't exist")
val name = FileUtil.getNameFromPath(normalizedPath)
val name = FileUtil.getNameFromPath(path)
val file = StorageFile(
parent,
fileManager.value.findFile(parent.abstractFile, name)
?: fileManager.value.create(parent.abstractFile,
listOf(FileSegment(name))
)!!,
parent.fileManager
parent.documentFile.findFile(name)
?: parent.documentFile.createFile(
MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.extension())!!,
name.withoutExtension()
)!!
)
storageFilePathDictionary[normalizedPath] = file
notExistingPathDictionary.remove(normalizedPath)
storageFilePathDictionary[path] = file
notExistingPathDictionary.remove(path)
return file
}
@ -199,89 +162,97 @@ class StorageFile private constructor(
}
fun getFromPath(path: String): StorageFile? {
val normalizedPath = normalizePath(path)
if (!normalizedPath.isUri()) {
val file = fileManager.value.fromPath(normalizedPath)
if (!fileManager.value.exists(file)) return null
return StorageFile(null, file, fileManager.value)
}
if (storageFilePathDictionary.containsKey(normalizedPath))
return storageFilePathDictionary[normalizedPath]!!
if (notExistingPathDictionary.contains(normalizedPath)) return null
if (storageFilePathDictionary.containsKey(path))
return storageFilePathDictionary[path]!!
if (notExistingPathDictionary.contains(path)) return null
val parent = getStorageFileForParentDirectory(normalizedPath)
val parent = getStorageFileForParentDirectory(path)
if (parent == null) {
notExistingPathDictionary.putIfAbsent(normalizedPath, normalizedPath)
notExistingPathDictionary.putIfAbsent(path, path)
return null
}
val fileName = FileUtil.getNameFromPath(normalizedPath)
// If the parent was fully listed, but the searched file isn't cached, it doesn't exists.
if (listedPathDictionary.containsKey(parent.path)) return null
val fileName = FileUtil.getNameFromPath(path)
var file: StorageFile? = null
//Timber.v("StorageFile getFromPath path: %s", path)
// Listing a bunch of files takes the same time in SAF as finding one,
// so we list and cache all of them for performance
parent.listFiles().forEach {
if (it.name == fileName) file = it
storageFilePathDictionary[it.path] = it
notExistingPathDictionary.remove(it.path)
}
listedPathDictionary[parent.path] = parent.path
if (file == null) {
notExistingPathDictionary.putIfAbsent(normalizedPath, normalizedPath)
notExistingPathDictionary.putIfAbsent(path, path)
return null
}
return file
}
@Synchronized
fun createDirsOnPath(path: String) {
val normalizedPath = normalizePath(path)
if (!normalizedPath.isUri()) {
File(normalizedPath).mkdirs()
return
}
val segments = getUriSegments(normalizedPath)
val segments = getUriSegments(path)
?: throw IOException("Can't get path because the root has changed")
var file = mediaRoot.value
segments.forEach { segment ->
file = StorageFile(
file,
fileManager.value.create(file.abstractFile, listOf(DirectorySegment(segment)))
?: throw IOException("Can't create directory"),
fileManager.value
file.documentFile.findFile(segment) ?:
file.documentFile.createDirectory(segment)
?: throw IOException("Can't create directory")
)
notExistingPathDictionary.remove(normalizePath(file.path))
notExistingPathDictionary.remove(file.path)
listedPathDictionary.remove(file.path)
}
}
fun rename(pathFrom: String, pathTo: String) {
val normalizedPathFrom = normalizePath(pathFrom)
val fileFrom = getFromPath(normalizedPathFrom) ?: throw IOException("File to rename doesn't exist")
val fileFrom = getFromPath(pathFrom) ?: throw IOException("File to rename doesn't exist")
rename(fileFrom, pathTo)
}
@Synchronized
fun rename(pathFrom: StorageFile?, pathTo: String) {
val normalizedPathTo = normalizePath(pathTo)
if (pathFrom == null || !pathFrom.fileManager.exists(pathFrom.abstractFile)) throw IOException("File to rename doesn't exist")
Timber.d("Renaming from %s to %s", pathFrom.path, normalizedPathTo)
if (pathFrom == null || !pathFrom.documentFile.exists()) throw IOException("File to rename doesn't exist")
Timber.d("Renaming from %s to %s", pathFrom.path, pathTo)
val parentTo = getFromPath(FileUtil.getParentPath(normalizedPathTo)!!) ?: throw IOException("Destination folder doesn't exist")
val fileTo = getFromParentAndName(parentTo, FileUtil.getNameFromPath(normalizedPathTo))
notExistingPathDictionary.remove(normalizedPathTo)
storageFilePathDictionary.remove(normalizePath(pathFrom.path))
val parentTo = getFromPath(FileUtil.getParentPath(pathTo)!!) ?: throw IOException("Destination folder doesn't exist")
val fileTo = getFromParentAndName(parentTo, FileUtil.getNameFromPath(pathTo))
fileManager.value.copyFileContents(pathFrom.abstractFile, fileTo.abstractFile)
copyFileContents(pathFrom.documentFile, fileTo.documentFile)
pathFrom.delete()
notExistingPathDictionary.remove(pathTo)
storageFilePathDictionary.remove(pathFrom.path)
}
private fun copyFileContents(sourceFile: DocumentFile, destinationFile: DocumentFile) {
UApp.applicationContext().contentResolver.openInputStream(sourceFile.uri)?.use { inputStream ->
UApp.applicationContext().contentResolver.openOutputStream(destinationFile.uri)?.use { outputStream ->
inputStream.copyInto(outputStream)
}
}
}
private fun getFromParentAndName(parent: StorageFile, name: String): StorageFile {
val file = parent.fileManager.findFile(parent.abstractFile, name)
?: parent.fileManager.createFile(parent.abstractFile, name)!!
return StorageFile(parent, file, parent.fileManager)
val file = parent.documentFile.findFile(name)
?: parent.documentFile.createFile(
MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.extension())!!,
name.withoutExtension()
)!!
return StorageFile(parent, file)
}
private fun getStorageFileForParentDirectory(path: String): StorageFile? {
@ -290,7 +261,11 @@ class StorageFile private constructor(
return storageFilePathDictionary[parentPath]!!
if (notExistingPathDictionary.contains(parentPath)) return null
//val start = System.currentTimeMillis()
val parent = findStorageFileForParentDirectory(parentPath)
//val end = System.currentTimeMillis()
//Timber.v("StorageFile getStorageFileForParentDirectory searching for %s, time: %d", parentPath, end-start)
if (parent == null) {
storageFilePathDictionary.remove(parentPath)
notExistingPathDictionary.putIfAbsent(parentPath, parentPath)
@ -306,14 +281,31 @@ class StorageFile private constructor(
val segments = getUriSegments(path)
?: throw IOException("Can't get path because the root has changed")
var file = StorageFile(null, mediaRoot.value.abstractFile, fileManager.value)
var file = StorageFile(null, mediaRoot.value.documentFile)
segments.forEach { segment ->
file = StorageFile(
file,
fileManager.value.findFile(file.abstractFile, segment)
?: return null,
file.fileManager
)
val currentPath = file.path + "/" + segment
if (notExistingPathDictionary.contains(currentPath)) return null
if (storageFilePathDictionary.containsKey(currentPath)) {
file = storageFilePathDictionary[currentPath]!!
} else {
// If the parent was fully listed, but the searched file isn't cached, it doesn't exists.
if (listedPathDictionary.containsKey(file.path)) return null
var foundFile: StorageFile? = null
file.listFiles().forEach {
if (it.name == segment) foundFile = it
storageFilePathDictionary[it.path] = it
notExistingPathDictionary.remove(it.path)
}
listedPathDictionary[file.path] = file.path
if (foundFile == null) {
notExistingPathDictionary.putIfAbsent(path, path)
return null
}
file = StorageFile(file, foundFile!!.documentFile)
}
}
return file
}
@ -324,32 +316,6 @@ class StorageFile private constructor(
val pathWithoutRoot = uri.substringAfter(rootPath)
return pathWithoutRoot.split('/').filter { it.isNotEmpty() }
}
private fun normalizePath(path: String): String {
// FSAF replaces spaces in paths with "_", so we must do the same everywhere
// TODO paths sometimes contain double "/". These are currently replaced to single one.
// The nice solution would be to check and fix why this happens
return path.replace(' ', '_').replace(Regex("(?<!:)//"), "/")
}
}
}
class MusicCacheBaseDirectory : BaseDirectory() {
override fun getDirFile(): File {
return FileUtil.defaultMusicDirectory
}
override fun getDirUri(): Uri? {
if (!Settings.cacheLocation.isUri()) return null
return Uri.parse(Settings.cacheLocation)
}
override fun currentActiveBaseDirType(): ActiveBaseDirType {
return when {
Settings.cacheLocation.isUri() -> ActiveBaseDirType.SafBaseDir
else -> ActiveBaseDirType.JavaFileBaseDir
}
}
}
@ -357,3 +323,29 @@ fun String.isUri(): Boolean {
// TODO is there a better way to tell apart a path and an URI?
return this.contains(':')
}
fun String.extension(): String {
val index = this.indexOfLast { ch -> ch == '.' }
if (index == -1) return ""
if (index == this.lastIndex) return ""
return this.substring(index + 1)
}
fun String.withoutExtension(): String {
val index = this.indexOfLast { ch -> ch == '.' }
if (index == -1) return this
return this.substring(0, index)
}
fun InputStream.copyInto(outputStream: OutputStream) {
var read: Int
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
while (true) {
read = this.read(buffer)
if (read == -1) {
break
}
outputStream.write(buffer, 0, read)
}
}