Moved from DocumentFile to DocumentsContract

Added separate handling for the old java File paths
This commit is contained in:
Nite 2021-12-12 13:00:53 +01:00
parent 34c5ced32e
commit fa4214a0ac
No known key found for this signature in database
GPG Key ID: 1D1AD59B1C6386C1
14 changed files with 470 additions and 292 deletions

View File

@ -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();

View File

@ -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

View File

@ -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) {

View File

@ -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

View 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

View 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)

View File

@ -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)

View File

@ -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)

View 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)
}

View File

@ -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)
} }
} }

View File

@ -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

View File

@ -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()
}
}

View File

@ -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(':')
}

View File

@ -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 ""