Simple-Voice-Recorder/app/src/main/kotlin/com/simplemobiletools/voicerecorder/fragments/PlayerFragment.kt

375 lines
13 KiB
Kotlin

package com.simplemobiletools.voicerecorder.fragments
import android.content.Context
import android.graphics.drawable.Drawable
import android.media.AudioManager
import android.media.MediaPlayer
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.provider.DocumentsContract
import android.util.AttributeSet
import android.widget.SeekBar
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.isQPlus
import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.activities.SimpleActivity
import com.simplemobiletools.voicerecorder.adapters.RecordingsAdapter
import com.simplemobiletools.voicerecorder.databinding.FragmentPlayerBinding
import com.simplemobiletools.voicerecorder.extensions.config
import com.simplemobiletools.voicerecorder.extensions.getAllRecordings
import com.simplemobiletools.voicerecorder.helpers.getAudioFileContentUri
import com.simplemobiletools.voicerecorder.interfaces.RefreshRecordingsListener
import com.simplemobiletools.voicerecorder.models.Events
import com.simplemobiletools.voicerecorder.models.Recording
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.util.Stack
import java.util.Timer
import java.util.TimerTask
class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerFragment(context, attributeSet), RefreshRecordingsListener {
private val FAST_FORWARD_SKIP_MS = 10000
private var player: MediaPlayer? = null
private var progressTimer = Timer()
private var playedRecordingIDs = Stack<Int>()
private var itemsIgnoringSearch = ArrayList<Recording>()
private var lastSearchQuery = ""
private var bus: EventBus? = null
private var prevSavePath = ""
private var prevRecycleBinState = context.config.useRecycleBin
private var playOnPreparation = true
private lateinit var binding: FragmentPlayerBinding
override fun onFinishInflate() {
super.onFinishInflate()
binding = FragmentPlayerBinding.bind(this)
}
override fun onResume() {
setupColors()
if (prevSavePath.isNotEmpty() && context!!.config.saveRecordingsFolder != prevSavePath || context.config.useRecycleBin != prevRecycleBinState) {
itemsIgnoringSearch = getRecordings()
setupAdapter(itemsIgnoringSearch)
} else {
getRecordingsAdapter()?.updateTextColor(context.getProperTextColor())
}
storePrevState()
}
override fun onDestroy() {
player?.stop()
player?.release()
player = null
bus?.unregister(this)
progressTimer.cancel()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
bus = EventBus.getDefault()
bus!!.register(this)
setupColors()
itemsIgnoringSearch = getRecordings()
setupAdapter(itemsIgnoringSearch)
initMediaPlayer()
setupViews()
storePrevState()
}
private fun setupViews() {
binding.playPauseBtn.setOnClickListener {
if (playedRecordingIDs.empty() || binding.playerProgressbar.max == 0) {
binding.nextBtn.callOnClick()
} else {
togglePlayPause()
}
}
binding.playerProgressCurrent.setOnClickListener {
skip(false)
}
binding.playerProgressMax.setOnClickListener {
skip(true)
}
binding.previousBtn.setOnClickListener {
if (playedRecordingIDs.isEmpty()) {
return@setOnClickListener
}
val adapter = getRecordingsAdapter() ?: return@setOnClickListener
var wantedRecordingID = playedRecordingIDs.pop()
if (wantedRecordingID == adapter.currRecordingId && !playedRecordingIDs.isEmpty()) {
wantedRecordingID = playedRecordingIDs.pop()
}
val prevRecordingIndex = adapter.recordings.indexOfFirst { it.id == wantedRecordingID }
val prevRecording = adapter.recordings.getOrNull(prevRecordingIndex) ?: return@setOnClickListener
playRecording(prevRecording, true)
}
binding.playerTitle.setOnLongClickListener {
if (binding.playerTitle.value.isNotEmpty()) {
context.copyToClipboard(binding.playerTitle.value)
}
true
}
binding.nextBtn.setOnClickListener {
val adapter = getRecordingsAdapter()
if (adapter == null || adapter.recordings.isEmpty()) {
return@setOnClickListener
}
val oldRecordingIndex = adapter.recordings.indexOfFirst { it.id == adapter.currRecordingId }
val newRecordingIndex = (oldRecordingIndex + 1) % adapter.recordings.size
val newRecording = adapter.recordings.getOrNull(newRecordingIndex) ?: return@setOnClickListener
playRecording(newRecording, true)
playedRecordingIDs.push(newRecording.id)
}
}
override fun refreshRecordings() {
itemsIgnoringSearch = getRecordings()
setupAdapter(itemsIgnoringSearch)
}
private fun setupAdapter(recordings: ArrayList<Recording>) {
binding.recordingsFastscroller.beVisibleIf(recordings.isNotEmpty())
binding.recordingsPlaceholder.beVisibleIf(recordings.isEmpty())
if (recordings.isEmpty()) {
val stringId = if (lastSearchQuery.isEmpty()) {
if (isQPlus()) {
R.string.no_recordings_found
} else {
R.string.no_recordings_in_folder_found
}
} else {
com.simplemobiletools.commons.R.string.no_items_found
}
binding.recordingsPlaceholder.text = context.getString(stringId)
resetProgress(null)
player?.stop()
}
val adapter = getRecordingsAdapter()
if (adapter == null) {
RecordingsAdapter(context as SimpleActivity, recordings, this, binding.recordingsList) {
playRecording(it as Recording, true)
if (playedRecordingIDs.isEmpty() || playedRecordingIDs.peek() != it.id) {
playedRecordingIDs.push(it.id)
}
}.apply {
binding.recordingsList.adapter = this
}
if (context.areSystemAnimationsEnabled) {
binding.recordingsList.scheduleLayoutAnimation()
}
} else {
adapter.updateItems(recordings)
}
}
private fun getRecordings(): ArrayList<Recording> {
return context.getAllRecordings().apply {
sortByDescending { it.timestamp }
}
}
private fun initMediaPlayer() {
player = MediaPlayer().apply {
setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK)
setAudioStreamType(AudioManager.STREAM_MUSIC)
setOnCompletionListener {
progressTimer.cancel()
binding.playerProgressbar.progress = binding.playerProgressbar.max
binding.playerProgressCurrent.text = binding.playerProgressMax.text
binding.playPauseBtn.setImageDrawable(getToggleButtonIcon(false))
}
setOnPreparedListener {
if (playOnPreparation) {
setupProgressTimer()
player?.start()
}
playOnPreparation = true
}
}
}
override fun playRecording(recording: Recording, playOnPrepared: Boolean) {
resetProgress(recording)
(binding.recordingsList.adapter as RecordingsAdapter).updateCurrentRecording(recording.id)
playOnPreparation = playOnPrepared
player!!.apply {
reset()
try {
val uri = Uri.parse(recording.path)
when {
DocumentsContract.isDocumentUri(context, uri) -> {
setDataSource(context, uri)
}
recording.path.isEmpty() -> {
setDataSource(context, getAudioFileContentUri(recording.id.toLong()))
}
else -> {
setDataSource(recording.path)
}
}
} catch (e: Exception) {
context?.showErrorToast(e)
return
}
try {
prepareAsync()
} catch (e: Exception) {
context.showErrorToast(e)
return
}
}
binding.playPauseBtn.setImageDrawable(getToggleButtonIcon(playOnPreparation))
binding.playerProgressbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
if (fromUser && !playedRecordingIDs.isEmpty()) {
player?.seekTo(progress * 1000)
binding.playerProgressCurrent.text = progress.getFormattedDuration()
resumePlayback()
}
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
}
private fun setupProgressTimer() {
progressTimer.cancel()
progressTimer = Timer()
progressTimer.scheduleAtFixedRate(getProgressUpdateTask(), 1000, 1000)
}
private fun getProgressUpdateTask() = object : TimerTask() {
override fun run() {
Handler(Looper.getMainLooper()).post {
if (player != null) {
val progress = Math.round(player!!.currentPosition / 1000.toDouble()).toInt()
updateCurrentProgress(progress)
binding.playerProgressbar.progress = progress
}
}
}
}
private fun updateCurrentProgress(seconds: Int) {
binding.playerProgressCurrent.text = seconds.getFormattedDuration()
}
private fun resetProgress(recording: Recording?) {
updateCurrentProgress(0)
binding.playerProgressbar.progress = 0
binding.playerProgressbar.max = recording?.duration ?: 0
binding.playerTitle.text = recording?.title ?: ""
binding.playerProgressMax.text = (recording?.duration ?: 0).getFormattedDuration()
}
fun onSearchTextChanged(text: String) {
lastSearchQuery = text
val filtered = itemsIgnoringSearch.filter { it.title.contains(text, true) }.toMutableList() as ArrayList<Recording>
setupAdapter(filtered)
}
private fun togglePlayPause() {
if (getIsPlaying()) {
pausePlayback()
} else {
resumePlayback()
}
}
private fun pausePlayback() {
player?.pause()
binding.playPauseBtn.setImageDrawable(getToggleButtonIcon(false))
progressTimer.cancel()
}
private fun resumePlayback() {
player?.start()
binding.playPauseBtn.setImageDrawable(getToggleButtonIcon(true))
setupProgressTimer()
}
private fun getToggleButtonIcon(isPlaying: Boolean): Drawable {
val drawable = if (isPlaying) com.simplemobiletools.commons.R.drawable.ic_pause_vector else com.simplemobiletools.commons.R.drawable.ic_play_vector
return resources.getColoredDrawableWithColor(drawable, context.getProperPrimaryColor().getContrastColor())
}
private fun skip(forward: Boolean) {
if (playedRecordingIDs.empty()) {
return
}
val curr = player?.currentPosition ?: return
var newProgress = if (forward) curr + FAST_FORWARD_SKIP_MS else curr - FAST_FORWARD_SKIP_MS
if (newProgress > player!!.duration) {
newProgress = player!!.duration
}
player!!.seekTo(newProgress)
resumePlayback()
}
private fun getIsPlaying() = player?.isPlaying == true
private fun getRecordingsAdapter() = binding.recordingsList.adapter as? RecordingsAdapter
private fun storePrevState() {
prevSavePath = context!!.config.saveRecordingsFolder
prevRecycleBinState = context.config.useRecycleBin
}
private fun setupColors() {
val properPrimaryColor = context.getProperPrimaryColor()
binding.recordingsFastscroller.updateColors(properPrimaryColor)
context.updateTextColors(binding.playerHolder)
val textColor = context.getProperTextColor()
arrayListOf(binding.previousBtn, binding.nextBtn).forEach {
it.applyColorFilter(textColor)
}
binding.playPauseBtn.background.applyColorFilter(properPrimaryColor)
binding.playPauseBtn.setImageDrawable(getToggleButtonIcon(false))
}
fun finishActMode() = getRecordingsAdapter()?.finishActMode()
@Subscribe(threadMode = ThreadMode.MAIN)
fun recordingCompleted(event: Events.RecordingCompleted) {
refreshRecordings()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun recordingMovedToRecycleBin(event: Events.RecordingTrashUpdated) {
refreshRecordings()
}
}