mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-04-27 22:22:17 +03:00
Moved from DocumentFile to DocumentsContract
Added separate handling for the old java File paths
This commit is contained in:
parent
34c5ced32e
commit
fa4214a0ac
@ -153,7 +153,7 @@ public class StreamProxy implements Runnable
|
|||||||
}
|
}
|
||||||
|
|
||||||
Timber.i("Processing request for file %s", localPath);
|
Timber.i("Processing request for file %s", localPath);
|
||||||
if (!StorageFile.Companion.isPathExists(localPath)) {
|
if (!Storage.INSTANCE.isPathExists(localPath)) {
|
||||||
Timber.e("File %s does not exist", localPath);
|
Timber.e("File %s does not exist", localPath);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -194,7 +194,7 @@ public class StreamProxy implements Runnable
|
|||||||
String file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteOrSaveFile() : downloadFile.getPartialFile();
|
String file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteOrSaveFile() : downloadFile.getPartialFile();
|
||||||
int cbSentThisBatch = 0;
|
int cbSentThisBatch = 0;
|
||||||
|
|
||||||
StorageFile storageFile = StorageFile.Companion.getFromPath(file);
|
AbstractFile storageFile = Storage.INSTANCE.getFromPath(file);
|
||||||
if (storageFile != null)
|
if (storageFile != null)
|
||||||
{
|
{
|
||||||
InputStream input = storageFile.getFileInputStream();
|
InputStream input = storageFile.getFileInputStream();
|
||||||
|
@ -49,7 +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.Storage
|
||||||
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
|
||||||
@ -214,7 +214,7 @@ class NavigationActivity : AppCompatActivity() {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
StorageFile.resetCaches()
|
Storage.reset()
|
||||||
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
|
||||||
|
@ -46,7 +46,7 @@ import org.moire.ultrasonic.util.Settings
|
|||||||
import org.moire.ultrasonic.util.Settings.preferences
|
import org.moire.ultrasonic.util.Settings.preferences
|
||||||
import org.moire.ultrasonic.util.Settings.shareGreeting
|
import org.moire.ultrasonic.util.Settings.shareGreeting
|
||||||
import org.moire.ultrasonic.util.Settings.shouldUseId3Tags
|
import org.moire.ultrasonic.util.Settings.shouldUseId3Tags
|
||||||
import org.moire.ultrasonic.util.StorageFile
|
import org.moire.ultrasonic.util.Storage
|
||||||
import org.moire.ultrasonic.util.TimeSpanPreference
|
import org.moire.ultrasonic.util.TimeSpanPreference
|
||||||
import org.moire.ultrasonic.util.TimeSpanPreferenceDialogFragmentCompat
|
import org.moire.ultrasonic.util.TimeSpanPreferenceDialogFragmentCompat
|
||||||
import org.moire.ultrasonic.util.Util.toast
|
import org.moire.ultrasonic.util.Util.toast
|
||||||
@ -456,7 +456,7 @@ class SettingsFragment :
|
|||||||
|
|
||||||
// Clear download queue.
|
// Clear download queue.
|
||||||
mediaPlayerControllerLazy.value.clear()
|
mediaPlayerControllerLazy.value.clear()
|
||||||
StorageFile.resetCaches()
|
Storage.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setDebugLogToFile(writeLog: Boolean) {
|
private fun setDebugLogToFile(writeLog: Boolean) {
|
||||||
|
@ -5,7 +5,6 @@ import android.graphics.BitmapFactory
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
import org.moire.ultrasonic.util.FileUtil
|
import org.moire.ultrasonic.util.FileUtil
|
||||||
import org.moire.ultrasonic.util.StorageFile
|
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -20,8 +20,6 @@ import org.moire.ultrasonic.api.subsonic.throwOnFailure
|
|||||||
import org.moire.ultrasonic.api.subsonic.toStreamResponse
|
import org.moire.ultrasonic.api.subsonic.toStreamResponse
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
import org.moire.ultrasonic.util.FileUtil
|
import org.moire.ultrasonic.util.FileUtil
|
||||||
import org.moire.ultrasonic.util.StorageFile
|
|
||||||
import org.moire.ultrasonic.util.Util
|
|
||||||
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.File
|
||||||
|
@ -22,7 +22,7 @@ import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
|||||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||||
import org.moire.ultrasonic.util.CacheCleaner
|
import org.moire.ultrasonic.util.CacheCleaner
|
||||||
import org.moire.ultrasonic.util.CancellableTask
|
import org.moire.ultrasonic.util.CancellableTask
|
||||||
import org.moire.ultrasonic.util.StorageFile
|
import org.moire.ultrasonic.util.Storage
|
||||||
import org.moire.ultrasonic.util.FileUtil
|
import org.moire.ultrasonic.util.FileUtil
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
@ -77,10 +77,10 @@ class DownloadFile(
|
|||||||
completeFile = FileUtil.getParentPath(saveFile) + "/" + FileUtil.getCompleteFile(FileUtil.getNameFromPath(saveFile))
|
completeFile = FileUtil.getParentPath(saveFile) + "/" + FileUtil.getCompleteFile(FileUtil.getNameFromPath(saveFile))
|
||||||
|
|
||||||
when {
|
when {
|
||||||
StorageFile.isPathExists(saveFile) -> {
|
Storage.isPathExists(saveFile) -> {
|
||||||
state = DownloadStatus.PINNED
|
state = DownloadStatus.PINNED
|
||||||
}
|
}
|
||||||
StorageFile.isPathExists(completeFile) -> {
|
Storage.isPathExists(completeFile) -> {
|
||||||
state = DownloadStatus.DONE
|
state = DownloadStatus.DONE
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@ -119,7 +119,7 @@ class DownloadFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val completeOrSaveFile: String
|
val completeOrSaveFile: String
|
||||||
get() = if (StorageFile.isPathExists(saveFile)) {
|
get() = if (Storage.isPathExists(saveFile)) {
|
||||||
saveFile
|
saveFile
|
||||||
} else {
|
} else {
|
||||||
completeFile
|
completeFile
|
||||||
@ -133,16 +133,16 @@ class DownloadFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val isSaved: Boolean
|
val isSaved: Boolean
|
||||||
get() = StorageFile.isPathExists(saveFile)
|
get() = Storage.isPathExists(saveFile)
|
||||||
|
|
||||||
@get:Synchronized
|
@get:Synchronized
|
||||||
val isCompleteFileAvailable: Boolean
|
val isCompleteFileAvailable: Boolean
|
||||||
get() = StorageFile.isPathExists(completeFile) || StorageFile.isPathExists(saveFile)
|
get() = Storage.isPathExists(completeFile) || Storage.isPathExists(saveFile)
|
||||||
|
|
||||||
@get:Synchronized
|
@get:Synchronized
|
||||||
val isWorkDone: Boolean
|
val isWorkDone: Boolean
|
||||||
get() = StorageFile.isPathExists(completeFile) && !shouldSave ||
|
get() = Storage.isPathExists(completeFile) && !shouldSave ||
|
||||||
StorageFile.isPathExists(saveFile) || saveWhenDone || completeWhenDone
|
Storage.isPathExists(saveFile) || saveWhenDone || completeWhenDone
|
||||||
|
|
||||||
@get:Synchronized
|
@get:Synchronized
|
||||||
val isDownloading: Boolean
|
val isDownloading: Boolean
|
||||||
@ -168,18 +168,18 @@ class DownloadFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun unpin() {
|
fun unpin() {
|
||||||
val file = StorageFile.getFromPath(saveFile) ?: return
|
val file = Storage.getFromPath(saveFile) ?: return
|
||||||
StorageFile.rename(file, completeFile)
|
Storage.rename(file, completeFile)
|
||||||
status.postValue(DownloadStatus.DONE)
|
status.postValue(DownloadStatus.DONE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cleanup(): Boolean {
|
fun cleanup(): Boolean {
|
||||||
var ok = true
|
var ok = true
|
||||||
if (StorageFile.isPathExists(completeFile) || StorageFile.isPathExists(saveFile)) {
|
if (Storage.isPathExists(completeFile) || Storage.isPathExists(saveFile)) {
|
||||||
ok = FileUtil.delete(partialFile)
|
ok = FileUtil.delete(partialFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StorageFile.isPathExists(saveFile)) {
|
if (Storage.isPathExists(saveFile)) {
|
||||||
ok = ok and FileUtil.delete(completeFile)
|
ok = ok and FileUtil.delete(completeFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,13 +224,13 @@ class DownloadFile(
|
|||||||
var inputStream: InputStream? = null
|
var inputStream: InputStream? = null
|
||||||
var outputStream: OutputStream? = null
|
var outputStream: OutputStream? = null
|
||||||
try {
|
try {
|
||||||
if (StorageFile.isPathExists(saveFile)) {
|
if (Storage.isPathExists(saveFile)) {
|
||||||
Timber.i("%s already exists. Skipping.", saveFile)
|
Timber.i("%s already exists. Skipping.", saveFile)
|
||||||
status.postValue(DownloadStatus.PINNED)
|
status.postValue(DownloadStatus.PINNED)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StorageFile.isPathExists(completeFile)) {
|
if (Storage.isPathExists(completeFile)) {
|
||||||
var newStatus: DownloadStatus = DownloadStatus.DONE
|
var newStatus: DownloadStatus = DownloadStatus.DONE
|
||||||
if (shouldSave) {
|
if (shouldSave) {
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
@ -251,7 +251,7 @@ class DownloadFile(
|
|||||||
// Some devices seem to throw error on partial file which doesn't exist
|
// Some devices seem to throw error on partial file which doesn't exist
|
||||||
val needsDownloading: Boolean
|
val needsDownloading: Boolean
|
||||||
val duration = song.duration
|
val duration = song.duration
|
||||||
val fileLength = StorageFile.getFromPath(partialFile)?.length ?: 0
|
val fileLength = Storage.getFromPath(partialFile)?.length ?: 0
|
||||||
|
|
||||||
needsDownloading = (
|
needsDownloading = (
|
||||||
desiredBitRate == 0 || duration == null ||
|
desiredBitRate == 0 || duration == null ||
|
||||||
@ -270,7 +270,7 @@ class DownloadFile(
|
|||||||
Timber.i("Executed partial HTTP GET, skipping %d bytes", fileLength)
|
Timber.i("Executed partial HTTP GET, skipping %d bytes", fileLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
outputStream = StorageFile.getOrCreateFileFromPath(partialFile).getFileOutputStream(isPartial)
|
outputStream = Storage.getOrCreateFileFromPath(partialFile).getFileOutputStream(isPartial)
|
||||||
|
|
||||||
val len = inputStream.copyTo(outputStream) { totalBytesCopied ->
|
val len = inputStream.copyTo(outputStream) { totalBytesCopied ->
|
||||||
setProgress(totalBytesCopied)
|
setProgress(totalBytesCopied)
|
||||||
|
@ -19,7 +19,7 @@ import android.os.PowerManager
|
|||||||
import android.os.PowerManager.PARTIAL_WAKE_LOCK
|
import android.os.PowerManager.PARTIAL_WAKE_LOCK
|
||||||
import android.os.PowerManager.WakeLock
|
import android.os.PowerManager.WakeLock
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import org.moire.ultrasonic.util.StorageFile
|
import org.moire.ultrasonic.util.Storage
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
@ -347,7 +347,7 @@ class LocalMediaPlayer : KoinComponent {
|
|||||||
try {
|
try {
|
||||||
downloadFile.setPlaying(false)
|
downloadFile.setPlaying(false)
|
||||||
|
|
||||||
val file = StorageFile.getFromPath(downloadFile.completeOrPartialFile)
|
val file = Storage.getFromPath(downloadFile.completeOrPartialFile)
|
||||||
val partial = !downloadFile.isCompleteFileAvailable
|
val partial = !downloadFile.isCompleteFileAvailable
|
||||||
|
|
||||||
// TODO this won't work with SAF, we should use something else, e.g. a recent list
|
// TODO this won't work with SAF, we should use something else, e.g. a recent list
|
||||||
@ -447,7 +447,7 @@ class LocalMediaPlayer : KoinComponent {
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
private fun setupNext(downloadFile: DownloadFile) {
|
private fun setupNext(downloadFile: DownloadFile) {
|
||||||
try {
|
try {
|
||||||
val file = StorageFile.getFromPath(downloadFile.completeOrPartialFile)
|
val file = Storage.getFromPath(downloadFile.completeOrPartialFile)
|
||||||
|
|
||||||
// Release the media player if it is not our active player
|
// Release the media player if it is not our active player
|
||||||
if (nextMediaPlayer != null && nextMediaPlayer != mediaPlayer) {
|
if (nextMediaPlayer != null && nextMediaPlayer != mediaPlayer) {
|
||||||
@ -615,7 +615,7 @@ class LocalMediaPlayer : KoinComponent {
|
|||||||
|
|
||||||
private fun bufferComplete(): Boolean {
|
private fun bufferComplete(): Boolean {
|
||||||
val completeFileAvailable = downloadFile.isWorkDone
|
val completeFileAvailable = downloadFile.isWorkDone
|
||||||
val size = StorageFile.getFromPath(partialFile)?.length ?: 0
|
val size = Storage.getFromPath(partialFile)?.length ?: 0
|
||||||
|
|
||||||
Timber.i(
|
Timber.i(
|
||||||
"Buffering %s (%d/%d, %s)",
|
"Buffering %s (%d/%d, %s)",
|
||||||
@ -673,7 +673,7 @@ class LocalMediaPlayer : KoinComponent {
|
|||||||
val state = (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED)
|
val state = (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED)
|
||||||
|
|
||||||
val length = if (partialFile == null) 0
|
val length = if (partialFile == null) 0
|
||||||
else StorageFile.getFromPath(partialFile)?.length ?: 0
|
else Storage.getFromPath(partialFile)?.length ?: 0
|
||||||
|
|
||||||
Timber.i("Buffering next %s (%d)", partialFile, length)
|
Timber.i("Buffering next %s (%d)", partialFile, length)
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ package org.moire.ultrasonic.service
|
|||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.BufferedWriter
|
import java.io.BufferedWriter
|
||||||
import org.moire.ultrasonic.util.StorageFile
|
import org.moire.ultrasonic.util.Storage
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.Reader
|
import java.io.Reader
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
@ -37,6 +37,7 @@ import org.moire.ultrasonic.domain.SearchCriteria
|
|||||||
import org.moire.ultrasonic.domain.SearchResult
|
import org.moire.ultrasonic.domain.SearchResult
|
||||||
import org.moire.ultrasonic.domain.Share
|
import org.moire.ultrasonic.domain.Share
|
||||||
import org.moire.ultrasonic.domain.UserInfo
|
import org.moire.ultrasonic.domain.UserInfo
|
||||||
|
import org.moire.ultrasonic.util.AbstractFile
|
||||||
import org.moire.ultrasonic.util.Constants
|
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
|
||||||
@ -102,7 +103,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
name: String?,
|
name: String?,
|
||||||
refresh: Boolean
|
refresh: Boolean
|
||||||
): MusicDirectory {
|
): MusicDirectory {
|
||||||
val dir = StorageFile.getFromPath(id)
|
val dir = Storage.getFromPath(id)
|
||||||
val result = MusicDirectory()
|
val result = MusicDirectory()
|
||||||
result.name = dir?.name ?: return result
|
result.name = dir?.name ?: return result
|
||||||
|
|
||||||
@ -211,7 +212,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
var line = buffer.readLine()
|
var line = buffer.readLine()
|
||||||
if ("#EXTM3U" != line) return playlist
|
if ("#EXTM3U" != line) return playlist
|
||||||
while (buffer.readLine().also { line = it } != null) {
|
while (buffer.readLine().also { line = it } != null) {
|
||||||
val entryFile = StorageFile.getFromPath(line) ?: continue
|
val entryFile = Storage.getFromPath(line) ?: continue
|
||||||
val entryName = getName(entryFile.name, entryFile.isDirectory)
|
val entryName = getName(entryFile.name, entryFile.isDirectory)
|
||||||
if (entryName != null) {
|
if (entryName != null) {
|
||||||
playlist.add(createEntry(entryFile, entryName))
|
playlist.add(createEntry(entryFile, entryName))
|
||||||
@ -235,7 +236,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
fw.write("#EXTM3U\n")
|
fw.write("#EXTM3U\n")
|
||||||
for (e in entries) {
|
for (e in entries) {
|
||||||
var filePath = FileUtil.getSongFile(e)
|
var filePath = FileUtil.getSongFile(e)
|
||||||
if (!StorageFile.isPathExists(filePath)) {
|
if (!Storage.isPathExists(filePath)) {
|
||||||
val ext = FileUtil.getExtension(filePath)
|
val ext = FileUtil.getExtension(filePath)
|
||||||
val base = FileUtil.getBaseName(filePath)
|
val base = FileUtil.getBaseName(filePath)
|
||||||
filePath = "$base.complete.$ext"
|
filePath = "$base.complete.$ext"
|
||||||
@ -257,7 +258,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
|
|
||||||
override fun getRandomSongs(size: Int): MusicDirectory {
|
override fun getRandomSongs(size: Int): MusicDirectory {
|
||||||
val root = FileUtil.musicDirectory
|
val root = FileUtil.musicDirectory
|
||||||
val children: MutableList<StorageFile> = LinkedList()
|
val children: MutableList<AbstractFile> = LinkedList()
|
||||||
listFilesRecursively(root, children)
|
listFilesRecursively(root, children)
|
||||||
val result = MusicDirectory()
|
val result = MusicDirectory()
|
||||||
if (children.isEmpty()) {
|
if (children.isEmpty()) {
|
||||||
@ -502,13 +503,13 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun createEntry(file: StorageFile, name: String?): MusicDirectory.Entry {
|
private fun createEntry(file: AbstractFile, name: String?): MusicDirectory.Entry {
|
||||||
val entry = MusicDirectory.Entry(file.path)
|
val entry = MusicDirectory.Entry(file.path)
|
||||||
entry.populateWithDataFrom(file, name)
|
entry.populateWithDataFrom(file, name)
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createAlbum(file: StorageFile, name: String?): MusicDirectory.Album {
|
private fun createAlbum(file: AbstractFile, name: String?): MusicDirectory.Album {
|
||||||
val album = MusicDirectory.Album(file.path)
|
val album = MusicDirectory.Album(file.path)
|
||||||
album.populateWithDataFrom(file, name)
|
album.populateWithDataFrom(file, name)
|
||||||
return album
|
return album
|
||||||
@ -517,7 +518,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
/*
|
/*
|
||||||
* Extracts some basic data from a File object and applies it to an Album or Entry
|
* Extracts some basic data from a File object and applies it to an Album or Entry
|
||||||
*/
|
*/
|
||||||
private fun MusicDirectory.Child.populateWithDataFrom(file: StorageFile, name: String?) {
|
private fun MusicDirectory.Child.populateWithDataFrom(file: AbstractFile, name: String?) {
|
||||||
isDirectory = file.isDirectory
|
isDirectory = file.isDirectory
|
||||||
parent = file.parent!!.path
|
parent = file.parent!!.path
|
||||||
val root = FileUtil.musicDirectory.path
|
val root = FileUtil.musicDirectory.path
|
||||||
@ -536,7 +537,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
* More extensive variant of Child.populateWithDataFrom(), which also parses the ID3 tags of
|
* More extensive variant of Child.populateWithDataFrom(), which also parses the ID3 tags of
|
||||||
* a given track file.
|
* a given track file.
|
||||||
*/
|
*/
|
||||||
private fun MusicDirectory.Entry.populateWithDataFrom(file: StorageFile, name: String?) {
|
private fun MusicDirectory.Entry.populateWithDataFrom(file: AbstractFile, name: String?) {
|
||||||
(this as MusicDirectory.Child).populateWithDataFrom(file, name)
|
(this as MusicDirectory.Child).populateWithDataFrom(file, name)
|
||||||
|
|
||||||
val meta = RawMetadata(null)
|
val meta = RawMetadata(null)
|
||||||
@ -607,7 +608,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
@Suppress("NestedBlockDepth")
|
@Suppress("NestedBlockDepth")
|
||||||
private fun recursiveAlbumSearch(
|
private fun recursiveAlbumSearch(
|
||||||
artistName: String,
|
artistName: String,
|
||||||
file: StorageFile,
|
file: AbstractFile,
|
||||||
criteria: SearchCriteria,
|
criteria: SearchCriteria,
|
||||||
albums: MutableList<MusicDirectory.Album>,
|
albums: MutableList<MusicDirectory.Album>,
|
||||||
songs: MutableList<MusicDirectory.Entry>
|
songs: MutableList<MusicDirectory.Entry>
|
||||||
@ -664,7 +665,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
return closeness
|
return closeness
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun listFilesRecursively(parent: StorageFile, children: MutableList<StorageFile>) {
|
private fun listFilesRecursively(parent: AbstractFile, children: MutableList<AbstractFile>) {
|
||||||
for (file in FileUtil.listMediaFiles(parent)) {
|
for (file in FileUtil.listMediaFiles(parent)) {
|
||||||
if (file.isFile) {
|
if (file.isFile) {
|
||||||
children.add(file)
|
children.add(file)
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* AbstractFile.kt
|
||||||
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.moire.ultrasonic.util
|
||||||
|
|
||||||
|
import android.content.res.AssetFileDescriptor
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
abstract class AbstractFile: Comparable<AbstractFile> {
|
||||||
|
abstract val name: String
|
||||||
|
abstract val isDirectory: Boolean
|
||||||
|
abstract val isFile: Boolean
|
||||||
|
abstract val length: Long
|
||||||
|
abstract val lastModified: Long
|
||||||
|
abstract val path: String
|
||||||
|
abstract val parent: AbstractFile?
|
||||||
|
|
||||||
|
override fun compareTo(other: AbstractFile): Int {
|
||||||
|
return path.compareTo(other.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun delete(): Boolean
|
||||||
|
|
||||||
|
abstract fun listFiles(): Array<AbstractFile>
|
||||||
|
|
||||||
|
abstract fun getFileOutputStream(append: Boolean): OutputStream
|
||||||
|
|
||||||
|
abstract fun getFileInputStream(): InputStream
|
||||||
|
|
||||||
|
abstract fun getDocumentFileDescriptor(openMode: String): AssetFileDescriptor?
|
||||||
|
|
||||||
|
abstract fun getOrCreateFileFromPath(path: String): AbstractFile
|
||||||
|
|
||||||
|
abstract fun isPathExists(path: String): Boolean
|
||||||
|
|
||||||
|
abstract fun getFromPath(path: String): AbstractFile?
|
||||||
|
|
||||||
|
abstract fun createDirsOnPath(path: String)
|
||||||
|
|
||||||
|
fun rename(pathFrom: String, pathTo: String) {
|
||||||
|
val fileFrom = getFromPath(pathFrom) ?: throw IOException("File to rename doesn't exist")
|
||||||
|
rename(fileFrom, pathTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun rename(pathFrom: AbstractFile, pathTo: String)
|
||||||
|
}
|
@ -69,8 +69,8 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
|||||||
|
|
||||||
private fun backgroundCleanup() {
|
private fun backgroundCleanup() {
|
||||||
try {
|
try {
|
||||||
val files: MutableList<StorageFile> = ArrayList()
|
val files: MutableList<AbstractFile> = ArrayList()
|
||||||
val dirs: MutableList<StorageFile> = ArrayList()
|
val dirs: MutableList<AbstractFile> = ArrayList()
|
||||||
|
|
||||||
findCandidatesForDeletion(musicDirectory, files, dirs)
|
findCandidatesForDeletion(musicDirectory, files, dirs)
|
||||||
sortByAscendingModificationTime(files)
|
sortByAscendingModificationTime(files)
|
||||||
@ -87,8 +87,8 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
|||||||
|
|
||||||
private fun backgroundSpaceCleanup() {
|
private fun backgroundSpaceCleanup() {
|
||||||
try {
|
try {
|
||||||
val files: MutableList<StorageFile> = ArrayList()
|
val files: MutableList<AbstractFile> = ArrayList()
|
||||||
val dirs: MutableList<StorageFile> = ArrayList()
|
val dirs: MutableList<AbstractFile> = ArrayList()
|
||||||
|
|
||||||
findCandidatesForDeletion(musicDirectory, files, dirs)
|
findCandidatesForDeletion(musicDirectory, files, dirs)
|
||||||
|
|
||||||
@ -136,28 +136,26 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
|||||||
private var playlistCleaning = false
|
private var playlistCleaning = false
|
||||||
|
|
||||||
private const val MIN_FREE_SPACE = 500 * 1024L * 1024L
|
private const val MIN_FREE_SPACE = 500 * 1024L * 1024L
|
||||||
private fun deleteEmptyDirs(dirs: Iterable<StorageFile>, doNotDelete: Collection<String>) {
|
private fun deleteEmptyDirs(dirs: Iterable<AbstractFile>, doNotDelete: Collection<String>) {
|
||||||
for (dir in dirs) {
|
for (dir in dirs) {
|
||||||
if (doNotDelete.contains(dir.path)) continue
|
if (doNotDelete.contains(dir.path)) continue
|
||||||
|
|
||||||
var children = dir.listFiles()
|
var children = dir.listFiles()
|
||||||
if (children != null) {
|
// No songs left in the folder
|
||||||
// No songs left in the folder
|
if (children.size == 1 && children[0].path == getAlbumArtFile(dir.path)) {
|
||||||
if (children.size == 1 && children[0].path == getAlbumArtFile(dir.path)) {
|
// Delete Artwork files
|
||||||
// Delete Artwork files
|
delete(getAlbumArtFile(dir.path))
|
||||||
delete(getAlbumArtFile(dir.path))
|
children = dir.listFiles()
|
||||||
children = dir.listFiles()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Delete empty directory
|
// Delete empty directory
|
||||||
if (children != null && children.isEmpty()) {
|
if (children.isEmpty()) {
|
||||||
delete(dir.path)
|
delete(dir.path)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMinimumDelete(files: List<StorageFile>): Long {
|
private fun getMinimumDelete(files: List<AbstractFile>): Long {
|
||||||
if (files.isEmpty()) return 0L
|
if (files.isEmpty()) return 0L
|
||||||
|
|
||||||
val cacheSizeBytes = cacheSizeMB * 1024L * 1024L
|
val cacheSizeBytes = cacheSizeMB * 1024L * 1024L
|
||||||
@ -197,17 +195,17 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
|||||||
return bytesToDelete
|
return bytesToDelete
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isPartial(file: StorageFile): Boolean {
|
private fun isPartial(file: AbstractFile): Boolean {
|
||||||
return file.name.endsWith(".partial") || file.name.contains(".partial.")
|
return file.name.endsWith(".partial") || file.name.contains(".partial.")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isComplete(file: StorageFile): Boolean {
|
private fun isComplete(file: AbstractFile): Boolean {
|
||||||
return file.name.endsWith(".complete") || file.name.contains(".complete.")
|
return file.name.endsWith(".complete") || file.name.contains(".complete.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("NestedBlockDepth")
|
@Suppress("NestedBlockDepth")
|
||||||
private fun deleteFiles(
|
private fun deleteFiles(
|
||||||
files: Collection<StorageFile>,
|
files: Collection<AbstractFile>,
|
||||||
doNotDelete: Collection<String>,
|
doNotDelete: Collection<String>,
|
||||||
bytesToDelete: Long,
|
bytesToDelete: Long,
|
||||||
deletePartials: Boolean
|
deletePartials: Boolean
|
||||||
@ -232,9 +230,9 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun findCandidatesForDeletion(
|
private fun findCandidatesForDeletion(
|
||||||
file: StorageFile,
|
file: AbstractFile,
|
||||||
files: MutableList<StorageFile>,
|
files: MutableList<AbstractFile>,
|
||||||
dirs: MutableList<StorageFile>
|
dirs: MutableList<AbstractFile>
|
||||||
) {
|
) {
|
||||||
if (file.isFile && (isPartial(file) || isComplete(file))) {
|
if (file.isFile && (isPartial(file) || isComplete(file))) {
|
||||||
files.add(file)
|
files.add(file)
|
||||||
@ -247,8 +245,8 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sortByAscendingModificationTime(files: MutableList<StorageFile>) {
|
private fun sortByAscendingModificationTime(files: MutableList<AbstractFile>) {
|
||||||
files.sortWith { a: StorageFile, b: StorageFile ->
|
files.sortWith { a: AbstractFile, b: AbstractFile ->
|
||||||
a.lastModified.compareTo(b.lastModified)
|
a.lastModified.compareTo(b.lastModified)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -209,7 +209,7 @@ object FileUtil {
|
|||||||
|
|
||||||
fun createDirectoryForParent(path: String) {
|
fun createDirectoryForParent(path: String) {
|
||||||
val dir = getParentPath(path) ?: return
|
val dir = getParentPath(path) ?: return
|
||||||
StorageFile.createDirsOnPath(dir)
|
Storage.createDirsOnPath(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("SameParameterValue")
|
@Suppress("SameParameterValue")
|
||||||
@ -239,8 +239,8 @@ object FileUtil {
|
|||||||
get() = getOrCreateDirectory("music")
|
get() = getOrCreateDirectory("music")
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
val musicDirectory: StorageFile
|
val musicDirectory: AbstractFile
|
||||||
get() = StorageFile.mediaRoot.value
|
get() = Storage.mediaRoot.value
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Suppress("ReturnCount")
|
@Suppress("ReturnCount")
|
||||||
@ -316,7 +316,7 @@ object FileUtil {
|
|||||||
* Never returns `null`, instead a warning is logged, and an empty set is returned.
|
* Never returns `null`, instead a warning is logged, and an empty set is returned.
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun listFiles(dir: StorageFile): SortedSet<StorageFile> {
|
fun listFiles(dir: AbstractFile): SortedSet<AbstractFile> {
|
||||||
val files = dir.listFiles()
|
val files = dir.listFiles()
|
||||||
if (files == null) {
|
if (files == null) {
|
||||||
Timber.w("Failed to list children for %s", dir.path)
|
Timber.w("Failed to list children for %s", dir.path)
|
||||||
@ -335,7 +335,7 @@ object FileUtil {
|
|||||||
return TreeSet(files.asList())
|
return TreeSet(files.asList())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun listMediaFiles(dir: StorageFile): SortedSet<StorageFile> {
|
fun listMediaFiles(dir: AbstractFile): SortedSet<AbstractFile> {
|
||||||
val files = listFiles(dir)
|
val files = listFiles(dir)
|
||||||
val iterator = files.iterator()
|
val iterator = files.iterator()
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
@ -347,7 +347,7 @@ object FileUtil {
|
|||||||
return files
|
return files
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isMediaFile(file: StorageFile): Boolean {
|
private fun isMediaFile(file: AbstractFile): Boolean {
|
||||||
val extension = getExtension(file.name)
|
val extension = getExtension(file.name)
|
||||||
return MUSIC_FILE_EXTENSIONS.contains(extension) ||
|
return MUSIC_FILE_EXTENSIONS.contains(extension) ||
|
||||||
VIDEO_FILE_EXTENSIONS.contains(extension)
|
VIDEO_FILE_EXTENSIONS.contains(extension)
|
||||||
@ -463,7 +463,7 @@ object FileUtil {
|
|||||||
for (e in playlist.getTracks()) {
|
for (e in playlist.getTracks()) {
|
||||||
var filePath = getSongFile(e)
|
var filePath = getSongFile(e)
|
||||||
|
|
||||||
if (!StorageFile.isPathExists(filePath)) {
|
if (!Storage.isPathExists(filePath)) {
|
||||||
val ext = getExtension(filePath)
|
val ext = getExtension(filePath)
|
||||||
val base = getBaseName(filePath)
|
val base = getBaseName(filePath)
|
||||||
filePath = "$base.complete.$ext"
|
filePath = "$base.complete.$ext"
|
||||||
@ -482,7 +482,7 @@ object FileUtil {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun renameFile(from: String, to: String) {
|
fun renameFile(from: String, to: String) {
|
||||||
StorageFile.rename(from, to)
|
Storage.rename(from, to)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@ -500,7 +500,7 @@ object FileUtil {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun delete(file: String?): Boolean {
|
fun delete(file: String?): Boolean {
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
val storageFile = StorageFile.getFromPath(file)
|
val storageFile = Storage.getFromPath(file)
|
||||||
if (storageFile != null && !storageFile.delete()) {
|
if (storageFile != null && !storageFile.delete()) {
|
||||||
Timber.w("Failed to delete file %s", file)
|
Timber.w("Failed to delete file %s", file)
|
||||||
return false
|
return false
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* JavaFile.kt
|
||||||
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.moire.ultrasonic.util
|
||||||
|
|
||||||
|
import android.content.res.AssetFileDescriptor
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import org.moire.ultrasonic.app.UApp
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
class JavaFile(override val parent: AbstractFile?, val file: File): AbstractFile() {
|
||||||
|
override val name: String = file.name
|
||||||
|
override val isDirectory: Boolean = file.isDirectory
|
||||||
|
override val isFile: Boolean = file.isFile
|
||||||
|
override val length: Long
|
||||||
|
get() = file.length()
|
||||||
|
override val lastModified: Long
|
||||||
|
get() = file.lastModified()
|
||||||
|
override val path: String
|
||||||
|
get() = file.absolutePath
|
||||||
|
|
||||||
|
override fun delete(): Boolean {
|
||||||
|
return file.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun listFiles(): Array<AbstractFile> {
|
||||||
|
val fileList = file.listFiles()
|
||||||
|
return fileList?.map { file -> JavaFile(this, file) }?.toTypedArray() ?: emptyArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFileOutputStream(append: Boolean): OutputStream {
|
||||||
|
return FileOutputStream(file, append)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFileInputStream(): InputStream {
|
||||||
|
return FileInputStream(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDocumentFileDescriptor(openMode: String): AssetFileDescriptor? {
|
||||||
|
val documentFile = DocumentFile.fromFile(file)
|
||||||
|
return UApp.applicationContext().contentResolver.openAssetFileDescriptor(
|
||||||
|
documentFile.uri,
|
||||||
|
openMode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOrCreateFileFromPath(path: String): AbstractFile {
|
||||||
|
File(path).createNewFile()
|
||||||
|
return JavaFile(null, File(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isPathExists(path: String): Boolean {
|
||||||
|
return File(path).exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFromPath(path: String): AbstractFile {
|
||||||
|
return JavaFile(null, File(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createDirsOnPath(path: String) {
|
||||||
|
File(path).mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun rename(pathFrom: AbstractFile, pathTo: String) {
|
||||||
|
val javaFile = pathFrom as JavaFile
|
||||||
|
javaFile.file.copyTo(File(pathTo))
|
||||||
|
javaFile.file.delete()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* Storage.kt
|
||||||
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.moire.ultrasonic.util
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import org.moire.ultrasonic.R
|
||||||
|
import java.io.File
|
||||||
|
import org.moire.ultrasonic.app.UApp
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides filesystem access abstraction which works
|
||||||
|
* both on File based paths and Storage Access Framework Uris
|
||||||
|
*/
|
||||||
|
object Storage {
|
||||||
|
|
||||||
|
val mediaRoot: ResettableLazy<AbstractFile> = ResettableLazy {
|
||||||
|
getRoot()!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRoot(): AbstractFile? {
|
||||||
|
return if (Settings.cacheLocation.isUri()) {
|
||||||
|
val documentFile = DocumentFile.fromTreeUri(
|
||||||
|
UApp.applicationContext(),
|
||||||
|
Uri.parse(Settings.cacheLocation)
|
||||||
|
) ?: return null
|
||||||
|
if (!documentFile.exists()) return null
|
||||||
|
StorageFile(null, documentFile.uri, documentFile.name!!, documentFile.isDirectory)
|
||||||
|
} else {
|
||||||
|
val file = File(Settings.cacheLocation)
|
||||||
|
if (!file.exists()) return null
|
||||||
|
JavaFile(null, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
StorageFile.storageFilePathDictionary.clear()
|
||||||
|
StorageFile.notExistingPathDictionary.clear()
|
||||||
|
mediaRoot.reset()
|
||||||
|
Timber.i("StorageFile caches were reset")
|
||||||
|
val root = getRoot()
|
||||||
|
if (root == null) {
|
||||||
|
Settings.cacheLocation = FileUtil.defaultMusicDirectory.path
|
||||||
|
Util.toast(UApp.applicationContext(), R.string.settings_cache_location_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOrCreateFileFromPath(path: String): AbstractFile {
|
||||||
|
return mediaRoot.value.getOrCreateFileFromPath(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isPathExists(path: String): Boolean {
|
||||||
|
return mediaRoot.value.isPathExists(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFromPath(path: String): AbstractFile? {
|
||||||
|
return mediaRoot.value.getFromPath(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createDirsOnPath(path: String) {
|
||||||
|
mediaRoot.value.createDirsOnPath(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rename(pathFrom: String, pathTo: String) {
|
||||||
|
mediaRoot.value.rename(pathFrom, pathTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rename(pathFrom: AbstractFile, pathTo: String) {
|
||||||
|
mediaRoot.value.rename(pathFrom, pathTo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.isUri(): Boolean {
|
||||||
|
// TODO is there a better way to tell apart a path and an URI?
|
||||||
|
return this.contains(':')
|
||||||
|
}
|
@ -1,242 +1,213 @@
|
|||||||
/*
|
|
||||||
* StorageFile.kt
|
|
||||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
|
||||||
*
|
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.moire.ultrasonic.util
|
package org.moire.ultrasonic.util
|
||||||
|
|
||||||
import android.content.res.AssetFileDescriptor
|
import android.content.res.AssetFileDescriptor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.provider.DocumentsContract
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.app.UApp
|
||||||
import java.io.File
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import org.moire.ultrasonic.app.UApp
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
/**
|
class StorageFile(
|
||||||
* Provides filesystem access abstraction which works
|
override val parent: StorageFile?,
|
||||||
* both on File based paths and Storage Access Framework Uris
|
var uri: Uri,
|
||||||
*/
|
override val name: String,
|
||||||
class StorageFile private constructor(
|
override val isDirectory: Boolean
|
||||||
private var parentStorageFile: StorageFile?,
|
): AbstractFile() {
|
||||||
private var documentFile: DocumentFile
|
private val documentFile: DocumentFile = DocumentFile.fromSingleUri(UApp.applicationContext(), uri)!!
|
||||||
): Comparable<StorageFile> {
|
|
||||||
|
|
||||||
override fun compareTo(other: StorageFile): Int {
|
override val isFile: Boolean = !isDirectory
|
||||||
return path.compareTo(other.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
override val length: Long
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
var name: String = documentFile.name!!
|
|
||||||
|
|
||||||
var isDirectory: Boolean = documentFile.isDirectory
|
|
||||||
|
|
||||||
var isFile: Boolean = documentFile.isFile
|
|
||||||
|
|
||||||
val length: Long
|
|
||||||
get() = documentFile.length()
|
get() = documentFile.length()
|
||||||
|
|
||||||
val lastModified: Long
|
override val lastModified: Long
|
||||||
get() = documentFile.lastModified()
|
get() = documentFile.lastModified()
|
||||||
|
|
||||||
fun delete(): Boolean {
|
override val path: String
|
||||||
val deleted = documentFile.delete()
|
get() {
|
||||||
|
// 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 (parent != null) return parent.path + "/" + name
|
||||||
|
return uri.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun delete(): Boolean {
|
||||||
|
val deleted = DocumentsContract.deleteDocument(
|
||||||
|
UApp.applicationContext().contentResolver,
|
||||||
|
uri
|
||||||
|
)
|
||||||
if (!deleted) return false
|
if (!deleted) return false
|
||||||
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> {
|
override fun listFiles(): Array<AbstractFile> {
|
||||||
val fileList = documentFile.listFiles()
|
return getChildren().toTypedArray()
|
||||||
return fileList.map { file -> StorageFile(this, file) }.toTypedArray()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFileOutputStream(append: Boolean): OutputStream {
|
override fun getFileOutputStream(append: Boolean): OutputStream {
|
||||||
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(
|
||||||
documentFile.uri, mode)
|
uri,
|
||||||
|
mode
|
||||||
|
)
|
||||||
return descriptor?.createOutputStream()
|
return descriptor?.createOutputStream()
|
||||||
?: throw IOException("Couldn't retrieve OutputStream")
|
?: throw IOException("Couldn't retrieve OutputStream")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFileInputStream(): InputStream {
|
override fun getFileInputStream(): InputStream {
|
||||||
return UApp.applicationContext().contentResolver.openInputStream(documentFile.uri)
|
return UApp.applicationContext().contentResolver.openInputStream(uri)
|
||||||
?: throw IOException("Couldn't retrieve InputStream")
|
?: throw IOException("Couldn't retrieve InputStream")
|
||||||
}
|
}
|
||||||
|
|
||||||
val path: String
|
override fun getDocumentFileDescriptor(openMode: String): AssetFileDescriptor? {
|
||||||
get() {
|
|
||||||
// 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 documentFile.uri.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
val parent: StorageFile?
|
|
||||||
get() {
|
|
||||||
return parentStorageFile
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDocumentFileDescriptor(openMode: String): AssetFileDescriptor? {
|
|
||||||
return UApp.applicationContext().contentResolver.openAssetFileDescriptor(
|
return UApp.applicationContext().contentResolver.openAssetFileDescriptor(
|
||||||
documentFile.uri,
|
uri,
|
||||||
openMode
|
openMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getOrCreateFileFromPath(path: String): AbstractFile {
|
||||||
|
if (storageFilePathDictionary.containsKey(path))
|
||||||
|
return storageFilePathDictionary[path]!!
|
||||||
|
|
||||||
|
val parent = getStorageFileForParentDirectory(path)
|
||||||
|
?: throw IOException("Parent directory doesn't exist")
|
||||||
|
|
||||||
|
val name = FileUtil.getNameFromPath(path)
|
||||||
|
val file = getFromParentAndName(parent, name)
|
||||||
|
|
||||||
|
storageFilePathDictionary[path] = file
|
||||||
|
notExistingPathDictionary.remove(path)
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isPathExists(path: String): Boolean {
|
||||||
|
return getFromPath(path) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFromPath(path: String): StorageFile? {
|
||||||
|
|
||||||
|
if (storageFilePathDictionary.containsKey(path))
|
||||||
|
return storageFilePathDictionary[path]!!
|
||||||
|
if (notExistingPathDictionary.contains(path)) return null
|
||||||
|
|
||||||
|
val parent = getStorageFileForParentDirectory(path)
|
||||||
|
if (parent == null) {
|
||||||
|
notExistingPathDictionary.putIfAbsent(path, path)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileName = FileUtil.getNameFromPath(path)
|
||||||
|
var file: StorageFile? = null
|
||||||
|
|
||||||
|
Timber.v("StorageFile getFromPath path: $path")
|
||||||
|
parent.listFiles().forEach {
|
||||||
|
if (it.name == fileName) file = it as StorageFile
|
||||||
|
storageFilePathDictionary[it.path] = it as StorageFile
|
||||||
|
notExistingPathDictionary.remove(it.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file == null) {
|
||||||
|
notExistingPathDictionary.putIfAbsent(path, path)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun createDirsOnPath(path: String) {
|
||||||
|
val segments = getUriSegments(path)
|
||||||
|
?: throw IOException("Can't get path because the root has changed")
|
||||||
|
|
||||||
|
var file = Storage.mediaRoot.value as StorageFile
|
||||||
|
segments.forEach { segment ->
|
||||||
|
val foundFile = file.listFiles().singleOrNull { it.name == segment }
|
||||||
|
if (foundFile != null) {
|
||||||
|
file = foundFile as StorageFile
|
||||||
|
} else {
|
||||||
|
val createdUri = DocumentsContract.createDocument(
|
||||||
|
UApp.applicationContext().contentResolver,
|
||||||
|
file.uri,
|
||||||
|
DocumentsContract.Document.MIME_TYPE_DIR,
|
||||||
|
segment
|
||||||
|
) ?: throw IOException("Can't create directory")
|
||||||
|
|
||||||
|
file = StorageFile(file, createdUri, segment, true)
|
||||||
|
}
|
||||||
|
notExistingPathDictionary.remove(file.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun rename(pathFrom: AbstractFile, pathTo: String) {
|
||||||
|
val storagePathFrom = pathFrom as StorageFile
|
||||||
|
if (!storagePathFrom.documentFile.exists()) throw IOException("File to rename doesn't exist")
|
||||||
|
Timber.d("Renaming from %s to %s", storagePathFrom.path, pathTo)
|
||||||
|
|
||||||
|
val parentTo = getFromPath(FileUtil.getParentPath(pathTo)!!) ?: throw IOException("Destination folder doesn't exist")
|
||||||
|
val fileTo = getFromParentAndName(parentTo, FileUtil.getNameFromPath(pathTo))
|
||||||
|
|
||||||
|
copyFileContents(storagePathFrom.documentFile, fileTo.documentFile)
|
||||||
|
storagePathFrom.delete()
|
||||||
|
|
||||||
|
notExistingPathDictionary.remove(pathTo)
|
||||||
|
storageFilePathDictionary.remove(storagePathFrom.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getChildren(): List<StorageFile> {
|
||||||
|
val resolver = UApp.applicationContext().contentResolver
|
||||||
|
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(
|
||||||
|
uri,
|
||||||
|
DocumentsContract.getDocumentId(uri)
|
||||||
|
)
|
||||||
|
|
||||||
|
return resolver.query(childrenUri, columns, null, null, null)?.use { cursor ->
|
||||||
|
val result = mutableListOf<StorageFile>()
|
||||||
|
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val documentId = cursor.getString(0)
|
||||||
|
val displayName = cursor.getString(1)
|
||||||
|
val mimeType = cursor.getString(2)
|
||||||
|
|
||||||
|
val documentUri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId)
|
||||||
|
|
||||||
|
val storageFile = StorageFile(
|
||||||
|
this,
|
||||||
|
documentUri,
|
||||||
|
displayName,
|
||||||
|
(mimeType == DocumentsContract.Document.MIME_TYPE_DIR)
|
||||||
|
)
|
||||||
|
|
||||||
|
result += storageFile
|
||||||
|
}
|
||||||
|
return@use result
|
||||||
|
} ?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// These caches are necessary because SAF is very slow, and the caching in FSAF is buggy.
|
// These caches are necessary because SAF is very slow.
|
||||||
// Ultrasonic assumes that the files won't change while it is in the foreground.
|
// Ultrasonic assumes that the files won't change while it is in the foreground.
|
||||||
// TODO to really handle concurrency we'd need API24.
|
// TODO to really handle concurrency we'd need API24.
|
||||||
// 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>()
|
val storageFilePathDictionary = ConcurrentHashMap<String, StorageFile>()
|
||||||
private val notExistingPathDictionary = ConcurrentHashMap<String, String>()
|
val notExistingPathDictionary = ConcurrentHashMap<String, String>()
|
||||||
private val listedPathDictionary = ConcurrentHashMap<String, String>()
|
|
||||||
|
|
||||||
val mediaRoot: ResettableLazy<StorageFile> = ResettableLazy {
|
val mimeTypeMap: MimeTypeMap = MimeTypeMap.getSingleton()
|
||||||
StorageFile(null, getRoot()!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getRoot(): DocumentFile? {
|
private val columns = arrayOf(
|
||||||
return if (Settings.cacheLocation.isUri()) {
|
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||||
DocumentFile.fromTreeUri(
|
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||||
UApp.applicationContext(),
|
DocumentsContract.Document.COLUMN_MIME_TYPE
|
||||||
Uri.parse(Settings.cacheLocation)
|
)
|
||||||
)
|
|
||||||
} else {
|
|
||||||
DocumentFile.fromFile(File(Settings.cacheLocation))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun resetCaches() {
|
|
||||||
storageFilePathDictionary.clear()
|
|
||||||
notExistingPathDictionary.clear()
|
|
||||||
listedPathDictionary.clear()
|
|
||||||
mediaRoot.reset()
|
|
||||||
Timber.i("StorageFile caches were reset")
|
|
||||||
val root = getRoot()
|
|
||||||
if (root == null || !root.exists()) {
|
|
||||||
Settings.cacheLocation = FileUtil.defaultMusicDirectory.path
|
|
||||||
Util.toast(UApp.applicationContext(), R.string.settings_cache_location_error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun getOrCreateFileFromPath(path: String): StorageFile {
|
|
||||||
if (storageFilePathDictionary.containsKey(path))
|
|
||||||
return storageFilePathDictionary[path]!!
|
|
||||||
|
|
||||||
val parent = getStorageFileForParentDirectory(path)
|
|
||||||
?: throw IOException("Parent directory doesn't exist")
|
|
||||||
|
|
||||||
val name = FileUtil.getNameFromPath(path)
|
|
||||||
val file = StorageFile(
|
|
||||||
parent,
|
|
||||||
parent.documentFile.findFile(name)
|
|
||||||
?: parent.documentFile.createFile(
|
|
||||||
MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.extension())!!,
|
|
||||||
name.withoutExtension()
|
|
||||||
)!!
|
|
||||||
)
|
|
||||||
|
|
||||||
storageFilePathDictionary[path] = file
|
|
||||||
notExistingPathDictionary.remove(path)
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isPathExists(path: String): Boolean {
|
|
||||||
return getFromPath(path) != null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFromPath(path: String): StorageFile? {
|
|
||||||
|
|
||||||
if (storageFilePathDictionary.containsKey(path))
|
|
||||||
return storageFilePathDictionary[path]!!
|
|
||||||
if (notExistingPathDictionary.contains(path)) return null
|
|
||||||
|
|
||||||
val parent = getStorageFileForParentDirectory(path)
|
|
||||||
if (parent == null) {
|
|
||||||
notExistingPathDictionary.putIfAbsent(path, path)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the parent was fully listed, but the searched file isn't cached, it doesn't exists.
|
|
||||||
if (listedPathDictionary.containsKey(parent.path)) return null
|
|
||||||
|
|
||||||
val fileName = FileUtil.getNameFromPath(path)
|
|
||||||
var file: StorageFile? = null
|
|
||||||
|
|
||||||
//Timber.v("StorageFile getFromPath path: %s", path)
|
|
||||||
// Listing a bunch of files takes the same time in SAF as finding one,
|
|
||||||
// so we list and cache all of them for performance
|
|
||||||
|
|
||||||
parent.listFiles().forEach {
|
|
||||||
if (it.name == fileName) file = it
|
|
||||||
storageFilePathDictionary[it.path] = it
|
|
||||||
notExistingPathDictionary.remove(it.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
listedPathDictionary[parent.path] = parent.path
|
|
||||||
|
|
||||||
if (file == null) {
|
|
||||||
notExistingPathDictionary.putIfAbsent(path, path)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun createDirsOnPath(path: String) {
|
|
||||||
val segments = getUriSegments(path)
|
|
||||||
?: throw IOException("Can't get path because the root has changed")
|
|
||||||
|
|
||||||
var file = mediaRoot.value
|
|
||||||
segments.forEach { segment ->
|
|
||||||
file = StorageFile(
|
|
||||||
file,
|
|
||||||
file.documentFile.findFile(segment) ?:
|
|
||||||
file.documentFile.createDirectory(segment)
|
|
||||||
?: throw IOException("Can't create directory")
|
|
||||||
)
|
|
||||||
|
|
||||||
notExistingPathDictionary.remove(file.path)
|
|
||||||
listedPathDictionary.remove(file.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun rename(pathFrom: String, pathTo: String) {
|
|
||||||
val fileFrom = getFromPath(pathFrom) ?: throw IOException("File to rename doesn't exist")
|
|
||||||
rename(fileFrom, pathTo)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun rename(pathFrom: StorageFile?, pathTo: String) {
|
|
||||||
if (pathFrom == null || !pathFrom.documentFile.exists()) throw IOException("File to rename doesn't exist")
|
|
||||||
Timber.d("Renaming from %s to %s", pathFrom.path, pathTo)
|
|
||||||
|
|
||||||
val parentTo = getFromPath(FileUtil.getParentPath(pathTo)!!) ?: throw IOException("Destination folder doesn't exist")
|
|
||||||
val fileTo = getFromParentAndName(parentTo, FileUtil.getNameFromPath(pathTo))
|
|
||||||
|
|
||||||
copyFileContents(pathFrom.documentFile, fileTo.documentFile)
|
|
||||||
pathFrom.delete()
|
|
||||||
|
|
||||||
notExistingPathDictionary.remove(pathTo)
|
|
||||||
storageFilePathDictionary.remove(pathFrom.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyFileContents(sourceFile: DocumentFile, destinationFile: DocumentFile) {
|
private fun copyFileContents(sourceFile: DocumentFile, destinationFile: DocumentFile) {
|
||||||
UApp.applicationContext().contentResolver.openInputStream(sourceFile.uri)?.use { inputStream ->
|
UApp.applicationContext().contentResolver.openInputStream(sourceFile.uri)?.use { inputStream ->
|
||||||
@ -247,12 +218,18 @@ class StorageFile private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getFromParentAndName(parent: StorageFile, name: String): StorageFile {
|
private fun getFromParentAndName(parent: StorageFile, name: String): StorageFile {
|
||||||
val file = parent.documentFile.findFile(name)
|
val foundFile = parent.listFiles().firstOrNull { it.name == name }
|
||||||
?: parent.documentFile.createFile(
|
|
||||||
MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.extension())!!,
|
if (foundFile != null) return foundFile as StorageFile
|
||||||
name.withoutExtension()
|
|
||||||
)!!
|
val createdUri = DocumentsContract.createDocument(
|
||||||
return StorageFile(parent, file)
|
UApp.applicationContext().contentResolver,
|
||||||
|
parent.uri,
|
||||||
|
mimeTypeMap.getMimeTypeFromExtension(name.extension())!!,
|
||||||
|
name.withoutExtension()
|
||||||
|
) ?: throw IOException("Can't create file")
|
||||||
|
|
||||||
|
return StorageFile(parent, createdUri, name, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getStorageFileForParentDirectory(path: String): StorageFile? {
|
private fun getStorageFileForParentDirectory(path: String): StorageFile? {
|
||||||
@ -261,10 +238,9 @@ 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 start = System.currentTimeMillis()
|
||||||
val parent = findStorageFileForParentDirectory(parentPath)
|
val parent = findStorageFileForParentDirectory(parentPath)
|
||||||
//val end = System.currentTimeMillis()
|
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)
|
||||||
@ -281,37 +257,33 @@ 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.documentFile)
|
var file = Storage.mediaRoot.value as StorageFile
|
||||||
segments.forEach { segment ->
|
segments.forEach { segment ->
|
||||||
val currentPath = file.path + "/" + segment
|
val currentPath = file.path + "/" + segment
|
||||||
if (notExistingPathDictionary.contains(currentPath)) return null
|
if (notExistingPathDictionary.contains(currentPath)) return null
|
||||||
if (storageFilePathDictionary.containsKey(currentPath)) {
|
if (storageFilePathDictionary.containsKey(currentPath)) {
|
||||||
file = storageFilePathDictionary[currentPath]!!
|
file = storageFilePathDictionary[currentPath]!!
|
||||||
} else {
|
} 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
|
var foundFile: StorageFile? = null
|
||||||
file.listFiles().forEach {
|
file.listFiles().forEach {
|
||||||
if (it.name == segment) foundFile = it
|
if (it.name == segment) foundFile = it as StorageFile
|
||||||
storageFilePathDictionary[it.path] = it
|
storageFilePathDictionary[it.path] = it as StorageFile
|
||||||
notExistingPathDictionary.remove(it.path)
|
notExistingPathDictionary.remove(it.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
listedPathDictionary[file.path] = file.path
|
|
||||||
|
|
||||||
if (foundFile == null) {
|
if (foundFile == null) {
|
||||||
notExistingPathDictionary.putIfAbsent(path, path)
|
notExistingPathDictionary.putIfAbsent(path, path)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
file = StorageFile(file, foundFile!!.documentFile)
|
file = foundFile!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return file
|
return file
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUriSegments(uri: String): List<String>? {
|
private fun getUriSegments(uri: String): List<String>? {
|
||||||
val rootPath = mediaRoot.value.path
|
val rootPath = Storage.mediaRoot.value.path
|
||||||
if (!uri.startsWith(rootPath)) return null
|
if (!uri.startsWith(rootPath)) return null
|
||||||
val pathWithoutRoot = uri.substringAfter(rootPath)
|
val pathWithoutRoot = uri.substringAfter(rootPath)
|
||||||
return pathWithoutRoot.split('/').filter { it.isNotEmpty() }
|
return pathWithoutRoot.split('/').filter { it.isNotEmpty() }
|
||||||
@ -319,11 +291,6 @@ class StorageFile private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun String.isUri(): Boolean {
|
|
||||||
// TODO is there a better way to tell apart a path and an URI?
|
|
||||||
return this.contains(':')
|
|
||||||
}
|
|
||||||
|
|
||||||
fun String.extension(): String {
|
fun String.extension(): String {
|
||||||
val index = this.indexOfLast { ch -> ch == '.' }
|
val index = this.indexOfLast { ch -> ch == '.' }
|
||||||
if (index == -1) return ""
|
if (index == -1) return ""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user