123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- /*
- * Nextcloud - Android Client
- *
- * SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
- * SPDX-FileCopyrightText: 2022 Álvaro Brey Vilas <alvaro@alvarobrey.com>
- * SPDX-FileCopyrightText: 2018-2020 Tobias Kaminsky <tobias@kaminsky.me>
- * SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
- * SPDX-FileCopyrightText: 2018 Andy Scherzinger <info@andy-scherzinger.de>
- * SPDX-FileCopyrightText: 2015 ownCloud Inc.
- * SPDX-FileCopyrightText: 2013 David A. Velasco <dvelasco@solidgear.es>
- * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later
- */
- package com.owncloud.android.media
- import android.content.Context
- import android.os.Handler
- import android.os.Looper
- import android.os.Message
- import android.util.AttributeSet
- import android.view.KeyEvent
- import android.view.LayoutInflater
- import android.view.View
- import android.view.accessibility.AccessibilityEvent
- import android.view.accessibility.AccessibilityNodeInfo
- import android.widget.LinearLayout
- import android.widget.SeekBar
- import android.widget.SeekBar.OnSeekBarChangeListener
- import androidx.core.content.ContextCompat
- import androidx.media3.common.Player
- import com.owncloud.android.MainApp
- import com.owncloud.android.R
- import com.owncloud.android.databinding.MediaControlBinding
- import com.owncloud.android.lib.common.utils.Log_OC
- import com.owncloud.android.utils.theme.ViewThemeUtils
- import java.util.Formatter
- import java.util.Locale
- import javax.inject.Inject
- /**
- * View containing controls for a MediaPlayer.
- *
- *
- * Holds buttons "play / pause", "rewind", "fast forward" and a progress slider.
- *
- *
- * It synchronizes itself with the state of the MediaPlayer.
- */
- class MediaControlView(context: Context, attrs: AttributeSet?) :
- LinearLayout(context, attrs),
- View.OnClickListener,
- OnSeekBarChangeListener {
- private var playerControl: Player? = null
- private var binding: MediaControlBinding
- private var isDragging = false
- @Inject
- lateinit var viewThemeUtils: ViewThemeUtils
- public override fun onFinishInflate() {
- super.onFinishInflate()
- }
- @Suppress("MagicNumber")
- fun setMediaPlayer(player: Player?) {
- playerControl = player
- handler.sendEmptyMessage(SHOW_PROGRESS)
- handler.postDelayed({
- updatePausePlay()
- setProgress()
- }, 100)
- }
- @Suppress("MagicNumber")
- private fun initControllerView() {
- binding.playBtn.requestFocus()
- binding.playBtn.setOnClickListener(this)
- binding.forwardBtn.setOnClickListener(this)
- binding.rewindBtn.setOnClickListener(this)
- binding.progressBar.run {
- viewThemeUtils.platform.themeHorizontalSeekBar(this)
- setMax(1000)
- }
- binding.progressBar.setOnSeekBarChangeListener(this)
- viewThemeUtils.material.run {
- colorMaterialButtonPrimaryTonal(binding.rewindBtn)
- colorMaterialButtonPrimaryTonal(binding.playBtn)
- colorMaterialButtonPrimaryTonal(binding.forwardBtn)
- }
- }
- /**
- * Disable pause or seek buttons if the stream cannot be paused or seeked.
- * This requires the control interface to be a MediaPlayerControlExt
- */
- private fun disableUnsupportedButtons() {
- try {
- if (playerControl!!.isCommandAvailable(Player.COMMAND_PLAY_PAUSE).not()) {
- binding.playBtn.isEnabled = false
- }
- if (playerControl!!.isCommandAvailable(Player.COMMAND_SEEK_BACK).not()) {
- binding.rewindBtn.isEnabled = false
- }
- if (playerControl!!.isCommandAvailable(Player.COMMAND_SEEK_FORWARD).not()) {
- binding.forwardBtn.isEnabled = false
- }
- } catch (ex: IncompatibleClassChangeError) {
- // We were given an old version of the interface, that doesn't have
- // the canPause/canSeekXYZ methods. This is OK, it just means we
- // assume the media can be paused and seeked, and so we don't disable
- // the buttons.
- Log_OC.i(TAG, "Old media interface detected")
- }
- }
- @Suppress("MagicNumber")
- private val handler: Handler = object : Handler(Looper.getMainLooper()) {
- override fun handleMessage(msg: Message) {
- if (msg.what == SHOW_PROGRESS) {
- updatePausePlay()
- val pos = setProgress()
- if (!isDragging) {
- sendMessageDelayed(obtainMessage(SHOW_PROGRESS), (1000 - pos % 1000))
- }
- }
- }
- }
- init {
- MainApp.getAppComponent().inject(this)
- val inflate = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
- binding = MediaControlBinding.inflate(inflate, this, true)
- initControllerView()
- isFocusable = true
- setFocusableInTouchMode(true)
- setDescendantFocusability(FOCUS_AFTER_DESCENDANTS)
- requestFocus()
- }
- @Suppress("MagicNumber")
- private fun formatTime(timeMs: Long): String {
- val totalSeconds = timeMs / 1000
- val seconds = totalSeconds % 60
- val minutes = totalSeconds / 60 % 60
- val hours = totalSeconds / 3600
- val mFormatBuilder = StringBuilder()
- val mFormatter = Formatter(mFormatBuilder, Locale.getDefault())
- return if (hours > 0) {
- mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString()
- } else {
- mFormatter.format("%02d:%02d", minutes, seconds).toString()
- }
- }
- @Suppress("MagicNumber")
- private fun setProgress(): Long {
- var position = 0L
- if (playerControl == null || isDragging) {
- position = 0
- }
- playerControl?.let { playerControl ->
- position = playerControl.currentPosition
- val duration = playerControl.duration
- if (duration > 0) {
- // use long to avoid overflow
- val pos = 1000L * position / duration
- binding.progressBar.progress = pos.toInt()
- }
- val percent = playerControl.bufferedPercentage
- binding.progressBar.setSecondaryProgress(percent * 10)
- val endTime = if (duration > 0) formatTime(duration) else "--:--"
- binding.totalTimeText.text = endTime
- binding.currentTimeText.text = formatTime(position)
- }
- return position
- }
- @Suppress("ReturnCount")
- override fun dispatchKeyEvent(event: KeyEvent): Boolean {
- val keyCode = event.keyCode
- val uniqueDown = (event.repeatCount == 0 && event.action == KeyEvent.ACTION_DOWN)
- when (keyCode) {
- KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_SPACE -> {
- if (uniqueDown) {
- doPauseResume()
- // show(sDefaultTimeout);
- binding.playBtn.requestFocus()
- }
- return true
- }
- KeyEvent.KEYCODE_MEDIA_PLAY -> {
- if (uniqueDown && playerControl?.playWhenReady == false) {
- playerControl?.play()
- updatePausePlay()
- }
- return true
- }
- KeyEvent.KEYCODE_MEDIA_STOP,
- KeyEvent.KEYCODE_MEDIA_PAUSE
- -> {
- if (uniqueDown && playerControl?.playWhenReady == true) {
- playerControl?.pause()
- updatePausePlay()
- }
- return true
- }
- else -> return super.dispatchKeyEvent(event)
- }
- }
- fun updatePausePlay() {
- binding.playBtn.icon = ContextCompat.getDrawable(
- context,
- // use isPlaying instead of playWhenReady
- // it represents only the play/pause state
- // which is needed to show play/pause icons
- if (playerControl?.isPlaying == true) {
- R.drawable.ic_pause
- } else {
- R.drawable.ic_play
- }
- )
- binding.forwardBtn.visibility = if (playerControl!!.isCommandAvailable(Player.COMMAND_SEEK_FORWARD)) {
- VISIBLE
- } else {
- INVISIBLE
- }
- binding.rewindBtn.visibility = if (playerControl!!.isCommandAvailable(Player.COMMAND_SEEK_BACK)) {
- VISIBLE
- } else {
- INVISIBLE
- }
- }
- private fun doPauseResume() {
- playerControl?.run {
- if (playWhenReady) {
- pause()
- } else {
- play()
- }
- }
- updatePausePlay()
- }
- override fun setEnabled(enabled: Boolean) {
- binding.playBtn.setEnabled(enabled)
- binding.forwardBtn.setEnabled(enabled)
- binding.rewindBtn.setEnabled(enabled)
- binding.progressBar.setEnabled(enabled)
- disableUnsupportedButtons()
- super.setEnabled(enabled)
- }
- @Suppress("MagicNumber")
- override fun onClick(v: View) {
- playerControl?.let { playerControl ->
- val playing = playerControl.playWhenReady
- val id = v.id
- when (id) {
- R.id.playBtn -> {
- doPauseResume()
- }
- R.id.rewindBtn -> {
- playerControl.seekBack()
- if (!playing) {
- playerControl.pause() // necessary in some 2.3.x devices
- }
- setProgress()
- }
- R.id.forwardBtn -> {
- playerControl.seekForward()
- if (!playing) {
- playerControl.pause() // necessary in some 2.3.x devices
- }
- setProgress()
- }
- else -> {
- }
- }
- }
- }
- @Suppress("MagicNumber")
- override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
- if (!fromUser) {
- // We're not interested in programmatically generated changes to
- // the progress bar's position.
- return
- }
- playerControl?.let { playerControl ->
- val duration = playerControl.duration
- val newPosition = duration * progress / 1000L
- playerControl.seekTo(newPosition)
- binding.currentTimeText.text = formatTime(newPosition)
- }
- }
- /**
- * Called in devices with touchpad when the user starts to adjust the position of the seekbar's thumb.
- *
- * Will be followed by several onProgressChanged notifications.
- */
- override fun onStartTrackingTouch(seekBar: SeekBar) {
- isDragging = true // monitors the duration of dragging
- handler.removeMessages(SHOW_PROGRESS) // grants no more updates with media player progress while dragging
- }
- /**
- * Called in devices with touchpad when the user finishes the adjusting of the seekbar.
- */
- override fun onStopTrackingTouch(seekBar: SeekBar) {
- isDragging = false
- setProgress()
- updatePausePlay()
- handler.sendEmptyMessage(SHOW_PROGRESS) // grants future updates with media player progress
- }
- override fun onInitializeAccessibilityEvent(event: AccessibilityEvent) {
- super.onInitializeAccessibilityEvent(event)
- event.setClassName(MediaControlView::class.java.getName())
- }
- override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
- super.onInitializeAccessibilityNodeInfo(info)
- info.setClassName(MediaControlView::class.java.getName())
- }
- companion object {
- private val TAG = MediaControlView::class.java.getSimpleName()
- private const val SHOW_PROGRESS = 1
- }
- }
|