From fa4214a0ac284c1bea342a1a906f93452f8ddced Mon Sep 17 00:00:00 2001 From: Nite Date: Sun, 12 Dec 2021 13:00:53 +0100 Subject: [PATCH] Moved from DocumentFile to DocumentsContract Added separate handling for the old java File paths --- .../moire/ultrasonic/util/StreamProxy.java | 4 +- .../ultrasonic/activity/NavigationActivity.kt | 4 +- .../ultrasonic/fragment/SettingsFragment.kt | 4 +- .../ultrasonic/imageloader/BitmapUtils.kt | 1 - .../ultrasonic/imageloader/ImageLoader.kt | 2 - .../moire/ultrasonic/service/DownloadFile.kt | 32 +- .../ultrasonic/service/LocalMediaPlayer.kt | 10 +- .../ultrasonic/service/OfflineMusicService.kt | 23 +- .../org/moire/ultrasonic/util/AbstractFile.kt | 56 +++ .../org/moire/ultrasonic/util/CacheCleaner.kt | 48 +-- .../org/moire/ultrasonic/util/FileUtil.kt | 18 +- .../org/moire/ultrasonic/util/JavaFile.kt | 77 ++++ .../org/moire/ultrasonic/util/Storage.kt | 82 ++++ .../org/moire/ultrasonic/util/StorageFile.kt | 401 ++++++++---------- 14 files changed, 470 insertions(+), 292 deletions(-) create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/AbstractFile.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/JavaFile.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Storage.kt diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/StreamProxy.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/StreamProxy.java index 182d413c..dd2cd185 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/StreamProxy.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/StreamProxy.java @@ -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(); diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index b5cde824..bbaea16b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt index 511ed20e..a321987e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt @@ -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) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/BitmapUtils.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/BitmapUtils.kt index a6b36b15..20fa626c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/BitmapUtils.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/BitmapUtils.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt index 41be0182..9b68e33b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ImageLoader.kt @@ -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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt index cb290c3b..642b50ef 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadFile.kt @@ -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) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt index f10aa2c5..9c727e08 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/LocalMediaPlayer.kt @@ -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) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index 68188b96..98e782d1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -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 = LinkedList() + val children: MutableList = 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, songs: MutableList @@ -664,7 +665,7 @@ class OfflineMusicService : MusicService, KoinComponent { return closeness } - private fun listFilesRecursively(parent: StorageFile, children: MutableList) { + private fun listFilesRecursively(parent: AbstractFile, children: MutableList) { for (file in FileUtil.listMediaFiles(parent)) { if (file.isFile) { children.add(file) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/AbstractFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/AbstractFile.kt new file mode 100644 index 00000000..1bfc2612 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/AbstractFile.kt @@ -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 { + 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 + + 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) +} \ No newline at end of file diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt index 75612911..b9c4abe8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt @@ -69,8 +69,8 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) { private fun backgroundCleanup() { try { - val files: MutableList = ArrayList() - val dirs: MutableList = ArrayList() + val files: MutableList = ArrayList() + val dirs: MutableList = 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 = ArrayList() - val dirs: MutableList = ArrayList() + val files: MutableList = ArrayList() + val dirs: MutableList = 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, doNotDelete: Collection) { + private fun deleteEmptyDirs(dirs: Iterable, doNotDelete: Collection) { 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): Long { + private fun getMinimumDelete(files: List): 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, + files: Collection, doNotDelete: Collection, bytesToDelete: Long, deletePartials: Boolean @@ -232,9 +230,9 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) { } private fun findCandidatesForDeletion( - file: StorageFile, - files: MutableList, - dirs: MutableList + file: AbstractFile, + files: MutableList, + dirs: MutableList ) { 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) { - files.sortWith { a: StorageFile, b: StorageFile -> + private fun sortByAscendingModificationTime(files: MutableList) { + files.sortWith { a: AbstractFile, b: AbstractFile -> a.lastModified.compareTo(b.lastModified) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt index a3305077..3c0ae36e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtil.kt @@ -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 { + fun listFiles(dir: AbstractFile): SortedSet { 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 { + fun listMediaFiles(dir: AbstractFile): SortedSet { 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 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/JavaFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/JavaFile.kt new file mode 100644 index 00000000..b3cb6e6a --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/JavaFile.kt @@ -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 { + 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() + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Storage.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Storage.kt new file mode 100644 index 00000000..7f1c4158 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Storage.kt @@ -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 = 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(':') +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/StorageFile.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/StorageFile.kt index b144b886..7bc97643 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/StorageFile.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/StorageFile.kt @@ -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 { +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 { - val fileList = documentFile.listFiles() - return fileList.map { file -> StorageFile(this, file) }.toTypedArray() + override fun listFiles(): Array { + 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 { + 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() + + 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() - private val notExistingPathDictionary = ConcurrentHashMap() - private val listedPathDictionary = ConcurrentHashMap() + val storageFilePathDictionary = ConcurrentHashMap() + val notExistingPathDictionary = ConcurrentHashMap() - val mediaRoot: ResettableLazy = 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? { - 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 ""