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

View File

@ -98,7 +98,6 @@ dependencies {
implementation androidSupport.navigationFragmentKtx implementation androidSupport.navigationFragmentKtx
implementation androidSupport.navigationUiKtx implementation androidSupport.navigationUiKtx
implementation androidSupport.navigationFeature implementation androidSupport.navigationFeature
implementation androidSupport.lifecycle
implementation other.kotlinStdlib implementation other.kotlinStdlib
implementation other.kotlinxCoroutines implementation other.kotlinxCoroutines
@ -106,7 +105,6 @@ dependencies {
implementation other.okhttpLogging implementation other.okhttpLogging
implementation other.fastScroll implementation other.fastScroll
implementation other.colorPickerView implementation other.colorPickerView
implementation other.fsaf
implementation other.rxJava implementation other.rxJava
implementation other.rxAndroid implementation other.rxAndroid
implementation other.multiType 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.Constants
import org.moire.ultrasonic.util.ServerColor import org.moire.ultrasonic.util.ServerColor
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.StorageFile
import org.moire.ultrasonic.util.UncaughtExceptionHandler import org.moire.ultrasonic.util.UncaughtExceptionHandler
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import timber.log.Timber import timber.log.Timber
@ -213,6 +214,7 @@ class NavigationActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
StorageFile.resetCaches()
setMenuForServerCapabilities() setMenuForServerCapabilities()
// Lifecycle support's constructor registers some event receivers so it should be created early // 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 if (status == cachedStatus) return
cachedStatus = status cachedStatus = status
Timber.w("STATUS: %s", status)
when (status) { when (status) {
DownloadStatus.DONE -> { DownloadStatus.DONE -> {
statusImage = imageHelper.downloadedImage statusImage = imageHelper.downloadedImage

View File

@ -1,10 +1,6 @@
package org.moire.ultrasonic.app package org.moire.ultrasonic.app
import android.content.Context 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 androidx.multidex.MultiDexApplication
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin 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.FileLoggerTree
import org.moire.ultrasonic.log.TimberKoinLogger import org.moire.ultrasonic.log.TimberKoinLogger
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.StorageFile
import timber.log.Timber import timber.log.Timber
import timber.log.Timber.DebugTree import timber.log.Timber.DebugTree
@ -57,8 +52,6 @@ class UApp : MultiDexApplication() {
mediaPlayerModule mediaPlayerModule
) )
} }
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleListener())
} }
companion object { 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.Preference
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import com.github.k1rakishou.fsaf.FileChooser
import kotlin.math.ceil import kotlin.math.ceil
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.java.KoinJavaComponent.get import org.koin.java.KoinJavaComponent.get

View File

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

View File

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

View File

@ -1,6 +1,5 @@
package org.moire.ultrasonic.util package org.moire.ultrasonic.util
import android.os.StatFs
import android.system.Os import android.system.Os
import java.util.ArrayList import java.util.ArrayList
import java.util.HashSet import java.util.HashSet
@ -174,21 +173,13 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
val bytesTotalFs: Long val bytesTotalFs: Long
val bytesAvailableFs: Long val bytesAvailableFs: Long
if (files[0].isRawFile) { val descriptor = files[0].getDocumentFileDescriptor("r")!!
val stat = StatFs(files[0].rawFilePath) val stat = Os.fstatvfs(descriptor.fileDescriptor)
bytesTotalFs = stat.blockCountLong * stat.blockSizeLong bytesTotalFs = stat.f_blocks * stat.f_bsize
bytesAvailableFs = stat.availableBlocksLong * stat.blockSizeLong bytesAvailableFs = stat.f_bfree * stat.f_bsize
bytesUsedFs = bytesTotalFs - bytesAvailableFs bytesUsedFs = bytesTotalFs - bytesAvailableFs
minFsAvailability = bytesTotalFs - MIN_FREE_SPACE minFsAvailability = bytesTotalFs - MIN_FREE_SPACE
} else { descriptor.close()
val descriptor = files[0].getDocumentFileDescriptor("r")!!
val stat = Os.fstatvfs(descriptor.fileDescriptor)
bytesTotalFs = stat.f_blocks * stat.f_bsize
bytesAvailableFs = stat.f_bfree * stat.f_bsize
bytesUsedFs = bytesTotalFs - bytesAvailableFs
minFsAvailability = bytesTotalFs - MIN_FREE_SPACE
descriptor.close()
}
val bytesToDeleteCacheLimit = (bytesUsedBySubsonic - cacheSizeBytes).coerceAtLeast(0L) val bytesToDeleteCacheLimit = (bytesUsedBySubsonic - cacheSizeBytes).coerceAtLeast(0L)
val bytesToDeleteFsLimit = (bytesUsedFs - minFsAvailability).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.content.res.AssetFileDescriptor
import android.net.Uri import android.net.Uri
import com.github.k1rakishou.fsaf.FileManager import android.webkit.MimeTypeMap
import com.github.k1rakishou.fsaf.document_file.CachingDocumentFile import androidx.documentfile.provider.DocumentFile
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 org.moire.ultrasonic.R import org.moire.ultrasonic.R
import java.io.File import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@ -33,8 +26,7 @@ import java.util.concurrent.ConcurrentHashMap
*/ */
class StorageFile private constructor( class StorageFile private constructor(
private var parentStorageFile: StorageFile?, private var parentStorageFile: StorageFile?,
private var abstractFile: AbstractFile, private var documentFile: DocumentFile
private var fileManager: FileManager
): Comparable<StorageFile> { ): Comparable<StorageFile> {
override fun compareTo(other: StorageFile): Int { override fun compareTo(other: StorageFile): Int {
@ -45,87 +37,64 @@ class StorageFile private constructor(
return name 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 val length: Long
get() = fileManager.getLength(abstractFile) get() = documentFile.length()
val lastModified: Long val lastModified: Long
get() = fileManager.lastModified(abstractFile) get() = documentFile.lastModified()
fun delete(): Boolean { fun delete(): Boolean {
val deleted = fileManager.delete(abstractFile) val deleted = documentFile.delete()
if (!deleted) return false if (!deleted) return false
val path = normalizePath(path)
storageFilePathDictionary.remove(path) storageFilePathDictionary.remove(path)
notExistingPathDictionary.putIfAbsent(path, path) notExistingPathDictionary.putIfAbsent(path, path)
listedPathDictionary.remove(path)
listedPathDictionary.remove(parent?.path)
return true return true
} }
fun listFiles(): Array<StorageFile> { fun listFiles(): Array<StorageFile> {
val fileList = fileManager.listFiles(abstractFile) val fileList = documentFile.listFiles()
return fileList.map { file -> StorageFile(this, file, fileManager) }.toTypedArray() return fileList.map { file -> StorageFile(this, file) }.toTypedArray()
} }
fun getFileOutputStream(append: Boolean): OutputStream { fun getFileOutputStream(append: Boolean): OutputStream {
if (isRawFile) return FileOutputStream(File(abstractFile.getFullPath()), append)
val mode = if (append) "wa" else "w" val mode = if (append) "wa" else "w"
val descriptor = UApp.applicationContext().contentResolver.openAssetFileDescriptor( val descriptor = UApp.applicationContext().contentResolver.openAssetFileDescriptor(
abstractFile.getFileRoot<CachingDocumentFile>().holder.uri(), mode) documentFile.uri, mode)
return descriptor?.createOutputStream() return descriptor?.createOutputStream()
?: throw IOException("Couldn't retrieve OutputStream") ?: throw IOException("Couldn't retrieve OutputStream")
} }
fun getFileInputStream(): InputStream { fun getFileInputStream(): InputStream {
if (isRawFile) return FileInputStream(abstractFile.getFullPath()) return UApp.applicationContext().contentResolver.openInputStream(documentFile.uri)
return fileManager.getInputStream(abstractFile)
?: throw IOException("Couldn't retrieve InputStream") ?: throw IOException("Couldn't retrieve InputStream")
} }
val path: String val path: String
get() { get() {
if (isRawFile) return abstractFile.getFullPath()
// We can't assume that the file's Uri is related to its path, // 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. // so we generate our own path by concatenating the names on the path.
if (parentStorageFile != null) return parentStorageFile!!.path + "/" + name if (parentStorageFile != null) return parentStorageFile!!.path + "/" + name
return Uri.parse(abstractFile.getFullPath()).toString() return documentFile.uri.toString()
} }
val parent: StorageFile? val parent: StorageFile?
get() { get() {
if (isRawFile) {
return StorageFile(
null,
fileManager.fromRawFile(File(abstractFile.getFullPath()).parentFile!!),
fileManager
)
}
return parentStorageFile 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? { fun getDocumentFileDescriptor(openMode: String): AssetFileDescriptor? {
return if (abstractFile !is RawFile) { return UApp.applicationContext().contentResolver.openAssetFileDescriptor(
UApp.applicationContext().contentResolver.openAssetFileDescriptor( documentFile.uri,
abstractFile.getFileRoot<CachingDocumentFile>().holder.uri(), openMode
openMode )
)
} else null
} }
companion object { companion object {
@ -135,62 +104,56 @@ class StorageFile private constructor(
// If this isn't good enough we can add locking. // If this isn't good enough we can add locking.
private val storageFilePathDictionary = ConcurrentHashMap<String, StorageFile>() private val storageFilePathDictionary = ConcurrentHashMap<String, StorageFile>()
private val notExistingPathDictionary = ConcurrentHashMap<String, String>() private val notExistingPathDictionary = ConcurrentHashMap<String, String>()
private val listedPathDictionary = ConcurrentHashMap<String, String>()
private val fileManager: ResettableLazy<FileManager> = ResettableLazy {
val manager = FileManager(UApp.applicationContext())
manager.registerBaseDir<MusicCacheBaseDirectory>(MusicCacheBaseDirectory())
manager
}
val mediaRoot: ResettableLazy<StorageFile> = ResettableLazy { val mediaRoot: ResettableLazy<StorageFile> = ResettableLazy {
StorageFile( StorageFile(null, getRoot()!!)
null, }
fileManager.value.newBaseDirectoryFile<MusicCacheBaseDirectory>()!!,
fileManager.value 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() { fun resetCaches() {
storageFilePathDictionary.clear() storageFilePathDictionary.clear()
notExistingPathDictionary.clear() notExistingPathDictionary.clear()
fileManager.value.unregisterBaseDir<MusicCacheBaseDirectory>() listedPathDictionary.clear()
fileManager.reset()
mediaRoot.reset() mediaRoot.reset()
Timber.v("StorageFile caches were reset") Timber.i("StorageFile caches were reset")
if (!fileManager.value.baseDirectoryExists<MusicCacheBaseDirectory>()) { val root = getRoot()
if (root == null || !root.exists()) {
Settings.cacheLocation = FileUtil.defaultMusicDirectory.path Settings.cacheLocation = FileUtil.defaultMusicDirectory.path
Util.toast(UApp.applicationContext(), R.string.settings_cache_location_error) Util.toast(UApp.applicationContext(), R.string.settings_cache_location_error)
} }
} }
@Synchronized
fun getOrCreateFileFromPath(path: String): StorageFile { fun getOrCreateFileFromPath(path: String): StorageFile {
val normalizedPath = normalizePath(path) if (storageFilePathDictionary.containsKey(path))
if (!normalizedPath.isUri()) { return storageFilePathDictionary[path]!!
File(normalizedPath).createNewFile()
return StorageFile(
null,
fileManager.value.fromPath(normalizedPath),
fileManager.value
)
}
if (storageFilePathDictionary.containsKey(normalizedPath)) val parent = getStorageFileForParentDirectory(path)
return storageFilePathDictionary[normalizedPath]!!
val parent = getStorageFileForParentDirectory(normalizedPath)
?: throw IOException("Parent directory doesn't exist") ?: throw IOException("Parent directory doesn't exist")
val name = FileUtil.getNameFromPath(normalizedPath) val name = FileUtil.getNameFromPath(path)
val file = StorageFile( val file = StorageFile(
parent, parent,
fileManager.value.findFile(parent.abstractFile, name) parent.documentFile.findFile(name)
?: fileManager.value.create(parent.abstractFile, ?: parent.documentFile.createFile(
listOf(FileSegment(name)) MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.extension())!!,
)!!, name.withoutExtension()
parent.fileManager )!!
) )
storageFilePathDictionary[normalizedPath] = file
notExistingPathDictionary.remove(normalizedPath) storageFilePathDictionary[path] = file
notExistingPathDictionary.remove(path)
return file return file
} }
@ -199,89 +162,97 @@ class StorageFile private constructor(
} }
fun getFromPath(path: String): StorageFile? { 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)) if (storageFilePathDictionary.containsKey(path))
return storageFilePathDictionary[normalizedPath]!! return storageFilePathDictionary[path]!!
if (notExistingPathDictionary.contains(normalizedPath)) return null if (notExistingPathDictionary.contains(path)) return null
val parent = getStorageFileForParentDirectory(normalizedPath) val parent = getStorageFileForParentDirectory(path)
if (parent == null) { if (parent == null) {
notExistingPathDictionary.putIfAbsent(normalizedPath, normalizedPath) notExistingPathDictionary.putIfAbsent(path, path)
return null 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 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, // Listing a bunch of files takes the same time in SAF as finding one,
// so we list and cache all of them for performance // so we list and cache all of them for performance
parent.listFiles().forEach { parent.listFiles().forEach {
if (it.name == fileName) file = it if (it.name == fileName) file = it
storageFilePathDictionary[it.path] = it storageFilePathDictionary[it.path] = it
notExistingPathDictionary.remove(it.path) notExistingPathDictionary.remove(it.path)
} }
listedPathDictionary[parent.path] = parent.path
if (file == null) { if (file == null) {
notExistingPathDictionary.putIfAbsent(normalizedPath, normalizedPath) notExistingPathDictionary.putIfAbsent(path, path)
return null return null
} }
return file return file
} }
@Synchronized
fun createDirsOnPath(path: String) { fun createDirsOnPath(path: String) {
val normalizedPath = normalizePath(path) val segments = getUriSegments(path)
if (!normalizedPath.isUri()) {
File(normalizedPath).mkdirs()
return
}
val segments = getUriSegments(normalizedPath)
?: throw IOException("Can't get path because the root has changed") ?: throw IOException("Can't get path because the root has changed")
var file = mediaRoot.value var file = mediaRoot.value
segments.forEach { segment -> segments.forEach { segment ->
file = StorageFile( file = StorageFile(
file, file,
fileManager.value.create(file.abstractFile, listOf(DirectorySegment(segment))) file.documentFile.findFile(segment) ?:
?: throw IOException("Can't create directory"), file.documentFile.createDirectory(segment)
fileManager.value ?: 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) { fun rename(pathFrom: String, pathTo: String) {
val normalizedPathFrom = normalizePath(pathFrom) val fileFrom = getFromPath(pathFrom) ?: throw IOException("File to rename doesn't exist")
val fileFrom = getFromPath(normalizedPathFrom) ?: throw IOException("File to rename doesn't exist")
rename(fileFrom, pathTo) rename(fileFrom, pathTo)
} }
@Synchronized
fun rename(pathFrom: StorageFile?, pathTo: String) { fun rename(pathFrom: StorageFile?, pathTo: String) {
val normalizedPathTo = normalizePath(pathTo) if (pathFrom == null || !pathFrom.documentFile.exists()) throw IOException("File to rename doesn't exist")
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, pathTo)
Timber.d("Renaming from %s to %s", pathFrom.path, normalizedPathTo)
val parentTo = getFromPath(FileUtil.getParentPath(normalizedPathTo)!!) ?: throw IOException("Destination folder doesn't exist") val parentTo = getFromPath(FileUtil.getParentPath(pathTo)!!) ?: throw IOException("Destination folder doesn't exist")
val fileTo = getFromParentAndName(parentTo, FileUtil.getNameFromPath(normalizedPathTo)) val fileTo = getFromParentAndName(parentTo, FileUtil.getNameFromPath(pathTo))
notExistingPathDictionary.remove(normalizedPathTo)
storageFilePathDictionary.remove(normalizePath(pathFrom.path))
fileManager.value.copyFileContents(pathFrom.abstractFile, fileTo.abstractFile) copyFileContents(pathFrom.documentFile, fileTo.documentFile)
pathFrom.delete() 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 { private fun getFromParentAndName(parent: StorageFile, name: String): StorageFile {
val file = parent.fileManager.findFile(parent.abstractFile, name) val file = parent.documentFile.findFile(name)
?: parent.fileManager.createFile(parent.abstractFile, name)!! ?: parent.documentFile.createFile(
return StorageFile(parent, file, parent.fileManager) MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.extension())!!,
name.withoutExtension()
)!!
return StorageFile(parent, file)
} }
private fun getStorageFileForParentDirectory(path: String): StorageFile? { private fun getStorageFileForParentDirectory(path: String): StorageFile? {
@ -290,7 +261,11 @@ class StorageFile private constructor(
return storageFilePathDictionary[parentPath]!! return storageFilePathDictionary[parentPath]!!
if (notExistingPathDictionary.contains(parentPath)) return null if (notExistingPathDictionary.contains(parentPath)) return null
//val start = System.currentTimeMillis()
val parent = findStorageFileForParentDirectory(parentPath) val parent = findStorageFileForParentDirectory(parentPath)
//val end = System.currentTimeMillis()
//Timber.v("StorageFile getStorageFileForParentDirectory searching for %s, time: %d", parentPath, end-start)
if (parent == null) { if (parent == null) {
storageFilePathDictionary.remove(parentPath) storageFilePathDictionary.remove(parentPath)
notExistingPathDictionary.putIfAbsent(parentPath, parentPath) notExistingPathDictionary.putIfAbsent(parentPath, parentPath)
@ -306,14 +281,31 @@ class StorageFile private constructor(
val segments = getUriSegments(path) val segments = getUriSegments(path)
?: throw IOException("Can't get path because the root has changed") ?: 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 -> segments.forEach { segment ->
file = StorageFile( val currentPath = file.path + "/" + segment
file, if (notExistingPathDictionary.contains(currentPath)) return null
fileManager.value.findFile(file.abstractFile, segment) if (storageFilePathDictionary.containsKey(currentPath)) {
?: return null, file = storageFilePathDictionary[currentPath]!!
file.fileManager } 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 return file
} }
@ -324,32 +316,6 @@ class StorageFile private constructor(
val pathWithoutRoot = uri.substringAfter(rootPath) val pathWithoutRoot = uri.substringAfter(rootPath)
return pathWithoutRoot.split('/').filter { it.isNotEmpty() } 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? // TODO is there a better way to tell apart a path and an URI?
return this.contains(':') 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)
}
}