MediaControlView.kt 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. /*
  2. * Nextcloud - Android Client
  3. *
  4. * SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
  5. * SPDX-FileCopyrightText: 2022 Álvaro Brey Vilas <alvaro@alvarobrey.com>
  6. * SPDX-FileCopyrightText: 2018-2020 Tobias Kaminsky <tobias@kaminsky.me>
  7. * SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
  8. * SPDX-FileCopyrightText: 2018 Andy Scherzinger <info@andy-scherzinger.de>
  9. * SPDX-FileCopyrightText: 2015 ownCloud Inc.
  10. * SPDX-FileCopyrightText: 2013 David A. Velasco <dvelasco@solidgear.es>
  11. * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later
  12. */
  13. package com.owncloud.android.media
  14. import android.content.Context
  15. import android.os.Handler
  16. import android.os.Looper
  17. import android.os.Message
  18. import android.util.AttributeSet
  19. import android.view.KeyEvent
  20. import android.view.LayoutInflater
  21. import android.view.View
  22. import android.view.accessibility.AccessibilityEvent
  23. import android.view.accessibility.AccessibilityNodeInfo
  24. import android.widget.LinearLayout
  25. import android.widget.SeekBar
  26. import android.widget.SeekBar.OnSeekBarChangeListener
  27. import androidx.core.content.ContextCompat
  28. import androidx.media3.common.Player
  29. import com.owncloud.android.MainApp
  30. import com.owncloud.android.R
  31. import com.owncloud.android.databinding.MediaControlBinding
  32. import com.owncloud.android.lib.common.utils.Log_OC
  33. import com.owncloud.android.utils.theme.ViewThemeUtils
  34. import java.util.Formatter
  35. import java.util.Locale
  36. import javax.inject.Inject
  37. /**
  38. * View containing controls for a MediaPlayer.
  39. *
  40. *
  41. * Holds buttons "play / pause", "rewind", "fast forward" and a progress slider.
  42. *
  43. *
  44. * It synchronizes itself with the state of the MediaPlayer.
  45. */
  46. class MediaControlView(context: Context, attrs: AttributeSet?) :
  47. LinearLayout(context, attrs),
  48. View.OnClickListener,
  49. OnSeekBarChangeListener {
  50. private var playerControl: Player? = null
  51. private var binding: MediaControlBinding
  52. private var isDragging = false
  53. @Inject
  54. lateinit var viewThemeUtils: ViewThemeUtils
  55. public override fun onFinishInflate() {
  56. super.onFinishInflate()
  57. }
  58. @Suppress("MagicNumber")
  59. fun setMediaPlayer(player: Player?) {
  60. playerControl = player
  61. handler.sendEmptyMessage(SHOW_PROGRESS)
  62. handler.postDelayed({
  63. updatePausePlay()
  64. setProgress()
  65. }, 100)
  66. }
  67. @Suppress("MagicNumber")
  68. private fun initControllerView() {
  69. binding.playBtn.requestFocus()
  70. binding.playBtn.setOnClickListener(this)
  71. binding.forwardBtn.setOnClickListener(this)
  72. binding.rewindBtn.setOnClickListener(this)
  73. binding.progressBar.run {
  74. viewThemeUtils.platform.themeHorizontalSeekBar(this)
  75. setMax(1000)
  76. }
  77. binding.progressBar.setOnSeekBarChangeListener(this)
  78. viewThemeUtils.material.run {
  79. colorMaterialButtonPrimaryTonal(binding.rewindBtn)
  80. colorMaterialButtonPrimaryTonal(binding.playBtn)
  81. colorMaterialButtonPrimaryTonal(binding.forwardBtn)
  82. }
  83. }
  84. /**
  85. * Disable pause or seek buttons if the stream cannot be paused or seeked.
  86. * This requires the control interface to be a MediaPlayerControlExt
  87. */
  88. private fun disableUnsupportedButtons() {
  89. try {
  90. if (playerControl!!.isCommandAvailable(Player.COMMAND_PLAY_PAUSE).not()) {
  91. binding.playBtn.isEnabled = false
  92. }
  93. if (playerControl!!.isCommandAvailable(Player.COMMAND_SEEK_BACK).not()) {
  94. binding.rewindBtn.isEnabled = false
  95. }
  96. if (playerControl!!.isCommandAvailable(Player.COMMAND_SEEK_FORWARD).not()) {
  97. binding.forwardBtn.isEnabled = false
  98. }
  99. } catch (ex: IncompatibleClassChangeError) {
  100. // We were given an old version of the interface, that doesn't have
  101. // the canPause/canSeekXYZ methods. This is OK, it just means we
  102. // assume the media can be paused and seeked, and so we don't disable
  103. // the buttons.
  104. Log_OC.i(TAG, "Old media interface detected")
  105. }
  106. }
  107. @Suppress("MagicNumber")
  108. private val handler: Handler = object : Handler(Looper.getMainLooper()) {
  109. override fun handleMessage(msg: Message) {
  110. if (msg.what == SHOW_PROGRESS) {
  111. updatePausePlay()
  112. val pos = setProgress()
  113. if (!isDragging) {
  114. sendMessageDelayed(obtainMessage(SHOW_PROGRESS), (1000 - pos % 1000))
  115. }
  116. }
  117. }
  118. }
  119. init {
  120. MainApp.getAppComponent().inject(this)
  121. val inflate = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
  122. binding = MediaControlBinding.inflate(inflate, this, true)
  123. initControllerView()
  124. isFocusable = true
  125. setFocusableInTouchMode(true)
  126. setDescendantFocusability(FOCUS_AFTER_DESCENDANTS)
  127. requestFocus()
  128. }
  129. @Suppress("MagicNumber")
  130. private fun formatTime(timeMs: Long): String {
  131. val totalSeconds = timeMs / 1000
  132. val seconds = totalSeconds % 60
  133. val minutes = totalSeconds / 60 % 60
  134. val hours = totalSeconds / 3600
  135. val mFormatBuilder = StringBuilder()
  136. val mFormatter = Formatter(mFormatBuilder, Locale.getDefault())
  137. return if (hours > 0) {
  138. mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString()
  139. } else {
  140. mFormatter.format("%02d:%02d", minutes, seconds).toString()
  141. }
  142. }
  143. @Suppress("MagicNumber")
  144. private fun setProgress(): Long {
  145. var position = 0L
  146. if (playerControl == null || isDragging) {
  147. position = 0
  148. }
  149. playerControl?.let { playerControl ->
  150. position = playerControl.currentPosition
  151. val duration = playerControl.duration
  152. if (duration > 0) {
  153. // use long to avoid overflow
  154. val pos = 1000L * position / duration
  155. binding.progressBar.progress = pos.toInt()
  156. }
  157. val percent = playerControl.bufferedPercentage
  158. binding.progressBar.setSecondaryProgress(percent * 10)
  159. val endTime = if (duration > 0) formatTime(duration) else "--:--"
  160. binding.totalTimeText.text = endTime
  161. binding.currentTimeText.text = formatTime(position)
  162. }
  163. return position
  164. }
  165. @Suppress("ReturnCount")
  166. override fun dispatchKeyEvent(event: KeyEvent): Boolean {
  167. val keyCode = event.keyCode
  168. val uniqueDown = (event.repeatCount == 0 && event.action == KeyEvent.ACTION_DOWN)
  169. when (keyCode) {
  170. KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_SPACE -> {
  171. if (uniqueDown) {
  172. doPauseResume()
  173. // show(sDefaultTimeout);
  174. binding.playBtn.requestFocus()
  175. }
  176. return true
  177. }
  178. KeyEvent.KEYCODE_MEDIA_PLAY -> {
  179. if (uniqueDown && playerControl?.playWhenReady == false) {
  180. playerControl?.play()
  181. updatePausePlay()
  182. }
  183. return true
  184. }
  185. KeyEvent.KEYCODE_MEDIA_STOP,
  186. KeyEvent.KEYCODE_MEDIA_PAUSE
  187. -> {
  188. if (uniqueDown && playerControl?.playWhenReady == true) {
  189. playerControl?.pause()
  190. updatePausePlay()
  191. }
  192. return true
  193. }
  194. else -> return super.dispatchKeyEvent(event)
  195. }
  196. }
  197. fun updatePausePlay() {
  198. binding.playBtn.icon = ContextCompat.getDrawable(
  199. context,
  200. // use isPlaying instead of playWhenReady
  201. // it represents only the play/pause state
  202. // which is needed to show play/pause icons
  203. if (playerControl?.isPlaying == true) {
  204. R.drawable.ic_pause
  205. } else {
  206. R.drawable.ic_play
  207. }
  208. )
  209. binding.forwardBtn.visibility = if (playerControl!!.isCommandAvailable(Player.COMMAND_SEEK_FORWARD)) {
  210. VISIBLE
  211. } else {
  212. INVISIBLE
  213. }
  214. binding.rewindBtn.visibility = if (playerControl!!.isCommandAvailable(Player.COMMAND_SEEK_BACK)) {
  215. VISIBLE
  216. } else {
  217. INVISIBLE
  218. }
  219. }
  220. private fun doPauseResume() {
  221. playerControl?.run {
  222. if (playWhenReady) {
  223. pause()
  224. } else {
  225. play()
  226. }
  227. }
  228. updatePausePlay()
  229. }
  230. override fun setEnabled(enabled: Boolean) {
  231. binding.playBtn.setEnabled(enabled)
  232. binding.forwardBtn.setEnabled(enabled)
  233. binding.rewindBtn.setEnabled(enabled)
  234. binding.progressBar.setEnabled(enabled)
  235. disableUnsupportedButtons()
  236. super.setEnabled(enabled)
  237. }
  238. @Suppress("MagicNumber")
  239. override fun onClick(v: View) {
  240. playerControl?.let { playerControl ->
  241. val playing = playerControl.playWhenReady
  242. val id = v.id
  243. when (id) {
  244. R.id.playBtn -> {
  245. doPauseResume()
  246. }
  247. R.id.rewindBtn -> {
  248. playerControl.seekBack()
  249. if (!playing) {
  250. playerControl.pause() // necessary in some 2.3.x devices
  251. }
  252. setProgress()
  253. }
  254. R.id.forwardBtn -> {
  255. playerControl.seekForward()
  256. if (!playing) {
  257. playerControl.pause() // necessary in some 2.3.x devices
  258. }
  259. setProgress()
  260. }
  261. else -> {
  262. }
  263. }
  264. }
  265. }
  266. @Suppress("MagicNumber")
  267. override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
  268. if (!fromUser) {
  269. // We're not interested in programmatically generated changes to
  270. // the progress bar's position.
  271. return
  272. }
  273. playerControl?.let { playerControl ->
  274. val duration = playerControl.duration
  275. val newPosition = duration * progress / 1000L
  276. playerControl.seekTo(newPosition)
  277. binding.currentTimeText.text = formatTime(newPosition)
  278. }
  279. }
  280. /**
  281. * Called in devices with touchpad when the user starts to adjust the position of the seekbar's thumb.
  282. *
  283. * Will be followed by several onProgressChanged notifications.
  284. */
  285. override fun onStartTrackingTouch(seekBar: SeekBar) {
  286. isDragging = true // monitors the duration of dragging
  287. handler.removeMessages(SHOW_PROGRESS) // grants no more updates with media player progress while dragging
  288. }
  289. /**
  290. * Called in devices with touchpad when the user finishes the adjusting of the seekbar.
  291. */
  292. override fun onStopTrackingTouch(seekBar: SeekBar) {
  293. isDragging = false
  294. setProgress()
  295. updatePausePlay()
  296. handler.sendEmptyMessage(SHOW_PROGRESS) // grants future updates with media player progress
  297. }
  298. override fun onInitializeAccessibilityEvent(event: AccessibilityEvent) {
  299. super.onInitializeAccessibilityEvent(event)
  300. event.setClassName(MediaControlView::class.java.getName())
  301. }
  302. override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
  303. super.onInitializeAccessibilityNodeInfo(info)
  304. info.setClassName(MediaControlView::class.java.getName())
  305. }
  306. companion object {
  307. private val TAG = MediaControlView::class.java.getSimpleName()
  308. private const val SHOW_PROGRESS = 1
  309. }
  310. }