Merge branch 'develop' into ready/rm-viewflipper
This commit is contained in:
commit
d84a0a3929
|
@ -4,13 +4,11 @@
|
|||
<CurrentIssues>
|
||||
<ID>ComplexCondition:DownloadHandler.kt$DownloadHandler.<no name provided>$!append && !playNext && !unpin && !background</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() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && ( playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED )</ID>
|
||||
<ID>ComplexMethod:DownloadFile.kt$DownloadFile.DownloadTask$override fun execute()</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:TrackCollectionFragment.kt$TrackCollectionFragment$private fun enableButtons()</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.DownloadTask$String.format("Download of '%s' was cancelled", 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.CheckCompletionTask$String.format("CheckCompletionTask (%s)", downloadFile)</ID>
|
||||
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler$String.format("%d:%s", timeSpanAmount, timeSpanType)</ID>
|
||||
<ID>ImplicitDefaultLocale:ShareHandler.kt$ShareHandler.<no name provided>$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("%s ", bitRate)</ID>
|
||||
<ID>ImplicitDefaultLocale:SongView.kt$SongView$String.format("%s > %s", suffix, transcodedSuffix)</ID>
|
||||
<ID>LargeClass:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</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: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: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>
|
||||
|
@ -39,27 +35,21 @@
|
|||
<ID>LongMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
|
||||
<ID>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) )</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.<no name provided>$60000</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$86400L</ID>
|
||||
<ID>MagicNumber:LocalMediaPlayer.kt$LocalMediaPlayer.BufferTask$8L</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$4</ID>
|
||||
<ID>MagicNumber:RESTMusicService.kt$RESTMusicService$206</ID>
|
||||
<ID>MagicNumber:SongView.kt$SongView$3</ID>
|
||||
<ID>MagicNumber:SongView.kt$SongView$4</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: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>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:TrackCollectionFragment.kt$TrackCollectionFragment$override fun onContextItemSelected(menuItem: MenuItem): Boolean</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.PositionCache$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>TooManyFunctions:MediaPlayerService.kt$MediaPlayerService : Service</ID>
|
||||
<ID>TooManyFunctions:RESTMusicService.kt$RESTMusicService : MusicService</ID>
|
||||
<ID>TooManyFunctions:TrackCollectionFragment.kt$TrackCollectionFragment : Fragment</ID>
|
||||
<ID>UtilityClassWithPublicConstructor:CommunicationErrorHandler.kt$CommunicationErrorHandler</ID>
|
||||
<ID>UtilityClassWithPublicConstructor:FragmentTitle.kt$FragmentTitle</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
|
@ -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<T> 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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Void?>? = 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<Void?>(activity) {
|
||||
override fun doInBackground(): Void? {
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
mediaPlayerController.previous()
|
||||
return null
|
||||
}
|
||||
|
||||
override fun done(result: Void?) {
|
||||
onCurrentChanged()
|
||||
onSliderProgressChanged()
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
|
||||
previousButton.setOnRepeatListener {
|
||||
|
@ -253,65 +249,43 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
|
|||
|
||||
nextButton.setOnClickListener {
|
||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||
object : SilentBackgroundTask<Boolean?>(activity) {
|
||||
override fun doInBackground(): Boolean {
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
mediaPlayerController.next()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun done(result: Boolean?) {
|
||||
if (result == true) {
|
||||
onCurrentChanged()
|
||||
onSliderProgressChanged()
|
||||
}
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
|
||||
nextButton.setOnRepeatListener {
|
||||
val incrementTime = Settings.incrementTime
|
||||
changeProgress(incrementTime)
|
||||
}
|
||||
|
||||
pauseButton.setOnClickListener {
|
||||
object : SilentBackgroundTask<Void?>(activity) {
|
||||
override fun doInBackground(): Void? {
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
mediaPlayerController.pause()
|
||||
return null
|
||||
}
|
||||
|
||||
override fun done(result: Void?) {
|
||||
onCurrentChanged()
|
||||
onSliderProgressChanged()
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
|
||||
stopButton.setOnClickListener {
|
||||
object : SilentBackgroundTask<Void?>(activity) {
|
||||
override fun doInBackground(): Void? {
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
mediaPlayerController.reset()
|
||||
return null
|
||||
}
|
||||
|
||||
override fun done(result: Void?) {
|
||||
onCurrentChanged()
|
||||
onSliderProgressChanged()
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
|
||||
startButton.setOnClickListener {
|
||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||
object : SilentBackgroundTask<Void?>(activity) {
|
||||
override fun doInBackground(): Void? {
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
start()
|
||||
return null
|
||||
}
|
||||
|
||||
override fun done(result: Void?) {
|
||||
onCurrentChanged()
|
||||
onSliderProgressChanged()
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
|
||||
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<Void?>(activity) {
|
||||
override fun doInBackground(): Void? {
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
mediaPlayerController.seekTo(progressBar.progress)
|
||||
return null
|
||||
}
|
||||
|
||||
override fun done(result: Void?) {
|
||||
onSliderProgressChanged()
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||
|
@ -356,18 +324,13 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
|
|||
|
||||
playlistView.setOnItemClickListener { _, _, position, _ ->
|
||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||
object : SilentBackgroundTask<Void?>(activity) {
|
||||
override fun doInBackground(): Void? {
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
mediaPlayerController.play(position)
|
||||
return null
|
||||
}
|
||||
|
||||
override fun done(result: Void?) {
|
||||
onCurrentChanged()
|
||||
onSliderProgressChanged()
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
|
||||
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<Void?>(activity) {
|
||||
@Throws(Throwable::class)
|
||||
override fun doInBackground(): Void? {
|
||||
val entries: MutableList<MusicDirectory.Entry> = LinkedList()
|
||||
for (downloadFile in mediaPlayerController.playList) {
|
||||
entries.add(downloadFile.song)
|
||||
|
||||
ioScope.launch {
|
||||
|
||||
val entries = mediaPlayerController.playList.map {
|
||||
it.song
|
||||
}
|
||||
val musicService = getMusicService()
|
||||
musicService.createPlaylist(null, playlistName, entries)
|
||||
return null
|
||||
}
|
||||
|
||||
override fun done(result: Void?) {
|
||||
}.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,32 +934,21 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "ComplexMethod")
|
||||
@Synchronized
|
||||
private fun onSliderProgressChanged() {
|
||||
if (onProgressChangedTask != null) {
|
||||
return
|
||||
}
|
||||
onProgressChangedTask = object : SilentBackgroundTask<Void?>(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
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override fun done(result: Void?) {
|
||||
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) {
|
||||
val millisTotal = if (duration == null) 0 else duration!!
|
||||
positionTextView.text = Util.formatTotalDuration(millisPlayed.toLong(), true)
|
||||
durationTextView.text = Util.formatTotalDuration(millisTotal.toLong(), true)
|
||||
durationTextView.text = Util.formatTotalDuration(duration.toLong(), true)
|
||||
progressBar.max =
|
||||
if (millisTotal == 0) 100 else millisTotal // Work-around for apparent bug.
|
||||
if (duration == 0) 100 else duration // Work-around for apparent bug.
|
||||
progressBar.progress = millisPlayed
|
||||
progressBar.isEnabled = currentPlaying!!.isWorkDone || isJukeboxEnabled
|
||||
} else {
|
||||
|
@ -1065,30 +1013,16 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
|
|||
// 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()
|
||||
}
|
||||
|
||||
private fun changeProgress(ms: Int) {
|
||||
object : SilentBackgroundTask<Void?>(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)
|
||||
launch(CommunicationError.getHandler(context)) {
|
||||
val msPlayed: Int = max(0, mediaPlayerController.playerPosition)
|
||||
val duration = mediaPlayerController.playerDuration
|
||||
val seekTo = (msPlayed + ms).coerceAtMost(duration)
|
||||
mediaPlayerController.seekTo(seekTo)
|
||||
return null
|
||||
}
|
||||
|
||||
override fun done(result: Void?) {
|
||||
progressBar.progress = seekTo
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
|
||||
override fun onDown(me: MotionEvent): Boolean {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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,42 +25,34 @@ 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<Playlist>) {
|
||||
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")
|
||||
launch(exceptionHandler("cleanPlaylists")) {
|
||||
backgroundPlaylistsCleanup(playlists)
|
||||
}
|
||||
}
|
||||
|
||||
private class BackgroundCleanup : AsyncTask<Void?, Void?, Void?>() {
|
||||
override fun doInBackground(vararg params: Void?): Void? {
|
||||
private fun backgroundCleanup() {
|
||||
try {
|
||||
Thread.currentThread().name = "BackgroundCleanup"
|
||||
|
||||
val files: MutableList<File> = ArrayList()
|
||||
val dirs: MutableList<File> = ArrayList()
|
||||
|
||||
|
@ -70,42 +65,32 @@ class CacheCleaner {
|
|||
} 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? {
|
||||
private fun backgroundSpaceCleanup() {
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
private class BackgroundPlaylistsCleanup : AsyncTask<List<Playlist>, Void?, Void?>() {
|
||||
override fun doInBackground(vararg params: List<Playlist>): Void? {
|
||||
private fun backgroundPlaylistsCleanup(vararg params: List<Playlist>) {
|
||||
try {
|
||||
val activeServerProvider = inject<ActiveServerProvider>(
|
||||
ActiveServerProvider::class.java
|
||||
)
|
||||
|
||||
Thread.currentThread().name = "BackgroundPlaylistsCleanup"
|
||||
|
||||
val server = activeServerProvider.value.getActiveServer().name
|
||||
val playlistFiles = listFiles(getPlaylistDirectory(server))
|
||||
val playlists = params[0]
|
||||
|
@ -120,8 +105,6 @@ class CacheCleaner {
|
|||
} catch (all: RuntimeException) {
|
||||
Timber.e(all, "Error in playlist cache cleaning.")
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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) {}
|
||||
}
|
|
@ -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)
|
|
@ -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"
|
||||
|
|
|
@ -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" />
|
||||
|
||||
<TextView
|
||||
a:id="@+id/current_total_duration"
|
||||
|
|
Loading…
Reference in New Issue