Merge branch 'develop' into ready/rm-viewflipper

This commit is contained in:
Nite 2021-11-18 20:30:20 +01:00 committed by GitHub
commit d84a0a3929
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 314 additions and 433 deletions

View File

@ -4,13 +4,11 @@
<CurrentIssues> <CurrentIssues>
<ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$!append &amp;&amp; !playNext &amp;&amp; !unpin &amp;&amp; !background</ID> <ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.&lt;no name provided&gt;$!append &amp;&amp; !playNext &amp;&amp; !unpin &amp;&amp; !background</ID>
<ID>ComplexCondition:FilePickerAdapter.kt$FilePickerAdapter$currentDirectory.absolutePath == "/" || currentDirectory.absolutePath == "/storage" || currentDirectory.absolutePath == "/storage/emulated" || currentDirectory.absolutePath == "/mnt"</ID> <ID>ComplexCondition:FilePickerAdapter.kt$FilePickerAdapter$currentDirectory.absolutePath == "/" || currentDirectory.absolutePath == "/storage" || currentDirectory.absolutePath == "/storage/emulated" || currentDirectory.absolutePath == "/mnt"</ID>
<ID>ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$Util.getGaplessPlaybackPreference() &amp;&amp; Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.JELLY_BEAN &amp;&amp; ( playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED )</ID>
<ID>ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID> <ID>ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>ComplexMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)</ID> <ID>ComplexMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)</ID>
<ID>ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)</ID> <ID>ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)</ID>
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun enableButtons()</ID> <ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun enableButtons()</ID>
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID> <ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
<ID>FunctionNaming:ThemeChangedEventDistributor.kt$ThemeChangedEventDistributor$fun RaiseThemeChangedEvent()</ID>
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile$String.format("DownloadFile (%s)", song)</ID> <ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile$String.format("DownloadFile (%s)", song)</ID>
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("Download of '%s' was cancelled", song)</ID> <ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("Download of '%s' was cancelled", song)</ID>
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("DownloadTask (%s)", song)</ID> <ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("DownloadTask (%s)", song)</ID>
@ -21,14 +19,12 @@
<ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$String.format("BufferTask (%s)", downloadFile)</ID> <ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$String.format("BufferTask (%s)", downloadFile)</ID>
<ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$String.format("CheckCompletionTask (%s)", downloadFile)</ID> <ID>ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$String.format("CheckCompletionTask (%s)", downloadFile)</ID>
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType)</ID> <ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType)</ID>
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler.&lt;no name provided&gt;$String.format("%s\n\n%s", Util.getShareGreeting(), result.url)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%02d.", trackNumber)</ID> <ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%02d.", trackNumber)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate)</ID> <ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate)</ID>
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s &gt; %s", suffix, transcodedSuffix)</ID> <ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s &gt; %s", suffix, transcodedSuffix)</ID>
<ID>LargeClass:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID> <ID>LargeClass:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
<ID>LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID> <ID>LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID> <ID>LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)</ID>
<ID>LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File)</ID>
<ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean)</ID> <ID>LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean)</ID>
<ID>LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?)</ID> <ID>LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?)</ID>
<ID>LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken )</ID> <ID>LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken )</ID>
@ -39,27 +35,21 @@
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID> <ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
<ID>LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, private var data: Array&lt;ServerSetting&gt;, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -&gt; Unit), private val serverEditRequestedCallback: ((Int) -&gt; Unit) )</ID> <ID>LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, private var data: Array&lt;ServerSetting&gt;, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -&gt; Unit), private val serverEditRequestedCallback: ((Int) -&gt; Unit) )</ID>
<ID>MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192</ID> <ID>MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192</ID>
<ID>MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$10</ID>
<ID>MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$60</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.&lt;no name provided&gt;$60000</ID> <ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.&lt;no name provided&gt;$60000</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$100000</ID> <ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$100000</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$1024L</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8</ID> <ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$86400L</ID> <ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$86400L</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8L</ID> <ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8L</ID>
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$5000L</ID> <ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$5000L</ID>
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$256</ID>
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$3</ID> <ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$3</ID>
<ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$4</ID> <ID>MagicNumber:MediaPlayerService.kt$MediaPlayerService$4</ID>
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID> <ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID>
<ID>MagicNumber:SongView.kt$SongView$3</ID> <ID>MagicNumber:SongView.kt$SongView$3</ID>
<ID>MagicNumber:SongView.kt$SongView$4</ID> <ID>MagicNumber:SongView.kt$SongView$4</ID>
<ID>MagicNumber:SongView.kt$SongView$60</ID> <ID>MagicNumber:SongView.kt$SongView$60</ID>
<ID>MagicNumber:TrackCollectionFragment.kt$TrackCollectionFragment$10</ID>
<ID>NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID> <ID>NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</ID>
<ID>NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID> <ID>NestedBlockDepth:DownloadHandler.kt$DownloadHandler$private fun downloadRecursively( fragment: Fragment, id: String, name: String?, isShare: Boolean, isDirectory: Boolean, save: Boolean, append: Boolean, autoPlay: Boolean, shuffle: Boolean, background: Boolean, playNext: Boolean, unpin: Boolean, isArtist: Boolean )</ID>
<ID>NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID> <ID>NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler()</ID>
<ID>ReturnCount:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String</ID>
<ID>ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID> <ID>ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean</ID>
<ID>ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID> <ID>ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</ID>
<ID>TooGenericExceptionCaught:DownloadFile.kt$DownloadFile$e: Exception</ID> <ID>TooGenericExceptionCaught:DownloadFile.kt$DownloadFile$e: Exception</ID>
@ -69,12 +59,10 @@
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$x: Exception</ID> <ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$x: Exception</ID>
<ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$e: Exception</ID> <ID>TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$e: Exception</ID>
<ID>TooGenericExceptionCaught:SongView.kt$SongView$e: Exception</ID> <ID>TooGenericExceptionCaught:SongView.kt$SongView$e: Exception</ID>
<ID>TooGenericExceptionCaught:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$x: Throwable</ID>
<ID>TooGenericExceptionThrown:DownloadFile.kt$DownloadFile.DownloadTask$throw Exception(String.format("Download of '%s' was cancelled", song))</ID> <ID>TooGenericExceptionThrown:DownloadFile.kt$DownloadFile.DownloadTask$throw Exception(String.format("Download of '%s' was cancelled", song))</ID>
<ID>TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service</ID> <ID>TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service</ID>
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID> <ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
<ID>TooManyFunctions:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID> <ID>TooManyFunctions:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
<ID>UtilityClassWithPublicConstructor:CommunicationErrorHandler.kt$CommunicationErrorHandler</ID>
<ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID> <ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
</CurrentIssues> </CurrentIssues>
</SmellBaseline> </SmellBaseline>

View File

@ -20,7 +20,6 @@ package org.moire.ultrasonic.util;
import android.app.Activity; import android.app.Activity;
import android.os.Handler; import android.os.Handler;
import org.moire.ultrasonic.service.CommunicationErrorHandler;
/** /**
* @author Sindre Mehus * @author Sindre Mehus
@ -54,12 +53,12 @@ public abstract class BackgroundTask<T> implements ProgressListener
protected void error(Throwable error) protected void error(Throwable error)
{ {
CommunicationErrorHandler.Companion.handleError(error, activity); CommunicationError.handleError(error, activity);
} }
protected String getErrorMessage(Throwable error) protected String getErrorMessage(Throwable error)
{ {
return CommunicationErrorHandler.Companion.getErrorMessage(error, activity); return CommunicationError.getErrorMessage(error, activity);
} }
@Override @Override

View File

@ -51,7 +51,7 @@ import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.PermissionUtil import org.moire.ultrasonic.util.PermissionUtil
import org.moire.ultrasonic.util.ServerColor import org.moire.ultrasonic.util.ServerColor
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.SubsonicUncaughtExceptionHandler import org.moire.ultrasonic.util.UncaughtExceptionHandler
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import timber.log.Timber import timber.log.Timber
@ -380,8 +380,8 @@ class NavigationActivity : AppCompatActivity() {
private fun setUncaughtExceptionHandler() { private fun setUncaughtExceptionHandler() {
val handler = Thread.getDefaultUncaughtExceptionHandler() val handler = Thread.getDefaultUncaughtExceptionHandler()
if (handler !is SubsonicUncaughtExceptionHandler) { if (handler !is UncaughtExceptionHandler) {
Thread.setDefaultUncaughtExceptionHandler(SubsonicUncaughtExceptionHandler(this)) Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler(this))
} }
} }

View File

@ -18,9 +18,9 @@ import org.koin.core.component.inject
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSetting import org.moire.ultrasonic.data.ServerSetting
import org.moire.ultrasonic.domain.MusicFolder import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.service.CommunicationErrorHandler
import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.MusicService
import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.util.CommunicationError
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
/** /**
@ -94,7 +94,7 @@ open class GenericListModel(application: Application) :
private fun handleException(exception: Exception, context: Context) { private fun handleException(exception: Exception, context: Context) {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
CommunicationErrorHandler.handleError(exception, context) CommunicationError.handleError(exception, context)
} }
} }

View File

@ -35,7 +35,6 @@ import android.widget.TextView
import android.widget.ViewFlipper import android.widget.ViewFlipper
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.Navigation import androidx.navigation.Navigation
import com.mobeta.android.dslv.DragSortListView import com.mobeta.android.dslv.DragSortListView
import com.mobeta.android.dslv.DragSortListView.DragSortListener import com.mobeta.android.dslv.DragSortListView.DragSortListener
@ -44,13 +43,16 @@ import java.text.DateFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.ArrayList import java.util.ArrayList
import java.util.Date import java.util.Date
import java.util.LinkedList
import java.util.Locale import java.util.Locale
import java.util.concurrent.CancellationException
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -74,9 +76,9 @@ import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
import org.moire.ultrasonic.subsonic.ShareHandler import org.moire.ultrasonic.subsonic.ShareHandler
import org.moire.ultrasonic.util.CancellationToken import org.moire.ultrasonic.util.CancellationToken
import org.moire.ultrasonic.util.CommunicationError
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.SilentBackgroundTask
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.view.AutoRepeatButton import org.moire.ultrasonic.view.AutoRepeatButton
import org.moire.ultrasonic.view.SongListAdapter import org.moire.ultrasonic.view.SongListAdapter
@ -85,13 +87,13 @@ import timber.log.Timber
/** /**
* Contains the Music Player screen of Ultrasonic with playback controls and the playlist * Contains the Music Player screen of Ultrasonic with playback controls and the playlist
*
* TODO: This class was more or less straight converted from Java legacy code.
* There are many places where further cleanup would be nice.
* The usage of threads and SilentBackgroundTask can be replaced with Coroutines.
*/ */
@Suppress("LargeClass", "TooManyFunctions", "MagicNumber") @Suppress("LargeClass", "TooManyFunctions", "MagicNumber")
class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinComponent { class PlayerFragment :
Fragment(),
GestureDetector.OnGestureListener,
KoinComponent,
CoroutineScope by CoroutineScope(Dispatchers.Main) {
private var swipeDistance = 0 private var swipeDistance = 0
private var swipeVelocity = 0 private var swipeVelocity = 0
private var jukeboxAvailable = false private var jukeboxAvailable = false
@ -112,8 +114,8 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
private lateinit var executorService: ScheduledExecutorService private lateinit var executorService: ScheduledExecutorService
private var currentPlaying: DownloadFile? = null private var currentPlaying: DownloadFile? = null
private var currentSong: MusicDirectory.Entry? = null private var currentSong: MusicDirectory.Entry? = null
private var onProgressChangedTask: SilentBackgroundTask<Void?>? = null
private var rxBusSubscription: Disposable? = null private var rxBusSubscription: Disposable? = null
private var ioScope = CoroutineScope(Dispatchers.IO)
// Views and UI Elements // Views and UI Elements
private lateinit var visualizerViewLayout: LinearLayout private lateinit var visualizerViewLayout: LinearLayout
@ -233,17 +235,11 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
previousButton.setOnClickListener { previousButton.setOnClickListener {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
object : SilentBackgroundTask<Void?>(activity) { launch(CommunicationError.getHandler(context)) {
override fun doInBackground(): Void? { mediaPlayerController.previous()
mediaPlayerController.previous() onCurrentChanged()
return null onSliderProgressChanged()
} }
override fun done(result: Void?) {
onCurrentChanged()
onSliderProgressChanged()
}
}.execute()
} }
previousButton.setOnRepeatListener { previousButton.setOnRepeatListener {
@ -253,65 +249,43 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
nextButton.setOnClickListener { nextButton.setOnClickListener {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
object : SilentBackgroundTask<Boolean?>(activity) { launch(CommunicationError.getHandler(context)) {
override fun doInBackground(): Boolean { mediaPlayerController.next()
mediaPlayerController.next() onCurrentChanged()
return true onSliderProgressChanged()
} }
override fun done(result: Boolean?) {
if (result == true) {
onCurrentChanged()
onSliderProgressChanged()
}
}
}.execute()
} }
nextButton.setOnRepeatListener { nextButton.setOnRepeatListener {
val incrementTime = Settings.incrementTime val incrementTime = Settings.incrementTime
changeProgress(incrementTime) changeProgress(incrementTime)
} }
pauseButton.setOnClickListener { pauseButton.setOnClickListener {
object : SilentBackgroundTask<Void?>(activity) { launch(CommunicationError.getHandler(context)) {
override fun doInBackground(): Void? { mediaPlayerController.pause()
mediaPlayerController.pause() onCurrentChanged()
return null onSliderProgressChanged()
} }
override fun done(result: Void?) {
onCurrentChanged()
onSliderProgressChanged()
}
}.execute()
} }
stopButton.setOnClickListener { stopButton.setOnClickListener {
object : SilentBackgroundTask<Void?>(activity) { launch(CommunicationError.getHandler(context)) {
override fun doInBackground(): Void? { mediaPlayerController.reset()
mediaPlayerController.reset() onCurrentChanged()
return null onSliderProgressChanged()
} }
override fun done(result: Void?) {
onCurrentChanged()
onSliderProgressChanged()
}
}.execute()
} }
startButton.setOnClickListener { startButton.setOnClickListener {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
object : SilentBackgroundTask<Void?>(activity) { launch(CommunicationError.getHandler(context)) {
override fun doInBackground(): Void? { start()
start() onCurrentChanged()
return null onSliderProgressChanged()
} }
override fun done(result: Void?) {
onCurrentChanged()
onSliderProgressChanged()
}
}.execute()
} }
shuffleButton.setOnClickListener { shuffleButton.setOnClickListener {
mediaPlayerController.shuffle() mediaPlayerController.shuffle()
Util.toast(activity, R.string.download_menu_shuffle_notification) Util.toast(activity, R.string.download_menu_shuffle_notification)
@ -338,16 +312,10 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
progressBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener { progressBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
override fun onStopTrackingTouch(seekBar: SeekBar) { override fun onStopTrackingTouch(seekBar: SeekBar) {
object : SilentBackgroundTask<Void?>(activity) { launch(CommunicationError.getHandler(context)) {
override fun doInBackground(): Void? { mediaPlayerController.seekTo(progressBar.progress)
mediaPlayerController.seekTo(progressBar.progress) onSliderProgressChanged()
return null }
}
override fun done(result: Void?) {
onSliderProgressChanged()
}
}.execute()
} }
override fun onStartTrackingTouch(seekBar: SeekBar) {} override fun onStartTrackingTouch(seekBar: SeekBar) {}
@ -356,18 +324,13 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
playlistView.setOnItemClickListener { _, _, position, _ -> playlistView.setOnItemClickListener { _, _, position, _ ->
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
object : SilentBackgroundTask<Void?>(activity) { launch(CommunicationError.getHandler(context)) {
override fun doInBackground(): Void? { mediaPlayerController.play(position)
mediaPlayerController.play(position) onCurrentChanged()
return null onSliderProgressChanged()
} }
override fun done(result: Void?) {
onCurrentChanged()
onSliderProgressChanged()
}
}.execute()
} }
registerForContextMenu(playlistView) registerForContextMenu(playlistView)
if (arguments != null && requireArguments().getBoolean( if (arguments != null && requireArguments().getBoolean(
@ -428,8 +391,8 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
onPlaylistChanged() onPlaylistChanged()
} }
// Query the Jukebox state off-thread // Query the Jukebox state in an IO Context
viewLifecycleOwner.lifecycleScope.launch { ioScope.launch(CommunicationError.getHandler(context)) {
try { try {
jukeboxAvailable = mediaPlayerController.isJukeboxAvailable jukeboxAvailable = mediaPlayerController.isJukeboxAvailable
} catch (all: Exception) { } catch (all: Exception) {
@ -491,6 +454,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
override fun onDestroyView() { override fun onDestroyView() {
rxBusSubscription?.dispose() rxBusSubscription?.dispose()
cancel("CoroutineScope cancelled because the view was destroyed")
cancellationToken.cancel() cancellationToken.cancel()
super.onDestroyView() super.onDestroyView()
} }
@ -819,33 +783,28 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
private fun savePlaylistInBackground(playlistName: String) { private fun savePlaylistInBackground(playlistName: String) {
Util.toast(context, resources.getString(R.string.download_playlist_saving, playlistName)) Util.toast(context, resources.getString(R.string.download_playlist_saving, playlistName))
mediaPlayerController.suggestedPlaylistName = playlistName mediaPlayerController.suggestedPlaylistName = playlistName
object : SilentBackgroundTask<Void?>(activity) {
@Throws(Throwable::class)
override fun doInBackground(): Void? {
val entries: MutableList<MusicDirectory.Entry> = LinkedList()
for (downloadFile in mediaPlayerController.playList) {
entries.add(downloadFile.song)
}
val musicService = getMusicService()
musicService.createPlaylist(null, playlistName, entries)
return null
}
override fun done(result: Void?) { ioScope.launch {
val entries = mediaPlayerController.playList.map {
it.song
}
val musicService = getMusicService()
musicService.createPlaylist(null, playlistName, entries)
}.invokeOnCompletion {
if (it == null || it is CancellationException) {
Util.toast(context, R.string.download_playlist_done) Util.toast(context, R.string.download_playlist_done)
} } else {
Timber.e(it, "Exception has occurred in savePlaylistInBackground")
override fun error(error: Throwable) {
Timber.e(error, "Exception has occurred in savePlaylistInBackground")
val msg = String.format( val msg = String.format(
Locale.ROOT, Locale.ROOT,
"%s %s", "%s %s",
resources.getString(R.string.download_playlist_error), resources.getString(R.string.download_playlist_error),
getErrorMessage(error) CommunicationError.getErrorMessage(it, context)
) )
Util.toast(context, msg) Util.toast(context, msg)
} }
}.execute() }
} }
private fun toggleFullScreenAlbumArt() { private fun toggleFullScreenAlbumArt() {
@ -975,120 +934,95 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
} }
} }
@Suppress("LongMethod", "ComplexMethod")
@Synchronized
private fun onSliderProgressChanged() { private fun onSliderProgressChanged() {
if (onProgressChangedTask != null) {
return val isJukeboxEnabled: Boolean = mediaPlayerController.isJukeboxEnabled
val millisPlayed: Int = max(0, mediaPlayerController.playerPosition)
val duration: Int = mediaPlayerController.playerDuration
val playerState: PlayerState = mediaPlayerController.playerState
if (cancellationToken.isCancellationRequested) return
if (currentPlaying != null) {
positionTextView.text = Util.formatTotalDuration(millisPlayed.toLong(), true)
durationTextView.text = Util.formatTotalDuration(duration.toLong(), true)
progressBar.max =
if (duration == 0) 100 else duration // Work-around for apparent bug.
progressBar.progress = millisPlayed
progressBar.isEnabled = currentPlaying!!.isWorkDone || isJukeboxEnabled
} else {
positionTextView.setText(R.string.util_zero_time)
durationTextView.setText(R.string.util_no_time)
progressBar.progress = 0
progressBar.max = 0
progressBar.isEnabled = false
} }
onProgressChangedTask = object : SilentBackgroundTask<Void?>(activity) {
var isJukeboxEnabled = false when (playerState) {
var millisPlayed = 0 PlayerState.DOWNLOADING -> {
var duration: Int? = null val progress =
var playerState: PlayerState? = null if (currentPlaying != null) currentPlaying!!.progress.value!! else 0
override fun doInBackground(): Void? { val downloadStatus = resources.getString(
isJukeboxEnabled = mediaPlayerController.isJukeboxEnabled R.string.download_playerstate_downloading,
millisPlayed = max(0, mediaPlayerController.playerPosition) Util.formatPercentage(progress)
duration = mediaPlayerController.playerDuration )
playerState = mediaPlayerController.playerState setTitle(this@PlayerFragment, downloadStatus)
return null
} }
PlayerState.PREPARING -> setTitle(
@Suppress("LongMethod") this@PlayerFragment,
override fun done(result: Void?) { R.string.download_playerstate_buffering
if (cancellationToken.isCancellationRequested) return )
if (currentPlaying != null) { PlayerState.STARTED -> {
val millisTotal = if (duration == null) 0 else duration!! if (mediaPlayerController.isShufflePlayEnabled) {
positionTextView.text = Util.formatTotalDuration(millisPlayed.toLong(), true) setTitle(
durationTextView.text = Util.formatTotalDuration(millisTotal.toLong(), true)
progressBar.max =
if (millisTotal == 0) 100 else millisTotal // Work-around for apparent bug.
progressBar.progress = millisPlayed
progressBar.isEnabled = currentPlaying!!.isWorkDone || isJukeboxEnabled
} else {
positionTextView.setText(R.string.util_zero_time)
durationTextView.setText(R.string.util_no_time)
progressBar.progress = 0
progressBar.max = 0
progressBar.isEnabled = false
}
when (playerState) {
PlayerState.DOWNLOADING -> {
val progress =
if (currentPlaying != null) currentPlaying!!.progress.value!! else 0
val downloadStatus = resources.getString(
R.string.download_playerstate_downloading,
Util.formatPercentage(progress)
)
setTitle(this@PlayerFragment, downloadStatus)
}
PlayerState.PREPARING -> setTitle(
this@PlayerFragment, this@PlayerFragment,
R.string.download_playerstate_buffering R.string.download_playerstate_playing_shuffle
) )
PlayerState.STARTED -> { } else {
if (mediaPlayerController.isShufflePlayEnabled) { setTitle(this@PlayerFragment, R.string.common_appname)
setTitle(
this@PlayerFragment,
R.string.download_playerstate_playing_shuffle
)
} else {
setTitle(this@PlayerFragment, R.string.common_appname)
}
}
PlayerState.IDLE,
PlayerState.PREPARED,
PlayerState.STOPPED,
PlayerState.PAUSED,
PlayerState.COMPLETED -> {
}
else -> setTitle(this@PlayerFragment, R.string.common_appname)
} }
}
PlayerState.IDLE,
PlayerState.PREPARED,
PlayerState.STOPPED,
PlayerState.PAUSED,
PlayerState.COMPLETED -> {
}
else -> setTitle(this@PlayerFragment, R.string.common_appname)
}
when (playerState) { when (playerState) {
PlayerState.STARTED -> { PlayerState.STARTED -> {
pauseButton.isVisible = true pauseButton.isVisible = true
stopButton.isVisible = false stopButton.isVisible = false
startButton.isVisible = false startButton.isVisible = false
} }
PlayerState.DOWNLOADING, PlayerState.PREPARING -> { PlayerState.DOWNLOADING, PlayerState.PREPARING -> {
pauseButton.isVisible = false pauseButton.isVisible = false
stopButton.isVisible = true stopButton.isVisible = true
startButton.isVisible = false startButton.isVisible = false
} }
else -> { else -> {
pauseButton.isVisible = false pauseButton.isVisible = false
stopButton.isVisible = false stopButton.isVisible = false
startButton.isVisible = true startButton.isVisible = true
}
}
// TODO: It would be a lot nicer if MediaPlayerController would send an event
// when this is necessary instead of updating every time
displaySongRating()
onProgressChangedTask = null
} }
} }
onProgressChangedTask!!.execute()
// TODO: It would be a lot nicer if MediaPlayerController would send an event
// when this is necessary instead of updating every time
displaySongRating()
} }
private fun changeProgress(ms: Int) { private fun changeProgress(ms: Int) {
object : SilentBackgroundTask<Void?>(activity) { launch(CommunicationError.getHandler(context)) {
var msPlayed = 0 val msPlayed: Int = max(0, mediaPlayerController.playerPosition)
var duration: Int? = null val duration = mediaPlayerController.playerDuration
var seekTo = 0 val seekTo = (msPlayed + ms).coerceAtMost(duration)
override fun doInBackground(): Void? { mediaPlayerController.seekTo(seekTo)
msPlayed = max(0, mediaPlayerController.playerPosition) progressBar.progress = seekTo
duration = mediaPlayerController.playerDuration }
val msTotal = duration!!
seekTo = (msPlayed + ms).coerceAtMost(msTotal)
mediaPlayerController.seekTo(seekTo)
return null
}
override fun done(result: Void?) {
progressBar.progress = seekTo
}
}.execute()
} }
override fun onDown(me: MotionEvent): Boolean { override fun onDown(me: MotionEvent): Boolean {

View File

@ -38,7 +38,6 @@ import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.getTitle import org.moire.ultrasonic.fragment.FragmentTitle.Companion.getTitle
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.service.CommunicationErrorHandler
import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.subsonic.DownloadHandler import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.subsonic.ImageLoaderProvider
@ -47,6 +46,7 @@ import org.moire.ultrasonic.subsonic.ShareHandler
import org.moire.ultrasonic.subsonic.VideoPlayer import org.moire.ultrasonic.subsonic.VideoPlayer
import org.moire.ultrasonic.util.AlbumHeader import org.moire.ultrasonic.util.AlbumHeader
import org.moire.ultrasonic.util.CancellationToken import org.moire.ultrasonic.util.CancellationToken
import org.moire.ultrasonic.util.CommunicationError
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
@ -211,7 +211,7 @@ class TrackCollectionFragment : Fragment() {
val handler = CoroutineExceptionHandler { _, exception -> val handler = CoroutineExceptionHandler { _, exception ->
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
CommunicationErrorHandler.handleError(exception, context) CommunicationError.handleError(exception, context)
} }
refreshAlbumListView!!.isRefreshing = false refreshAlbumListView!!.isRefreshing = false
} }

View File

@ -1,88 +0,0 @@
/*
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 2020 (C) Jozsef Varga
*/
package org.moire.ultrasonic.service
import android.app.AlertDialog
import android.content.Context
import com.fasterxml.jackson.core.JsonParseException
import java.io.FileNotFoundException
import java.io.IOException
import java.security.cert.CertPathValidatorException
import java.security.cert.CertificateException
import javax.net.ssl.SSLException
import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
import org.moire.ultrasonic.api.subsonic.SubsonicRESTException
import org.moire.ultrasonic.subsonic.getLocalizedErrorMessage
import org.moire.ultrasonic.util.Util
import timber.log.Timber
/**
* Contains helper functions to handle the exceptions
* thrown during the communication with a Subsonic server
*/
class CommunicationErrorHandler {
companion object {
fun handleError(error: Throwable?, context: Context?) {
Timber.w(error)
if (context == null) return
AlertDialog.Builder(context)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.error_label)
.setMessage(getErrorMessage(error!!, context))
.setCancelable(true)
.setPositiveButton(R.string.common_ok) { _, _ -> }
.create().show()
}
fun getErrorMessage(error: Throwable, context: Context): String {
if (error is IOException && !Util.isNetworkConnected()) {
return context.resources.getString(R.string.background_task_no_network)
} else if (error is FileNotFoundException) {
return context.resources.getString(R.string.background_task_not_found)
} else if (error is JsonParseException) {
return context.resources.getString(R.string.background_task_parse_error)
} else if (error is SSLException) {
return if (
error.cause is CertificateException &&
error.cause?.cause is CertPathValidatorException
) {
context.resources
.getString(
R.string.background_task_ssl_cert_error, error.cause?.cause?.message
)
} else {
context.resources.getString(R.string.background_task_ssl_error)
}
} else if (error is ApiNotSupportedException) {
return context.resources.getString(
R.string.background_task_unsupported_api, error.serverApiVersion
)
} else if (error is IOException) {
return context.resources.getString(R.string.background_task_network_error)
} else if (error is SubsonicRESTException) {
return error.getLocalizedErrorMessage(context)
}
val message = error.message
return message ?: error.javaClass.simpleName
}
}
}

View File

@ -413,6 +413,10 @@ class MediaPlayerController(
} }
} }
/**
* This function calls the music service directly and
* therefore can't be called from the main thread
*/
val isJukeboxAvailable: Boolean val isJukeboxAvailable: Boolean
get() { get() {
try { try {

View File

@ -1,10 +1,13 @@
package org.moire.ultrasonic.util package org.moire.ultrasonic.util
import android.os.AsyncTask
import android.os.StatFs import android.os.StatFs
import java.io.File import java.io.File
import java.util.ArrayList import java.util.ArrayList
import java.util.HashSet import java.util.HashSet
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koin.java.KoinJavaComponent.inject import org.koin.java.KoinJavaComponent.inject
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Playlist import org.moire.ultrasonic.domain.Playlist
@ -22,105 +25,85 @@ import timber.log.Timber
/** /**
* Responsible for cleaning up files from the offline download cache on the filesystem. * Responsible for cleaning up files from the offline download cache on the filesystem.
*/ */
class CacheCleaner { class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
private fun exceptionHandler(tag: String): CoroutineExceptionHandler {
return CoroutineExceptionHandler { _, exception ->
Timber.w(exception, "Exception in CacheCleaner.$tag")
}
}
fun clean() { fun clean() {
try { launch(exceptionHandler("clean")) {
BackgroundCleanup().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) backgroundCleanup()
} catch (all: Exception) {
// If an exception is thrown, assume we execute correctly the next time
Timber.w(all, "Exception in CacheCleaner.clean")
} }
} }
fun cleanSpace() { fun cleanSpace() {
try { launch(exceptionHandler("cleanSpace")) {
BackgroundSpaceCleanup().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) backgroundSpaceCleanup()
} catch (all: Exception) {
// If an exception is thrown, assume we execute correctly the next time
Timber.w(all, "Exception in CacheCleaner.cleanSpace")
} }
} }
fun cleanPlaylists(playlists: List<Playlist>) { fun cleanPlaylists(playlists: List<Playlist>) {
launch(exceptionHandler("cleanPlaylists")) {
backgroundPlaylistsCleanup(playlists)
}
}
private fun backgroundCleanup() {
try { try {
BackgroundPlaylistsCleanup().executeOnExecutor( val files: MutableList<File> = ArrayList()
AsyncTask.THREAD_POOL_EXECUTOR, val dirs: MutableList<File> = ArrayList()
playlists
) findCandidatesForDeletion(musicDirectory, files, dirs)
} catch (all: Exception) { sortByAscendingModificationTime(files)
// If an exception is thrown, assume we execute correctly the next time val filesToNotDelete = findFilesToNotDelete()
Timber.w(all, "Exception in CacheCleaner.cleanPlaylists")
deleteFiles(files, filesToNotDelete, getMinimumDelete(files), true)
deleteEmptyDirs(dirs, filesToNotDelete)
} catch (all: RuntimeException) {
Timber.e(all, "Error in cache cleaning.")
} }
} }
private class BackgroundCleanup : AsyncTask<Void?, Void?, Void?>() { private fun backgroundSpaceCleanup() {
override fun doInBackground(vararg params: Void?): Void? { try {
try { val files: MutableList<File> = ArrayList()
Thread.currentThread().name = "BackgroundCleanup" val dirs: MutableList<File> = ArrayList()
val files: MutableList<File> = ArrayList() findCandidatesForDeletion(musicDirectory, files, dirs)
val dirs: MutableList<File> = ArrayList()
findCandidatesForDeletion(musicDirectory, files, dirs)
sortByAscendingModificationTime(files)
val filesToNotDelete = findFilesToNotDelete()
deleteFiles(files, filesToNotDelete, getMinimumDelete(files), true)
deleteEmptyDirs(dirs, filesToNotDelete)
} catch (all: RuntimeException) {
Timber.e(all, "Error in cache cleaning.")
}
return null
}
}
private class BackgroundSpaceCleanup : AsyncTask<Void?, Void?, Void?>() {
override fun doInBackground(vararg params: Void?): Void? {
try {
Thread.currentThread().name = "BackgroundSpaceCleanup"
val files: MutableList<File> = ArrayList()
val dirs: MutableList<File> = ArrayList()
findCandidatesForDeletion(musicDirectory, files, dirs)
val bytesToDelete = getMinimumDelete(files)
if (bytesToDelete <= 0L) return null
val bytesToDelete = getMinimumDelete(files)
if (bytesToDelete > 0L) {
sortByAscendingModificationTime(files) sortByAscendingModificationTime(files)
val filesToNotDelete = findFilesToNotDelete() val filesToNotDelete = findFilesToNotDelete()
deleteFiles(files, filesToNotDelete, bytesToDelete, false) deleteFiles(files, filesToNotDelete, bytesToDelete, false)
} catch (all: RuntimeException) {
Timber.e(all, "Error in cache cleaning.")
} }
return null } catch (all: RuntimeException) {
Timber.e(all, "Error in cache cleaning.")
} }
} }
private class BackgroundPlaylistsCleanup : AsyncTask<List<Playlist>, Void?, Void?>() { private fun backgroundPlaylistsCleanup(vararg params: List<Playlist>) {
override fun doInBackground(vararg params: List<Playlist>): Void? { try {
try { val activeServerProvider = inject<ActiveServerProvider>(
val activeServerProvider = inject<ActiveServerProvider>( ActiveServerProvider::class.java
ActiveServerProvider::class.java )
)
Thread.currentThread().name = "BackgroundPlaylistsCleanup" val server = activeServerProvider.value.getActiveServer().name
val playlistFiles = listFiles(getPlaylistDirectory(server))
val playlists = params[0]
val server = activeServerProvider.value.getActiveServer().name for ((_, name) in playlists) {
val playlistFiles = listFiles(getPlaylistDirectory(server)) playlistFiles.remove(getPlaylistFile(server, name))
val playlists = params[0]
for ((_, name) in playlists) {
playlistFiles.remove(getPlaylistFile(server, name))
}
for (playlist in playlistFiles) {
playlist.delete()
}
} catch (all: RuntimeException) {
Timber.e(all, "Error in playlist cache cleaning.")
} }
return null
for (playlist in playlistFiles) {
playlist.delete()
}
} catch (all: RuntimeException) {
Timber.e(all, "Error in playlist cache cleaning.")
} }
} }

View File

@ -0,0 +1,91 @@
/*
* CommunicationErrorUtil.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.util
import android.app.AlertDialog
import android.content.Context
import android.os.Handler
import android.os.Looper
import com.fasterxml.jackson.core.JsonParseException
import java.io.FileNotFoundException
import java.io.IOException
import java.security.cert.CertPathValidatorException
import java.security.cert.CertificateException
import javax.net.ssl.SSLException
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineExceptionHandler
import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
import org.moire.ultrasonic.api.subsonic.SubsonicRESTException
import org.moire.ultrasonic.subsonic.getLocalizedErrorMessage
import timber.log.Timber
/**
* Contains helper functions to handle the exceptions
* thrown during the communication with a Subsonic server
*/
object CommunicationError {
fun getHandler(context: Context?, handler: ((CoroutineContext, Throwable) -> Unit)? = null):
CoroutineExceptionHandler {
return CoroutineExceptionHandler { coroutineContext, exception ->
Handler(Looper.getMainLooper()).post {
handleError(exception, context)
handler?.invoke(coroutineContext, exception)
}
}
}
@JvmStatic
fun handleError(error: Throwable?, context: Context?) {
Timber.w(error)
if (context == null) return
AlertDialog.Builder(context)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.error_label)
.setMessage(getErrorMessage(error!!, context))
.setCancelable(true)
.setPositiveButton(R.string.common_ok) { _, _ -> }
.create().show()
}
@JvmStatic
@Suppress("ReturnCount")
fun getErrorMessage(error: Throwable, context: Context?): String {
if (context == null) return "Couldn't get Error message, Context is null"
if (error is IOException && !Util.isNetworkConnected()) {
return context.resources.getString(R.string.background_task_no_network)
} else if (error is FileNotFoundException) {
return context.resources.getString(R.string.background_task_not_found)
} else if (error is JsonParseException) {
return context.resources.getString(R.string.background_task_parse_error)
} else if (error is SSLException) {
return if (
error.cause is CertificateException &&
error.cause?.cause is CertPathValidatorException
) {
context.resources
.getString(
R.string.background_task_ssl_cert_error, error.cause?.cause?.message
)
} else {
context.resources.getString(R.string.background_task_ssl_error)
}
} else if (error is ApiNotSupportedException) {
return context.resources.getString(
R.string.background_task_unsupported_api, error.serverApiVersion
)
} else if (error is IOException) {
return context.resources.getString(R.string.background_task_network_error)
} else if (error is SubsonicRESTException) {
return error.getLocalizedErrorMessage(context)
}
val message = error.message
return message ?: error.javaClass.simpleName
}
}

View File

@ -1,32 +0,0 @@
/*
* SilentBackgroundTask.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.util
import android.app.Activity
/**
* @author Sindre Mehus
*/
abstract class SilentBackgroundTask<T>(activity: Activity?) : BackgroundTask<T>(activity) {
override fun execute() {
val thread: Thread = object : Thread() {
override fun run() {
try {
val result = doInBackground()
handler.post { done(result) }
} catch (all: Throwable) {
handler.post { error(all) }
}
}
}
thread.start()
}
override fun updateProgress(messageId: Int) {}
override fun updateProgress(message: String) {}
}

View File

@ -9,7 +9,7 @@ import timber.log.Timber
/** /**
* Logs the stack trace of uncaught exceptions to a file on the SD card. * Logs the stack trace of uncaught exceptions to a file on the SD card.
*/ */
class SubsonicUncaughtExceptionHandler( class UncaughtExceptionHandler(
private val context: Context private val context: Context
) : Thread.UncaughtExceptionHandler { ) : Thread.UncaughtExceptionHandler {
private val defaultHandler: Thread.UncaughtExceptionHandler? = private val defaultHandler: Thread.UncaughtExceptionHandler? =
@ -31,8 +31,8 @@ class SubsonicUncaughtExceptionHandler(
throwable.printStackTrace(printWriter) throwable.printStackTrace(printWriter)
Timber.e(throwable, "Uncaught Exception! %s", logMessage) Timber.e(throwable, "Uncaught Exception! %s", logMessage)
Timber.i("Stack trace written to %s", file) Timber.i("Stack trace written to %s", file)
} catch (x: Throwable) { } catch (all: Throwable) {
Timber.e(x, "Failed to write stack trace to %s", file) Timber.e(all, "Failed to write stack trace to %s", file)
} finally { } finally {
Util.close(printWriter) Util.close(printWriter)
defaultHandler?.uncaughtException(thread, throwable) defaultHandler?.uncaughtException(thread, throwable)

View File

@ -11,7 +11,6 @@
a:id="@+id/button_shuffle" a:id="@+id/button_shuffle"
a:layout_width="0dip" a:layout_width="0dip"
a:layout_height="26dp" a:layout_height="26dp"
a:layout_alignParentLeft="true"
a:layout_gravity="center" a:layout_gravity="center"
a:layout_weight="1" a:layout_weight="1"
a:adjustViewBounds="true" a:adjustViewBounds="true"

View File

@ -18,7 +18,9 @@
a:id="@+id/current_playing_song" a:id="@+id/current_playing_song"
a:layout_width="wrap_content" a:layout_width="wrap_content"
a:layout_height="wrap_content" a:layout_height="wrap_content"
a:ellipsize="start" a:layout_marginEnd="10dip"
a:paddingRight="30dip"
a:ellipsize="marquee"
a:gravity="left" a:gravity="left"
a:singleLine="true" a:singleLine="true"
a:textAppearance="?android:attr/textAppearanceLarge" a:textAppearance="?android:attr/textAppearanceLarge"
@ -29,7 +31,7 @@
a:id="@+id/current_playing_artist" a:id="@+id/current_playing_artist"
a:layout_width="wrap_content" a:layout_width="wrap_content"
a:layout_height="wrap_content" a:layout_height="wrap_content"
a:ellipsize="start" a:ellipsize="marquee"
a:gravity="left" a:gravity="left"
a:singleLine="true" a:singleLine="true"
a:textAppearance="?android:attr/textAppearanceSmall" a:textAppearance="?android:attr/textAppearanceSmall"
@ -62,7 +64,8 @@
a:ellipsize="start" a:ellipsize="start"
a:gravity="right" a:gravity="right"
a:text="0 / 0" a:text="0 / 0"
a:textAppearance="?android:attr/textAppearanceSmall" /> a:textAppearance="?android:attr/textAppearanceSmall"
tools:ignore="HardcodedText" />
<TextView <TextView
a:id="@+id/current_total_duration" a:id="@+id/current_total_duration"