Refactored to use DocumentFile instead of FSAF
This commit is contained in:
parent
90638e5fd7
commit
34c5ced32e
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue