NowPlayingFragment.kt 14KB

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