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
MagicNumber:
# 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
ignorePropertyDeclaration: true
UnnecessaryAbstractClass:

View File

@ -10,6 +10,12 @@ package org.moire.ultrasonic.service
import android.net.wifi.WifiManager.WifiLock
import android.text.TextUtils
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.inject
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.Util
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.

View File

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

View File

@ -1,61 +1,47 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
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
* FileUtil.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.util
import android.content.Context
import android.os.Build
import android.os.Environment
import android.text.TextUtils
import org.koin.java.KoinJavaComponent.inject
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.BufferedWriter
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.FileWriter
import java.io.IOException
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.io.Serializable
import java.util.Arrays
import java.util.Locale
import java.util.SortedSet
import java.util.TreeSet
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 {
private val FILE_SYSTEM_UNSAFE = arrayOf("/", "\\", "..", ":", "\"", "?", "*", "<", ">", "|")
private val FILE_SYSTEM_UNSAFE_DIR = arrayOf("\\", "..", ":", "\"", "?", "*", "<", ">", "|")
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 =
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 TITLE_WITH_TRACK = Pattern.compile("^\\d\\d-.*")
const val SUFFIX_LARGE = ".jpeg"
const val SUFFIX_SMALL = ".jpeg-small"
private val permissionUtil = inject<PermissionUtil>(
private val permissionUtil = KoinJavaComponent.inject<PermissionUtil>(
PermissionUtil::class.java
)
@ -71,8 +57,8 @@ object FileUtil {
val fileName = StringBuilder(256)
val track = song.track
//check if filename already had track number
if (!TITLE_WITH_TRACK.matcher(song.title).matches()) {
// check if filename already had track number
if (song.title != null && !TITLE_WITH_TRACK.matcher(song.title!!).matches()) {
if (track != null) {
if (track < 10) {
fileName.append('0')
@ -90,13 +76,13 @@ object FileUtil {
}
@JvmStatic
fun getPlaylistFile(server: String?, name: String): File {
fun getPlaylistFile(server: String?, name: String): File {
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
val playlistDirectory: File
val playlistDirectory: File
get() {
val playlistDir = File(ultrasonicDirectory, "playlists")
ensureDirectoryExistsAndIsReadWritable(playlistDir)
@ -104,7 +90,12 @@ object FileUtil {
}
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)
return playlistDir
}
@ -136,21 +127,21 @@ object FileUtil {
* @param large Whether to get the key for the large or the default image
* @return String The hash key
*/
fun getAlbumArtKey(albumDir: File?, large: Boolean): String? {
private fun getAlbumArtKey(albumDir: File?, large: Boolean): String? {
if (albumDir == null) {
return null
}
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? {
val albumArtDir = albumArtDirectory
if (albumArtDir == null || username == null) {
if (username == null) {
return null
}
val md5Hex = md5Hex(username)
return File(albumArtDir, String.format("%s%s", md5Hex, SUFFIX_LARGE))
val albumArtDir = albumArtDirectory
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? {
val albumArtDir = albumArtDirectory
val key = getAlbumArtKey(albumDir, true)
return if (key == null || albumArtDir == null) {
return if (key == null) {
null
} else File(albumArtDir, key)
}
@ -173,7 +164,7 @@ object FileUtil {
*/
fun getAlbumArtFile(cacheKey: String?): File? {
val albumArtDir = albumArtDirectory
return if (albumArtDir == null || cacheKey == null) {
return if (cacheKey == null) {
null
} else File(albumArtDir, cacheKey)
}
@ -195,9 +186,10 @@ object FileUtil {
val f = File(fileSystemSafeDir(entry.path))
dir = File(
String.format(
Locale.ROOT,
"%s/%s",
musicDirectory.path,
if (entry.isDirectory) f.path else f.parent
if (entry.isDirectory) f.path else f.parent!!
)
)
} else {
@ -206,20 +198,21 @@ object FileUtil {
if ("unnamed" == album) {
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
}
fun createDirectoryForParent(file: File) {
val dir = file.parentFile
if (!dir.exists()) {
if (dir != null && !dir.exists()) {
if (!dir.mkdirs()) {
Timber.e("Failed to create directory %s", dir)
}
}
}
@Suppress("SameParameterValue")
private fun getOrCreateDirectory(name: String): File {
val dir = File(ultrasonicDirectory, name)
if (!dir.exists() && !dir.mkdirs()) {
@ -228,34 +221,36 @@ object FileUtil {
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.
@JvmStatic
val ultrasonicDirectory: File?
// 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.
@JvmStatic
val ultrasonicDirectory: File?
get() = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) File(
Environment.getExternalStorageDirectory(),
"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.
@JvmStatic
val defaultMusicDirectory: File
// 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.
@JvmStatic
val defaultMusicDirectory: File
get() = getOrCreateDirectory("music")
@JvmStatic
val musicDirectory: File
val musicDirectory: File
get() {
val defaultMusicDirectory = defaultMusicDirectory
val path = getPreferences().getString(
Constants.PREFERENCES_KEY_CACHE_LOCATION,
defaultMusicDirectory.path
)
val dir = File(path)
val path = Util.getPreferences()
.getString(Constants.PREFERENCES_KEY_CACHE_LOCATION, defaultMusicDirectory.path)
val dir = File(path!!)
val hasAccess = ensureDirectoryExistsAndIsReadWritable(dir)
if (!hasAccess) permissionUtil.value.handlePermissionFailed(null)
return if (hasAccess) dir else defaultMusicDirectory
}
@JvmStatic
fun ensureDirectoryExistsAndIsReadWritable(dir: File?): Boolean {
@Suppress("ReturnCount")
fun ensureDirectoryExistsAndIsReadWritable(dir: File?): Boolean {
if (dir == null) {
return false
}
@ -287,12 +282,12 @@ object FileUtil {
* Makes a given filename safe by replacing special characters like slashes ("/" and "\")
* with dashes ("-").
*
* @param filename The filename in question.
* @param name The filename in question.
* @return The filename with special characters replaced by hyphens.
*/
private fun fileSystemSafe(filename: String): String {
var filename = filename
if (filename == null || filename.trim { it <= ' ' }.isEmpty()) {
private fun fileSystemSafe(name: String): String {
var filename = name
if (filename.trim { it <= ' ' }.isEmpty()) {
return "unnamed"
}
for (s in FILE_SYSTEM_UNSAFE) {
@ -308,29 +303,29 @@ object FileUtil {
* @param path The path of the directory in question.
* @return The the directory name with special characters replaced by hyphens.
*/
private fun fileSystemSafeDir(path: String?): String? {
var path = path
if (path == null || path.trim { it <= ' ' }.isEmpty()) {
private fun fileSystemSafeDir(path: String?): String {
var filepath = path
if (filepath == null || filepath.trim { it <= ' ' }.isEmpty()) {
return ""
}
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.
* Never returns `null`, instead a warning is logged, and an empty set is returned.
*/
@JvmStatic
fun listFiles(dir: File): SortedSet<File> {
@JvmStatic
fun listFiles(dir: File): SortedSet<File> {
val files = dir.listFiles()
if (files == null) {
Timber.w("Failed to list children for %s", dir.path)
return TreeSet()
}
return TreeSet(Arrays.asList(*files))
return TreeSet(files.asList())
}
fun listMediaFiles(dir: File): SortedSet<File> {
@ -347,7 +342,8 @@ object FileUtil {
private fun isMediaFile(file: File): Boolean {
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 {
@ -364,7 +360,7 @@ object FileUtil {
*/
fun getExtension(name: String): String {
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
*/
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
*/
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
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)
var out: ObjectOutputStream? = null
return try {
@ -408,32 +404,62 @@ object FileUtil {
out.writeObject(obj)
Timber.i("Serialized object to %s", file)
true
} catch (x: Throwable) {
} catch (ignored: Exception) {
Timber.w("Failed to serialize object to %s", file)
false
} finally {
close(out)
Util.close(out)
}
}
@Suppress("UNCHECKED_CAST")
@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)
if (!file.exists() || !file.isFile) {
return null
}
var `in`: ObjectInputStream? = null
var inStream: ObjectInputStream? = null
return try {
`in` = ObjectInputStream(FileInputStream(file))
val `object` = `in`.readObject()
val result = `object` as T
inStream = ObjectInputStream(FileInputStream(file))
val readObject = inStream.readObject()
val result = readObject as T
Timber.i("Deserialized object from %s", file)
result
} catch (x: Throwable) {
Timber.w(x, "Failed to deserialize object from %s", file)
} catch (all: Throwable) {
Timber.w(all, "Failed to deserialize object from %s", file)
null
} 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.media.utils.MediaConstants
import androidx.preference.PreferenceManager
import java.io.ByteArrayOutputStream
import java.io.Closeable
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.io.UnsupportedEncodingException
import java.security.MessageDigest
import java.text.DecimalFormat