ultrasonic-app-subsonic-and.../ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt

1208 lines
47 KiB
Kotlin

/*
* PlayerFragment.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.fragment
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.graphics.Point
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.Handler
import android.view.ContextMenu
import android.view.ContextMenu.ContextMenuInfo
import android.view.GestureDetector
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.animation.AnimationUtils
import android.widget.AdapterView.AdapterContextMenuInfo
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import android.widget.TextView
import android.widget.ViewFlipper
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.navigation.Navigation
import com.mobeta.android.dslv.DragSortListView
import com.mobeta.android.dslv.DragSortListView.DragSortListener
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.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
import kotlin.math.abs
import kotlin.math.max
import org.koin.android.ext.android.inject
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.moire.ultrasonic.R
import org.moire.ultrasonic.audiofx.EqualizerController
import org.moire.ultrasonic.audiofx.VisualizerController
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.PlayerState
import org.moire.ultrasonic.domain.RepeatMode
import org.moire.ultrasonic.featureflags.Feature
import org.moire.ultrasonic.featureflags.FeatureStorage
import org.moire.ultrasonic.fragment.FragmentTitle.Companion.setTitle
import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.LocalMediaPlayer
import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
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.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
import org.moire.ultrasonic.view.VisualizerView
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 {
// Settings
private var currentRevision: Long = 0
private var swipeDistance = 0
private var swipeVelocity = 0
private var jukeboxAvailable = false
private var useFiveStarRating = false
private var isEqualizerAvailable = false
private var isVisualizerAvailable = false
// Detectors & Callbacks
private lateinit var gestureScanner: GestureDetector
private lateinit var cancellationToken: CancellationToken
// Data & Services
private val networkAndStorageChecker: NetworkAndStorageChecker by inject()
private val mediaPlayerController: MediaPlayerController by inject()
private val localMediaPlayer: LocalMediaPlayer by inject()
private val shareHandler: ShareHandler by inject()
private val imageLoaderProvider: ImageLoaderProvider by inject()
private lateinit var executorService: ScheduledExecutorService
private var currentPlaying: DownloadFile? = null
private var currentSong: MusicDirectory.Entry? = null
private var onProgressChangedTask: SilentBackgroundTask<Void?>? = null
// Views and UI Elements
private lateinit var visualizerViewLayout: LinearLayout
private lateinit var visualizerView: VisualizerView
private lateinit var playlistNameView: EditText
private lateinit var starMenuItem: MenuItem
private lateinit var fiveStar1ImageView: ImageView
private lateinit var fiveStar2ImageView: ImageView
private lateinit var fiveStar3ImageView: ImageView
private lateinit var fiveStar4ImageView: ImageView
private lateinit var fiveStar5ImageView: ImageView
private lateinit var playlistFlipper: ViewFlipper
private lateinit var emptyTextView: TextView
private lateinit var songTitleTextView: TextView
private lateinit var albumTextView: TextView
private lateinit var artistTextView: TextView
private lateinit var albumArtImageView: ImageView
private lateinit var playlistView: DragSortListView
private lateinit var positionTextView: TextView
private lateinit var downloadTrackTextView: TextView
private lateinit var downloadTotalDurationTextView: TextView
private lateinit var durationTextView: TextView
private lateinit var pauseButton: View
private lateinit var stopButton: View
private lateinit var startButton: View
private lateinit var repeatButton: ImageView
private lateinit var hollowStar: Drawable
private lateinit var fullStar: Drawable
private lateinit var progressBar: SeekBar
override fun onCreate(savedInstanceState: Bundle?) {
Util.applyTheme(this.context)
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.current_playing, container, false)
}
private fun findViews(view: View) {
playlistFlipper = view.findViewById(R.id.current_playing_playlist_flipper)
emptyTextView = view.findViewById(R.id.playlist_empty)
songTitleTextView = view.findViewById(R.id.current_playing_song)
albumTextView = view.findViewById(R.id.current_playing_album)
artistTextView = view.findViewById(R.id.current_playing_artist)
albumArtImageView = view.findViewById(R.id.current_playing_album_art_image)
positionTextView = view.findViewById(R.id.current_playing_position)
downloadTrackTextView = view.findViewById(R.id.current_playing_track)
downloadTotalDurationTextView = view.findViewById(R.id.current_total_duration)
durationTextView = view.findViewById(R.id.current_playing_duration)
progressBar = view.findViewById(R.id.current_playing_progress_bar)
playlistView = view.findViewById(R.id.playlist_view)
pauseButton = view.findViewById(R.id.button_pause)
stopButton = view.findViewById(R.id.button_stop)
startButton = view.findViewById(R.id.button_start)
repeatButton = view.findViewById(R.id.button_repeat)
visualizerViewLayout = view.findViewById(R.id.current_playing_visualizer_layout)
fiveStar1ImageView = view.findViewById(R.id.song_five_star_1)
fiveStar2ImageView = view.findViewById(R.id.song_five_star_2)
fiveStar3ImageView = view.findViewById(R.id.song_five_star_3)
fiveStar4ImageView = view.findViewById(R.id.song_five_star_4)
fiveStar5ImageView = view.findViewById(R.id.song_five_star_5)
}
@Suppress("LongMethod")
@SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
cancellationToken = CancellationToken()
setTitle(this, R.string.common_appname)
val windowManager = requireActivity().windowManager
val display = windowManager.defaultDisplay
val size = Point()
display.getSize(size)
val width = size.x
val height = size.y
setHasOptionsMenu(true)
useFiveStarRating = get<FeatureStorage>().isFeatureEnabled(Feature.FIVE_STAR_RATING)
swipeDistance = (width + height) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100
swipeVelocity = swipeDistance
gestureScanner = GestureDetector(context, this)
// The secondary progress is an indicator of how far the song is cached.
localMediaPlayer.secondaryProgress.observe(
viewLifecycleOwner,
{
progressBar.secondaryProgress = it
}
)
findViews(view)
val previousButton: AutoRepeatButton = view.findViewById(R.id.button_previous)
val nextButton: AutoRepeatButton = view.findViewById(R.id.button_next)
val shuffleButton = view.findViewById<View>(R.id.button_shuffle)
val ratingLinearLayout = view.findViewById<LinearLayout>(R.id.song_rating)
if (!useFiveStarRating) ratingLinearLayout.isVisible = false
hollowStar = Util.getDrawableFromAttribute(view.context, R.attr.star_hollow)
fullStar = Util.getDrawableFromAttribute(context, R.attr.star_full)
fiveStar1ImageView.setOnClickListener { setSongRating(1) }
fiveStar2ImageView.setOnClickListener { setSongRating(2) }
fiveStar3ImageView.setOnClickListener { setSongRating(3) }
fiveStar4ImageView.setOnClickListener { setSongRating(4) }
fiveStar5ImageView.setOnClickListener { setSongRating(5) }
albumArtImageView.setOnTouchListener { _, me ->
gestureScanner.onTouchEvent(me)
}
albumArtImageView.setOnClickListener {
toggleFullScreenAlbumArt()
}
previousButton.setOnClickListener {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
object : SilentBackgroundTask<Void?>(activity) {
override fun doInBackground(): Void? {
mediaPlayerController.previous()
return null
}
override fun done(result: Void?) {
onCurrentChanged()
onSliderProgressChanged()
}
}.execute()
}
previousButton.setOnRepeatListener {
val incrementTime = Settings.incrementTime
changeProgress(-incrementTime)
}
nextButton.setOnClickListener {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
object : SilentBackgroundTask<Boolean?>(activity) {
override fun doInBackground(): Boolean {
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? {
mediaPlayerController.pause()
return null
}
override fun done(result: Void?) {
onCurrentChanged()
onSliderProgressChanged()
}
}.execute()
}
stopButton.setOnClickListener {
object : SilentBackgroundTask<Void?>(activity) {
override fun doInBackground(): Void? {
mediaPlayerController.reset()
return null
}
override fun done(result: Void?) {
onCurrentChanged()
onSliderProgressChanged()
}
}.execute()
}
startButton.setOnClickListener {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
object : SilentBackgroundTask<Void?>(activity) {
override fun doInBackground(): Void? {
start()
return null
}
override fun done(result: Void?) {
onCurrentChanged()
onSliderProgressChanged()
}
}.execute()
}
shuffleButton.setOnClickListener {
mediaPlayerController.shuffle()
Util.toast(activity, R.string.download_menu_shuffle_notification)
}
repeatButton.setOnClickListener {
val repeatMode = mediaPlayerController.repeatMode.next()
mediaPlayerController.repeatMode = repeatMode
onPlaylistChanged()
when (repeatMode) {
RepeatMode.OFF -> Util.toast(
context, R.string.download_repeat_off
)
RepeatMode.ALL -> Util.toast(
context, R.string.download_repeat_all
)
RepeatMode.SINGLE -> Util.toast(
context, R.string.download_repeat_single
)
else -> {
}
}
}
progressBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
override fun onStopTrackingTouch(seekBar: SeekBar) {
object : SilentBackgroundTask<Void?>(activity) {
override fun doInBackground(): Void? {
mediaPlayerController.seekTo(progressBar.progress)
return null
}
override fun done(result: Void?) {
onSliderProgressChanged()
}
}.execute()
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
})
playlistView.setOnItemClickListener { _, _, position, _ ->
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
object : SilentBackgroundTask<Void?>(activity) {
override fun doInBackground(): Void? {
mediaPlayerController.play(position)
return null
}
override fun done(result: Void?) {
onCurrentChanged()
onSliderProgressChanged()
}
}.execute()
}
registerForContextMenu(playlistView)
if (arguments != null && requireArguments().getBoolean(
Constants.INTENT_EXTRA_NAME_SHUFFLE,
false
)
) {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
mediaPlayerController.isShufflePlayEnabled = true
}
visualizerViewLayout.isVisible = false
VisualizerController.get().observe(
requireActivity(),
{ visualizerController ->
if (visualizerController != null) {
Timber.d("VisualizerController Observer.onChanged received controller")
visualizerView = VisualizerView(context)
visualizerViewLayout.addView(
visualizerView,
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
)
)
visualizerViewLayout.isVisible = visualizerView.isActive
visualizerView.setOnTouchListener { _, _ ->
visualizerView.isActive = !visualizerView.isActive
mediaPlayerController.showVisualization = visualizerView.isActive
true
}
isVisualizerAvailable = true
} else {
Timber.d("VisualizerController Observer.onChanged has no controller")
visualizerViewLayout.isVisible = false
isVisualizerAvailable = false
}
}
)
EqualizerController.get().observe(
requireActivity(),
{ equalizerController ->
isEqualizerAvailable = if (equalizerController != null) {
Timber.d("EqualizerController Observer.onChanged received controller")
true
} else {
Timber.d("EqualizerController Observer.onChanged has no controller")
false
}
}
)
Thread {
try {
jukeboxAvailable = mediaPlayerController.isJukeboxAvailable
} catch (all: Exception) {
Timber.e(all)
}
}.start()
view.setOnTouchListener { _, event -> gestureScanner.onTouchEvent(event) }
}
override fun onResume() {
super.onResume()
if (mediaPlayerController.currentPlaying == null) {
playlistFlipper.displayedChild = 1
} else {
// Download list and Album art must be updated when Resumed
onPlaylistChanged()
onCurrentChanged()
}
val handler = Handler()
val runnable = Runnable { handler.post { update(cancellationToken) } }
executorService = Executors.newSingleThreadScheduledExecutor()
executorService.scheduleWithFixedDelay(runnable, 0L, 250L, TimeUnit.MILLISECONDS)
if (mediaPlayerController.keepScreenOn) {
requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
if (::visualizerView.isInitialized) {
visualizerView.isActive = mediaPlayerController.showVisualization
}
requireActivity().invalidateOptionsMenu()
}
// Scroll to current playing.
private fun scrollToCurrent() {
val adapter = playlistView.adapter
if (adapter != null) {
val count = adapter.count
for (i in 0 until count) {
if (currentPlaying == playlistView.getItemAtPosition(i)) {
playlistView.smoothScrollToPositionFromTop(i, 40)
return
}
}
}
}
override fun onPause() {
super.onPause()
executorService.shutdown()
if (::visualizerView.isInitialized) {
visualizerView.isActive = mediaPlayerController.showVisualization
}
}
override fun onDestroyView() {
cancellationToken.cancel()
super.onDestroyView()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.nowplaying, menu)
super.onCreateOptionsMenu(menu, inflater)
}
@Suppress("ComplexMethod", "LongMethod", "NestedBlockDepth")
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
val screenOption = menu.findItem(R.id.menu_item_screen_on_off)
val jukeboxOption = menu.findItem(R.id.menu_item_jukebox)
val equalizerMenuItem = menu.findItem(R.id.menu_item_equalizer)
val visualizerMenuItem = menu.findItem(R.id.menu_item_visualizer)
val shareMenuItem = menu.findItem(R.id.menu_item_share)
val shareSongMenuItem = menu.findItem(R.id.menu_item_share_song)
starMenuItem = menu.findItem(R.id.menu_item_star)
val bookmarkMenuItem = menu.findItem(R.id.menu_item_bookmark_set)
val bookmarkRemoveMenuItem = menu.findItem(R.id.menu_item_bookmark_delete)
if (isOffline()) {
if (shareMenuItem != null) {
shareMenuItem.isVisible = false
}
starMenuItem.isVisible = false
if (bookmarkMenuItem != null) {
bookmarkMenuItem.isVisible = false
}
if (bookmarkRemoveMenuItem != null) {
bookmarkRemoveMenuItem.isVisible = false
}
}
if (equalizerMenuItem != null) {
equalizerMenuItem.isEnabled = isEqualizerAvailable
equalizerMenuItem.isVisible = isEqualizerAvailable
}
if (visualizerMenuItem != null) {
visualizerMenuItem.isEnabled = isVisualizerAvailable
visualizerMenuItem.isVisible = isVisualizerAvailable
}
val mediaPlayerController = mediaPlayerController
val downloadFile = mediaPlayerController.currentPlaying
if (downloadFile != null) {
currentSong = downloadFile.song
}
if (useFiveStarRating) starMenuItem.isVisible = false
if (currentSong != null) {
starMenuItem.icon = if (currentSong!!.starred) fullStar else hollowStar
shareSongMenuItem.isVisible = true
} else {
starMenuItem.icon = hollowStar
shareSongMenuItem.isVisible = false
}
if (mediaPlayerController.keepScreenOn) {
screenOption?.setTitle(R.string.download_menu_screen_off)
} else {
screenOption?.setTitle(R.string.download_menu_screen_on)
}
if (jukeboxOption != null) {
jukeboxOption.isEnabled = jukeboxAvailable
jukeboxOption.isVisible = jukeboxAvailable
if (mediaPlayerController.isJukeboxEnabled) {
jukeboxOption.setTitle(R.string.download_menu_jukebox_off)
} else {
jukeboxOption.setTitle(R.string.download_menu_jukebox_on)
}
}
}
override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?) {
super.onCreateContextMenu(menu, view, menuInfo)
if (view === playlistView) {
val info = menuInfo as AdapterContextMenuInfo?
val downloadFile = playlistView.getItemAtPosition(info!!.position) as DownloadFile
val menuInflater = requireActivity().menuInflater
menuInflater.inflate(R.menu.nowplaying_context, menu)
val song: MusicDirectory.Entry?
song = downloadFile.song
if (song.parent == null) {
val menuItem = menu.findItem(R.id.menu_show_album)
if (menuItem != null) {
menuItem.isVisible = false
}
}
if (isOffline() || !Settings.shouldUseId3Tags) {
menu.findItem(R.id.menu_show_artist)?.isVisible = false
}
if (isOffline()) {
menu.findItem(R.id.menu_lyrics)?.isVisible = false
}
}
}
override fun onContextItemSelected(menuItem: MenuItem): Boolean {
val info = menuItem.menuInfo as AdapterContextMenuInfo
val downloadFile = playlistView.getItemAtPosition(info.position) as DownloadFile
return menuItemSelected(menuItem.itemId, downloadFile) || super.onContextItemSelected(
menuItem
)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return menuItemSelected(item.itemId, null) || super.onOptionsItemSelected(item)
}
@Suppress("ComplexMethod", "LongMethod", "ReturnCount")
private fun menuItemSelected(menuItemId: Int, song: DownloadFile?): Boolean {
var entry: MusicDirectory.Entry? = null
val bundle: Bundle
if (song != null) {
entry = song.song
}
when (menuItemId) {
R.id.menu_show_artist -> {
if (entry == null) return false
if (Settings.shouldUseId3Tags) {
bundle = Bundle()
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, entry.artistId)
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.artist)
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, entry.artistId)
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, true)
Navigation.findNavController(requireView())
.navigate(R.id.playerToSelectAlbum, bundle)
}
return true
}
R.id.menu_show_album -> {
if (entry == null) return false
val albumId = if (Settings.shouldUseId3Tags) entry.albumId else entry.parent
bundle = Bundle()
bundle.putString(Constants.INTENT_EXTRA_NAME_ID, albumId)
bundle.putString(Constants.INTENT_EXTRA_NAME_NAME, entry.album)
bundle.putString(Constants.INTENT_EXTRA_NAME_PARENT_ID, entry.parent)
bundle.putBoolean(Constants.INTENT_EXTRA_NAME_IS_ALBUM, true)
Navigation.findNavController(requireView())
.navigate(R.id.playerToSelectAlbum, bundle)
return true
}
R.id.menu_lyrics -> {
if (entry == null) return false
bundle = Bundle()
bundle.putString(Constants.INTENT_EXTRA_NAME_ARTIST, entry.artist)
bundle.putString(Constants.INTENT_EXTRA_NAME_TITLE, entry.title)
Navigation.findNavController(requireView()).navigate(R.id.playerToLyrics, bundle)
return true
}
R.id.menu_remove -> {
mediaPlayerController.removeFromPlaylist(song!!)
onPlaylistChanged()
return true
}
R.id.menu_item_screen_on_off -> {
val window = requireActivity().window
if (mediaPlayerController.keepScreenOn) {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
mediaPlayerController.keepScreenOn = false
} else {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
mediaPlayerController.keepScreenOn = true
}
return true
}
R.id.menu_shuffle -> {
mediaPlayerController.shuffle()
Util.toast(context, R.string.download_menu_shuffle_notification)
return true
}
R.id.menu_item_equalizer -> {
Navigation.findNavController(requireView()).navigate(R.id.playerToEqualizer)
return true
}
R.id.menu_item_visualizer -> {
val active = !visualizerView.isActive
visualizerView.isActive = active
visualizerViewLayout.isVisible = visualizerView.isActive
mediaPlayerController.showVisualization = visualizerView.isActive
Util.toast(
context,
if (active) R.string.download_visualizer_on
else R.string.download_visualizer_off
)
return true
}
R.id.menu_item_jukebox -> {
val jukeboxEnabled = !mediaPlayerController.isJukeboxEnabled
mediaPlayerController.isJukeboxEnabled = jukeboxEnabled
Util.toast(
context,
if (jukeboxEnabled) R.string.download_jukebox_on
else R.string.download_jukebox_off,
false
)
return true
}
R.id.menu_item_toggle_list -> {
toggleFullScreenAlbumArt()
return true
}
R.id.menu_item_clear_playlist -> {
mediaPlayerController.isShufflePlayEnabled = false
mediaPlayerController.clear()
onPlaylistChanged()
return true
}
R.id.menu_item_save_playlist -> {
if (mediaPlayerController.playlistSize > 0) {
showSavePlaylistDialog()
}
return true
}
R.id.menu_item_star -> {
if (currentSong == null) return true
val isStarred = currentSong!!.starred
val id = currentSong!!.id
if (isStarred) {
starMenuItem.icon = hollowStar
currentSong!!.starred = false
} else {
starMenuItem.icon = fullStar
currentSong!!.starred = true
}
Thread {
val musicService = getMusicService()
try {
if (isStarred) {
musicService.unstar(id, null, null)
} else {
musicService.star(id, null, null)
}
} catch (all: Exception) {
Timber.e(all)
}
}.start()
return true
}
R.id.menu_item_bookmark_set -> {
if (currentSong == null) return true
val songId = currentSong!!.id
val playerPosition = mediaPlayerController.playerPosition
currentSong!!.bookmarkPosition = playerPosition
val bookmarkTime = Util.formatTotalDuration(playerPosition.toLong(), true)
Thread {
val musicService = getMusicService()
try {
musicService.createBookmark(songId, playerPosition)
} catch (all: Exception) {
Timber.e(all)
}
}.start()
val msg = resources.getString(
R.string.download_bookmark_set_at_position,
bookmarkTime
)
Util.toast(context, msg)
return true
}
R.id.menu_item_bookmark_delete -> {
if (currentSong == null) return true
val bookmarkSongId = currentSong!!.id
currentSong!!.bookmarkPosition = 0
Thread {
val musicService = getMusicService()
try {
musicService.deleteBookmark(bookmarkSongId)
} catch (all: Exception) {
Timber.e(all)
}
}.start()
Util.toast(context, R.string.download_bookmark_removed)
return true
}
R.id.menu_item_share -> {
val mediaPlayerController = mediaPlayerController
val entries: MutableList<MusicDirectory.Entry?> = ArrayList()
val downloadServiceSongs = mediaPlayerController.playList
for (downloadFile in downloadServiceSongs) {
val playlistEntry = downloadFile.song
entries.add(playlistEntry)
}
shareHandler.createShare(this, entries, null, cancellationToken)
return true
}
R.id.menu_item_share_song -> {
if (currentSong == null) return true
val entries: MutableList<MusicDirectory.Entry?> = ArrayList()
entries.add(currentSong)
shareHandler.createShare(this, entries, null, cancellationToken)
return true
}
else -> return false
}
}
private fun update(cancel: CancellationToken?) {
if (cancel!!.isCancellationRequested) return
val mediaPlayerController = mediaPlayerController
if (currentRevision != mediaPlayerController.playListUpdateRevision) {
onPlaylistChanged()
}
if (currentPlaying != mediaPlayerController.currentPlaying) {
onCurrentChanged()
}
onSliderProgressChanged()
requireActivity().invalidateOptionsMenu()
}
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)
}
val musicService = getMusicService()
musicService.createPlaylist(null, playlistName, entries)
return null
}
override fun done(result: Void?) {
Util.toast(context, R.string.download_playlist_done)
}
override fun error(error: Throwable) {
Timber.e(error, "Exception has occurred in savePlaylistInBackground")
val msg = String.format(
Locale.ROOT,
"%s %s",
resources.getString(R.string.download_playlist_error),
getErrorMessage(error)
)
Util.toast(context, msg)
}
}.execute()
}
private fun toggleFullScreenAlbumArt() {
if (playlistFlipper.displayedChild == 1) {
playlistFlipper.inAnimation =
AnimationUtils.loadAnimation(context, R.anim.push_down_in)
playlistFlipper.outAnimation =
AnimationUtils.loadAnimation(context, R.anim.push_down_out)
playlistFlipper.displayedChild = 0
} else {
playlistFlipper.inAnimation =
AnimationUtils.loadAnimation(context, R.anim.push_up_in)
playlistFlipper.outAnimation =
AnimationUtils.loadAnimation(context, R.anim.push_up_out)
playlistFlipper.displayedChild = 1
}
scrollToCurrent()
}
private fun start() {
val service = mediaPlayerController
val state = service.playerState
if (state === PlayerState.PAUSED ||
state === PlayerState.COMPLETED || state === PlayerState.STOPPED
) {
service.start()
} else if (state === PlayerState.IDLE) {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
val current = mediaPlayerController.currentPlayingNumberOnPlaylist
if (current == -1) {
service.play(0)
} else {
service.play(current)
}
}
}
private fun onPlaylistChanged() {
val mediaPlayerController = mediaPlayerController
val list = mediaPlayerController.playList
emptyTextView.setText(R.string.download_empty)
val adapter = SongListAdapter(context, list)
playlistView.adapter = adapter
playlistView.setDragSortListener(object : DragSortListener {
override fun drop(from: Int, to: Int) {
if (from != to) {
val item = adapter.getItem(from)
adapter.remove(item)
adapter.notifyDataSetChanged()
adapter.insert(item, to)
adapter.notifyDataSetChanged()
}
}
override fun drag(from: Int, to: Int) {}
override fun remove(which: Int) {
val item = adapter.getItem(which) ?: return
val currentPlaying = mediaPlayerController.currentPlaying
if (currentPlaying == item) {
mediaPlayerController.next()
}
adapter.remove(item)
adapter.notifyDataSetChanged()
val songRemoved = String.format(
resources.getString(R.string.download_song_removed),
item.song.title
)
Util.toast(context, songRemoved)
onPlaylistChanged()
onCurrentChanged()
}
})
emptyTextView.isVisible = list.isEmpty()
currentRevision = mediaPlayerController.playListUpdateRevision
when (mediaPlayerController.repeatMode) {
RepeatMode.OFF -> repeatButton.setImageDrawable(
Util.getDrawableFromAttribute(
context, R.attr.media_repeat_off
)
)
RepeatMode.ALL -> repeatButton.setImageDrawable(
Util.getDrawableFromAttribute(
context, R.attr.media_repeat_all
)
)
RepeatMode.SINGLE -> repeatButton.setImageDrawable(
Util.getDrawableFromAttribute(
context, R.attr.media_repeat_single
)
)
else -> {
}
}
}
private fun onCurrentChanged() {
currentPlaying = mediaPlayerController.currentPlaying
scrollToCurrent()
val totalDuration = mediaPlayerController.playListDuration
val totalSongs = mediaPlayerController.playlistSize.toLong()
val currentSongIndex = mediaPlayerController.currentPlayingNumberOnPlaylist + 1
val duration = Util.formatTotalDuration(totalDuration)
val trackFormat =
String.format(Locale.getDefault(), "%d / %d", currentSongIndex, totalSongs)
if (currentPlaying != null) {
currentSong = currentPlaying!!.song
songTitleTextView.text = currentSong!!.title
albumTextView.text = currentSong!!.album
artistTextView.text = currentSong!!.artist
downloadTrackTextView.text = trackFormat
downloadTotalDurationTextView.text = duration
imageLoaderProvider.getImageLoader()
.loadImage(albumArtImageView, currentSong, true, 0)
displaySongRating()
} else {
currentSong = null
songTitleTextView.text = null
albumTextView.text = null
artistTextView.text = null
downloadTrackTextView.text = null
downloadTotalDurationTextView.text = null
imageLoaderProvider.getImageLoader()
.loadImage(albumArtImageView, null, true, 0)
}
}
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?) {
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(
this@PlayerFragment,
R.string.download_playerstate_buffering
)
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)
}
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
}
}
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)
mediaPlayerController.seekTo(seekTo)
return null
}
override fun done(result: Void?) {
progressBar.progress = seekTo
}
}.execute()
}
override fun onDown(me: MotionEvent): Boolean {
return false
}
@Suppress("ReturnCount")
override fun onFling(
e1: MotionEvent,
e2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
val e1X = e1.x
val e2X = e2.x
val e1Y = e1.y
val e2Y = e2.y
val absX = abs(velocityX)
val absY = abs(velocityY)
// Right to Left swipe
if (e1X - e2X > swipeDistance && absX > swipeVelocity) {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
mediaPlayerController.next()
onCurrentChanged()
onSliderProgressChanged()
return true
}
// Left to Right swipe
if (e2X - e1X > swipeDistance && absX > swipeVelocity) {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
mediaPlayerController.previous()
onCurrentChanged()
onSliderProgressChanged()
return true
}
// Top to Bottom swipe
if (e2Y - e1Y > swipeDistance && absY > swipeVelocity) {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
mediaPlayerController.seekTo(mediaPlayerController.playerPosition + 30000)
onSliderProgressChanged()
return true
}
// Bottom to Top swipe
if (e1Y - e2Y > swipeDistance && absY > swipeVelocity) {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
mediaPlayerController.seekTo(mediaPlayerController.playerPosition - 8000)
onSliderProgressChanged()
return true
}
return false
}
override fun onLongPress(e: MotionEvent) {}
override fun onScroll(
e1: MotionEvent,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
return false
}
override fun onShowPress(e: MotionEvent) {}
override fun onSingleTapUp(e: MotionEvent): Boolean {
return false
}
private fun displaySongRating() {
var rating = 0
if (currentSong?.userRating != null) {
rating = currentSong!!.userRating!!
}
fiveStar1ImageView.setImageDrawable(if (rating > 0) fullStar else hollowStar)
fiveStar2ImageView.setImageDrawable(if (rating > 1) fullStar else hollowStar)
fiveStar3ImageView.setImageDrawable(if (rating > 2) fullStar else hollowStar)
fiveStar4ImageView.setImageDrawable(if (rating > 3) fullStar else hollowStar)
fiveStar5ImageView.setImageDrawable(if (rating > 4) fullStar else hollowStar)
}
private fun setSongRating(rating: Int) {
if (currentSong == null) return
displaySongRating()
mediaPlayerController.setSongRating(rating)
}
private fun showSavePlaylistDialog() {
val layout = LayoutInflater.from(this.context).inflate(R.layout.save_playlist, null)
playlistNameView = layout.findViewById(R.id.save_playlist_name)
val builder: AlertDialog.Builder = AlertDialog.Builder(context)
builder.setTitle(R.string.download_playlist_title)
builder.setMessage(R.string.download_playlist_name)
builder.setPositiveButton(R.string.common_save) { _, _ ->
savePlaylistInBackground(
playlistNameView.text.toString()
)
}
builder.setNegativeButton(R.string.common_cancel) { dialog, _ -> dialog.cancel() }
builder.setView(layout)
builder.setCancelable(true)
val dialog = builder.create()
val playlistName = mediaPlayerController.suggestedPlaylistName
if (playlistName != null) {
playlistNameView.setText(playlistName)
} else {
val dateFormat: DateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
playlistNameView.setText(dateFormat.format(Date()))
}
dialog.show()
}
companion object {
private const val PERCENTAGE_OF_SCREEN_FOR_SWIPE = 5
}
}