mirror of
https://github.com/ultrasonic/ultrasonic
synced 2025-02-16 11:41:16 +01: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);
|
||||
if (!StorageFile.Companion.isPathExists(localPath)) {
|
||||
if (!Storage.INSTANCE.isPathExists(localPath)) {
|
||||
Timber.e("File %s does not exist", localPath);
|
||||
return false;
|
||||
}
|
||||
@ -194,7 +194,7 @@ public class StreamProxy implements Runnable
|
||||
String file = downloadFile.isCompleteFileAvailable() ? downloadFile.getCompleteOrSaveFile() : downloadFile.getPartialFile();
|
||||
int cbSentThisBatch = 0;
|
||||
|
||||
StorageFile storageFile = StorageFile.Companion.getFromPath(file);
|
||||
AbstractFile storageFile = Storage.INSTANCE.getFromPath(file);
|
||||
if (storageFile != null)
|
||||
{
|
||||
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.ServerColor
|
||||
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.Util
|
||||
import timber.log.Timber
|
||||
@ -214,7 +214,7 @@ class NavigationActivity : AppCompatActivity() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
StorageFile.resetCaches()
|
||||
Storage.reset()
|
||||
setMenuForServerCapabilities()
|
||||
|
||||
// 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.shareGreeting
|
||||
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.TimeSpanPreferenceDialogFragmentCompat
|
||||
import org.moire.ultrasonic.util.Util.toast
|
||||
@ -456,7 +456,7 @@ class SettingsFragment :
|
||||
|
||||
// Clear download queue.
|
||||
mediaPlayerControllerLazy.value.clear()
|
||||
StorageFile.resetCaches()
|
||||
Storage.reset()
|
||||
}
|
||||
|
||||
private fun setDebugLogToFile(writeLog: Boolean) {
|
||||
|
@ -5,7 +5,6 @@ import android.graphics.BitmapFactory
|
||||
import android.os.Build
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.util.FileUtil
|
||||
import org.moire.ultrasonic.util.StorageFile
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import timber.log.Timber
|
||||
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.domain.MusicDirectory
|
||||
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 timber.log.Timber
|
||||
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.util.CacheCleaner
|
||||
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.Settings
|
||||
import org.moire.ultrasonic.util.Util
|
||||
@ -77,10 +77,10 @@ class DownloadFile(
|
||||
completeFile = FileUtil.getParentPath(saveFile) + "/" + FileUtil.getCompleteFile(FileUtil.getNameFromPath(saveFile))
|
||||
|
||||
when {
|
||||
StorageFile.isPathExists(saveFile) -> {
|
||||
Storage.isPathExists(saveFile) -> {
|
||||
state = DownloadStatus.PINNED
|
||||
}
|
||||
StorageFile.isPathExists(completeFile) -> {
|
||||
Storage.isPathExists(completeFile) -> {
|
||||
state = DownloadStatus.DONE
|
||||
}
|
||||
else -> {
|
||||
@ -119,7 +119,7 @@ class DownloadFile(
|
||||
}
|
||||
|
||||
val completeOrSaveFile: String
|
||||
get() = if (StorageFile.isPathExists(saveFile)) {
|
||||
get() = if (Storage.isPathExists(saveFile)) {
|
||||
saveFile
|
||||
} else {
|
||||
completeFile
|
||||
@ -133,16 +133,16 @@ class DownloadFile(
|
||||
}
|
||||
|
||||
val isSaved: Boolean
|
||||
get() = StorageFile.isPathExists(saveFile)
|
||||
get() = Storage.isPathExists(saveFile)
|
||||
|
||||
@get:Synchronized
|
||||
val isCompleteFileAvailable: Boolean
|
||||
get() = StorageFile.isPathExists(completeFile) || StorageFile.isPathExists(saveFile)
|
||||
get() = Storage.isPathExists(completeFile) || Storage.isPathExists(saveFile)
|
||||
|
||||
@get:Synchronized
|
||||
val isWorkDone: Boolean
|
||||
get() = StorageFile.isPathExists(completeFile) && !shouldSave ||
|
||||
StorageFile.isPathExists(saveFile) || saveWhenDone || completeWhenDone
|
||||
get() = Storage.isPathExists(completeFile) && !shouldSave ||
|
||||
Storage.isPathExists(saveFile) || saveWhenDone || completeWhenDone
|
||||
|
||||
@get:Synchronized
|
||||
val isDownloading: Boolean
|
||||
@ -168,18 +168,18 @@ class DownloadFile(
|
||||
}
|
||||
|
||||
fun unpin() {
|
||||
val file = StorageFile.getFromPath(saveFile) ?: return
|
||||
StorageFile.rename(file, completeFile)
|
||||
val file = Storage.getFromPath(saveFile) ?: return
|
||||
Storage.rename(file, completeFile)
|
||||
status.postValue(DownloadStatus.DONE)
|
||||
}
|
||||
|
||||
fun cleanup(): Boolean {
|
||||
var ok = true
|
||||
if (StorageFile.isPathExists(completeFile) || StorageFile.isPathExists(saveFile)) {
|
||||
if (Storage.isPathExists(completeFile) || Storage.isPathExists(saveFile)) {
|
||||
ok = FileUtil.delete(partialFile)
|
||||
}
|
||||
|
||||
if (StorageFile.isPathExists(saveFile)) {
|
||||
if (Storage.isPathExists(saveFile)) {
|
||||
ok = ok and FileUtil.delete(completeFile)
|
||||
}
|
||||
|
||||
@ -224,13 +224,13 @@ class DownloadFile(
|
||||
var inputStream: InputStream? = null
|
||||
var outputStream: OutputStream? = null
|
||||
try {
|
||||
if (StorageFile.isPathExists(saveFile)) {
|
||||
if (Storage.isPathExists(saveFile)) {
|
||||
Timber.i("%s already exists. Skipping.", saveFile)
|
||||
status.postValue(DownloadStatus.PINNED)
|
||||
return
|
||||
}
|
||||
|
||||
if (StorageFile.isPathExists(completeFile)) {
|
||||
if (Storage.isPathExists(completeFile)) {
|
||||
var newStatus: DownloadStatus = DownloadStatus.DONE
|
||||
if (shouldSave) {
|
||||
if (isPlaying) {
|
||||
@ -251,7 +251,7 @@ class DownloadFile(
|
||||
// Some devices seem to throw error on partial file which doesn't exist
|
||||
val needsDownloading: Boolean
|
||||
val duration = song.duration
|
||||
val fileLength = StorageFile.getFromPath(partialFile)?.length ?: 0
|
||||
val fileLength = Storage.getFromPath(partialFile)?.length ?: 0
|
||||
|
||||
needsDownloading = (
|
||||
desiredBitRate == 0 || duration == null ||
|
||||
@ -270,7 +270,7 @@ class DownloadFile(
|
||||
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 ->
|
||||
setProgress(totalBytesCopied)
|
||||
|
@ -19,7 +19,7 @@ import android.os.PowerManager
|
||||
import android.os.PowerManager.PARTIAL_WAKE_LOCK
|
||||
import android.os.PowerManager.WakeLock
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.moire.ultrasonic.util.StorageFile
|
||||
import org.moire.ultrasonic.util.Storage
|
||||
import java.net.URLEncoder
|
||||
import java.util.Locale
|
||||
import kotlin.math.abs
|
||||
@ -347,7 +347,7 @@ class LocalMediaPlayer : KoinComponent {
|
||||
try {
|
||||
downloadFile.setPlaying(false)
|
||||
|
||||
val file = StorageFile.getFromPath(downloadFile.completeOrPartialFile)
|
||||
val file = Storage.getFromPath(downloadFile.completeOrPartialFile)
|
||||
val partial = !downloadFile.isCompleteFileAvailable
|
||||
|
||||
// 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
|
||||
private fun setupNext(downloadFile: DownloadFile) {
|
||||
try {
|
||||
val file = StorageFile.getFromPath(downloadFile.completeOrPartialFile)
|
||||
val file = Storage.getFromPath(downloadFile.completeOrPartialFile)
|
||||
|
||||
// Release the media player if it is not our active player
|
||||
if (nextMediaPlayer != null && nextMediaPlayer != mediaPlayer) {
|
||||
@ -615,7 +615,7 @@ class LocalMediaPlayer : KoinComponent {
|
||||
|
||||
private fun bufferComplete(): Boolean {
|
||||
val completeFileAvailable = downloadFile.isWorkDone
|
||||
val size = StorageFile.getFromPath(partialFile)?.length ?: 0
|
||||
val size = Storage.getFromPath(partialFile)?.length ?: 0
|
||||
|
||||
Timber.i(
|
||||
"Buffering %s (%d/%d, %s)",
|
||||
@ -673,7 +673,7 @@ class LocalMediaPlayer : KoinComponent {
|
||||
val state = (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED)
|
||||
|
||||
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)
|
||||
|
||||
|
@ -9,7 +9,7 @@ package org.moire.ultrasonic.service
|
||||
import android.media.MediaMetadataRetriever
|
||||
import java.io.BufferedReader
|
||||
import java.io.BufferedWriter
|
||||
import org.moire.ultrasonic.util.StorageFile
|
||||
import org.moire.ultrasonic.util.Storage
|
||||
import java.io.InputStream
|
||||
import java.io.Reader
|
||||
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.Share
|
||||
import org.moire.ultrasonic.domain.UserInfo
|
||||
import org.moire.ultrasonic.util.AbstractFile
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.FileUtil
|
||||
import org.moire.ultrasonic.util.Util.safeClose
|
||||
@ -102,7 +103,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
name: String?,
|
||||
refresh: Boolean
|
||||
): MusicDirectory {
|
||||
val dir = StorageFile.getFromPath(id)
|
||||
val dir = Storage.getFromPath(id)
|
||||
val result = MusicDirectory()
|
||||
result.name = dir?.name ?: return result
|
||||
|
||||
@ -211,7 +212,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
var line = buffer.readLine()
|
||||
if ("#EXTM3U" != line) return playlist
|
||||
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)
|
||||
if (entryName != null) {
|
||||
playlist.add(createEntry(entryFile, entryName))
|
||||
@ -235,7 +236,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
fw.write("#EXTM3U\n")
|
||||
for (e in entries) {
|
||||
var filePath = FileUtil.getSongFile(e)
|
||||
if (!StorageFile.isPathExists(filePath)) {
|
||||
if (!Storage.isPathExists(filePath)) {
|
||||
val ext = FileUtil.getExtension(filePath)
|
||||
val base = FileUtil.getBaseName(filePath)
|
||||
filePath = "$base.complete.$ext"
|
||||
@ -257,7 +258,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
|
||||
override fun getRandomSongs(size: Int): MusicDirectory {
|
||||
val root = FileUtil.musicDirectory
|
||||
val children: MutableList<StorageFile> = LinkedList()
|
||||
val children: MutableList<AbstractFile> = LinkedList()
|
||||
listFilesRecursively(root, children)
|
||||
val result = MusicDirectory()
|
||||
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)
|
||||
entry.populateWithDataFrom(file, name)
|
||||
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)
|
||||
album.populateWithDataFrom(file, name)
|
||||
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
|
||||
*/
|
||||
private fun MusicDirectory.Child.populateWithDataFrom(file: StorageFile, name: String?) {
|
||||
private fun MusicDirectory.Child.populateWithDataFrom(file: AbstractFile, name: String?) {
|
||||
isDirectory = file.isDirectory
|
||||
parent = file.parent!!.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
|
||||
* 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)
|
||||
|
||||
val meta = RawMetadata(null)
|
||||
@ -607,7 +608,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
@Suppress("NestedBlockDepth")
|
||||
private fun recursiveAlbumSearch(
|
||||
artistName: String,
|
||||
file: StorageFile,
|
||||
file: AbstractFile,
|
||||
criteria: SearchCriteria,
|
||||
albums: MutableList<MusicDirectory.Album>,
|
||||
songs: MutableList<MusicDirectory.Entry>
|
||||
@ -664,7 +665,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
return closeness
|
||||
}
|
||||
|
||||
private fun listFilesRecursively(parent: StorageFile, children: MutableList<StorageFile>) {
|
||||
private fun listFilesRecursively(parent: AbstractFile, children: MutableList<AbstractFile>) {
|
||||
for (file in FileUtil.listMediaFiles(parent)) {
|
||||
if (file.isFile) {
|
||||
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() {
|
||||
try {
|
||||
val files: MutableList<StorageFile> = ArrayList()
|
||||
val dirs: MutableList<StorageFile> = ArrayList()
|
||||
val files: MutableList<AbstractFile> = ArrayList()
|
||||
val dirs: MutableList<AbstractFile> = ArrayList()
|
||||
|
||||
findCandidatesForDeletion(musicDirectory, files, dirs)
|
||||
sortByAscendingModificationTime(files)
|
||||
@ -87,8 +87,8 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||
|
||||
private fun backgroundSpaceCleanup() {
|
||||
try {
|
||||
val files: MutableList<StorageFile> = ArrayList()
|
||||
val dirs: MutableList<StorageFile> = ArrayList()
|
||||
val files: MutableList<AbstractFile> = ArrayList()
|
||||
val dirs: MutableList<AbstractFile> = ArrayList()
|
||||
|
||||
findCandidatesForDeletion(musicDirectory, files, dirs)
|
||||
|
||||
@ -136,28 +136,26 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||
private var playlistCleaning = false
|
||||
|
||||
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) {
|
||||
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].path == getAlbumArtFile(dir.path)) {
|
||||
// Delete Artwork files
|
||||
delete(getAlbumArtFile(dir.path))
|
||||
children = dir.listFiles()
|
||||
}
|
||||
// No songs left in the folder
|
||||
if (children.size == 1 && children[0].path == getAlbumArtFile(dir.path)) {
|
||||
// Delete Artwork files
|
||||
delete(getAlbumArtFile(dir.path))
|
||||
children = dir.listFiles()
|
||||
}
|
||||
|
||||
// Delete empty directory
|
||||
if (children != null && children.isEmpty()) {
|
||||
delete(dir.path)
|
||||
}
|
||||
// Delete empty directory
|
||||
if (children.isEmpty()) {
|
||||
delete(dir.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMinimumDelete(files: List<StorageFile>): Long {
|
||||
private fun getMinimumDelete(files: List<AbstractFile>): Long {
|
||||
if (files.isEmpty()) return 0L
|
||||
|
||||
val cacheSizeBytes = cacheSizeMB * 1024L * 1024L
|
||||
@ -197,17 +195,17 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||
return bytesToDelete
|
||||
}
|
||||
|
||||
private fun isPartial(file: StorageFile): Boolean {
|
||||
private fun isPartial(file: AbstractFile): Boolean {
|
||||
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.")
|
||||
}
|
||||
|
||||
@Suppress("NestedBlockDepth")
|
||||
private fun deleteFiles(
|
||||
files: Collection<StorageFile>,
|
||||
files: Collection<AbstractFile>,
|
||||
doNotDelete: Collection<String>,
|
||||
bytesToDelete: Long,
|
||||
deletePartials: Boolean
|
||||
@ -232,9 +230,9 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||
}
|
||||
|
||||
private fun findCandidatesForDeletion(
|
||||
file: StorageFile,
|
||||
files: MutableList<StorageFile>,
|
||||
dirs: MutableList<StorageFile>
|
||||
file: AbstractFile,
|
||||
files: MutableList<AbstractFile>,
|
||||
dirs: MutableList<AbstractFile>
|
||||
) {
|
||||
if (file.isFile && (isPartial(file) || isComplete(file))) {
|
||||
files.add(file)
|
||||
@ -247,8 +245,8 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortByAscendingModificationTime(files: MutableList<StorageFile>) {
|
||||
files.sortWith { a: StorageFile, b: StorageFile ->
|
||||
private fun sortByAscendingModificationTime(files: MutableList<AbstractFile>) {
|
||||
files.sortWith { a: AbstractFile, b: AbstractFile ->
|
||||
a.lastModified.compareTo(b.lastModified)
|
||||
}
|
||||
}
|
||||
|
@ -209,7 +209,7 @@ object FileUtil {
|
||||
|
||||
fun createDirectoryForParent(path: String) {
|
||||
val dir = getParentPath(path) ?: return
|
||||
StorageFile.createDirsOnPath(dir)
|
||||
Storage.createDirsOnPath(dir)
|
||||
}
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
@ -239,8 +239,8 @@ object FileUtil {
|
||||
get() = getOrCreateDirectory("music")
|
||||
|
||||
@JvmStatic
|
||||
val musicDirectory: StorageFile
|
||||
get() = StorageFile.mediaRoot.value
|
||||
val musicDirectory: AbstractFile
|
||||
get() = Storage.mediaRoot.value
|
||||
|
||||
@JvmStatic
|
||||
@Suppress("ReturnCount")
|
||||
@ -316,7 +316,7 @@ object FileUtil {
|
||||
* Never returns `null`, instead a warning is logged, and an empty set is returned.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun listFiles(dir: StorageFile): SortedSet<StorageFile> {
|
||||
fun listFiles(dir: AbstractFile): SortedSet<AbstractFile> {
|
||||
val files = dir.listFiles()
|
||||
if (files == null) {
|
||||
Timber.w("Failed to list children for %s", dir.path)
|
||||
@ -335,7 +335,7 @@ object FileUtil {
|
||||
return TreeSet(files.asList())
|
||||
}
|
||||
|
||||
fun listMediaFiles(dir: StorageFile): SortedSet<StorageFile> {
|
||||
fun listMediaFiles(dir: AbstractFile): SortedSet<AbstractFile> {
|
||||
val files = listFiles(dir)
|
||||
val iterator = files.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
@ -347,7 +347,7 @@ object FileUtil {
|
||||
return files
|
||||
}
|
||||
|
||||
private fun isMediaFile(file: StorageFile): Boolean {
|
||||
private fun isMediaFile(file: AbstractFile): Boolean {
|
||||
val extension = getExtension(file.name)
|
||||
return MUSIC_FILE_EXTENSIONS.contains(extension) ||
|
||||
VIDEO_FILE_EXTENSIONS.contains(extension)
|
||||
@ -463,7 +463,7 @@ object FileUtil {
|
||||
for (e in playlist.getTracks()) {
|
||||
var filePath = getSongFile(e)
|
||||
|
||||
if (!StorageFile.isPathExists(filePath)) {
|
||||
if (!Storage.isPathExists(filePath)) {
|
||||
val ext = getExtension(filePath)
|
||||
val base = getBaseName(filePath)
|
||||
filePath = "$base.complete.$ext"
|
||||
@ -482,7 +482,7 @@ object FileUtil {
|
||||
@JvmStatic
|
||||
@Throws(IOException::class)
|
||||
fun renameFile(from: String, to: String) {
|
||||
StorageFile.rename(from, to)
|
||||
Storage.rename(from, to)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@ -500,7 +500,7 @@ object FileUtil {
|
||||
@JvmStatic
|
||||
fun delete(file: String?): Boolean {
|
||||
if (file != null) {
|
||||
val storageFile = StorageFile.getFromPath(file)
|
||||
val storageFile = Storage.getFromPath(file)
|
||||
if (storageFile != null && !storageFile.delete()) {
|
||||
Timber.w("Failed to delete file %s", file)
|
||||
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
|
||||
|
||||
import android.content.res.AssetFileDescriptor
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import org.moire.ultrasonic.R
|
||||
import java.io.File
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
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 parentStorageFile: StorageFile?,
|
||||
private var documentFile: DocumentFile
|
||||
): Comparable<StorageFile> {
|
||||
class StorageFile(
|
||||
override val parent: StorageFile?,
|
||||
var uri: Uri,
|
||||
override val name: String,
|
||||
override val isDirectory: Boolean
|
||||
): AbstractFile() {
|
||||
private val documentFile: DocumentFile = DocumentFile.fromSingleUri(UApp.applicationContext(), uri)!!
|
||||
|
||||
override fun compareTo(other: StorageFile): Int {
|
||||
return path.compareTo(other.path)
|
||||
}
|
||||
override val isFile: Boolean = !isDirectory
|
||||
|
||||
override fun toString(): String {
|
||||
return name
|
||||
}
|
||||
|
||||
var name: String = documentFile.name!!
|
||||
|
||||
var isDirectory: Boolean = documentFile.isDirectory
|
||||
|
||||
var isFile: Boolean = documentFile.isFile
|
||||
|
||||
val length: Long
|
||||
override val length: Long
|
||||
get() = documentFile.length()
|
||||
|
||||
val lastModified: Long
|
||||
override val lastModified: Long
|
||||
get() = documentFile.lastModified()
|
||||
|
||||
fun delete(): Boolean {
|
||||
val deleted = documentFile.delete()
|
||||
override val path: String
|
||||
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
|
||||
storageFilePathDictionary.remove(path)
|
||||
notExistingPathDictionary.putIfAbsent(path, path)
|
||||
listedPathDictionary.remove(path)
|
||||
listedPathDictionary.remove(parent?.path)
|
||||
return true
|
||||
}
|
||||
|
||||
fun listFiles(): Array<StorageFile> {
|
||||
val fileList = documentFile.listFiles()
|
||||
return fileList.map { file -> StorageFile(this, file) }.toTypedArray()
|
||||
override fun listFiles(): Array<AbstractFile> {
|
||||
return getChildren().toTypedArray()
|
||||
}
|
||||
|
||||
fun getFileOutputStream(append: Boolean): OutputStream {
|
||||
override fun getFileOutputStream(append: Boolean): OutputStream {
|
||||
val mode = if (append) "wa" else "w"
|
||||
val descriptor = UApp.applicationContext().contentResolver.openAssetFileDescriptor(
|
||||
documentFile.uri, mode)
|
||||
uri,
|
||||
mode
|
||||
)
|
||||
return descriptor?.createOutputStream()
|
||||
?: throw IOException("Couldn't retrieve OutputStream")
|
||||
}
|
||||
|
||||
fun getFileInputStream(): InputStream {
|
||||
return UApp.applicationContext().contentResolver.openInputStream(documentFile.uri)
|
||||
override fun getFileInputStream(): InputStream {
|
||||
return UApp.applicationContext().contentResolver.openInputStream(uri)
|
||||
?: throw IOException("Couldn't retrieve InputStream")
|
||||
}
|
||||
|
||||
val path: String
|
||||
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? {
|
||||
override fun getDocumentFileDescriptor(openMode: String): AssetFileDescriptor? {
|
||||
return UApp.applicationContext().contentResolver.openAssetFileDescriptor(
|
||||
documentFile.uri,
|
||||
uri,
|
||||
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 {
|
||||
// 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.
|
||||
// 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 listedPathDictionary = ConcurrentHashMap<String, String>()
|
||||
val storageFilePathDictionary = ConcurrentHashMap<String, StorageFile>()
|
||||
val notExistingPathDictionary = ConcurrentHashMap<String, String>()
|
||||
|
||||
val mediaRoot: ResettableLazy<StorageFile> = ResettableLazy {
|
||||
StorageFile(null, getRoot()!!)
|
||||
}
|
||||
val mimeTypeMap: MimeTypeMap = MimeTypeMap.getSingleton()
|
||||
|
||||
private fun getRoot(): DocumentFile? {
|
||||
return if (Settings.cacheLocation.isUri()) {
|
||||
DocumentFile.fromTreeUri(
|
||||
UApp.applicationContext(),
|
||||
Uri.parse(Settings.cacheLocation)
|
||||
)
|
||||
} else {
|
||||
DocumentFile.fromFile(File(Settings.cacheLocation))
|
||||
}
|
||||
}
|
||||
|
||||
fun resetCaches() {
|
||||
storageFilePathDictionary.clear()
|
||||
notExistingPathDictionary.clear()
|
||||
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 val columns = arrayOf(
|
||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE
|
||||
)
|
||||
|
||||
private fun copyFileContents(sourceFile: DocumentFile, destinationFile: DocumentFile) {
|
||||
UApp.applicationContext().contentResolver.openInputStream(sourceFile.uri)?.use { inputStream ->
|
||||
@ -247,12 +218,18 @@ class StorageFile private constructor(
|
||||
}
|
||||
|
||||
private fun getFromParentAndName(parent: StorageFile, name: String): StorageFile {
|
||||
val file = parent.documentFile.findFile(name)
|
||||
?: parent.documentFile.createFile(
|
||||
MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.extension())!!,
|
||||
name.withoutExtension()
|
||||
)!!
|
||||
return StorageFile(parent, file)
|
||||
val foundFile = parent.listFiles().firstOrNull { it.name == name }
|
||||
|
||||
if (foundFile != null) return foundFile as StorageFile
|
||||
|
||||
val createdUri = DocumentsContract.createDocument(
|
||||
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? {
|
||||
@ -261,10 +238,9 @@ class StorageFile private constructor(
|
||||
return storageFilePathDictionary[parentPath]!!
|
||||
if (notExistingPathDictionary.contains(parentPath)) return null
|
||||
|
||||
//val start = System.currentTimeMillis()
|
||||
val start = System.currentTimeMillis()
|
||||
val parent = findStorageFileForParentDirectory(parentPath)
|
||||
//val end = System.currentTimeMillis()
|
||||
//Timber.v("StorageFile getStorageFileForParentDirectory searching for %s, time: %d", parentPath, end-start)
|
||||
val end = System.currentTimeMillis()
|
||||
|
||||
if (parent == null) {
|
||||
storageFilePathDictionary.remove(parentPath)
|
||||
@ -281,37 +257,33 @@ class StorageFile private constructor(
|
||||
val segments = getUriSegments(path)
|
||||
?: throw IOException("Can't get path because the root has changed")
|
||||
|
||||
var file = StorageFile(null, mediaRoot.value.documentFile)
|
||||
var file = Storage.mediaRoot.value as StorageFile
|
||||
segments.forEach { segment ->
|
||||
val currentPath = file.path + "/" + segment
|
||||
if (notExistingPathDictionary.contains(currentPath)) return null
|
||||
if (storageFilePathDictionary.containsKey(currentPath)) {
|
||||
file = storageFilePathDictionary[currentPath]!!
|
||||
} else {
|
||||
// If the parent was fully listed, but the searched file isn't cached, it doesn't exists.
|
||||
if (listedPathDictionary.containsKey(file.path)) return null
|
||||
var foundFile: StorageFile? = null
|
||||
file.listFiles().forEach {
|
||||
if (it.name == segment) foundFile = it
|
||||
storageFilePathDictionary[it.path] = it
|
||||
if (it.name == segment) foundFile = it as StorageFile
|
||||
storageFilePathDictionary[it.path] = it as StorageFile
|
||||
notExistingPathDictionary.remove(it.path)
|
||||
}
|
||||
|
||||
listedPathDictionary[file.path] = file.path
|
||||
|
||||
if (foundFile == null) {
|
||||
notExistingPathDictionary.putIfAbsent(path, path)
|
||||
return null
|
||||
}
|
||||
|
||||
file = StorageFile(file, foundFile!!.documentFile)
|
||||
file = foundFile!!
|
||||
}
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
private fun getUriSegments(uri: String): List<String>? {
|
||||
val rootPath = mediaRoot.value.path
|
||||
val rootPath = Storage.mediaRoot.value.path
|
||||
if (!uri.startsWith(rootPath)) return null
|
||||
val pathWithoutRoot = uri.substringAfter(rootPath)
|
||||
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 {
|
||||
val index = this.indexOfLast { ch -> ch == '.' }
|
||||
if (index == -1) return ""
|
||||
|
Loading…
x
Reference in New Issue
Block a user