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

View File

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

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

View File

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

View 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

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

View File

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

View File

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

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

View File

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

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