NowPlayingFragment.kt 13KB

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