mirror of
https://github.com/ultrasonic/ultrasonic
synced 2025-02-04 02:57:32 +01:00
Added file caches to speed up SAF
Updated settings page with default music cache path reset Fixed a bunch of bugs
This commit is contained in:
parent
5c7cde2349
commit
5cf914f555
@ -24,6 +24,7 @@ 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",
|
||||
@ -66,6 +67,7 @@ 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",
|
||||
|
@ -98,6 +98,7 @@ dependencies {
|
||||
implementation androidSupport.navigationFragmentKtx
|
||||
implementation androidSupport.navigationUiKtx
|
||||
implementation androidSupport.navigationFeature
|
||||
implementation androidSupport.lifecycle
|
||||
|
||||
implementation other.kotlinStdlib
|
||||
implementation other.kotlinxCoroutines
|
||||
|
@ -194,9 +194,10 @@ public class StreamProxy implements Runnable
|
||||
String file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteOrSaveFile() : downloadFile.getPartialFile();
|
||||
int cbSentThisBatch = 0;
|
||||
|
||||
if (StorageFile.Companion.isPathExists(file))
|
||||
StorageFile storageFile = StorageFile.Companion.getFromPath(file);
|
||||
if (storageFile != null)
|
||||
{
|
||||
InputStream input = StorageFile.Companion.getFromPath(file).getFileInputStream();
|
||||
InputStream input = storageFile.getFileInputStream();
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -1,6 +1,10 @@
|
||||
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
|
||||
@ -15,6 +19,7 @@ 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
|
||||
|
||||
@ -52,6 +57,8 @@ class UApp : MultiDexApplication() {
|
||||
mediaPlayerModule
|
||||
)
|
||||
}
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleListener())
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -62,3 +69,11 @@ class UApp : MultiDexApplication() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppLifecycleListener : LifecycleObserver {
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
fun onMoveToForeground() {
|
||||
StorageFile.resetCaches()
|
||||
}
|
||||
}
|
@ -413,9 +413,11 @@ class PlayerFragment :
|
||||
onCurrentChanged()
|
||||
}
|
||||
val handler = Handler()
|
||||
|
||||
// TODO Use Rx for Update instead of polling!
|
||||
val runnable = Runnable { handler.post { update(cancellationToken) } }
|
||||
executorService = Executors.newSingleThreadScheduledExecutor()
|
||||
executorService.scheduleWithFixedDelay(runnable, 0L, 250L, TimeUnit.MILLISECONDS)
|
||||
executorService.scheduleWithFixedDelay(runnable, 0L, 500L, TimeUnit.MILLISECONDS)
|
||||
|
||||
if (mediaPlayerController.keepScreenOn) {
|
||||
requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
|
@ -20,6 +20,7 @@ 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
|
||||
@ -46,9 +47,11 @@ import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Settings.preferences
|
||||
import org.moire.ultrasonic.util.Settings.shareGreeting
|
||||
import org.moire.ultrasonic.util.Settings.shouldUseId3Tags
|
||||
import org.moire.ultrasonic.util.StorageFile
|
||||
import org.moire.ultrasonic.util.TimeSpanPreference
|
||||
import org.moire.ultrasonic.util.TimeSpanPreferenceDialogFragmentCompat
|
||||
import org.moire.ultrasonic.util.Util.toast
|
||||
import org.moire.ultrasonic.util.isUri
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
@ -89,6 +92,7 @@ class SettingsFragment :
|
||||
private var resumeOnBluetoothDevice: Preference? = null
|
||||
private var pauseOnBluetoothDevice: Preference? = null
|
||||
private var debugLogToFile: CheckBoxPreference? = null
|
||||
private var customCacheLocation: CheckBoxPreference? = null
|
||||
|
||||
private val mediaPlayerControllerLazy = inject<MediaPlayerController>(
|
||||
MediaPlayerController::class.java
|
||||
@ -137,6 +141,8 @@ class SettingsFragment :
|
||||
pauseOnBluetoothDevice = findPreference(Constants.PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE)
|
||||
debugLogToFile = findPreference(Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE)
|
||||
showArtistPicture = findPreference(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE)
|
||||
customCacheLocation = findPreference(Constants.PREFERENCES_KEY_CUSTOM_CACHE_LOCATION)
|
||||
|
||||
sharingDefaultGreeting!!.text = shareGreeting
|
||||
setupClearSearchPreference()
|
||||
setupFeatureFlagsPreferences()
|
||||
@ -186,7 +192,7 @@ class SettingsFragment :
|
||||
|
||||
contentResolver.takePersistableUriPermission(uri, RW_FLAG)
|
||||
|
||||
setCacheLocation(uri)
|
||||
setCacheLocation(uri.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@ -224,6 +230,9 @@ class SettingsFragment :
|
||||
Constants.PREFERENCES_KEY_THEME -> {
|
||||
RxBus.themeChangedEventPublisher.onNext(Unit)
|
||||
}
|
||||
Constants.PREFERENCES_KEY_CUSTOM_CACHE_LOCATION -> {
|
||||
setupCacheLocationPreference()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -247,12 +256,19 @@ class SettingsFragment :
|
||||
}
|
||||
|
||||
private fun setupCacheLocationPreference() {
|
||||
// TODO add means to reset cache directory to its default value
|
||||
val isDefault = Settings.cacheLocation == defaultMusicDirectory.path
|
||||
|
||||
if (!Settings.customCacheLocation) {
|
||||
cacheLocation?.isVisible = false
|
||||
if (!isDefault) setCacheLocation(defaultMusicDirectory.path)
|
||||
return
|
||||
}
|
||||
|
||||
cacheLocation?.isVisible = true
|
||||
val uri = Uri.parse(Settings.cacheLocation)
|
||||
cacheLocation!!.summary = uri.path
|
||||
cacheLocation!!.onPreferenceClickListener =
|
||||
Preference.OnPreferenceClickListener {
|
||||
val isDefault = Settings.cacheLocation == defaultMusicDirectory.path
|
||||
|
||||
// Choose a directory using the system's file picker.
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
@ -434,14 +450,17 @@ class SettingsFragment :
|
||||
sendBluetoothAlbumArt!!.isEnabled = enabled
|
||||
}
|
||||
|
||||
private fun setCacheLocation(uri: Uri) {
|
||||
if (uri.path != null) {
|
||||
cacheLocation!!.summary = uri.path
|
||||
Settings.cacheLocation = uri.toString()
|
||||
|
||||
// Clear download queue.
|
||||
mediaPlayerControllerLazy.value.clear()
|
||||
private fun setCacheLocation(path: String) {
|
||||
if (path.isUri()) {
|
||||
val uri = Uri.parse(path)
|
||||
cacheLocation!!.summary = uri.path ?: ""
|
||||
}
|
||||
|
||||
Settings.cacheLocation = path
|
||||
|
||||
// Clear download queue.
|
||||
mediaPlayerControllerLazy.value.clear()
|
||||
StorageFile.resetCaches()
|
||||
}
|
||||
|
||||
private fun setDebugLogToFile(writeLog: Boolean) {
|
||||
|
@ -46,6 +46,7 @@ class DownloadFile(
|
||||
private val desiredBitRate: Int = Settings.maxBitRate
|
||||
|
||||
var priority = 100
|
||||
var downloadPrepared = false
|
||||
|
||||
@Volatile
|
||||
private var isPlaying = false
|
||||
@ -75,6 +76,13 @@ class DownloadFile(
|
||||
return if (song.bitRate == null) desiredBitRate else song.bitRate!!
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun prepare() {
|
||||
// It is necessary to signal that the download will begin shortly on another thread
|
||||
// so it won't get cleaned up accidentally
|
||||
downloadPrepared = true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun download() {
|
||||
FileUtil.createDirectoryForParent(saveFile)
|
||||
@ -85,9 +93,7 @@ class DownloadFile(
|
||||
|
||||
@Synchronized
|
||||
fun cancelDownload() {
|
||||
if (downloadTask != null) {
|
||||
downloadTask!!.cancel()
|
||||
}
|
||||
downloadTask?.cancel()
|
||||
}
|
||||
|
||||
val completeOrSaveFile: String
|
||||
@ -109,20 +115,20 @@ class DownloadFile(
|
||||
|
||||
@get:Synchronized
|
||||
val isCompleteFileAvailable: Boolean
|
||||
get() = StorageFile.isPathExists(saveFile) || StorageFile.isPathExists(completeFile)
|
||||
get() = StorageFile.isPathExists(completeFile) || StorageFile.isPathExists(saveFile)
|
||||
|
||||
@get:Synchronized
|
||||
val isWorkDone: Boolean
|
||||
get() = StorageFile.isPathExists(saveFile) || StorageFile.isPathExists(completeFile) && !save ||
|
||||
saveWhenDone || completeWhenDone
|
||||
get() = StorageFile.isPathExists(completeFile) && !save ||
|
||||
StorageFile.isPathExists(saveFile) || saveWhenDone || completeWhenDone
|
||||
|
||||
@get:Synchronized
|
||||
val isDownloading: Boolean
|
||||
get() = downloadTask != null && downloadTask!!.isRunning
|
||||
get() = downloadPrepared || (downloadTask != null && downloadTask!!.isRunning)
|
||||
|
||||
@get:Synchronized
|
||||
val isDownloadCancelled: Boolean
|
||||
get() = downloadTask != null && downloadTask!!.isCancelled
|
||||
get() = downloadTask != null && downloadTask!!.isCancelled
|
||||
|
||||
fun shouldSave(): Boolean {
|
||||
return save
|
||||
@ -142,9 +148,8 @@ class DownloadFile(
|
||||
}
|
||||
|
||||
fun unpin() {
|
||||
if (StorageFile.isPathExists(saveFile)) {
|
||||
StorageFile.rename(saveFile, completeFile)
|
||||
}
|
||||
val file = StorageFile.getFromPath(saveFile) ?: return
|
||||
StorageFile.rename(file, completeFile)
|
||||
}
|
||||
|
||||
fun cleanup(): Boolean {
|
||||
@ -194,6 +199,7 @@ class DownloadFile(
|
||||
|
||||
override fun execute() {
|
||||
|
||||
downloadPrepared = false
|
||||
var inputStream: InputStream? = null
|
||||
var outputStream: OutputStream? = null
|
||||
try {
|
||||
@ -222,18 +228,12 @@ class DownloadFile(
|
||||
// Some devices seem to throw error on partial file which doesn't exist
|
||||
val needsDownloading: Boolean
|
||||
val duration = song.duration
|
||||
var fileLength: Long = 0
|
||||
|
||||
if (!StorageFile.isPathExists(partialFile)) {
|
||||
fileLength = 0
|
||||
} else {
|
||||
fileLength = StorageFile.getFromPath(partialFile).length()
|
||||
}
|
||||
val fileLength = StorageFile.getFromPath(partialFile)?.length ?: 0
|
||||
|
||||
needsDownloading = (
|
||||
desiredBitRate == 0 || duration == null ||
|
||||
duration == 0 || fileLength == 0L
|
||||
)
|
||||
desiredBitRate == 0 || duration == null ||
|
||||
duration == 0 || fileLength == 0L
|
||||
)
|
||||
|
||||
if (needsDownloading) {
|
||||
// Attempt partial HTTP GET, appending to the file if it exists.
|
||||
@ -372,7 +372,7 @@ class DownloadFile(
|
||||
}
|
||||
|
||||
override val id: String
|
||||
get() = song.id
|
||||
get() = song.id
|
||||
|
||||
companion object {
|
||||
const val MAX_RETRIES = 5
|
||||
|
@ -133,6 +133,7 @@ class Downloader(
|
||||
return
|
||||
}
|
||||
|
||||
Timber.v("Downloader checkDownloadsInternal checking downloads")
|
||||
// Check the active downloads for failures or completions and remove them
|
||||
// Store the result in a flag to know if changes have occurred
|
||||
var listChanged = cleanupActiveDownloads()
|
||||
@ -183,6 +184,7 @@ class Downloader(
|
||||
if (listChanged) {
|
||||
updateLiveData()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun updateLiveData() {
|
||||
@ -190,6 +192,7 @@ class Downloader(
|
||||
}
|
||||
|
||||
private fun startDownloadOnService(task: DownloadFile) {
|
||||
task.prepare()
|
||||
MediaPlayerService.executeOnStartedMediaPlayerService {
|
||||
task.download()
|
||||
}
|
||||
|
@ -368,7 +368,7 @@ class LocalMediaPlayer : KoinComponent {
|
||||
}
|
||||
dataSource = String.format(
|
||||
Locale.getDefault(), "http://127.0.0.1:%d/%s",
|
||||
proxy!!.port, URLEncoder.encode(dataSource, Constants.UTF_8)
|
||||
proxy!!.port, URLEncoder.encode(file!!.path, Constants.UTF_8)
|
||||
)
|
||||
Timber.i("Data Source: %s", dataSource)
|
||||
} else if (proxy != null) {
|
||||
@ -379,7 +379,7 @@ class LocalMediaPlayer : KoinComponent {
|
||||
Timber.i("Preparing media player")
|
||||
|
||||
if (dataSource != null) mediaPlayer.setDataSource(dataSource)
|
||||
else if (file.isRawFile()) mediaPlayer.setDataSource(file.getRawFilePath())
|
||||
else if (file!!.isRawFile) mediaPlayer.setDataSource(file.rawFilePath)
|
||||
else {
|
||||
val descriptor = file.getDocumentFileDescriptor("r")!!
|
||||
mediaPlayer.setDataSource(descriptor.fileDescriptor)
|
||||
@ -465,7 +465,7 @@ class LocalMediaPlayer : KoinComponent {
|
||||
} catch (ignored: Throwable) {
|
||||
}
|
||||
|
||||
if (file.isRawFile()) nextMediaPlayer!!.setDataSource(file.getRawFilePath())
|
||||
if (file!!.isRawFile) nextMediaPlayer!!.setDataSource(file.rawFilePath)
|
||||
else {
|
||||
val descriptor = file.getDocumentFileDescriptor("r")!!
|
||||
nextMediaPlayer!!.setDataSource(descriptor.fileDescriptor)
|
||||
@ -614,8 +614,7 @@ class LocalMediaPlayer : KoinComponent {
|
||||
|
||||
private fun bufferComplete(): Boolean {
|
||||
val completeFileAvailable = downloadFile.isWorkDone
|
||||
val size = if (!StorageFile.isPathExists(partialFile)) 0
|
||||
else StorageFile.getFromPath(partialFile).length()
|
||||
val size = StorageFile.getFromPath(partialFile)?.length ?: 0
|
||||
|
||||
Timber.i(
|
||||
"Buffering %s (%d/%d, %s)",
|
||||
@ -672,8 +671,8 @@ class LocalMediaPlayer : KoinComponent {
|
||||
val completeFileAvailable = downloadFile!!.isWorkDone
|
||||
val state = (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED)
|
||||
|
||||
val length = if (partialFile == null || !StorageFile.isPathExists(partialFile)) 0
|
||||
else StorageFile.getFromPath(partialFile).length()
|
||||
val length = if (partialFile == null) 0
|
||||
else StorageFile.getFromPath(partialFile)?.length ?: 0
|
||||
|
||||
Timber.i("Buffering next %s (%d)", partialFile, length)
|
||||
|
||||
|
@ -205,6 +205,9 @@ class MediaPlayerService : Service() {
|
||||
|
||||
@Synchronized
|
||||
fun setNextPlaying() {
|
||||
// Download the next few songs if necessary
|
||||
downloader.checkDownloads()
|
||||
|
||||
if (!Settings.gaplessPlayback) {
|
||||
localMediaPlayer.clearNextPlaying(true)
|
||||
return
|
||||
@ -289,7 +292,6 @@ class MediaPlayerService : Service() {
|
||||
localMediaPlayer.play(downloader.getPlaylist()[index])
|
||||
}
|
||||
}
|
||||
downloader.checkDownloads()
|
||||
setNextPlaying()
|
||||
}
|
||||
}
|
||||
|
@ -55,8 +55,8 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
val root = FileUtil.musicDirectory
|
||||
for (file in FileUtil.listFiles(root)) {
|
||||
if (file.isDirectory) {
|
||||
val index = Index(file.getPath())
|
||||
index.id = file.getPath()
|
||||
val index = Index(file.path)
|
||||
index.id = file.path
|
||||
index.index = file.name.substring(0, 1)
|
||||
index.name = file.name
|
||||
indexes.add(index)
|
||||
@ -102,7 +102,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
): MusicDirectory {
|
||||
val dir = StorageFile.getFromPath(id)
|
||||
val result = MusicDirectory()
|
||||
result.name = dir.name
|
||||
result.name = dir?.name ?: return result
|
||||
|
||||
val seen: MutableCollection<String?> = HashSet()
|
||||
|
||||
@ -127,7 +127,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
val artistName = artistFile.name
|
||||
if (artistFile.isDirectory) {
|
||||
if (matchCriteria(criteria, artistName).also { closeness = it } > 0) {
|
||||
val artist = Artist(artistFile.getPath())
|
||||
val artist = Artist(artistFile.path)
|
||||
artist.index = artistFile.name.substring(0, 1)
|
||||
artist.name = artistName
|
||||
artist.closeness = closeness
|
||||
@ -205,12 +205,10 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
var line = buffer.readLine()
|
||||
if ("#EXTM3U" != line) return playlist
|
||||
while (buffer.readLine().also { line = it } != null) {
|
||||
if (StorageFile.isPathExists(line)) {
|
||||
val entryFile = StorageFile.getFromPath(line)
|
||||
val entryName = getName(entryFile.name, entryFile.isDirectory)
|
||||
if (entryName != null) {
|
||||
playlist.addChild(createEntry(entryFile, entryName))
|
||||
}
|
||||
val entryFile = StorageFile.getFromPath(line) ?: continue
|
||||
val entryName = getName(entryFile.name, entryFile.isDirectory)
|
||||
if (entryName != null) {
|
||||
playlist.addChild(createEntry(entryFile, entryName))
|
||||
}
|
||||
}
|
||||
playlist
|
||||
@ -500,12 +498,12 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
|
||||
@Suppress("TooGenericExceptionCaught", "ComplexMethod", "LongMethod", "NestedBlockDepth")
|
||||
private fun createEntry(file: StorageFile, name: String?): MusicDirectory.Entry {
|
||||
val entry = MusicDirectory.Entry(file.getPath())
|
||||
val entry = MusicDirectory.Entry(file.path)
|
||||
entry.isDirectory = file.isDirectory
|
||||
entry.parent = file.getParent()!!.getPath()
|
||||
entry.size = if (file.isFile) file.length() else 0
|
||||
val root = FileUtil.musicDirectory.getPath()
|
||||
entry.path = file.getPath().replaceFirst(
|
||||
entry.parent = file.parent!!.path
|
||||
entry.size = if (file.isFile) file.length else 0
|
||||
val root = FileUtil.musicDirectory.path
|
||||
entry.path = file.path.replaceFirst(
|
||||
String.format(Locale.ROOT, "^%s/", root).toRegex(), ""
|
||||
)
|
||||
entry.title = name
|
||||
@ -522,7 +520,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
try {
|
||||
val mmr = MediaMetadataRetriever()
|
||||
|
||||
if (file.isRawFile()) mmr.setDataSource(file.getRawFilePath())
|
||||
if (file.isRawFile) mmr.setDataSource(file.rawFilePath)
|
||||
else {
|
||||
val descriptor = file.getDocumentFileDescriptor("r")!!
|
||||
mmr.setDataSource(descriptor.fileDescriptor)
|
||||
@ -541,8 +539,8 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
mmr.release()
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
entry.artist = artist ?: file.getParent()!!.getParent()!!.name
|
||||
entry.album = album ?: file.getParent()!!.name
|
||||
entry.artist = artist ?: file.parent!!.parent!!.name
|
||||
entry.album = album ?: file.parent!!.name
|
||||
if (title != null) {
|
||||
entry.title = title
|
||||
}
|
||||
|
@ -33,21 +33,38 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||
}
|
||||
}
|
||||
|
||||
// Cache cleaning shouldn't run concurrently, as it is started after every completed download
|
||||
// TODO serializing and throttling these is an ideal task for Rx
|
||||
fun clean() {
|
||||
launch(exceptionHandler("clean")) {
|
||||
backgroundCleanup()
|
||||
if (cleaning) return
|
||||
synchronized(lock) {
|
||||
if (cleaning) return
|
||||
cleaning = true
|
||||
launch(exceptionHandler("clean")) {
|
||||
backgroundCleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cleanSpace() {
|
||||
launch(exceptionHandler("cleanSpace")) {
|
||||
backgroundSpaceCleanup()
|
||||
if (spaceCleaning) return
|
||||
synchronized(lock) {
|
||||
if (spaceCleaning) return
|
||||
spaceCleaning = true
|
||||
launch(exceptionHandler("cleanSpace")) {
|
||||
backgroundSpaceCleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cleanPlaylists(playlists: List<Playlist>) {
|
||||
launch(exceptionHandler("cleanPlaylists")) {
|
||||
backgroundPlaylistsCleanup(playlists)
|
||||
if (playlistCleaning) return
|
||||
synchronized(lock) {
|
||||
if (playlistCleaning) return
|
||||
playlistCleaning = true
|
||||
launch(exceptionHandler("cleanPlaylists")) {
|
||||
backgroundPlaylistsCleanup(playlists)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,6 +81,8 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||
deleteEmptyDirs(dirs, filesToNotDelete)
|
||||
} catch (all: RuntimeException) {
|
||||
Timber.e(all, "Error in cache cleaning.")
|
||||
} finally {
|
||||
cleaning = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,6 +101,8 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||
}
|
||||
} catch (all: RuntimeException) {
|
||||
Timber.e(all, "Error in cache cleaning.")
|
||||
} finally {
|
||||
spaceCleaning = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,27 +125,34 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||
}
|
||||
} catch (all: RuntimeException) {
|
||||
Timber.e(all, "Error in playlist cache cleaning.")
|
||||
} finally {
|
||||
playlistCleaning = false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val lock = Object()
|
||||
private var cleaning = false
|
||||
private var spaceCleaning = false
|
||||
private var playlistCleaning = false
|
||||
|
||||
private const val MIN_FREE_SPACE = 500 * 1024L * 1024L
|
||||
private fun deleteEmptyDirs(dirs: Iterable<StorageFile>, doNotDelete: Collection<String>) {
|
||||
for (dir in dirs) {
|
||||
if (doNotDelete.contains(dir.getPath())) continue
|
||||
if (doNotDelete.contains(dir.path)) continue
|
||||
|
||||
var children = dir.listFiles()
|
||||
if (children != null) {
|
||||
// No songs left in the folder
|
||||
if (children.size == 1 && children[0].getPath() == getAlbumArtFile(dir.getPath())) {
|
||||
if (children.size == 1 && children[0].path == getAlbumArtFile(dir.path)) {
|
||||
// Delete Artwork files
|
||||
delete(getAlbumArtFile(dir.getPath()))
|
||||
delete(getAlbumArtFile(dir.path))
|
||||
children = dir.listFiles()
|
||||
}
|
||||
|
||||
// Delete empty directory
|
||||
if (children != null && children.isEmpty()) {
|
||||
delete(dir.getPath())
|
||||
delete(dir.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -137,7 +165,7 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||
var bytesUsedBySubsonic = 0L
|
||||
|
||||
for (file in files) {
|
||||
bytesUsedBySubsonic += file.length()
|
||||
bytesUsedBySubsonic += file.length
|
||||
}
|
||||
|
||||
// Ensure that file system is not more than 95% full.
|
||||
@ -146,8 +174,8 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||
val bytesTotalFs: Long
|
||||
val bytesAvailableFs: Long
|
||||
|
||||
if (files[0].isRawFile()) {
|
||||
val stat = StatFs(files[0].getRawFilePath())
|
||||
if (files[0].isRawFile) {
|
||||
val stat = StatFs(files[0].rawFilePath)
|
||||
bytesTotalFs = stat.blockCountLong * stat.blockSizeLong
|
||||
bytesAvailableFs = stat.availableBlocksLong * stat.blockSizeLong
|
||||
bytesUsedFs = bytesTotalFs - bytesAvailableFs
|
||||
@ -201,9 +229,9 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||
for (file in files) {
|
||||
if (!deletePartials && bytesDeleted > bytesToDelete) break
|
||||
if (bytesToDelete > bytesDeleted || deletePartials && isPartial(file)) {
|
||||
if (!doNotDelete.contains(file.getPath()) && file.name != Constants.ALBUM_ART_FILE) {
|
||||
val size = file.length()
|
||||
if (delete(file.getPath())) {
|
||||
if (!doNotDelete.contains(file.path) && file.name != Constants.ALBUM_ART_FILE) {
|
||||
val size = file.length
|
||||
if (delete(file.path)) {
|
||||
bytesDeleted += size
|
||||
}
|
||||
}
|
||||
@ -230,7 +258,7 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||
|
||||
private fun sortByAscendingModificationTime(files: MutableList<StorageFile>) {
|
||||
files.sortWith { a: StorageFile, b: StorageFile ->
|
||||
a.lastModified().compareTo(b.lastModified())
|
||||
a.lastModified.compareTo(b.lastModified)
|
||||
}
|
||||
}
|
||||
|
||||
@ -245,7 +273,7 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||
filesToNotDelete.add(downloadFile.completeOrSaveFile)
|
||||
}
|
||||
|
||||
filesToNotDelete.add(musicDirectory.getPath())
|
||||
filesToNotDelete.add(musicDirectory.path)
|
||||
return filesToNotDelete
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,7 @@ object Constants {
|
||||
const val PREFERENCES_KEY_MAX_BITRATE_WIFI = "maxBitrateWifi"
|
||||
const val PREFERENCES_KEY_MAX_BITRATE_MOBILE = "maxBitrateMobile"
|
||||
const val PREFERENCES_KEY_CACHE_SIZE = "cacheSize"
|
||||
const val PREFERENCES_KEY_CUSTOM_CACHE_LOCATION = "customCacheLocation"
|
||||
const val PREFERENCES_KEY_CACHE_LOCATION = "cacheLocation"
|
||||
const val PREFERENCES_KEY_PRELOAD_COUNT = "preloadCount"
|
||||
const val PREFERENCES_KEY_HIDE_MEDIA = "hideMedia"
|
||||
|
@ -131,7 +131,7 @@ object FileUtil {
|
||||
*/
|
||||
fun getArtistArtKey(name: String?, large: Boolean): String {
|
||||
val artist = fileSystemSafe(name)
|
||||
val dir = String.format(Locale.ROOT, "%s/%s/%s", musicDirectory.getPath(), artist, UNNAMED)
|
||||
val dir = String.format(Locale.ROOT, "%s/%s/%s", musicDirectory.path, artist, UNNAMED)
|
||||
return getAlbumArtKey(dir, large)
|
||||
}
|
||||
|
||||
@ -194,7 +194,7 @@ object FileUtil {
|
||||
dir = String.format(
|
||||
Locale.ROOT,
|
||||
"%s/%s",
|
||||
musicDirectory.getPath(),
|
||||
musicDirectory.path,
|
||||
if (entry.isDirectory) f else getParentPath(f) ?: ""
|
||||
)
|
||||
} else {
|
||||
@ -203,7 +203,7 @@ object FileUtil {
|
||||
if (UNNAMED == album) {
|
||||
album = fileSystemSafe(entry.title)
|
||||
}
|
||||
dir = String.format(Locale.ROOT, "%s/%s/%s", musicDirectory.getPath(), artist, album)
|
||||
dir = String.format(Locale.ROOT, "%s/%s/%s", musicDirectory.path, artist, album)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
@ -241,7 +241,7 @@ object FileUtil {
|
||||
|
||||
@JvmStatic
|
||||
val musicDirectory: StorageFile
|
||||
get() = StorageFile.getMediaRoot()
|
||||
get() = StorageFile.mediaRoot.value
|
||||
|
||||
@JvmStatic
|
||||
@Suppress("ReturnCount")
|
||||
@ -320,7 +320,7 @@ object FileUtil {
|
||||
fun listFiles(dir: StorageFile): SortedSet<StorageFile> {
|
||||
val files = dir.listFiles()
|
||||
if (files == null) {
|
||||
Timber.w("Failed to list children for %s", dir.getPath())
|
||||
Timber.w("Failed to list children for %s", dir.path)
|
||||
return TreeSet()
|
||||
}
|
||||
return TreeSet(files.asList())
|
||||
@ -500,8 +500,9 @@ object FileUtil {
|
||||
|
||||
@JvmStatic
|
||||
fun delete(file: String?): Boolean {
|
||||
if (file != null && StorageFile.isPathExists(file)) {
|
||||
if (!StorageFile.getFromPath(file).delete()) {
|
||||
if (file != null) {
|
||||
val storageFile = StorageFile.getFromPath(file)
|
||||
if (storageFile != null && !storageFile.delete()) {
|
||||
Timber.w("Failed to delete file %s", file)
|
||||
return false
|
||||
}
|
||||
@ -509,5 +510,4 @@ object FileUtil {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
package org.moire.ultrasonic.util
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
class ResettableLazy<T>(private val initializer: () -> T) {
|
||||
private val lazyRef: AtomicReference<Lazy<T>> = AtomicReference(
|
||||
lazy(
|
||||
initializer
|
||||
)
|
||||
)
|
||||
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
|
||||
return lazyRef.get().getValue(thisRef, property)
|
||||
}
|
||||
|
||||
val value: T
|
||||
get() {
|
||||
return lazyRef.get().value
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
lazyRef.set(lazy(initializer))
|
||||
}
|
||||
}
|
@ -105,6 +105,12 @@ object Settings {
|
||||
return if (cacheSize == -1) Int.MAX_VALUE else cacheSize
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
var customCacheLocation by BooleanSetting(
|
||||
Constants.PREFERENCES_KEY_CUSTOM_CACHE_LOCATION,
|
||||
false
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
var cacheLocation by StringSetting(
|
||||
Constants.PREFERENCES_KEY_CACHE_LOCATION,
|
||||
|
@ -12,8 +12,11 @@ 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 org.moire.ultrasonic.R
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
@ -22,19 +25,20 @@ import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Provides filesystem access abstraction which works
|
||||
* both on File based paths and Storage Access Framework Uris
|
||||
*/
|
||||
class StorageFile private constructor(
|
||||
private var parent: StorageFile?,
|
||||
private var parentStorageFile: StorageFile?,
|
||||
private var abstractFile: AbstractFile,
|
||||
private var fileManager: FileManager
|
||||
): Comparable<StorageFile> {
|
||||
|
||||
override fun compareTo(other: StorageFile): Int {
|
||||
return getPath().compareTo(other.getPath())
|
||||
return path.compareTo(other.path)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
@ -47,25 +51,28 @@ class StorageFile private constructor(
|
||||
|
||||
var isFile: Boolean = fileManager.isFile(abstractFile)
|
||||
|
||||
fun length(): Long = fileManager.getLength(abstractFile)
|
||||
val length: Long
|
||||
get() = fileManager.getLength(abstractFile)
|
||||
|
||||
fun lastModified(): Long = fileManager.lastModified(abstractFile)
|
||||
val lastModified: Long
|
||||
get() = fileManager.lastModified(abstractFile)
|
||||
|
||||
fun delete(): Boolean = fileManager.delete(abstractFile)
|
||||
fun delete(): Boolean {
|
||||
val deleted = fileManager.delete(abstractFile)
|
||||
if (!deleted) return false
|
||||
val path = normalizePath(path)
|
||||
storageFilePathDictionary.remove(path)
|
||||
notExistingPathDictionary.putIfAbsent(path, path)
|
||||
return true
|
||||
}
|
||||
|
||||
fun listFiles(): Array<StorageFile> {
|
||||
val fileList = fileManager.listFiles(abstractFile)
|
||||
return fileList.map { file -> StorageFile(this, file, fileManager) }.toTypedArray()
|
||||
}
|
||||
|
||||
fun getFileOutputStream(): OutputStream {
|
||||
if (isRawFile()) return File(abstractFile.getFullPath()).outputStream()
|
||||
return fileManager.getOutputStream(abstractFile)
|
||||
?: throw IOException("Couldn't retrieve OutputStream")
|
||||
}
|
||||
|
||||
fun getFileOutputStream(append: Boolean): OutputStream {
|
||||
if (isRawFile()) return FileOutputStream(File(abstractFile.getFullPath()), append)
|
||||
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)
|
||||
@ -74,38 +81,43 @@ class StorageFile private constructor(
|
||||
}
|
||||
|
||||
fun getFileInputStream(): InputStream {
|
||||
if (isRawFile()) return FileInputStream(abstractFile.getFullPath())
|
||||
if (isRawFile) return FileInputStream(abstractFile.getFullPath())
|
||||
return fileManager.getInputStream(abstractFile)
|
||||
?: throw IOException("Couldn't retrieve InputStream")
|
||||
}
|
||||
|
||||
// TODO there are a few functions which could be getters
|
||||
// They are functions for now to help us distinguish them from similar getters in File. These can be changed after the refactor is complete.
|
||||
fun getPath(): String {
|
||||
if (isRawFile()) return abstractFile.getFullPath()
|
||||
if (getParent() != null) return getParent()!!.getPath() + "/" + name
|
||||
return Uri.parse(abstractFile.getFullPath()).toString()
|
||||
}
|
||||
val path: String
|
||||
get() {
|
||||
if (isRawFile) return abstractFile.getFullPath()
|
||||
|
||||
fun getParent(): StorageFile? {
|
||||
if (isRawFile()) {
|
||||
return StorageFile(
|
||||
null,
|
||||
fileManager.fromRawFile(File(abstractFile.getFullPath()).parentFile!!),
|
||||
fileManager
|
||||
)
|
||||
// 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 parent
|
||||
}
|
||||
|
||||
fun isRawFile(): Boolean {
|
||||
return abstractFile is RawFile
|
||||
}
|
||||
val parent: StorageFile?
|
||||
get() {
|
||||
if (isRawFile) {
|
||||
return StorageFile(
|
||||
null,
|
||||
fileManager.fromRawFile(File(abstractFile.getFullPath()).parentFile!!),
|
||||
fileManager
|
||||
)
|
||||
}
|
||||
return parentStorageFile
|
||||
}
|
||||
|
||||
fun getRawFilePath(): String? {
|
||||
return if (abstractFile is RawFile) abstractFile.getFullPath()
|
||||
else null
|
||||
}
|
||||
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) {
|
||||
@ -117,53 +129,38 @@ class StorageFile private constructor(
|
||||
}
|
||||
|
||||
companion object {
|
||||
// TODO it would be nice to check the access rights and reset the cache directory on error
|
||||
private val MusicCacheFileManager: Lazy<FileManager> = lazy {
|
||||
// These caches are necessary because SAF is very slow, and the caching in FSAF is buggy.
|
||||
// Ultrasonic assumes that the files won't change while it is in the foreground.
|
||||
// TODO to really handle concurrency we'd need API24.
|
||||
// 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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fun getMediaRoot(): StorageFile {
|
||||
return StorageFile(
|
||||
val mediaRoot: ResettableLazy<StorageFile> = ResettableLazy {
|
||||
StorageFile(
|
||||
null,
|
||||
MusicCacheFileManager.value.newBaseDirectoryFile<MusicCacheBaseDirectory>()!!,
|
||||
MusicCacheFileManager.value
|
||||
fileManager.value.newBaseDirectoryFile<MusicCacheBaseDirectory>()!!,
|
||||
fileManager.value
|
||||
)
|
||||
}
|
||||
|
||||
// TODO sometimes getFromPath is called after isPathExists, but the file may be gone because it was deleted in another thread.
|
||||
// Create a function where these two are merged
|
||||
fun getFromPath(path: String): StorageFile {
|
||||
Timber.v("StorageFile getFromPath %s", path)
|
||||
val normalizedPath = normalizePath(path)
|
||||
if (!normalizedPath.isUri()) {
|
||||
return StorageFile(
|
||||
null,
|
||||
MusicCacheFileManager.value.fromPath(normalizedPath),
|
||||
MusicCacheFileManager.value
|
||||
)
|
||||
fun resetCaches() {
|
||||
storageFilePathDictionary.clear()
|
||||
notExistingPathDictionary.clear()
|
||||
fileManager.value.unregisterBaseDir<MusicCacheBaseDirectory>()
|
||||
fileManager.reset()
|
||||
mediaRoot.reset()
|
||||
Timber.v("StorageFile caches were reset")
|
||||
if (!fileManager.value.baseDirectoryExists<MusicCacheBaseDirectory>()) {
|
||||
Settings.cacheLocation = FileUtil.defaultMusicDirectory.path
|
||||
Util.toast(UApp.applicationContext(), R.string.settings_cache_location_error)
|
||||
}
|
||||
|
||||
val segments = getUriSegments(normalizedPath)
|
||||
?: throw IOException("Can't get path because the root has changed")
|
||||
|
||||
var file = StorageFile(null, getMediaRoot().abstractFile, MusicCacheFileManager.value)
|
||||
segments.forEach { segment ->
|
||||
file = StorageFile(
|
||||
file,
|
||||
MusicCacheFileManager.value.findFile(file.abstractFile, segment)
|
||||
?: throw IOException("File not found"),
|
||||
file.fileManager
|
||||
)
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
fun getOrCreateFileFromPath(path: String): StorageFile {
|
||||
@ -172,37 +169,70 @@ class StorageFile private constructor(
|
||||
File(normalizedPath).createNewFile()
|
||||
return StorageFile(
|
||||
null,
|
||||
MusicCacheFileManager.value.fromPath(normalizedPath),
|
||||
MusicCacheFileManager.value
|
||||
fileManager.value.fromPath(normalizedPath),
|
||||
fileManager.value
|
||||
)
|
||||
}
|
||||
|
||||
val segments = getUriSegments(normalizedPath)
|
||||
?: throw IOException("Can't get path because the root has changed")
|
||||
if (storageFilePathDictionary.containsKey(normalizedPath))
|
||||
return storageFilePathDictionary[normalizedPath]!!
|
||||
|
||||
var file = StorageFile(null, getMediaRoot().abstractFile, MusicCacheFileManager.value)
|
||||
segments.forEach { segment ->
|
||||
file = StorageFile(
|
||||
file,
|
||||
MusicCacheFileManager.value.findFile(file.abstractFile, segment)
|
||||
?: MusicCacheFileManager.value.createFile(file.abstractFile, segment)!!,
|
||||
file.fileManager
|
||||
)
|
||||
}
|
||||
val parent = getStorageFileForParentDirectory(normalizedPath)
|
||||
?: throw IOException("Parent directory doesn't exist")
|
||||
|
||||
val name = FileUtil.getNameFromPath(normalizedPath)
|
||||
val file = StorageFile(
|
||||
parent,
|
||||
fileManager.value.findFile(parent.abstractFile, name)
|
||||
?: fileManager.value.create(parent.abstractFile,
|
||||
listOf(FileSegment(name))
|
||||
)!!,
|
||||
parent.fileManager
|
||||
)
|
||||
storageFilePathDictionary[normalizedPath] = file
|
||||
notExistingPathDictionary.remove(normalizedPath)
|
||||
return file
|
||||
}
|
||||
|
||||
fun isPathExists(path: String): Boolean {
|
||||
return getFromPath(path) != null
|
||||
}
|
||||
|
||||
fun getFromPath(path: String): StorageFile? {
|
||||
val normalizedPath = normalizePath(path)
|
||||
if (!normalizedPath.isUri()) return File(normalizedPath).exists()
|
||||
|
||||
val segments = getUriSegments(normalizedPath) ?: return false
|
||||
|
||||
var file = getMediaRoot().abstractFile
|
||||
segments.forEach { segment ->
|
||||
file = MusicCacheFileManager.value.findFile(file, segment) ?: return false
|
||||
if (!normalizedPath.isUri()) {
|
||||
val file = fileManager.value.fromPath(normalizedPath)
|
||||
if (!fileManager.value.exists(file)) return null
|
||||
return StorageFile(null, file, fileManager.value)
|
||||
}
|
||||
return true
|
||||
|
||||
if (storageFilePathDictionary.containsKey(normalizedPath))
|
||||
return storageFilePathDictionary[normalizedPath]!!
|
||||
if (notExistingPathDictionary.contains(normalizedPath)) return null
|
||||
|
||||
val parent = getStorageFileForParentDirectory(normalizedPath)
|
||||
if (parent == null) {
|
||||
notExistingPathDictionary.putIfAbsent(normalizedPath, normalizedPath)
|
||||
return null
|
||||
}
|
||||
|
||||
val fileName = FileUtil.getNameFromPath(normalizedPath)
|
||||
var file: StorageFile? = null
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
if (file == null) {
|
||||
notExistingPathDictionary.putIfAbsent(normalizedPath, normalizedPath)
|
||||
return null
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
fun createDirsOnPath(path: String) {
|
||||
@ -215,29 +245,81 @@ class StorageFile private constructor(
|
||||
val segments = getUriSegments(normalizedPath)
|
||||
?: throw IOException("Can't get path because the root has changed")
|
||||
|
||||
var file = getMediaRoot().abstractFile
|
||||
var file = mediaRoot.value
|
||||
segments.forEach { segment ->
|
||||
file = MusicCacheFileManager.value.createDir(file, segment)
|
||||
?: throw IOException("Can't create directory")
|
||||
file = StorageFile(
|
||||
file,
|
||||
fileManager.value.create(file.abstractFile, listOf(DirectorySegment(segment)))
|
||||
?: throw IOException("Can't create directory"),
|
||||
fileManager.value
|
||||
)
|
||||
|
||||
notExistingPathDictionary.remove(normalizePath(file.path))
|
||||
}
|
||||
}
|
||||
|
||||
fun rename(pathFrom: String, pathTo: String) {
|
||||
val normalizedPathFrom = normalizePath(pathFrom)
|
||||
val fileFrom = getFromPath(normalizedPathFrom) ?: throw IOException("File to rename doesn't exist")
|
||||
rename(fileFrom, pathTo)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
Timber.d("Renaming from %s to %s", normalizedPathFrom, normalizedPathTo)
|
||||
|
||||
val fileFrom = getFromPath(normalizedPathFrom)
|
||||
val parentTo = getFromPath(FileUtil.getParentPath(normalizedPathTo)!!)
|
||||
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))
|
||||
|
||||
MusicCacheFileManager.value.copyFileContents(fileFrom.abstractFile, fileTo.abstractFile)
|
||||
fileFrom.delete()
|
||||
fileManager.value.copyFileContents(pathFrom.abstractFile, fileTo.abstractFile)
|
||||
pathFrom.delete()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
private fun getStorageFileForParentDirectory(path: String): StorageFile? {
|
||||
val parentPath = FileUtil.getParentPath(path)!!
|
||||
if (storageFilePathDictionary.containsKey(parentPath))
|
||||
return storageFilePathDictionary[parentPath]!!
|
||||
if (notExistingPathDictionary.contains(parentPath)) return null
|
||||
|
||||
val parent = findStorageFileForParentDirectory(parentPath)
|
||||
if (parent == null) {
|
||||
storageFilePathDictionary.remove(parentPath)
|
||||
notExistingPathDictionary.putIfAbsent(parentPath, parentPath)
|
||||
} else {
|
||||
storageFilePathDictionary[parentPath] = parent
|
||||
notExistingPathDictionary.remove(parentPath)
|
||||
}
|
||||
|
||||
return parent
|
||||
}
|
||||
|
||||
private fun findStorageFileForParentDirectory(path: String): StorageFile? {
|
||||
val segments = getUriSegments(path)
|
||||
?: throw IOException("Can't get path because the root has changed")
|
||||
|
||||
var file = StorageFile(null, mediaRoot.value.abstractFile, fileManager.value)
|
||||
segments.forEach { segment ->
|
||||
file = StorageFile(
|
||||
file,
|
||||
fileManager.value.findFile(file.abstractFile, segment)
|
||||
?: return null,
|
||||
file.fileManager
|
||||
)
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
private fun getUriSegments(uri: String): List<String>? {
|
||||
val rootPath = getMediaRoot().getPath()
|
||||
val rootPath = mediaRoot.value.path
|
||||
if (!uri.startsWith(rootPath)) return null
|
||||
val pathWithoutRoot = uri.substringAfter(rootPath)
|
||||
return pathWithoutRoot.split('/').filter { it.isNotEmpty() }
|
||||
|
@ -756,7 +756,7 @@ object Util {
|
||||
fun scanMedia(file: String?) {
|
||||
// TODO this doesn't work for URIs
|
||||
MediaScannerConnection.scanFile(
|
||||
UApp.applicationContext(), arrayOf(file),
|
||||
applicationContext(), arrayOf(file),
|
||||
null, null)
|
||||
}
|
||||
|
||||
|
@ -172,6 +172,7 @@
|
||||
<string name="settings.buffer_length_5">5 seconds</string>
|
||||
<string name="settings.buffer_length_60">1 minute</string>
|
||||
<string name="settings.buffer_length_8">8 seconds</string>
|
||||
<string name="settings.custom_cache_location">Use Custom Cache Location</string>
|
||||
<string name="settings.cache_location">Cache Location</string>
|
||||
<string name="settings.cache_location_error">Invalid cache location. Using default.</string>
|
||||
<string name="settings.cache_size">Cache Size</string>
|
||||
|
@ -261,6 +261,10 @@
|
||||
a:key="cacheSize"
|
||||
a:title="@string/settings.cache_size"
|
||||
app:iconSpaceReserved="false"/>
|
||||
<CheckBoxPreference
|
||||
a:key="customCacheLocation"
|
||||
a:title="@string/settings.custom_cache_location"
|
||||
app:iconSpaceReserved="false"/>
|
||||
<Preference
|
||||
a:key="cacheLocation"
|
||||
a:title="@string/settings.cache_location"
|
||||
|
Loading…
x
Reference in New Issue
Block a user