diff --git a/detekt-baseline.xml b/detekt-baseline.xml index a85a67ab..d983b21f 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -4,13 +4,11 @@ ComplexCondition:DownloadHandler.kt$DownloadHandler.<no name provided>$!append && !playNext && !unpin && !background ComplexCondition:FilePickerAdapter.kt$FilePickerAdapter$currentDirectory.absolutePath == "/" || currentDirectory.absolutePath == "/storage" || currentDirectory.absolutePath == "/storage/emulated" || currentDirectory.absolutePath == "/mnt" - ComplexCondition:LocalMediaPlayer.kt$LocalMediaPlayer$Util.getGaplessPlaybackPreference() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && ( playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED ) ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute() ComplexMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File) ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean) ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun enableButtons() ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory) - FunctionNaming:ThemeChangedEventDistributor.kt$ThemeChangedEventDistributor$fun RaiseThemeChangedEvent() ImplicitDefaultLocale:DownloadFile.kt$DownloadFile$String.format("DownloadFile (%s)", song) ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("Download of '%s' was cancelled", song) ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("DownloadTask (%s)", song) @@ -21,14 +19,12 @@ ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$String.format("BufferTask (%s)", downloadFile) ImplicitDefaultLocale:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$String.format("CheckCompletionTask (%s)", downloadFile) ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType) - ImplicitDefaultLocale:ShareHandler.kt$ShareHandler.<no name provided>$String.format("%s\n\n%s", Util.getShareGreeting(), result.url) ImplicitDefaultLocale:SongView.kt$SongView$String.format("%02d.", trackNumber) ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s ", bitRate) ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s > %s", suffix, transcodedSuffix) LargeClass:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment LongMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute() LongMethod:EditServerFragment.kt$EditServerFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) - LongMethod:FilePickerAdapter.kt$FilePickerAdapter$private fun fileLister(currentDirectory: File) LongMethod:LocalMediaPlayer.kt$LocalMediaPlayer$@Synchronized private fun doPlay(downloadFile: DownloadFile, position: Int, start: Boolean) LongMethod:NavigationActivity.kt$NavigationActivity$override fun onCreate(savedInstanceState: Bundle?) LongMethod:ShareHandler.kt$ShareHandler$private fun showDialog( fragment: Fragment, shareDetails: ShareDetails, swipe: SwipeRefreshLayout?, cancellationToken: CancellationToken ) @@ -39,27 +35,21 @@ LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory) LongParameterList:ServerRowAdapter.kt$ServerRowAdapter$( private var context: Context, private var data: Array<ServerSetting>, private val model: ServerSettingsModel, private val activeServerProvider: ActiveServerProvider, private val manageMode: Boolean, private val serverDeletedCallback: ((Int) -> Unit), private val serverEditRequestedCallback: ((Int) -> Unit) ) MagicNumber:ActiveServerProvider.kt$ActiveServerProvider$8192 - MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$10 - MagicNumber:DownloadFile.kt$DownloadFile.DownloadTask$60 MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.<no name provided>$60000 MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$100000 - MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$1024L MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8 MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$86400L MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8L MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.CheckCompletionTask$5000L - MagicNumber:MediaPlayerService.kt$MediaPlayerService$256 MagicNumber:MediaPlayerService.kt$MediaPlayerService$3 MagicNumber:MediaPlayerService.kt$MediaPlayerService$4 MagicNumber:RESTMusicService.kt$RESTMusicService$206 MagicNumber:SongView.kt$SongView$3 MagicNumber:SongView.kt$SongView$4 MagicNumber:SongView.kt$SongView$60 - MagicNumber:TrackCollectionFragment.kt$TrackCollectionFragment$10 NestedBlockDepth:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute() 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 ) NestedBlockDepth:MediaPlayerService.kt$MediaPlayerService$private fun setupOnSongCompletedHandler() - ReturnCount:CommunicationErrorHandler.kt$CommunicationErrorHandler.Companion$fun getErrorMessage(error: Throwable, context: Context): String ReturnCount:ServerRowAdapter.kt$ServerRowAdapter$ private fun popupMenuItemClick(menuItem: MenuItem, position: Int): Boolean ReturnCount:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean TooGenericExceptionCaught:DownloadFile.kt$DownloadFile$e: Exception @@ -69,12 +59,10 @@ TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer$x: Exception TooGenericExceptionCaught:LocalMediaPlayer.kt$LocalMediaPlayer.PositionCache$e: Exception TooGenericExceptionCaught:SongView.kt$SongView$e: Exception - TooGenericExceptionCaught:SubsonicUncaughtExceptionHandler.kt$SubsonicUncaughtExceptionHandler$x: Throwable TooGenericExceptionThrown:DownloadFile.kt$DownloadFile.DownloadTask$throw Exception(String.format("Download of '%s' was cancelled", song)) TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService TooManyFunctions:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment - UtilityClassWithPublicConstructor:CommunicationErrorHandler.kt$CommunicationErrorHandler UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java index 3bd56c61..8ae51dee 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/BackgroundTask.java @@ -20,7 +20,6 @@ package org.moire.ultrasonic.util; import android.app.Activity; import android.os.Handler; -import org.moire.ultrasonic.service.CommunicationErrorHandler; /** * @author Sindre Mehus @@ -54,12 +53,12 @@ public abstract class BackgroundTask implements ProgressListener protected void error(Throwable error) { - CommunicationErrorHandler.Companion.handleError(error, activity); + CommunicationError.handleError(error, activity); } protected String getErrorMessage(Throwable error) { - return CommunicationErrorHandler.Companion.getErrorMessage(error, activity); + return CommunicationError.getErrorMessage(error, activity); } @Override diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index 62040423..af38aaf4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -51,7 +51,7 @@ import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.PermissionUtil import org.moire.ultrasonic.util.ServerColor 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 timber.log.Timber @@ -380,8 +380,8 @@ class NavigationActivity : AppCompatActivity() { private fun setUncaughtExceptionHandler() { val handler = Thread.getDefaultUncaughtExceptionHandler() - if (handler !is SubsonicUncaughtExceptionHandler) { - Thread.setDefaultUncaughtExceptionHandler(SubsonicUncaughtExceptionHandler(this)) + if (handler !is UncaughtExceptionHandler) { + Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler(this)) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt index 693923ea..5ec1db0e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt @@ -18,9 +18,9 @@ import org.koin.core.component.inject import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSetting import org.moire.ultrasonic.domain.MusicFolder -import org.moire.ultrasonic.service.CommunicationErrorHandler import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.MusicServiceFactory +import org.moire.ultrasonic.util.CommunicationError import org.moire.ultrasonic.util.Settings /** @@ -94,7 +94,7 @@ open class GenericListModel(application: Application) : private fun handleException(exception: Exception, context: Context) { Handler(Looper.getMainLooper()).post { - CommunicationErrorHandler.handleError(exception, context) + CommunicationError.handleError(exception, context) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 14cbbdf5..7556d148 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -35,7 +35,6 @@ import android.widget.TextView import android.widget.ViewFlipper import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope import androidx.navigation.Navigation import com.mobeta.android.dslv.DragSortListView import com.mobeta.android.dslv.DragSortListView.DragSortListener @@ -44,13 +43,16 @@ import java.text.DateFormat import java.text.SimpleDateFormat import java.util.ArrayList import java.util.Date -import java.util.LinkedList import java.util.Locale +import java.util.concurrent.CancellationException import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit import kotlin.math.abs import kotlin.math.max +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import org.koin.android.ext.android.inject 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.ShareHandler import org.moire.ultrasonic.util.CancellationToken +import org.moire.ultrasonic.util.CommunicationError import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Settings -import org.moire.ultrasonic.util.SilentBackgroundTask import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.view.AutoRepeatButton 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 - * - * 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") -class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinComponent { +class PlayerFragment : + Fragment(), + GestureDetector.OnGestureListener, + KoinComponent, + CoroutineScope by CoroutineScope(Dispatchers.Main) { private var swipeDistance = 0 private var swipeVelocity = 0 private var jukeboxAvailable = false @@ -112,8 +114,8 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon private lateinit var executorService: ScheduledExecutorService private var currentPlaying: DownloadFile? = null private var currentSong: MusicDirectory.Entry? = null - private var onProgressChangedTask: SilentBackgroundTask? = null private var rxBusSubscription: Disposable? = null + private var ioScope = CoroutineScope(Dispatchers.IO) // Views and UI Elements private lateinit var visualizerViewLayout: LinearLayout @@ -233,17 +235,11 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon previousButton.setOnClickListener { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - object : SilentBackgroundTask(activity) { - override fun doInBackground(): Void? { - mediaPlayerController.previous() - return null - } - - override fun done(result: Void?) { - onCurrentChanged() - onSliderProgressChanged() - } - }.execute() + launch(CommunicationError.getHandler(context)) { + mediaPlayerController.previous() + onCurrentChanged() + onSliderProgressChanged() + } } previousButton.setOnRepeatListener { @@ -253,65 +249,43 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon nextButton.setOnClickListener { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - object : SilentBackgroundTask(activity) { - override fun doInBackground(): Boolean { - mediaPlayerController.next() - return true - } - - override fun done(result: Boolean?) { - if (result == true) { - onCurrentChanged() - onSliderProgressChanged() - } - } - }.execute() + launch(CommunicationError.getHandler(context)) { + mediaPlayerController.next() + onCurrentChanged() + onSliderProgressChanged() + } } nextButton.setOnRepeatListener { val incrementTime = Settings.incrementTime changeProgress(incrementTime) } + pauseButton.setOnClickListener { - object : SilentBackgroundTask(activity) { - override fun doInBackground(): Void? { - mediaPlayerController.pause() - return null - } - - override fun done(result: Void?) { - onCurrentChanged() - onSliderProgressChanged() - } - }.execute() + launch(CommunicationError.getHandler(context)) { + mediaPlayerController.pause() + onCurrentChanged() + onSliderProgressChanged() + } } + stopButton.setOnClickListener { - object : SilentBackgroundTask(activity) { - override fun doInBackground(): Void? { - mediaPlayerController.reset() - return null - } - - override fun done(result: Void?) { - onCurrentChanged() - onSliderProgressChanged() - } - }.execute() + launch(CommunicationError.getHandler(context)) { + mediaPlayerController.reset() + onCurrentChanged() + onSliderProgressChanged() + } } + startButton.setOnClickListener { networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - object : SilentBackgroundTask(activity) { - override fun doInBackground(): Void? { - start() - return null - } - - override fun done(result: Void?) { - onCurrentChanged() - onSliderProgressChanged() - } - }.execute() + launch(CommunicationError.getHandler(context)) { + start() + onCurrentChanged() + onSliderProgressChanged() + } } + shuffleButton.setOnClickListener { mediaPlayerController.shuffle() Util.toast(activity, R.string.download_menu_shuffle_notification) @@ -338,16 +312,10 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon progressBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener { override fun onStopTrackingTouch(seekBar: SeekBar) { - object : SilentBackgroundTask(activity) { - override fun doInBackground(): Void? { - mediaPlayerController.seekTo(progressBar.progress) - return null - } - - override fun done(result: Void?) { - onSliderProgressChanged() - } - }.execute() + launch(CommunicationError.getHandler(context)) { + mediaPlayerController.seekTo(progressBar.progress) + onSliderProgressChanged() + } } override fun onStartTrackingTouch(seekBar: SeekBar) {} @@ -356,18 +324,13 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon playlistView.setOnItemClickListener { _, _, position, _ -> networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() - object : SilentBackgroundTask(activity) { - override fun doInBackground(): Void? { - mediaPlayerController.play(position) - return null - } - - override fun done(result: Void?) { - onCurrentChanged() - onSliderProgressChanged() - } - }.execute() + launch(CommunicationError.getHandler(context)) { + mediaPlayerController.play(position) + onCurrentChanged() + onSliderProgressChanged() + } } + registerForContextMenu(playlistView) if (arguments != null && requireArguments().getBoolean( @@ -428,8 +391,8 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon onPlaylistChanged() } - // Query the Jukebox state off-thread - viewLifecycleOwner.lifecycleScope.launch { + // Query the Jukebox state in an IO Context + ioScope.launch(CommunicationError.getHandler(context)) { try { jukeboxAvailable = mediaPlayerController.isJukeboxAvailable } catch (all: Exception) { @@ -491,6 +454,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon override fun onDestroyView() { rxBusSubscription?.dispose() + cancel("CoroutineScope cancelled because the view was destroyed") cancellationToken.cancel() super.onDestroyView() } @@ -819,33 +783,28 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon private fun savePlaylistInBackground(playlistName: String) { Util.toast(context, resources.getString(R.string.download_playlist_saving, playlistName)) mediaPlayerController.suggestedPlaylistName = playlistName - object : SilentBackgroundTask(activity) { - @Throws(Throwable::class) - override fun doInBackground(): Void? { - val entries: MutableList = 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) - } - - override fun error(error: Throwable) { - Timber.e(error, "Exception has occurred in savePlaylistInBackground") + } else { + Timber.e(it, "Exception has occurred in savePlaylistInBackground") val msg = String.format( Locale.ROOT, "%s %s", resources.getString(R.string.download_playlist_error), - getErrorMessage(error) + CommunicationError.getErrorMessage(it, context) ) Util.toast(context, msg) } - }.execute() + } } private fun toggleFullScreenAlbumArt() { @@ -975,120 +934,95 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon } } + @Suppress("LongMethod", "ComplexMethod") + @Synchronized 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(activity) { - var isJukeboxEnabled = false - var millisPlayed = 0 - var duration: Int? = null - var playerState: PlayerState? = null - override fun doInBackground(): Void? { - isJukeboxEnabled = mediaPlayerController.isJukeboxEnabled - millisPlayed = max(0, mediaPlayerController.playerPosition) - duration = mediaPlayerController.playerDuration - playerState = mediaPlayerController.playerState - return null + + 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) } - - @Suppress("LongMethod") - override fun done(result: Void?) { - if (cancellationToken.isCancellationRequested) return - if (currentPlaying != null) { - val millisTotal = if (duration == null) 0 else duration!! - positionTextView.text = Util.formatTotalDuration(millisPlayed.toLong(), true) - 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( + PlayerState.PREPARING -> setTitle( + this@PlayerFragment, + R.string.download_playerstate_buffering + ) + PlayerState.STARTED -> { + if (mediaPlayerController.isShufflePlayEnabled) { + setTitle( this@PlayerFragment, - R.string.download_playerstate_buffering + R.string.download_playerstate_playing_shuffle ) - PlayerState.STARTED -> { - if (mediaPlayerController.isShufflePlayEnabled) { - 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) + } 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) { - PlayerState.STARTED -> { - pauseButton.isVisible = true - stopButton.isVisible = false - startButton.isVisible = false - } - PlayerState.DOWNLOADING, PlayerState.PREPARING -> { - pauseButton.isVisible = false - stopButton.isVisible = true - startButton.isVisible = false - } - else -> { - pauseButton.isVisible = false - stopButton.isVisible = false - 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 + when (playerState) { + PlayerState.STARTED -> { + pauseButton.isVisible = true + stopButton.isVisible = false + startButton.isVisible = false + } + PlayerState.DOWNLOADING, PlayerState.PREPARING -> { + pauseButton.isVisible = false + stopButton.isVisible = true + startButton.isVisible = false + } + else -> { + pauseButton.isVisible = false + stopButton.isVisible = false + startButton.isVisible = true } } - 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) { - object : SilentBackgroundTask(activity) { - var msPlayed = 0 - var duration: Int? = null - var seekTo = 0 - override fun doInBackground(): Void? { - msPlayed = max(0, mediaPlayerController.playerPosition) - 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() + launch(CommunicationError.getHandler(context)) { + val msPlayed: Int = max(0, mediaPlayerController.playerPosition) + val duration = mediaPlayerController.playerDuration + val seekTo = (msPlayed + ms).coerceAtMost(duration) + mediaPlayerController.seekTo(seekTo) + progressBar.progress = seekTo + } } override fun onDown(me: MotionEvent): Boolean { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 9d785a30..6394b08a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -38,7 +38,6 @@ import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.fragment.FragmentTitle.Companion.getTitle import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle -import org.moire.ultrasonic.service.CommunicationErrorHandler import org.moire.ultrasonic.service.MediaPlayerController import org.moire.ultrasonic.subsonic.DownloadHandler 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.util.AlbumHeader import org.moire.ultrasonic.util.CancellationToken +import org.moire.ultrasonic.util.CommunicationError import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator import org.moire.ultrasonic.util.Settings @@ -211,7 +211,7 @@ class TrackCollectionFragment : Fragment() { val handler = CoroutineExceptionHandler { _, exception -> Handler(Looper.getMainLooper()).post { - CommunicationErrorHandler.handleError(exception, context) + CommunicationError.handleError(exception, context) } refreshAlbumListView!!.isRefreshing = false } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt deleted file mode 100644 index bfda4aab..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CommunicationErrorHandler.kt +++ /dev/null @@ -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 . - - 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 - } - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt index 05e80330..e3322213 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerController.kt @@ -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 get() { try { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt index 2342db04..bf703884 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt @@ -1,10 +1,13 @@ package org.moire.ultrasonic.util -import android.os.AsyncTask import android.os.StatFs import java.io.File import java.util.ArrayList 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.moire.ultrasonic.data.ActiveServerProvider 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. */ -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() { - try { - BackgroundCleanup().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) - } catch (all: Exception) { - // If an exception is thrown, assume we execute correctly the next time - Timber.w(all, "Exception in CacheCleaner.clean") + launch(exceptionHandler("clean")) { + backgroundCleanup() } } fun cleanSpace() { - try { - BackgroundSpaceCleanup().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) - } catch (all: Exception) { - // If an exception is thrown, assume we execute correctly the next time - Timber.w(all, "Exception in CacheCleaner.cleanSpace") + launch(exceptionHandler("cleanSpace")) { + backgroundSpaceCleanup() } } fun cleanPlaylists(playlists: List) { + launch(exceptionHandler("cleanPlaylists")) { + backgroundPlaylistsCleanup(playlists) + } + } + + private fun backgroundCleanup() { try { - BackgroundPlaylistsCleanup().executeOnExecutor( - AsyncTask.THREAD_POOL_EXECUTOR, - playlists - ) - } catch (all: Exception) { - // If an exception is thrown, assume we execute correctly the next time - Timber.w(all, "Exception in CacheCleaner.cleanPlaylists") + val files: MutableList = ArrayList() + val dirs: MutableList = 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.") } } - private class BackgroundCleanup : AsyncTask() { - override fun doInBackground(vararg params: Void?): Void? { - try { - Thread.currentThread().name = "BackgroundCleanup" + private fun backgroundSpaceCleanup() { + try { + val files: MutableList = ArrayList() + val dirs: MutableList = ArrayList() - val files: MutableList = ArrayList() - val dirs: MutableList = 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() { - override fun doInBackground(vararg params: Void?): Void? { - try { - Thread.currentThread().name = "BackgroundSpaceCleanup" - - val files: MutableList = ArrayList() - val dirs: MutableList = ArrayList() - - findCandidatesForDeletion(musicDirectory, files, dirs) - - val bytesToDelete = getMinimumDelete(files) - if (bytesToDelete <= 0L) return null + findCandidatesForDeletion(musicDirectory, files, dirs) + val bytesToDelete = getMinimumDelete(files) + if (bytesToDelete > 0L) { sortByAscendingModificationTime(files) val filesToNotDelete = findFilesToNotDelete() 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, Void?, Void?>() { - override fun doInBackground(vararg params: List): Void? { - try { - val activeServerProvider = inject( - ActiveServerProvider::class.java - ) + private fun backgroundPlaylistsCleanup(vararg params: List) { + try { + val activeServerProvider = inject( + 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 - val playlistFiles = listFiles(getPlaylistDirectory(server)) - 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.") + for ((_, name) in playlists) { + playlistFiles.remove(getPlaylistFile(server, name)) } - return null + + for (playlist in playlistFiles) { + playlist.delete() + } + } catch (all: RuntimeException) { + Timber.e(all, "Error in playlist cache cleaning.") } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CommunicationError.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CommunicationError.kt new file mode 100644 index 00000000..2a722e26 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CommunicationError.kt @@ -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 + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SilentBackgroundTask.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SilentBackgroundTask.kt deleted file mode 100644 index 3639aa2c..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SilentBackgroundTask.kt +++ /dev/null @@ -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(activity: Activity?) : BackgroundTask(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) {} -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SubsonicUncaughtExceptionHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/UncaughtExceptionHandler.kt similarity index 91% rename from ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SubsonicUncaughtExceptionHandler.kt rename to ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/UncaughtExceptionHandler.kt index 658d2943..d0d713e1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/SubsonicUncaughtExceptionHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/UncaughtExceptionHandler.kt @@ -9,7 +9,7 @@ import timber.log.Timber /** * Logs the stack trace of uncaught exceptions to a file on the SD card. */ -class SubsonicUncaughtExceptionHandler( +class UncaughtExceptionHandler( private val context: Context ) : Thread.UncaughtExceptionHandler { private val defaultHandler: Thread.UncaughtExceptionHandler? = @@ -31,8 +31,8 @@ class SubsonicUncaughtExceptionHandler( throwable.printStackTrace(printWriter) Timber.e(throwable, "Uncaught Exception! %s", logMessage) Timber.i("Stack trace written to %s", file) - } catch (x: Throwable) { - Timber.e(x, "Failed to write stack trace to %s", file) + } catch (all: Throwable) { + Timber.e(all, "Failed to write stack trace to %s", file) } finally { Util.close(printWriter) defaultHandler?.uncaughtException(thread, throwable) diff --git a/ultrasonic/src/main/res/layout/media_buttons.xml b/ultrasonic/src/main/res/layout/media_buttons.xml index a9eef057..88332cb1 100644 --- a/ultrasonic/src/main/res/layout/media_buttons.xml +++ b/ultrasonic/src/main/res/layout/media_buttons.xml @@ -11,7 +11,6 @@ a:id="@+id/button_shuffle" a:layout_width="0dip" a:layout_height="26dp" - a:layout_alignParentLeft="true" a:layout_gravity="center" a:layout_weight="1" a:adjustViewBounds="true" diff --git a/ultrasonic/src/main/res/layout/player_media_info.xml b/ultrasonic/src/main/res/layout/player_media_info.xml index 1851e4fa..fe23ebd9 100644 --- a/ultrasonic/src/main/res/layout/player_media_info.xml +++ b/ultrasonic/src/main/res/layout/player_media_info.xml @@ -18,7 +18,9 @@ a:id="@+id/current_playing_song" a:layout_width="wrap_content" a:layout_height="wrap_content" - a:ellipsize="start" + a:layout_marginEnd="10dip" + a:paddingRight="30dip" + a:ellipsize="marquee" a:gravity="left" a:singleLine="true" a:textAppearance="?android:attr/textAppearanceLarge" @@ -29,7 +31,7 @@ a:id="@+id/current_playing_artist" a:layout_width="wrap_content" a:layout_height="wrap_content" - a:ellipsize="start" + a:ellipsize="marquee" a:gravity="left" a:singleLine="true" a:textAppearance="?android:attr/textAppearanceSmall" @@ -62,7 +64,8 @@ a:ellipsize="start" a:gravity="right" a:text="0 / 0" - a:textAppearance="?android:attr/textAppearanceSmall" /> + a:textAppearance="?android:attr/textAppearanceSmall" + tools:ignore="HardcodedText" />