Merge FileUtil functions into a single class.

This commit is contained in:
tzugen 2021-09-12 12:01:24 +02:00
parent ec49775d7e
commit 5ff4d21abc
No known key found for this signature in database
GPG Key ID: 61E9C34BC10EC930
6 changed files with 124 additions and 149 deletions

View File

@ -70,7 +70,7 @@ style:
excludeImportStatements: false excludeImportStatements: false
MagicNumber: MagicNumber:
# 100 common in percentage, 1000 in milliseconds # 100 common in percentage, 1000 in milliseconds
ignoreNumbers: ['-1', '0', '1', '2', '100', '1000'] ignoreNumbers: ['-1', '0', '1', '2', '10', '100', '256', '512', '1000', '1024']
ignoreEnums: true ignoreEnums: true
ignorePropertyDeclaration: true ignorePropertyDeclaration: true
UnnecessaryAbstractClass: UnnecessaryAbstractClass:

View File

@ -10,6 +10,12 @@ package org.moire.ultrasonic.service
import android.net.wifi.WifiManager.WifiLock import android.net.wifi.WifiManager.WifiLock
import android.text.TextUtils import android.text.TextUtils
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.io.RandomAccessFile
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
@ -20,12 +26,6 @@ import org.moire.ultrasonic.util.CancellableTask
import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil
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.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.io.RandomAccessFile
/** /**
* This class represents a singe Song or Video that can be downloaded. * This class represents a singe Song or Video that can be downloaded.

View File

@ -42,7 +42,6 @@ import org.moire.ultrasonic.domain.toDomainEntityList
import org.moire.ultrasonic.domain.toIndexList import org.moire.ultrasonic.domain.toIndexList
import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity
import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.FileUtilKt
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import timber.log.Timber import timber.log.Timber
@ -242,7 +241,7 @@ open class RESTMusicService(
activeServerProvider.getActiveServer().name, name activeServerProvider.getActiveServer().name, name
) )
FileUtilKt.savePlaylist(playlistFile, playlist, name) FileUtil.savePlaylist(playlistFile, playlist, name)
} }
@Throws(Exception::class) @Throws(Exception::class)

View File

@ -1,61 +1,47 @@
/* /*
This file is part of Subsonic. * FileUtil.kt
* Copyright (C) 2009-2021 Ultrasonic developers
Subsonic is free software: you can redistribute it and/or modify *
it under the terms of the GNU General Public License as published by * Distributed under terms of the GNU GPLv3 license.
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/ */
package org.moire.ultrasonic.util package org.moire.ultrasonic.util
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.text.TextUtils import android.text.TextUtils
import org.koin.java.KoinJavaComponent.inject import java.io.BufferedWriter
import org.moire.ultrasonic.app.UApp.Companion.applicationContext
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.util.Util.close
import org.moire.ultrasonic.util.Util.getPreferences
import org.moire.ultrasonic.util.Util.md5Hex
import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.FileWriter
import java.io.IOException
import java.io.ObjectInputStream import java.io.ObjectInputStream
import java.io.ObjectOutputStream import java.io.ObjectOutputStream
import java.io.Serializable import java.io.Serializable
import java.util.Arrays
import java.util.Locale import java.util.Locale
import java.util.SortedSet import java.util.SortedSet
import java.util.TreeSet import java.util.TreeSet
import java.util.regex.Pattern import java.util.regex.Pattern
import org.koin.java.KoinJavaComponent
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.domain.MusicDirectory
import timber.log.Timber
/**
* @author Sindre Mehus
*/
object FileUtil { object FileUtil {
private val FILE_SYSTEM_UNSAFE = arrayOf("/", "\\", "..", ":", "\"", "?", "*", "<", ">", "|") private val FILE_SYSTEM_UNSAFE = arrayOf("/", "\\", "..", ":", "\"", "?", "*", "<", ">", "|")
private val FILE_SYSTEM_UNSAFE_DIR = arrayOf("\\", "..", ":", "\"", "?", "*", "<", ">", "|") private val FILE_SYSTEM_UNSAFE_DIR = arrayOf("\\", "..", ":", "\"", "?", "*", "<", ">", "|")
private val MUSIC_FILE_EXTENSIONS = private val MUSIC_FILE_EXTENSIONS =
Arrays.asList("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma", "opus") listOf("mp3", "ogg", "aac", "flac", "m4a", "wav", "wma", "opus")
private val VIDEO_FILE_EXTENSIONS = private val VIDEO_FILE_EXTENSIONS =
Arrays.asList("flv", "mp4", "m4v", "wmv", "avi", "mov", "mpg", "mkv") listOf("flv", "mp4", "m4v", "wmv", "avi", "mov", "mpg", "mkv")
private val PLAYLIST_FILE_EXTENSIONS = listOf("m3u") private val PLAYLIST_FILE_EXTENSIONS = listOf("m3u")
private val TITLE_WITH_TRACK = Pattern.compile("^\\d\\d-.*") private val TITLE_WITH_TRACK = Pattern.compile("^\\d\\d-.*")
const val SUFFIX_LARGE = ".jpeg" const val SUFFIX_LARGE = ".jpeg"
const val SUFFIX_SMALL = ".jpeg-small" const val SUFFIX_SMALL = ".jpeg-small"
private val permissionUtil = inject<PermissionUtil>( private val permissionUtil = KoinJavaComponent.inject<PermissionUtil>(
PermissionUtil::class.java PermissionUtil::class.java
) )
@ -71,8 +57,8 @@ object FileUtil {
val fileName = StringBuilder(256) val fileName = StringBuilder(256)
val track = song.track val track = song.track
//check if filename already had track number // check if filename already had track number
if (!TITLE_WITH_TRACK.matcher(song.title).matches()) { if (song.title != null && !TITLE_WITH_TRACK.matcher(song.title!!).matches()) {
if (track != null) { if (track != null) {
if (track < 10) { if (track < 10) {
fileName.append('0') fileName.append('0')
@ -90,13 +76,13 @@ object FileUtil {
} }
@JvmStatic @JvmStatic
fun getPlaylistFile(server: String?, name: String): File { fun getPlaylistFile(server: String?, name: String): File {
val playlistDir = getPlaylistDirectory(server) val playlistDir = getPlaylistDirectory(server)
return File(playlistDir, String.format("%s.m3u", fileSystemSafe(name))) return File(playlistDir, String.format(Locale.ROOT, "%s.m3u", fileSystemSafe(name)))
} }
@JvmStatic @JvmStatic
val playlistDirectory: File val playlistDirectory: File
get() { get() {
val playlistDir = File(ultrasonicDirectory, "playlists") val playlistDir = File(ultrasonicDirectory, "playlists")
ensureDirectoryExistsAndIsReadWritable(playlistDir) ensureDirectoryExistsAndIsReadWritable(playlistDir)
@ -104,7 +90,12 @@ object FileUtil {
} }
fun getPlaylistDirectory(server: String?): File { fun getPlaylistDirectory(server: String?): File {
val playlistDir = File(playlistDirectory, server) val playlistDir: File
if (server != null) {
playlistDir = File(playlistDirectory, server)
} else {
playlistDir = playlistDirectory
}
ensureDirectoryExistsAndIsReadWritable(playlistDir) ensureDirectoryExistsAndIsReadWritable(playlistDir)
return playlistDir return playlistDir
} }
@ -136,21 +127,21 @@ object FileUtil {
* @param large Whether to get the key for the large or the default image * @param large Whether to get the key for the large or the default image
* @return String The hash key * @return String The hash key
*/ */
fun getAlbumArtKey(albumDir: File?, large: Boolean): String? { private fun getAlbumArtKey(albumDir: File?, large: Boolean): String? {
if (albumDir == null) { if (albumDir == null) {
return null return null
} }
val suffix = if (large) SUFFIX_LARGE else SUFFIX_SMALL val suffix = if (large) SUFFIX_LARGE else SUFFIX_SMALL
return String.format(Locale.ROOT, "%s%s", md5Hex(albumDir.path), suffix) return String.format(Locale.ROOT, "%s%s", Util.md5Hex(albumDir.path), suffix)
} }
fun getAvatarFile(username: String?): File? { fun getAvatarFile(username: String?): File? {
val albumArtDir = albumArtDirectory if (username == null) {
if (albumArtDir == null || username == null) {
return null return null
} }
val md5Hex = md5Hex(username) val albumArtDir = albumArtDirectory
return File(albumArtDir, String.format("%s%s", md5Hex, SUFFIX_LARGE)) val md5Hex = Util.md5Hex(username)
return File(albumArtDir, String.format(Locale.ROOT, "%s%s", md5Hex, SUFFIX_LARGE))
} }
/** /**
@ -161,7 +152,7 @@ object FileUtil {
fun getAlbumArtFile(albumDir: File?): File? { fun getAlbumArtFile(albumDir: File?): File? {
val albumArtDir = albumArtDirectory val albumArtDir = albumArtDirectory
val key = getAlbumArtKey(albumDir, true) val key = getAlbumArtKey(albumDir, true)
return if (key == null || albumArtDir == null) { return if (key == null) {
null null
} else File(albumArtDir, key) } else File(albumArtDir, key)
} }
@ -173,7 +164,7 @@ object FileUtil {
*/ */
fun getAlbumArtFile(cacheKey: String?): File? { fun getAlbumArtFile(cacheKey: String?): File? {
val albumArtDir = albumArtDirectory val albumArtDir = albumArtDirectory
return if (albumArtDir == null || cacheKey == null) { return if (cacheKey == null) {
null null
} else File(albumArtDir, cacheKey) } else File(albumArtDir, cacheKey)
} }
@ -195,9 +186,10 @@ object FileUtil {
val f = File(fileSystemSafeDir(entry.path)) val f = File(fileSystemSafeDir(entry.path))
dir = File( dir = File(
String.format( String.format(
Locale.ROOT,
"%s/%s", "%s/%s",
musicDirectory.path, musicDirectory.path,
if (entry.isDirectory) f.path else f.parent if (entry.isDirectory) f.path else f.parent!!
) )
) )
} else { } else {
@ -206,20 +198,21 @@ object FileUtil {
if ("unnamed" == album) { if ("unnamed" == album) {
album = fileSystemSafe(entry.title!!) album = fileSystemSafe(entry.title!!)
} }
dir = File(String.format("%s/%s/%s", musicDirectory.path, artist, album)) dir = File(String.format(Locale.ROOT, "%s/%s/%s", musicDirectory.path, artist, album))
} }
return dir return dir
} }
fun createDirectoryForParent(file: File) { fun createDirectoryForParent(file: File) {
val dir = file.parentFile val dir = file.parentFile
if (!dir.exists()) { if (dir != null && !dir.exists()) {
if (!dir.mkdirs()) { if (!dir.mkdirs()) {
Timber.e("Failed to create directory %s", dir) Timber.e("Failed to create directory %s", dir)
} }
} }
} }
@Suppress("SameParameterValue")
private fun getOrCreateDirectory(name: String): File { private fun getOrCreateDirectory(name: String): File {
val dir = File(ultrasonicDirectory, name) val dir = File(ultrasonicDirectory, name)
if (!dir.exists() && !dir.mkdirs()) { if (!dir.exists() && !dir.mkdirs()) {
@ -228,34 +221,36 @@ object FileUtil {
return dir return dir
} }
// After Android M, the location of the files must be queried differently. GetExternalFilesDir will always return a directory which Ultrasonic can access without any extra privileges. // After Android M, the location of the files must be queried differently.
@JvmStatic // GetExternalFilesDir will always return a directory which Ultrasonic
val ultrasonicDirectory: File? // can access without any extra privileges.
@JvmStatic
val ultrasonicDirectory: File?
get() = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) File( get() = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) File(
Environment.getExternalStorageDirectory(), Environment.getExternalStorageDirectory(),
"Android/data/org.moire.ultrasonic" "Android/data/org.moire.ultrasonic"
) else applicationContext().getExternalFilesDir(null) ) else UApp.applicationContext().getExternalFilesDir(null)
// After Android M, the location of the files must be queried differently. GetExternalFilesDir will always return a directory which Ultrasonic can access without any extra privileges. // After Android M, the location of the files must be queried differently.
@JvmStatic // GetExternalFilesDir will always return a directory which Ultrasonic
val defaultMusicDirectory: File // can access without any extra privileges.
@JvmStatic
val defaultMusicDirectory: File
get() = getOrCreateDirectory("music") get() = getOrCreateDirectory("music")
@JvmStatic @JvmStatic
val musicDirectory: File val musicDirectory: File
get() { get() {
val defaultMusicDirectory = defaultMusicDirectory val path = Util.getPreferences()
val path = getPreferences().getString( .getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, defaultMusicDirectory.path)
Constants.PREFERENCES_KEY_CACHE_LOCATION, val dir = File(path!!)
defaultMusicDirectory.path
)
val dir = File(path)
val hasAccess = ensureDirectoryExistsAndIsReadWritable(dir) val hasAccess = ensureDirectoryExistsAndIsReadWritable(dir)
if (!hasAccess) permissionUtil.value.handlePermissionFailed(null) if (!hasAccess) permissionUtil.value.handlePermissionFailed(null)
return if (hasAccess) dir else defaultMusicDirectory return if (hasAccess) dir else defaultMusicDirectory
} }
@JvmStatic @JvmStatic
fun ensureDirectoryExistsAndIsReadWritable(dir: File?): Boolean { @Suppress("ReturnCount")
fun ensureDirectoryExistsAndIsReadWritable(dir: File?): Boolean {
if (dir == null) { if (dir == null) {
return false return false
} }
@ -287,12 +282,12 @@ object FileUtil {
* Makes a given filename safe by replacing special characters like slashes ("/" and "\") * Makes a given filename safe by replacing special characters like slashes ("/" and "\")
* with dashes ("-"). * with dashes ("-").
* *
* @param filename The filename in question. * @param name The filename in question.
* @return The filename with special characters replaced by hyphens. * @return The filename with special characters replaced by hyphens.
*/ */
private fun fileSystemSafe(filename: String): String { private fun fileSystemSafe(name: String): String {
var filename = filename var filename = name
if (filename == null || filename.trim { it <= ' ' }.isEmpty()) { if (filename.trim { it <= ' ' }.isEmpty()) {
return "unnamed" return "unnamed"
} }
for (s in FILE_SYSTEM_UNSAFE) { for (s in FILE_SYSTEM_UNSAFE) {
@ -308,29 +303,29 @@ object FileUtil {
* @param path The path of the directory in question. * @param path The path of the directory in question.
* @return The the directory name with special characters replaced by hyphens. * @return The the directory name with special characters replaced by hyphens.
*/ */
private fun fileSystemSafeDir(path: String?): String? { private fun fileSystemSafeDir(path: String?): String {
var path = path var filepath = path
if (path == null || path.trim { it <= ' ' }.isEmpty()) { if (filepath == null || filepath.trim { it <= ' ' }.isEmpty()) {
return "" return ""
} }
for (s in FILE_SYSTEM_UNSAFE_DIR) { for (s in FILE_SYSTEM_UNSAFE_DIR) {
path = path!!.replace(s, "-") filepath = filepath!!.replace(s, "-")
} }
return path return filepath!!
} }
/** /**
* Similar to [File.listFiles], but returns a sorted set. * Similar to [File.listFiles], but returns a sorted set.
* 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: File): SortedSet<File> { fun listFiles(dir: File): SortedSet<File> {
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)
return TreeSet() return TreeSet()
} }
return TreeSet(Arrays.asList(*files)) return TreeSet(files.asList())
} }
fun listMediaFiles(dir: File): SortedSet<File> { fun listMediaFiles(dir: File): SortedSet<File> {
@ -347,7 +342,8 @@ object FileUtil {
private fun isMediaFile(file: File): Boolean { private fun isMediaFile(file: File): Boolean {
val extension = getExtension(file.name) val extension = getExtension(file.name)
return MUSIC_FILE_EXTENSIONS.contains(extension) || VIDEO_FILE_EXTENSIONS.contains(extension) return MUSIC_FILE_EXTENSIONS.contains(extension) ||
VIDEO_FILE_EXTENSIONS.contains(extension)
} }
fun isPlaylistFile(file: File): Boolean { fun isPlaylistFile(file: File): Boolean {
@ -364,7 +360,7 @@ object FileUtil {
*/ */
fun getExtension(name: String): String { fun getExtension(name: String): String {
val index = name.lastIndexOf('.') val index = name.lastIndexOf('.')
return if (index == -1) "" else name.substring(index + 1).toLowerCase() return if (index == -1) "" else name.substring(index + 1).lowercase(Locale.ROOT)
} }
/** /**
@ -386,7 +382,7 @@ object FileUtil {
* @return The .partial file name * @return The .partial file name
*/ */
fun getPartialFile(name: String): String { fun getPartialFile(name: String): String {
return String.format("%s.partial.%s", getBaseName(name), getExtension(name)) return String.format(Locale.ROOT, "%s.partial.%s", getBaseName(name), getExtension(name))
} }
/** /**
@ -396,11 +392,11 @@ object FileUtil {
* @return The .complete file name * @return The .complete file name
*/ */
fun getCompleteFile(name: String): String { fun getCompleteFile(name: String): String {
return String.format("%s.complete.%s", getBaseName(name), getExtension(name)) return String.format(Locale.ROOT, "%s.complete.%s", getBaseName(name), getExtension(name))
} }
@JvmStatic @JvmStatic
fun <T : Serializable?> serialize(context: Context, obj: T, fileName: String?): Boolean { fun <T : Serializable?> serialize(context: Context, obj: T, fileName: String): Boolean {
val file = File(context.cacheDir, fileName) val file = File(context.cacheDir, fileName)
var out: ObjectOutputStream? = null var out: ObjectOutputStream? = null
return try { return try {
@ -408,32 +404,62 @@ object FileUtil {
out.writeObject(obj) out.writeObject(obj)
Timber.i("Serialized object to %s", file) Timber.i("Serialized object to %s", file)
true true
} catch (x: Throwable) { } catch (ignored: Exception) {
Timber.w("Failed to serialize object to %s", file) Timber.w("Failed to serialize object to %s", file)
false false
} finally { } finally {
close(out) Util.close(out)
} }
} }
@Suppress("UNCHECKED_CAST")
@JvmStatic @JvmStatic
fun <T : Serializable?> deserialize(context: Context, fileName: String?): T? { fun <T : Serializable?> deserialize(context: Context, fileName: String): T? {
val file = File(context.cacheDir, fileName) val file = File(context.cacheDir, fileName)
if (!file.exists() || !file.isFile) { if (!file.exists() || !file.isFile) {
return null return null
} }
var `in`: ObjectInputStream? = null var inStream: ObjectInputStream? = null
return try { return try {
`in` = ObjectInputStream(FileInputStream(file)) inStream = ObjectInputStream(FileInputStream(file))
val `object` = `in`.readObject() val readObject = inStream.readObject()
val result = `object` as T val result = readObject as T
Timber.i("Deserialized object from %s", file) Timber.i("Deserialized object from %s", file)
result result
} catch (x: Throwable) { } catch (all: Throwable) {
Timber.w(x, "Failed to deserialize object from %s", file) Timber.w(all, "Failed to deserialize object from %s", file)
null null
} finally { } finally {
close(`in`) Util.close(inStream)
}
}
fun savePlaylist(
playlistFile: File?,
playlist: MusicDirectory,
name: String
) {
val fw = FileWriter(playlistFile)
val bw = BufferedWriter(fw)
try {
fw.write("#EXTM3U\n")
for (e in playlist.getChildren()) {
var filePath = getSongFile(e).absolutePath
if (!File(filePath).exists()) {
val ext = getExtension(filePath)
val base = getBaseName(filePath)
filePath = "$base.complete.$ext"
}
fw.write(filePath + "\n")
}
} catch (e: IOException) {
Timber.w("Failed to save playlist: %s", name)
throw e
} finally {
bw.close()
fw.close()
} }
} }
} }

View File

@ -1,47 +0,0 @@
/*
* FileUtil.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.util
import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
import java.io.IOException
import org.moire.ultrasonic.domain.MusicDirectory
import timber.log.Timber
// TODO: Convert FileUtil.java and merge into here.
object FileUtilKt {
fun savePlaylist(
playlistFile: File?,
playlist: MusicDirectory,
name: String
) {
val fw = FileWriter(playlistFile)
val bw = BufferedWriter(fw)
try {
fw.write("#EXTM3U\n")
for (e in playlist.getChildren()) {
var filePath = FileUtil.getSongFile(e).absolutePath
if (!File(filePath).exists()) {
val ext = FileUtil.getExtension(filePath)
val base = FileUtil.getBaseName(filePath)
filePath = "$base.complete.$ext"
}
fw.write(filePath + "\n")
}
} catch (e: IOException) {
Timber.w("Failed to save playlist: %s", name)
throw e
} finally {
bw.close()
fw.close()
}
}
}

View File

@ -40,14 +40,11 @@ import android.widget.Toast
import androidx.annotation.AnyRes import androidx.annotation.AnyRes
import androidx.media.utils.MediaConstants import androidx.media.utils.MediaConstants
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import java.io.ByteArrayOutputStream
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.io.UnsupportedEncodingException import java.io.UnsupportedEncodingException
import java.security.MessageDigest import java.security.MessageDigest
import java.text.DecimalFormat import java.text.DecimalFormat