NowPlayingFragment.kt 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. package fr.riff_app.riff.ui.nowplaying
  2. import android.annotation.SuppressLint
  3. import android.content.ClipboardManager
  4. import android.content.Context
  5. import androidx.lifecycle.ViewModelProviders
  6. import android.os.Bundle
  7. import android.support.v4.media.session.PlaybackStateCompat
  8. import androidx.fragment.app.Fragment
  9. import android.view.LayoutInflater
  10. import android.view.View
  11. import android.view.ViewGroup
  12. import android.widget.*
  13. import androidx.constraintlayout.widget.ConstraintLayout
  14. import androidx.constraintlayout.widget.ConstraintSet
  15. import androidx.lifecycle.Observer
  16. import com.google.android.material.snackbar.BaseTransientBottomBar
  17. import com.google.android.material.snackbar.Snackbar
  18. import fr.riff_app.riff.*
  19. import fr.riff_app.riff.alarm.RadioSleeper
  20. import fr.riff_app.riff.planning.Planning
  21. import fr.riff_app.riff.playerstore.PlayerStore
  22. import fr.riff_app.riff.playerstore.Song
  23. class NowPlayingFragment : Fragment() {
  24. private lateinit var root: View
  25. private lateinit var nowPlayingViewModel: NowPlayingViewModel
  26. @SuppressLint("SetTextI18n")
  27. override fun onCreateView(
  28. inflater: LayoutInflater,
  29. container: ViewGroup?,
  30. savedInstanceState: Bundle?
  31. ): View? {
  32. nowPlayingViewModel = ViewModelProviders.of(this).get(NowPlayingViewModel::class.java)
  33. root = inflater.inflate(R.layout.fragment_nowplaying, container, false)
  34. // View bindings to the ViewModel
  35. val songTitleText: TextView = root.findViewById(R.id.text_song_title)
  36. val songArtistText: TextView = root.findViewById(R.id.text_song_artist)
  37. val seekBarVolume: SeekBar = root.findViewById(R.id.seek_bar_volume)
  38. val volumeText: TextView = root.findViewById(R.id.volume_text)
  39. val progressBar: ProgressBar = root.findViewById(R.id.progressBar)
  40. val volumeIconImage : ImageView = root.findViewById(R.id.volume_icon)
  41. val currentProgrammeText: TextView = root.findViewById(R.id.current_programme)
  42. val streamerPictureImageView: ImageView = root.findViewById(R.id.streamerPicture)
  43. // Note: these values are not used in the generic app, but if you want to, you can use them.
  44. val songTitleNextText: TextView = root.findViewById(R.id.text_song_title_next)
  45. val songArtistNextText: TextView = root.findViewById(R.id.text_song_artist_next)
  46. /*
  47. val streamerNameText : TextView = root.findViewById(R.id.streamerName)
  48. val listenersText : TextView = root.findViewById(R.id.listenersCount)
  49. */
  50. /*
  51. TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(
  52. streamerNameText,8, 20, 2, TypedValue.COMPLEX_UNIT_SP)
  53. TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(
  54. listenersText,8, 16, 2, TypedValue.COMPLEX_UNIT_SP)
  55. */
  56. // trick : I can't observe the queue because it's an ArrayDeque that doesn't trigger any change...
  57. // so I observe a dedicated Mutable that gets set when the queue is updated.
  58. PlayerStore.instance.isQueueUpdated.observe(viewLifecycleOwner, Observer {
  59. val t = if (PlayerStore.instance.queue.size > 0) PlayerStore.instance.queue[0] else Song(noConnectionValue) // (it.peekFirst != null ? it.peekFirst : Song() )
  60. songTitleNextText.text = t.title.value
  61. songArtistNextText.text = t.artist.value
  62. })
  63. /*
  64. PlayerStore.instance.streamerName.observe(viewLifecycleOwner, Observer {
  65. streamerNameText.text = it
  66. })
  67. PlayerStore.instance.listenersCount.observe(viewLifecycleOwner, Observer {
  68. listenersText.text = "${getString(R.string.listeners)}: $it"
  69. })
  70. */
  71. PlayerStore.instance.currentSong.title.observe(viewLifecycleOwner, Observer {
  72. songTitleText.text = it
  73. })
  74. Planning.instance.currentProgramme.observe(viewLifecycleOwner, Observer {
  75. currentProgrammeText.text = "${requireContext().getString(R.string.current_programme)} $it"
  76. })
  77. PlayerStore.instance.currentSong.artist.observe(viewLifecycleOwner, Observer {
  78. songArtistText.text = it
  79. })
  80. PlayerStore.instance.playbackState.observe(viewLifecycleOwner, Observer {
  81. syncPlayPauseButtonImage(root)
  82. })
  83. fun volumeIcon(it: Int)
  84. {
  85. volumeText.text = "$it%"
  86. when {
  87. it > 66 -> volumeIconImage.setImageResource(R.drawable.ic_volume_high)
  88. it in 33..66 -> volumeIconImage.setImageResource(R.drawable.ic_volume_medium)
  89. it in 0..33 -> volumeIconImage.setImageResource(R.drawable.ic_volume_low)
  90. else -> volumeIconImage.setImageResource(R.drawable.ic_volume_off)
  91. }
  92. }
  93. PlayerStore.instance.volume.observe(viewLifecycleOwner, Observer {
  94. volumeIcon(it)
  95. seekBarVolume.progress = it // this updates the seekbar if it's set by something else when going to sleep.
  96. })
  97. PlayerStore.instance.isMuted.observe(viewLifecycleOwner, Observer {
  98. if (it)
  99. volumeIconImage.setImageResource(R.drawable.ic_volume_off)
  100. else
  101. volumeIcon(PlayerStore.instance.volume.value!!)
  102. })
  103. PlayerStore.instance.streamerPicture.observe(viewLifecycleOwner, Observer { pic ->
  104. streamerPictureImageView.setImageBitmap(pic)
  105. })
  106. // fuck it, do it on main thread
  107. PlayerStore.instance.currentTime.observe(viewLifecycleOwner, Observer {
  108. val dd = (PlayerStore.instance.currentTime.value!! - PlayerStore.instance.currentSong.startTime.value!!).toInt()
  109. progressBar.progress = dd
  110. })
  111. PlayerStore.instance.currentSong.stopTime.observe(viewLifecycleOwner, Observer {
  112. val dd = (PlayerStore.instance.currentSong.stopTime.value!! - PlayerStore.instance.currentSong.startTime.value!!).toInt()
  113. progressBar.max = dd
  114. })
  115. PlayerStore.instance.currentSong.stopTime.observe(viewLifecycleOwner, Observer {
  116. val t : TextView= root.findViewById(R.id.endTime)
  117. val minutes: String = ((PlayerStore.instance.currentSong.stopTime.value!! - PlayerStore.instance.currentSong.startTime.value!!)/60/1000).toString()
  118. val seconds: String = ((PlayerStore.instance.currentSong.stopTime.value!! - PlayerStore.instance.currentSong.startTime.value!!)/1000%60).toString()
  119. t.text = "$minutes:${if (seconds.toInt() < 10) "0" else ""}$seconds"
  120. })
  121. PlayerStore.instance.currentTime.observe(viewLifecycleOwner, Observer {
  122. val t : TextView= root.findViewById(R.id.currentTime)
  123. val minutes: String = ((PlayerStore.instance.currentTime.value!! - PlayerStore.instance.currentSong.startTime.value!!)/60/1000).toString()
  124. val seconds: String = ((PlayerStore.instance.currentTime.value!! - PlayerStore.instance.currentSong.startTime.value!!)/1000%60).toString()
  125. t.text = "$minutes:${if (seconds.toInt() < 10) "0" else ""}$seconds"
  126. val sleepInfoText = root.findViewById<TextView>(R.id.sleepInfo)
  127. val sleepAtMillis = RadioSleeper.instance.sleepAtMillis.value
  128. if (sleepAtMillis != null)
  129. {
  130. val duration = ((sleepAtMillis - System.currentTimeMillis()).toFloat() / (60f * 1000f) + 1).toInt() // I put 1 + it because the division rounds to the lower integer. I'd like to display the round up, like it's usually done.
  131. sleepInfoText.text = String.format(getString(R.string.willCloseIn), duration) // "Will close in $duration minute${if (duration > 1) "s" else ""}"
  132. sleepInfoText.visibility = View.VISIBLE
  133. } else {
  134. sleepInfoText.visibility = View.GONE
  135. }
  136. })
  137. seekBarVolume.progress = PlayerStore.instance.volume.value!!
  138. seekBarVolume.setOnSeekBarChangeListener(nowPlayingViewModel.seekBarChangeListener)
  139. progressBar.max = 100
  140. progressBar.progress = 0
  141. syncPlayPauseButtonImage(root)
  142. // initialize the value for isPlaying when displaying the fragment
  143. PlayerStore.instance.isPlaying.value = PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_PLAYING
  144. val button: ImageButton = root.findViewById(R.id.play_pause)
  145. button.setOnClickListener{
  146. PlayerStore.instance.isPlaying.value = PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_STOPPED
  147. }
  148. val setClipboardListener: View.OnLongClickListener = View.OnLongClickListener {
  149. val text = PlayerStore.instance.currentSong.artist.value + " - " + PlayerStore.instance.currentSong.title.value
  150. val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
  151. val clip = android.content.ClipData.newPlainText("Copied Text", text)
  152. clipboard.setPrimaryClip(clip)
  153. val snackBarLength = if (preferenceStore.getBoolean("snackbarPersistent", false))
  154. Snackbar.LENGTH_INDEFINITE
  155. else Snackbar.LENGTH_LONG
  156. val snackBar = Snackbar.make(it, "", snackBarLength)
  157. if (snackBarLength == Snackbar.LENGTH_INDEFINITE)
  158. snackBar.setAction("OK") { snackBar.dismiss() }
  159. snackBar.behavior = BaseTransientBottomBar.Behavior().apply {
  160. setSwipeDirection(BaseTransientBottomBar.Behavior.SWIPE_DIRECTION_ANY)
  161. }
  162. snackBar.setText(getString(R.string.song_to_clipboard))
  163. snackBar.show()
  164. true
  165. }
  166. songTitleText.setOnLongClickListener(setClipboardListener)
  167. songArtistText.setOnLongClickListener(setClipboardListener)
  168. if (preferenceStore.getBoolean("splitLayout", true))
  169. root.addOnLayoutChangeListener(splitLayoutListener)
  170. return root
  171. }
  172. private val splitLayoutListener : View.OnLayoutChangeListener = View.OnLayoutChangeListener { _: View, _: Int, _: Int, _: Int, _: Int, _: Int, _: Int, _: Int, _: Int ->
  173. val isSplitLayout = preferenceStore.getBoolean("splitLayout", true)
  174. val viewHeight = (root.rootView?.height ?: 1)
  175. val viewWidth = (root.rootView?.width ?: 1)
  176. val newRatio = if (viewWidth > 0)
  177. (viewHeight*100)/viewWidth
  178. else
  179. 100
  180. if (isSplitLayout && nowPlayingViewModel.screenRatio != newRatio) {
  181. onOrientation()
  182. }
  183. }
  184. override fun onViewStateRestored(savedInstanceState: Bundle?) {
  185. onOrientation()
  186. super.onViewStateRestored(savedInstanceState)
  187. }
  188. private fun onOrientation() {
  189. val viewHeight = (root.rootView?.height ?: 1)
  190. val viewWidth = (root.rootView?.width ?: 1)
  191. val isSplitLayout = preferenceStore.getBoolean("splitLayout", true)
  192. // modify layout to adapt for portrait/landscape
  193. val isLandscape = viewHeight.toDouble()/viewWidth.toDouble() < 1
  194. val parentLayout = root.findViewById<ConstraintLayout>(R.id.parentNowPlaying)
  195. val constraintSet = ConstraintSet()
  196. constraintSet.clone(parentLayout)
  197. if (isLandscape && isSplitLayout)
  198. {
  199. constraintSet.connect(R.id.layoutBlock1, ConstraintSet.BOTTOM, R.id.parentNowPlaying, ConstraintSet.BOTTOM)
  200. constraintSet.connect(R.id.layoutBlock1, ConstraintSet.END, R.id.splitHorizontalLayout, ConstraintSet.END)
  201. constraintSet.connect(R.id.layoutBlock2, ConstraintSet.TOP, R.id.parentNowPlaying, ConstraintSet.TOP)
  202. constraintSet.connect(R.id.layoutBlock2, ConstraintSet.START, R.id.splitHorizontalLayout, ConstraintSet.END)
  203. constraintSet.setMargin(R.id.layoutBlock1, ConstraintSet.END, 16)
  204. constraintSet.setMargin(R.id.layoutBlock2, ConstraintSet.START, 16)
  205. } else {
  206. constraintSet.connect(R.id.layoutBlock1, ConstraintSet.BOTTOM, R.id.splitVerticalLayout, ConstraintSet.BOTTOM)
  207. constraintSet.connect(R.id.layoutBlock1, ConstraintSet.END, R.id.parentNowPlaying, ConstraintSet.END)
  208. constraintSet.connect(R.id.layoutBlock2, ConstraintSet.TOP, R.id.splitVerticalLayout, ConstraintSet.BOTTOM)
  209. constraintSet.connect(R.id.layoutBlock2, ConstraintSet.START, R.id.parentNowPlaying, ConstraintSet.START)
  210. constraintSet.setMargin(R.id.layoutBlock1, ConstraintSet.END, 0)
  211. constraintSet.setMargin(R.id.layoutBlock2, ConstraintSet.START, 0)
  212. }
  213. constraintSet.applyTo(parentLayout)
  214. // note : we have to COMPARE numbers that are FRACTIONS. And everyone knows that we should NEVER compare DOUBLES because of the imprecision at the end.
  215. // So instead, I multiply the result by 100 (to give 2 significant numbers), and do an INTEGER DIVISION. This is the right way to compare ratios.
  216. nowPlayingViewModel.screenRatio = if (viewWidth > 0)
  217. (viewHeight*100)/viewWidth
  218. else
  219. 100
  220. //[REMOVE LOG CALLS]Log.d(tag, "orientation set")
  221. }
  222. override fun onResume() {
  223. super.onResume()
  224. onOrientation()
  225. }
  226. private fun syncPlayPauseButtonImage(v: View)
  227. {
  228. val img = v.findViewById<ImageButton>(R.id.play_pause)
  229. if (PlayerStore.instance.playbackState.value == PlaybackStateCompat.STATE_STOPPED) {
  230. img.setImageResource(R.drawable.exo_controls_play)
  231. } else {
  232. img.setImageResource(R.drawable.exo_controls_pause)
  233. }
  234. }
  235. }