Merge changes

This commit is contained in:
Nite 2021-11-19 19:09:27 +01:00
commit 3f570636dd
No known key found for this signature in database
GPG Key ID: 1D1AD59B1C6386C1
25 changed files with 199 additions and 253 deletions

View File

@ -7,7 +7,7 @@ ext.versions = [
navigation : "2.3.5", navigation : "2.3.5",
gradlePlugin : "4.2.2", gradlePlugin : "4.2.2",
androidxcore : "1.5.0", androidxcore : "1.6.0",
ktlint : "0.37.1", ktlint : "0.37.1",
ktlintGradle : "10.2.0", ktlintGradle : "10.2.0",
detekt : "1.18.1", detekt : "1.18.1",
@ -23,7 +23,7 @@ ext.versions = [
room : "2.3.0", room : "2.3.0",
kotlin : "1.5.31", kotlin : "1.5.31",
kotlinxCoroutines : "1.5.2-native-mt", kotlinxCoroutines : "1.5.2-native-mt",
viewModelKtx : "2.2.0", viewModelKtx : "2.3.0",
retrofit : "2.6.4", retrofit : "2.6.4",
jackson : "2.9.5", jackson : "2.9.5",
@ -35,7 +35,7 @@ ext.versions = [
junit4 : "4.13.2", junit4 : "4.13.2",
junit5 : "5.8.1", junit5 : "5.8.1",
mockito : "4.0.0", mockito : "4.0.0",
mockitoKotlin : "3.2.0", mockitoKotlin : "4.0.0",
kluent : "1.68", kluent : "1.68",
apacheCodecs : "1.15", apacheCodecs : "1.15",
robolectric : "4.6.1", robolectric : "4.6.1",

View File

@ -1,6 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<issues format="5" by="lint 4.2.2" client="gradle" variant="release" version="4.2.2"> <issues format="5" by="lint 4.2.2" client="gradle" variant="release" version="4.2.2">
<issue
id="ObsoleteLintCustomCheck"
message="Lint found an issue registry (`androidx.appcompat.AppCompatIssueRegistry`) which is older than the current API level; these checks may not work correctly.&#xA;&#xA;Recompile the checks against the latest version. Custom check API version is 7 (4.0), current lint API level is 8 (4.1)">
<location
file="../../../../.gradle/caches/transforms-3/2f10f1fe0ff7ab74304d702879de0789/transformed/appcompat-1.2.0/jars/lint.jar"/>
</issue>
<issue
id="ObsoleteLintCustomCheck"
message="Lint found an issue registry (`timber.lint.TimberIssueRegistry`) which is older than the current API level; these checks may not work correctly.&#xA;&#xA;Recompile the checks against the latest version. Custom check API version is 1 (3.1), current lint API level is 8 (4.1)">
<location
file="../../../../.gradle/caches/transforms-3/e9d816753daf5450613abd98ccf3b80c/transformed/jetified-timber-4.7.1/jars/lint.jar"/>
</issue>
<issue <issue
id="IncludeLayoutParam" id="IncludeLayoutParam"
message="Layout parameter `layout_gravity` ignored unless both `layout_width` and `layout_height` are also specified on `&lt;include>` tag" message="Layout parameter `layout_gravity` ignored unless both `layout_width` and `layout_height` are also specified on `&lt;include>` tag"
@ -188,39 +202,6 @@
column="5"/> column="5"/>
</issue> </issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.btn_bg` appears to be unused"
errorLine1=" &lt;item android:drawable=&quot;@color/ics_opaque&quot; android:state_pressed=&quot;true&quot;/>"
errorLine2="^">
<location
file="src/main/res/drawable/btn_bg.xml"
line="14"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.color.ics_opaque` appears to be unused"
errorLine1=" &lt;color name=&quot;ics_opaque&quot;>#8033b5e5&lt;/color>"
errorLine2=" ~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/colors.xml"
line="9"
column="12"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.color.md__transparent` appears to be unused"
errorLine1=" &lt;color name=&quot;md__transparent&quot;>#00000000&lt;/color>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/values/colors.xml"
line="10"
column="12"/>
</issue>
<issue <issue
id="UnusedResources" id="UnusedResources"
message="The resource `R.drawable.ic_menu_arrow` appears to be unused" message="The resource `R.drawable.ic_menu_arrow` appears to be unused"
@ -232,39 +213,6 @@
column="1"/> column="1"/>
</issue> </issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.line` appears to be unused"
errorLine1="&lt;shape xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
errorLine2="^">
<location
file="src/main/res/drawable/line.xml"
line="2"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.drawable.line_drawable` appears to be unused"
errorLine1="&lt;selector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;>"
errorLine2="^">
<location
file="src/main/res/drawable/line_drawable.xml"
line="2"
column="1"/>
</issue>
<issue
id="UnusedResources"
message="The resource `R.menu.main` appears to be unused"
errorLine1="&lt;menu"
errorLine2="^">
<location
file="src/main/res/menu/main.xml"
line="2"
column="1"/>
</issue>
<issue <issue
id="UnusedResources" id="UnusedResources"
message="The resource `R.drawable.menu_arrow` appears to be unused" message="The resource `R.drawable.menu_arrow` appears to be unused"
@ -1730,28 +1678,6 @@
column="10"/> column="10"/>
</issue> </issue>
<issue
id="ContentDescription"
message="Missing `contentDescription` attribute on image"
errorLine1=" &lt;ImageView"
errorLine2=" ~~~~~~~~~">
<location
file="src/main/res/layout/now_playing.xml"
line="18"
column="10"/>
</issue>
<issue
id="ContentDescription"
message="Missing `contentDescription` attribute on image"
errorLine1=" &lt;ImageView"
errorLine2=" ~~~~~~~~~">
<location
file="src/main/res/layout/now_playing.xml"
line="55"
column="10"/>
</issue>
<issue <issue
id="ContentDescription" id="ContentDescription"
message="Missing `contentDescription` attribute on image" message="Missing `contentDescription` attribute on image"

View File

@ -2,8 +2,8 @@ package org.moire.ultrasonic.data
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.room.Room import androidx.room.Room
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -24,7 +24,7 @@ import timber.log.Timber
*/ */
class ActiveServerProvider( class ActiveServerProvider(
private val repository: ServerSettingDao private val repository: ServerSettingDao
) { ) : CoroutineScope by CoroutineScope(Dispatchers.IO) {
private var cachedServer: ServerSetting? = null private var cachedServer: ServerSetting? = null
private var cachedDatabase: MetaDatabase? = null private var cachedDatabase: MetaDatabase? = null
private var cachedServerId: Int? = null private var cachedServerId: Int? = null
@ -83,7 +83,7 @@ class ActiveServerProvider(
return return
} }
GlobalScope.launch(Dispatchers.IO) { launch {
val serverId = repository.findByIndex(index)?.id ?: 0 val serverId = repository.findByIndex(index)?.id ?: 0
setActiveServerId(serverId) setActiveServerId(serverId)
} }
@ -133,7 +133,7 @@ class ActiveServerProvider(
* Sets the minimum Subsonic API version of the current server. * Sets the minimum Subsonic API version of the current server.
*/ */
fun setMinimumApiVersion(apiVersion: String) { fun setMinimumApiVersion(apiVersion: String) {
GlobalScope.launch(Dispatchers.IO) { launch {
if (cachedServer != null) { if (cachedServer != null) {
cachedServer!!.minimumApiVersion = apiVersion cachedServer!!.minimumApiVersion = apiVersion
repository.update(cachedServer!!) repository.update(cachedServer!!)

View File

@ -55,6 +55,7 @@ import java.io.File
/** /**
* Shows main app settings. * Shows main app settings.
*/ */
@Suppress("TooManyFunctions")
class SettingsFragment : class SettingsFragment :
PreferenceFragmentCompat(), PreferenceFragmentCompat(),
OnSharedPreferenceChangeListener, OnSharedPreferenceChangeListener,

View File

@ -22,6 +22,7 @@ import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.StorageFile import org.moire.ultrasonic.util.StorageFile
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.util.Util.safeClose
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
@ -187,10 +188,10 @@ class ImageLoader(
outputStream = FileOutputStream(file) outputStream = FileOutputStream(file)
outputStream.write(bytes) outputStream.write(bytes)
} finally { } finally {
Util.close(outputStream) outputStream.safeClose()
} }
} finally { } finally {
Util.close(inputStream) inputStream.safeClose()
} }
} }
} }

View File

@ -5,7 +5,7 @@ import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util.safeClose
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
@ -38,7 +38,7 @@ class FileLoggerTree : Timber.DebugTree() {
// Using base class DebugTree here, we don't want to try to log this into file // Using base class DebugTree here, we don't want to try to log this into file
super.log(6, TAG, String.format("Failed to write log to %s", file), x) super.log(6, TAG, String.format("Failed to write log to %s", file), x)
} finally { } finally {
if (writer != null) Util.close(writer) writer.safeClose()
} }
} }

View File

@ -26,6 +26,7 @@ import org.moire.ultrasonic.util.StorageFile
import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.util.Util.safeClose
import timber.log.Timber import timber.log.Timber
/** /**
@ -133,9 +134,9 @@ class DownloadFile(
fun delete() { fun delete() {
cancelDownload() cancelDownload()
Util.delete(partialFile) FileUtil.delete(partialFile)
Util.delete(completeFile) FileUtil.delete(completeFile)
Util.delete(saveFile) FileUtil.delete(saveFile)
Util.scanMedia(saveFile) Util.scanMedia(saveFile)
} }
@ -149,11 +150,11 @@ class DownloadFile(
fun cleanup(): Boolean { fun cleanup(): Boolean {
var ok = true var ok = true
if (StorageFile.isPathExists(completeFile) || StorageFile.isPathExists(saveFile)) { if (StorageFile.isPathExists(completeFile) || StorageFile.isPathExists(saveFile)) {
ok = Util.delete(partialFile) ok = FileUtil.delete(partialFile)
} }
if (StorageFile.isPathExists(saveFile)) { if (StorageFile.isPathExists(saveFile)) {
ok = ok and Util.delete(completeFile) ok = ok and FileUtil.delete(completeFile)
} }
return ok return ok
@ -168,14 +169,14 @@ class DownloadFile(
private fun doPendingRename() { private fun doPendingRename() {
try { try {
if (saveWhenDone) { if (saveWhenDone) {
Util.renameFile(completeFile, saveFile) FileUtil.renameFile(completeFile, saveFile)
saveWhenDone = false saveWhenDone = false
} else if (completeWhenDone) { } else if (completeWhenDone) {
if (save) { if (save) {
Util.renameFile(partialFile, saveFile) FileUtil.renameFile(partialFile, saveFile)
Util.scanMedia(saveFile) Util.scanMedia(saveFile)
} else { } else {
Util.renameFile(partialFile, completeFile) FileUtil.renameFile(partialFile, completeFile)
} }
completeWhenDone = false completeWhenDone = false
} }
@ -207,7 +208,7 @@ class DownloadFile(
if (isPlaying) { if (isPlaying) {
saveWhenDone = true saveWhenDone = true
} else { } else {
Util.renameFile(completeFile, saveFile) FileUtil.renameFile(completeFile, saveFile)
} }
} else { } else {
Timber.i("%s already exists. Skipping.", completeFile) Timber.i("%s already exists. Skipping.", completeFile)
@ -276,16 +277,16 @@ class DownloadFile(
completeWhenDone = true completeWhenDone = true
} else { } else {
if (save) { if (save) {
Util.renameFile(partialFile, saveFile) FileUtil.renameFile(partialFile, saveFile)
Util.scanMedia(saveFile) Util.scanMedia(saveFile)
} else { } else {
Util.renameFile(partialFile, completeFile) FileUtil.renameFile(partialFile, completeFile)
} }
} }
} catch (all: Exception) { } catch (all: Exception) {
Util.close(outputStream) outputStream.safeClose()
Util.delete(completeFile) FileUtil.delete(completeFile)
Util.delete(saveFile) FileUtil.delete(saveFile)
if (!isCancelled) { if (!isCancelled) {
isFailed = true isFailed = true
if (retryCount > 1) { if (retryCount > 1) {
@ -298,8 +299,8 @@ class DownloadFile(
Timber.w(all, "Failed to download '%s'.", song) Timber.w(all, "Failed to download '%s'.", song)
} }
} finally { } finally {
Util.close(inputStream) inputStream.safeClose()
Util.close(outputStream) outputStream.safeClose()
CacheCleaner().cleanSpace() CacheCleaner().cleanSpace()
downloader.checkDownloads() downloader.checkDownloads()
} }

View File

@ -39,7 +39,7 @@ import org.moire.ultrasonic.domain.Share
import org.moire.ultrasonic.domain.UserInfo import org.moire.ultrasonic.domain.UserInfo
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util.safeClose
import timber.log.Timber import timber.log.Timber
import java.io.FileReader import java.io.FileReader
import java.io.FileWriter import java.io.FileWriter
@ -215,8 +215,8 @@ class OfflineMusicService : MusicService, KoinComponent {
} }
playlist playlist
} finally { } finally {
Util.close(buffer) buffer.safeClose()
Util.close(reader) reader.safeClose()
} }
} }

View File

@ -1,6 +1,5 @@
package org.moire.ultrasonic.util package org.moire.ultrasonic.util
import android.net.Uri
import android.os.AsyncTask import android.os.AsyncTask
import android.os.StatFs import android.os.StatFs
import android.system.Os import android.system.Os
@ -16,7 +15,7 @@ import org.moire.ultrasonic.util.FileUtil.getPlaylistFile
import org.moire.ultrasonic.util.FileUtil.listFiles import org.moire.ultrasonic.util.FileUtil.listFiles
import org.moire.ultrasonic.util.FileUtil.musicDirectory import org.moire.ultrasonic.util.FileUtil.musicDirectory
import org.moire.ultrasonic.util.Settings.cacheSizeMB import org.moire.ultrasonic.util.Settings.cacheSizeMB
import org.moire.ultrasonic.util.Util.delete import org.moire.ultrasonic.util.FileUtil.delete
import org.moire.ultrasonic.util.Util.formatBytes import org.moire.ultrasonic.util.Util.formatBytes
import timber.log.Timber import timber.log.Timber
@ -60,9 +59,12 @@ class CacheCleaner {
Thread.currentThread().name = "BackgroundCleanup" Thread.currentThread().name = "BackgroundCleanup"
val files: MutableList<StorageFile> = ArrayList() val files: MutableList<StorageFile> = ArrayList()
val dirs: MutableList<StorageFile> = ArrayList() val dirs: MutableList<StorageFile> = ArrayList()
findCandidatesForDeletion(musicDirectory, files, dirs) findCandidatesForDeletion(musicDirectory, files, dirs)
sortByAscendingModificationTime(files) sortByAscendingModificationTime(files)
val filesToNotDelete = findFilesToNotDelete() val filesToNotDelete = findFilesToNotDelete()
deleteFiles(files, filesToNotDelete, getMinimumDelete(files), true) deleteFiles(files, filesToNotDelete, getMinimumDelete(files), true)
deleteEmptyDirs(dirs, filesToNotDelete) deleteEmptyDirs(dirs, filesToNotDelete)
} catch (all: RuntimeException) { } catch (all: RuntimeException) {
@ -76,10 +78,14 @@ class CacheCleaner {
override fun doInBackground(vararg params: Void?): Void? { override fun doInBackground(vararg params: Void?): Void? {
try { try {
Thread.currentThread().name = "BackgroundSpaceCleanup" Thread.currentThread().name = "BackgroundSpaceCleanup"
val files: MutableList<StorageFile> = ArrayList() val files: MutableList<StorageFile> = ArrayList()
val dirs: MutableList<StorageFile> = ArrayList() val dirs: MutableList<StorageFile> = ArrayList()
findCandidatesForDeletion(musicDirectory, files, dirs) findCandidatesForDeletion(musicDirectory, files, dirs)
val bytesToDelete = getMinimumDelete(files) val bytesToDelete = getMinimumDelete(files)
if (bytesToDelete > 0L) { if (bytesToDelete > 0L) {
sortByAscendingModificationTime(files) sortByAscendingModificationTime(files)
val filesToNotDelete = findFilesToNotDelete() val filesToNotDelete = findFilesToNotDelete()
@ -99,12 +105,15 @@ class CacheCleaner {
ActiveServerProvider::class.java ActiveServerProvider::class.java
) )
Thread.currentThread().name = "BackgroundPlaylistsCleanup" Thread.currentThread().name = "BackgroundPlaylistsCleanup"
val server = activeServerProvider.value.getActiveServer().name val server = activeServerProvider.value.getActiveServer().name
val playlistFiles = listFiles(getPlaylistDirectory(server)) val playlistFiles = listFiles(getPlaylistDirectory(server))
val playlists = params[0] val playlists = params[0]
for ((_, name) in playlists) { for ((_, name) in playlists) {
playlistFiles.remove(getPlaylistFile(server, name)) playlistFiles.remove(getPlaylistFile(server, name))
} }
for (playlist in playlistFiles) { for (playlist in playlistFiles) {
playlist.delete() playlist.delete()
} }
@ -119,9 +128,8 @@ class CacheCleaner {
private const val MIN_FREE_SPACE = 500 * 1024L * 1024L private const val MIN_FREE_SPACE = 500 * 1024L * 1024L
private fun deleteEmptyDirs(dirs: Iterable<StorageFile>, doNotDelete: Collection<String>) { private fun deleteEmptyDirs(dirs: Iterable<StorageFile>, doNotDelete: Collection<String>) {
for (dir in dirs) { for (dir in dirs) {
if (doNotDelete.contains(dir.getPath())) { if (doNotDelete.contains(dir.getPath())) continue
continue
}
var children = dir.listFiles() var children = dir.listFiles()
if (children != null) { if (children != null) {
// No songs left in the folder // No songs left in the folder
@ -140,11 +148,11 @@ class CacheCleaner {
} }
private fun getMinimumDelete(files: List<StorageFile>): Long { private fun getMinimumDelete(files: List<StorageFile>): Long {
if (files.isEmpty()) { if (files.isEmpty()) return 0L
return 0L
}
val cacheSizeBytes = cacheSizeMB * 1024L * 1024L val cacheSizeBytes = cacheSizeMB * 1024L * 1024L
var bytesUsedBySubsonic = 0L var bytesUsedBySubsonic = 0L
for (file in files) { for (file in files) {
bytesUsedBySubsonic += file.length() bytesUsedBySubsonic += file.length()
} }
@ -154,6 +162,7 @@ class CacheCleaner {
val minFsAvailability: Long val minFsAvailability: Long
val bytesTotalFs: Long val bytesTotalFs: Long
val bytesAvailableFs: Long val bytesAvailableFs: Long
if (files[0].isRawFile()) { if (files[0].isRawFile()) {
val stat = StatFs(files[0].getRawFilePath()) val stat = StatFs(files[0].getRawFilePath())
bytesTotalFs = stat.blockCountLong * stat.blockSizeLong bytesTotalFs = stat.blockCountLong * stat.blockSizeLong
@ -169,6 +178,7 @@ class CacheCleaner {
minFsAvailability = bytesTotalFs - MIN_FREE_SPACE minFsAvailability = bytesTotalFs - MIN_FREE_SPACE
descriptor.close() descriptor.close()
} }
val bytesToDeleteCacheLimit = (bytesUsedBySubsonic - cacheSizeBytes).coerceAtLeast(0L) val bytesToDeleteCacheLimit = (bytesUsedBySubsonic - cacheSizeBytes).coerceAtLeast(0L)
val bytesToDeleteFsLimit = (bytesUsedFs - minFsAvailability).coerceAtLeast(0L) val bytesToDeleteFsLimit = (bytesUsedFs - minFsAvailability).coerceAtLeast(0L)
val bytesToDelete = bytesToDeleteCacheLimit.coerceAtLeast(bytesToDeleteFsLimit) val bytesToDelete = bytesToDeleteCacheLimit.coerceAtLeast(bytesToDeleteFsLimit)
@ -181,6 +191,7 @@ class CacheCleaner {
Timber.i("Cache limit : %s", formatBytes(cacheSizeBytes)) Timber.i("Cache limit : %s", formatBytes(cacheSizeBytes))
Timber.i("Cache size before : %s", formatBytes(bytesUsedBySubsonic)) Timber.i("Cache size before : %s", formatBytes(bytesUsedBySubsonic))
Timber.i("Minimum to delete : %s", formatBytes(bytesToDelete)) Timber.i("Minimum to delete : %s", formatBytes(bytesToDelete))
return bytesToDelete return bytesToDelete
} }
@ -203,6 +214,7 @@ class CacheCleaner {
return return
} }
var bytesDeleted = 0L var bytesDeleted = 0L
for (file in files) { for (file in files) {
if (!deletePartials && bytesDeleted > bytesToDelete) break if (!deletePartials && bytesDeleted > bytesToDelete) break
if (bytesToDelete > bytesDeleted || deletePartials && isPartial(file)) { if (bytesToDelete > bytesDeleted || deletePartials && isPartial(file)) {
@ -244,10 +256,12 @@ class CacheCleaner {
val downloader = inject<Downloader>( val downloader = inject<Downloader>(
Downloader::class.java Downloader::class.java
) )
for (downloadFile in downloader.value.all) { for (downloadFile in downloader.value.all) {
filesToNotDelete.add(downloadFile.partialFile) filesToNotDelete.add(downloadFile.partialFile)
filesToNotDelete.add(downloadFile.completeOrSaveFile) filesToNotDelete.add(downloadFile.completeOrSaveFile)
} }
filesToNotDelete.add(musicDirectory.getPath()) filesToNotDelete.add(musicDirectory.getPath())
return filesToNotDelete return filesToNotDelete
} }

View File

@ -26,9 +26,11 @@ import java.util.TreeSet
import java.util.regex.Pattern import java.util.regex.Pattern
import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.util.Util.safeClose
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
@Suppress("TooManyFunctions")
object FileUtil { object FileUtil {
private val FILE_SYSTEM_UNSAFE = arrayOf("/", "\\", "..", ":", "\"", "?", "*", "<", ">", "|") private val FILE_SYSTEM_UNSAFE = arrayOf("/", "\\", "..", ":", "\"", "?", "*", "<", ">", "|")
@ -423,7 +425,7 @@ object FileUtil {
Timber.w("Failed to serialize object to %s", file) Timber.w("Failed to serialize object to %s", file)
false false
} finally { } finally {
Util.close(out) out.safeClose()
} }
} }
@ -445,7 +447,7 @@ object FileUtil {
Timber.w(all, "Failed to deserialize object from %s", file) Timber.w(all, "Failed to deserialize object from %s", file)
null null
} finally { } finally {
Util.close(inStream) inStream.safeClose()
} }
} }
@ -473,8 +475,39 @@ object FileUtil {
Timber.w("Failed to save playlist: %s", name) Timber.w("Failed to save playlist: %s", name)
throw e throw e
} finally { } finally {
bw.close() bw.safeClose()
fw.close() fw.safeClose()
} }
} }
@JvmStatic
@Throws(IOException::class)
fun renameFile(from: String, to: String) {
StorageFile.rename(from, to)
}
@JvmStatic
fun delete(file: File?): Boolean {
if (file != null && file.exists()) {
if (!file.delete()) {
Timber.w("Failed to delete file %s", file)
return false
}
Timber.i("Deleted file %s", file)
}
return true
}
@JvmStatic
fun delete(file: String?): Boolean {
if (file != null && StorageFile.isPathExists(file)) {
if (!StorageFile.getFromPath(file).delete()) {
Timber.w("Failed to delete file %s", file)
return false
}
Timber.i("Deleted file %s", file)
}
return true
}
} }

View File

@ -9,7 +9,6 @@ package org.moire.ultrasonic.util
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.ConnectivityManager
import android.os.Build import android.os.Build
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import java.util.regex.Pattern import java.util.regex.Pattern
@ -70,16 +69,20 @@ object Settings {
@JvmStatic @JvmStatic
val maxBitRate: Int val maxBitRate: Int
get() { get() {
val manager = Util.getConnectivityManager() val network = Util.networkInfo()
val networkInfo = manager.activeNetworkInfo ?: return 0
val wifi = networkInfo.type == ConnectivityManager.TYPE_WIFI if (!network.connected) return 0
val preferences = preferences
return preferences.getString( if (network.unmetered) {
if (wifi) Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI return maxWifiBitRate
else Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE, } else {
"0" return maxMobileBitRate
)!!.toInt()
} }
}
private var maxWifiBitRate by StringIntSetting(Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI)
private var maxMobileBitRate by StringIntSetting(Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE)
@JvmStatic @JvmStatic
val preloadCount: Int val preloadCount: Int

View File

@ -3,6 +3,7 @@ package org.moire.ultrasonic.util
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import java.io.PrintWriter import java.io.PrintWriter
import org.moire.ultrasonic.util.Util.safeClose
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
@ -34,7 +35,7 @@ class SubsonicUncaughtExceptionHandler(
} catch (x: Throwable) { } catch (x: Throwable) {
Timber.e(x, "Failed to write stack trace to %s", file) Timber.e(x, "Failed to write stack trace to %s", file)
} finally { } finally {
Util.close(printWriter) printWriter.safeClose()
defaultHandler?.uncaughtException(thread, throwable) defaultHandler?.uncaughtException(thread, throwable)
} }
} }

View File

@ -23,9 +23,13 @@ import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED
import android.net.Uri import android.net.Uri
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.WifiLock import android.net.wifi.WifiManager.WifiLock
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.os.Parcelable import android.os.Parcelable
@ -40,6 +44,7 @@ import androidx.annotation.AnyRes
import androidx.media.utils.MediaConstants import androidx.media.utils.MediaConstants
import java.io.Closeable import java.io.Closeable
import java.io.IOException import java.io.IOException
import java.io.File
import java.io.UnsupportedEncodingException import java.io.UnsupportedEncodingException
import java.security.MessageDigest import java.security.MessageDigest
import java.text.DecimalFormat import java.text.DecimalFormat
@ -57,7 +62,6 @@ import org.moire.ultrasonic.domain.PlayerState
import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.domain.SearchResult
import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.DownloadFile
import timber.log.Timber import timber.log.Timber
import java.io.File
private const val LINE_LENGTH = 60 private const val LINE_LENGTH = 60
private const val DEGRADE_PRECISION_AFTER = 10 private const val DEGRADE_PRECISION_AFTER = 10
@ -98,7 +102,7 @@ object Util {
when (Settings.theme.lowercase()) { when (Settings.theme.lowercase()) {
Constants.PREFERENCES_KEY_THEME_DARK, Constants.PREFERENCES_KEY_THEME_DARK,
"fullscreen" -> { "fullscreen" -> {
context!!.setTheme(R.style.UltrasonicTheme) context!!.setTheme(R.style.UltrasonicTheme_Dark)
} }
Constants.PREFERENCES_KEY_THEME_BLACK -> { Constants.PREFERENCES_KEY_THEME_BLACK -> {
context!!.setTheme(R.style.UltrasonicTheme_Black) context!!.setTheme(R.style.UltrasonicTheme_Black)
@ -110,44 +114,6 @@ object Util {
} }
} }
@JvmStatic
@Throws(IOException::class)
fun renameFile(from: String, to: String) {
StorageFile.rename(from, to)
}
@JvmStatic
fun close(closeable: Closeable?) {
try {
closeable?.close()
} catch (_: Throwable) {
// Ignored
}
}
@JvmStatic
fun delete(file: String?): Boolean {
if (file != null && StorageFile.isPathExists(file)) {
if (!StorageFile.getFromPath(file).delete()) {
Timber.w("Failed to delete file %s", file)
return false
}
Timber.i("Deleted file %s", file)
}
return true
}
fun delete(file: File?): Boolean {
if (file != null && file.exists()) {
if (!file.delete()) {
Timber.w("Failed to delete file %s", file)
return false
}
Timber.i("Deleted file %s", file)
}
return true
}
@JvmStatic @JvmStatic
@JvmOverloads @JvmOverloads
fun toast(context: Context?, messageId: Int, shortDuration: Boolean = true) { fun toast(context: Context?, messageId: Int, shortDuration: Boolean = true) {
@ -356,14 +322,45 @@ object Util {
return null return null
} }
/**
* Check if a usable network for downloading media is available
*
* @return Boolean
*/
@JvmStatic @JvmStatic
fun isNetworkConnected(): Boolean { fun isNetworkConnected(): Boolean {
val manager = getConnectivityManager() val info = networkInfo()
val networkInfo = manager.activeNetworkInfo val isUnmetered = info.unmetered
val connected = networkInfo != null && networkInfo.isConnected
val wifiConnected = connected && networkInfo!!.type == ConnectivityManager.TYPE_WIFI
val wifiRequired = Settings.isWifiRequiredForDownload val wifiRequired = Settings.isWifiRequiredForDownload
return connected && (!wifiRequired || wifiConnected) return info.connected && (!wifiRequired || isUnmetered)
}
/**
* Query connectivity status
*
* @return NetworkInfo object
*/
@Suppress("DEPRECATION")
fun networkInfo(): NetworkInfo {
val manager = getConnectivityManager()
val info = NetworkInfo()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network: Network? = manager.activeNetwork
val capabilities = manager.getNetworkCapabilities(network)
if (capabilities != null) {
info.unmetered = capabilities.hasCapability(NET_CAPABILITY_NOT_METERED)
info.connected = capabilities.hasCapability(NET_CAPABILITY_INTERNET)
}
} else {
val networkInfo = manager.activeNetworkInfo
if (networkInfo != null) {
info.unmetered = networkInfo.type == ConnectivityManager.TYPE_WIFI
info.connected = networkInfo.isConnected
}
}
return info
} }
@JvmStatic @JvmStatic
@ -904,4 +901,23 @@ object Util {
val context = appContext() val context = appContext()
return context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager return context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
} }
/**
* Small data class to store information about the current network
**/
data class NetworkInfo(
var connected: Boolean = false,
var unmetered: Boolean = false
)
/**
* Closes a Closeable while ignoring any errors.
**/
fun Closeable?.safeClose() {
try {
this?.close()
} catch (_: Exception) {
// Ignored
}
}
} }

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2010 The Android Open Source Project Licensed under the
Apache License, Version 2.0 (the "License"); you may not use this file except
in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
OR CONDITIONS OF ANY KIND, either express or implied. See the License for
the specific language governing permissions and limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android" android:exitFadeDuration="@android:integer/config_mediumAnimTime">
<item android:drawable="@color/ics_opaque" android:state_pressed="true"/>
<item android:drawable="@color/ics_opaque" android:state_enabled="true" android:state_focused="true"/>
</selector>

View File

@ -4,8 +4,8 @@
<gradient <gradient
android:angle="90" android:angle="90"
android:endColor="#00000000" android:endColor="@android:color/transparent"
android:startColor="#80000000" android:startColor="#73111111"
android:type="linear" /> android:type="linear" />
</shape> </shape>

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/darker_gray" />
<padding
android:top="10dp"
android:left="10dp"
android:right="10dp"
android:bottom="10dp"/>
</shape>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_pressed="true"
android:drawable="@drawable/line"/>
<item
android:drawable="@drawable/line"/>
</selector>

View File

@ -21,7 +21,8 @@
android:layout_height="64.0dip" android:layout_height="64.0dip"
android:focusable="true" android:focusable="true"
android:gravity="center" android:gravity="center"
android:layout_marginStart="6dp" /> android:layout_marginStart="6dp"
android:importantForAccessibility="no"/>
<LinearLayout <LinearLayout
android:layout_width="0.0dp" android:layout_width="0.0dp"
@ -62,7 +63,8 @@
android:layout_weight="0.0" android:layout_weight="0.0"
android:focusable="false" android:focusable="false"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="?attr/media_pause" /> android:src="?attr/media_pause"
android:contentDescription="@string/buttons.play"/>
</LinearLayout> </LinearLayout>

View File

@ -7,7 +7,7 @@
a:id="@+id/select_album_art" a:id="@+id/select_album_art"
a:layout_width="160dip" a:layout_width="160dip"
a:layout_height="160dip" a:layout_height="160dip"
a:layout_alignParentLeft="true" a:layout_alignParentStart="true"
a:layout_alignParentTop="true" a:layout_alignParentTop="true"
a:layout_marginEnd="10dip" a:layout_marginEnd="10dip"
a:contentDescription="@null" a:contentDescription="@null"

View File

@ -26,10 +26,10 @@
a:layout_height="wrap_content" a:layout_height="wrap_content"
a:layout_gravity="left|center_vertical" a:layout_gravity="left|center_vertical"
a:layout_weight="1" a:layout_weight="1"
a:drawablePadding="6dip" a:drawablePadding="4dip"
a:ellipsize="end" a:ellipsize="end"
a:paddingStart="4dip" a:paddingStart="6dip"
a:paddingEnd="2dip" a:paddingEnd="4dip"
a:singleLine="true" a:singleLine="true"
a:textAppearance="?android:attr/textAppearanceMedium"/> a:textAppearance="?android:attr/textAppearanceMedium"/>
@ -55,7 +55,8 @@
a:layout_gravity="left|center_vertical" a:layout_gravity="left|center_vertical"
a:layout_weight="1" a:layout_weight="1"
a:ellipsize="middle" a:ellipsize="middle"
a:paddingStart="4dip" a:paddingStart="1dip"
a:paddingEnd="4dip"
a:singleLine="true" a:singleLine="true"
a:textAppearance="?android:attr/textAppearanceSmall"/> a:textAppearance="?android:attr/textAppearanceSmall"/>

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
<item
a:id="@+id/main_shuffle"
a:icon="?attr/media_shuffle"
app:showAsAction="ifRoom|withText"
a:title="@string/main.shuffle"/>
</menu>

View File

@ -6,8 +6,6 @@
<color name="cyan">#0099cc</color> <color name="cyan">#0099cc</color>
<color name="navigation_header_light">#6200EE</color> <color name="navigation_header_light">#6200EE</color>
<color name="navigation_header_dark">#BB86FC</color> <color name="navigation_header_dark">#BB86FC</color>
<color name="ics_opaque">#8033b5e5</color>
<color name="md__transparent">#00000000</color>
<color name="translucent">#80000000</color> <color name="translucent">#80000000</color>
<color name="background_color_dark">#000000</color> <color name="background_color_dark">#000000</color>
<color name="background_color_grey">#333333</color> <color name="background_color_grey">#333333</color>

View File

@ -339,8 +339,8 @@
<string name="settings.view_refresh_4000">4 seconds</string> <string name="settings.view_refresh_4000">4 seconds</string>
<string name="settings.view_refresh_4500">4.5 seconds</string> <string name="settings.view_refresh_4500">4.5 seconds</string>
<string name="settings.view_refresh_5000">5 seconds</string> <string name="settings.view_refresh_5000">5 seconds</string>
<string name="settings.wifi_required_summary">Only stream media if connected to Wi-Fi</string> <string name="settings.wifi_required_summary">Only download media on unmetered connections</string>
<string name="settings.wifi_required_title">Wi-Fi Streaming Only</string> <string name="settings.wifi_required_title">Download on Wi-Fi only</string>
<string name="song_details.all">%1$s%2$s</string> <string name="song_details.all">%1$s%2$s</string>
<string name="song_details.kbps">%d kbps</string> <string name="song_details.kbps">%d kbps</string>
<string name="util.bytes_format.byte">0 B</string> <string name="util.bytes_format.byte">0 B</string>

View File

@ -64,7 +64,7 @@
<item name="list_selector_holo_selected">@drawable/list_selector_holo_dark_selected</item> <item name="list_selector_holo_selected">@drawable/list_selector_holo_dark_selected</item>
</style> </style>
<style name="UltrasonicTheme" parent="Theme.MaterialComponents"> <style name="UltrasonicTheme.Dark" parent="Theme.MaterialComponents">
<item name="windowActionBar">false</item> <item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item> <item name="windowNoTitle">true</item>
<item name="color_background">@color/background_color_grey</item> <item name="color_background">@color/background_color_grey</item>

View File

@ -19,7 +19,7 @@
a:title="@string/settings.server_scaling_title" a:title="@string/settings.server_scaling_title"
app:iconSpaceReserved="false"/> app:iconSpaceReserved="false"/>
<CheckBoxPreference <CheckBoxPreference
a:defaultValue="true" a:defaultValue="false"
a:key="displayBitrateWithArtist" a:key="displayBitrateWithArtist"
a:summary="@string/settings.display_bitrate_summary" a:summary="@string/settings.display_bitrate_summary"
a:title="@string/settings.display_bitrate" a:title="@string/settings.display_bitrate"