NowPlayingFragment.kt 13KB

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