NowPlayingFragment.kt 13KB

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