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" />