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

View File

@ -1,6 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<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
id="IncludeLayoutParam"
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"/>
</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
id="UnusedResources"
message="The resource `R.drawable.ic_menu_arrow` appears to be unused"
@ -232,39 +213,6 @@
column="1"/>
</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
id="UnusedResources"
message="The resource `R.drawable.menu_arrow` appears to be unused"
@ -1730,28 +1678,6 @@
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="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
id="ContentDescription"
message="Missing `contentDescription` attribute on image"

View File

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

View File

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

View File

@ -22,6 +22,7 @@ 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
@ -187,10 +188,10 @@ class ImageLoader(
outputStream = FileOutputStream(file)
outputStream.write(bytes)
} finally {
Util.close(outputStream)
outputStream.safeClose()
}
} finally {
Util.close(inputStream)
inputStream.safeClose()
}
}
}

View File

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

View File

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

View File

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

View File

@ -26,9 +26,11 @@ import java.util.TreeSet
import java.util.regex.Pattern
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.util.Util.safeClose
import timber.log.Timber
import java.io.File
@Suppress("TooManyFunctions")
object FileUtil {
private val FILE_SYSTEM_UNSAFE = arrayOf("/", "\\", "..", ":", "\"", "?", "*", "<", ">", "|")
@ -423,7 +425,7 @@ object FileUtil {
Timber.w("Failed to serialize object to %s", file)
false
} finally {
Util.close(out)
out.safeClose()
}
}
@ -445,7 +447,7 @@ object FileUtil {
Timber.w(all, "Failed to deserialize object from %s", file)
null
} finally {
Util.close(inStream)
inStream.safeClose()
}
}
@ -473,8 +475,39 @@ object FileUtil {
Timber.w("Failed to save playlist: %s", name)
throw e
} finally {
bw.close()
fw.close()
bw.safeClose()
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.SharedPreferences
import android.net.ConnectivityManager
import android.os.Build
import androidx.preference.PreferenceManager
import java.util.regex.Pattern
@ -70,17 +69,21 @@ object Settings {
@JvmStatic
val maxBitRate: Int
get() {
val manager = Util.getConnectivityManager()
val networkInfo = manager.activeNetworkInfo ?: return 0
val wifi = networkInfo.type == ConnectivityManager.TYPE_WIFI
val preferences = preferences
return preferences.getString(
if (wifi) Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI
else Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE,
"0"
)!!.toInt()
val network = Util.networkInfo()
if (!network.connected) return 0
if (network.unmetered) {
return maxWifiBitRate
} else {
return maxMobileBitRate
}
}
private var maxWifiBitRate by StringIntSetting(Constants.PREFERENCES_KEY_MAX_BITRATE_WIFI)
private var maxMobileBitRate by StringIntSetting(Constants.PREFERENCES_KEY_MAX_BITRATE_MOBILE)
@JvmStatic
val preloadCount: Int
get() {

View File

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

View File

@ -23,9 +23,13 @@ import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.media.MediaScannerConnection
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.wifi.WifiManager
import android.net.wifi.WifiManager.WifiLock
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Parcelable
@ -40,6 +44,7 @@ import androidx.annotation.AnyRes
import androidx.media.utils.MediaConstants
import java.io.Closeable
import java.io.IOException
import java.io.File
import java.io.UnsupportedEncodingException
import java.security.MessageDigest
import java.text.DecimalFormat
@ -57,7 +62,6 @@ import org.moire.ultrasonic.domain.PlayerState
import org.moire.ultrasonic.domain.SearchResult
import org.moire.ultrasonic.service.DownloadFile
import timber.log.Timber
import java.io.File
private const val LINE_LENGTH = 60
private const val DEGRADE_PRECISION_AFTER = 10
@ -98,7 +102,7 @@ object Util {
when (Settings.theme.lowercase()) {
Constants.PREFERENCES_KEY_THEME_DARK,
"fullscreen" -> {
context!!.setTheme(R.style.UltrasonicTheme)
context!!.setTheme(R.style.UltrasonicTheme_Dark)
}
Constants.PREFERENCES_KEY_THEME_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
@JvmOverloads
fun toast(context: Context?, messageId: Int, shortDuration: Boolean = true) {
@ -356,14 +322,45 @@ object Util {
return null
}
/**
* Check if a usable network for downloading media is available
*
* @return Boolean
*/
@JvmStatic
fun isNetworkConnected(): Boolean {
val manager = getConnectivityManager()
val networkInfo = manager.activeNetworkInfo
val connected = networkInfo != null && networkInfo.isConnected
val wifiConnected = connected && networkInfo!!.type == ConnectivityManager.TYPE_WIFI
val info = networkInfo()
val isUnmetered = info.unmetered
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
@ -904,4 +901,23 @@ object Util {
val context = appContext()
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
android:angle="90"
android:endColor="#00000000"
android:startColor="#80000000"
android:endColor="@android:color/transparent"
android:startColor="#73111111"
android:type="linear" />
</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

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

View File

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

View File

@ -26,10 +26,10 @@
a:layout_height="wrap_content"
a:layout_gravity="left|center_vertical"
a:layout_weight="1"
a:drawablePadding="6dip"
a:drawablePadding="4dip"
a:ellipsize="end"
a:paddingStart="4dip"
a:paddingEnd="2dip"
a:paddingStart="6dip"
a:paddingEnd="4dip"
a:singleLine="true"
a:textAppearance="?android:attr/textAppearanceMedium"/>
@ -55,7 +55,8 @@
a:layout_gravity="left|center_vertical"
a:layout_weight="1"
a:ellipsize="middle"
a:paddingStart="4dip"
a:paddingStart="1dip"
a:paddingEnd="4dip"
a:singleLine="true"
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="navigation_header_light">#6200EE</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="background_color_dark">#000000</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_4500">4.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_title">Wi-Fi Streaming Only</string>
<string name="settings.wifi_required_summary">Only download media on unmetered connections</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.kbps">%d kbps</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>
</style>
<style name="UltrasonicTheme" parent="Theme.MaterialComponents">
<style name="UltrasonicTheme.Dark" parent="Theme.MaterialComponents">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="color_background">@color/background_color_grey</item>

View File

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