Musicoo/app/src/main/java/relax/offline/music/activity/MoPlayDetailsActivity.kt
2024-05-24 20:36:01 +08:00

695 lines
26 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package relax.offline.music.activity
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.view.View
import android.view.animation.AnimationUtils
import androidx.annotation.OptIn
import androidx.core.net.toUri
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.Download
import androidx.media3.exoplayer.offline.DownloadManager
import androidx.media3.exoplayer.offline.DownloadRequest
import androidx.media3.exoplayer.offline.DownloadService
import androidx.recyclerview.widget.LinearLayoutManager
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.gyf.immersionbar.ktx.immersionBar
import relax.offline.music.R
import relax.offline.music.adapter.PlayListAdapter
import relax.offline.music.databinding.ActivityMoPlayDetailsBinding
import relax.offline.music.innertube.Innertube
import relax.offline.music.media.MediaControllerManager
import relax.offline.music.media.SongRadio
import relax.offline.music.service.MyDownloadService
import relax.offline.music.service.ViewModelMain
import relax.offline.music.sp.AppStore
import relax.offline.music.util.DownloadUtil
import relax.offline.music.util.LogTag.LogD
import relax.offline.music.util.PlayMode
import relax.offline.music.util.asMediaItem
import relax.offline.music.util.convertMillisToMinutesAndSecondsString
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
@OptIn(UnstableApi::class)
class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
companion object {
const val PLAY_DETAILS_VIDEO_ID = "play_details_videoId"
const val PLAY_DETAILS_PLAY_LIST_ID = "play_details_playlistId"
const val PLAY_DETAILS_PLAY_LIST_SET_VIDEO_ID = "play_details_play_list_set_video_id"
const val PLAY_DETAILS_PLAY_PARAMS = "play_details_play_params"
const val PLAY_DETAILS_NAME = "play_details_name"
const val PLAY_DETAILS_DESC = "play_details_desc"
const val PLAY_DETAILS_COME_FROM = "PLAY_DETAILS_COME_FROM"
}
private lateinit var binding: ActivityMoPlayDetailsBinding
private var currentVideoID = ""
private var comeFrom: Class<*>? = null
private var playListAdapter: PlayListAdapter? = null
private var downloadManager: DownloadManager? = null
private fun initImmersionBar() {
immersionBar {
statusBarDarkFont(false)
statusBarView(binding.view)
}
}
override suspend fun main() {
binding = ActivityMoPlayDetailsBinding.inflate(layoutInflater)
setContentView(binding.root)
initImmersionBar()
initClick()
initPlayerListener()
initPlayListAdapter()
updatePlayModeUi()
val videoId = intent.getStringExtra(PLAY_DETAILS_VIDEO_ID)
val playlistId = intent.getStringExtra(PLAY_DETAILS_PLAY_LIST_ID)
val playlistSetVideoId = intent.getStringExtra(PLAY_DETAILS_PLAY_LIST_SET_VIDEO_ID)
val params = intent.getStringExtra(PLAY_DETAILS_PLAY_PARAMS)
comeFrom = intent.getSerializableExtra(PLAY_DETAILS_COME_FROM) as Class<*>?
if (comeFrom != null && comeFrom == PrimaryActivity::class.java) {
LogD(TAG, "从当前播放的悬浮layout进入")
// 处理来自 PrimaryActivity 的情况
updateCurrentMediaItemInfo()
if (meController != null && meController.currentMediaItem != null) {
updateInfoUi(meController.currentMediaItem)
}
} else {
LogD(TAG, "从点击任意歌曲进入")
// if (meController != null && meController.currentMediaItem != null && videoId == meController.currentMediaItem?.mediaId) {
// //进入的id与当前的id一样就不重新去获取播放
// updateCurrentMediaItemInfo()
// updateInfoUi(meController.currentMediaItem)
// } else {
//
// }
binding.nameTv.text = intent.getStringExtra(PLAY_DETAILS_NAME)
binding.descTv.text = intent.getStringExtra(PLAY_DETAILS_DESC)
if (videoId.isNullOrEmpty()) {
finish()
return
}
//要加载数据的话就隐藏喜欢和下载按钮
binding.likeAndDownloadLayout.visibility = View.GONE
//传入进来的ID就是进入此界面的当前ID
currentVideoID = videoId
//根据进来界面的当前ID来获取资源。
initData(
videoId,
playlistId,
playlistSetVideoId,
params
)
}
initDownloadFlow()
onReceive()
}
private suspend fun onReceive() {
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ActivityOnResume -> {
activityOnResume()
}
else -> {}
}
}
}
}
}
private fun activityOnResume() {
// if (meController != null && meController.currentMediaItem != null) {
// updateInfoUi(meController.currentMediaItem)
// }
}
private fun initDownloadFlow() {
ViewModelMain.modelDownloadsFlow.observe(this) { downloads ->
if (meController != null && meController.currentMediaItem != null) {
val id = meController.currentMediaItem?.mediaId
LogD(TAG, "initDownloadFlow id ->${id}")
val currentScreenDownloads = downloads[id]
if (currentScreenDownloads != null) {
LogD(TAG, "initDownloadFlow Download id->${currentScreenDownloads?.request?.id}")
updateDownloadUI(currentScreenDownloads)
}
}
}
}
private fun updateDownloadUI(download: Download) {
when (download.state) {
Download.STATE_DOWNLOADING -> {
binding.downloadLoading.visibility = View.VISIBLE
binding.downloadImg.setImageResource(R.drawable.download_icon)
binding.downloadImg.visibility = View.GONE
binding.downloadBtn.isClickable = false
binding.downloadBtn.isEnabled = false
}
Download.STATE_COMPLETED -> {
binding.downloadLoading.visibility = View.GONE
binding.downloadImg.setImageResource(R.drawable.download_done_icon)
binding.downloadImg.visibility = View.VISIBLE
}
Download.STATE_FAILED -> {
binding.downloadLoading.visibility = View.GONE
binding.downloadImg.setImageResource(R.drawable.error)
binding.downloadImg.visibility = View.VISIBLE
binding.downloadBtn.isClickable = true
binding.downloadBtn.isEnabled = true
}
else -> {
binding.downloadLoading.visibility = View.GONE
binding.downloadImg.setImageResource(R.drawable.download_icon)
binding.downloadImg.visibility = View.VISIBLE
binding.downloadBtn.isClickable = true
binding.downloadBtn.isEnabled = true
}
}
}
private fun updateDownloadUi(id: String) {
if (DownloadUtil.downloadResourceExist(id)) {//已经下载,按钮不可点击
binding.downloadImg.setImageResource(R.drawable.download_done_icon)
binding.downloadBtn.isClickable = false
binding.downloadBtn.isEnabled = false
} else {
binding.downloadImg.setImageResource(R.drawable.download_icon)
binding.downloadBtn.isClickable = true
binding.downloadBtn.isEnabled = true
}
}
private fun initPlayerListener() {
meController?.addListener(playerListener)
}
private fun updateCurrentMediaItemInfo() {
if (meController != null && meController.currentMediaItem != null) {
binding.playbackErrorLayout.visibility = View.GONE
binding.loadingView.visibility = View.GONE
binding.disableClicksLayout.visibility = View.GONE
val currentString =
convertMillisToMinutesAndSecondsString(MediaControllerManager.getCurrentPosition())
binding.progressDurationTv.text = currentString
if (MediaControllerManager.getDuration() > 0) {
binding.totalDurationTv.visibility = View.VISIBLE
} else {
binding.totalDurationTv.visibility = View.GONE
}
binding.totalDurationTv.text =
convertMillisToMinutesAndSecondsString(MediaControllerManager.getDuration())
binding.sbProgress.valueTo = MediaControllerManager.getDuration().toFloat()
binding.sbProgress.value = MediaControllerManager.getCurrentPosition().toFloat()
updateProgressState()
binding.progressBar.progress = MediaControllerManager.getBufferedPosition().toInt()
binding.progressBar.max = MediaControllerManager.getDuration().toInt()
updateProgressBufferingState()
updatePlayListDataAndAdapter()
}
}
@SuppressLint("NotifyDataSetChanged")
private fun updatePlayListDataAndAdapter() {
if (meController != null && meController.currentMediaItem != null) {
val mediaItemCount = meController.mediaItemCount
val allMediaItems: MutableList<MediaItem> = mutableListOf()
for (index in 0 until mediaItemCount) {
val mediaItemAt = meController.getMediaItemAt(index)
allMediaItems.add(mediaItemAt)
}
playList.clear()
playList.addAll(allMediaItems)
playListAdapter?.notifyDataSetChanged()
}
}
private var playList: MutableList<MediaItem> = mutableListOf()
private fun initPlayListAdapter() {
playListAdapter = PlayListAdapter(
this@MoPlayDetailsActivity,
playList
)
binding.playListRv.layoutManager =
LinearLayoutManager(
this@MoPlayDetailsActivity,
LinearLayoutManager.VERTICAL,
false
)
binding.playListRv.adapter = playListAdapter
}
@SuppressLint("NotifyDataSetChanged")
private fun initClick() {
binding.backBtn.setOnClickListener {
finish()
}
binding.tryAgainBtn.setOnClickListener {
if (meController != null) {
updateInfoUi(meController.currentMediaItem)
updateProgressState()
if (!meController.isPlaying) {
meController.prepare()
meController.play()
}
}
}
binding.playModeBtn.setOnClickListener {
if (meController != null) {
val playModeCounter = (appStore.playMusicMode + 1) % 3
appStore.playMusicMode = when (playModeCounter) {
0 -> PlayMode.LIST_LOOP.value
1 -> PlayMode.SINGLE_LOOP.value
else -> PlayMode.RANDOM.value
}
when (AppStore(this).playMusicMode) {
PlayMode.LIST_LOOP.value -> {
meController.repeatMode = Player.REPEAT_MODE_ALL
meController.shuffleModeEnabled = false
}
PlayMode.SINGLE_LOOP.value -> {
meController.repeatMode = Player.REPEAT_MODE_ONE
meController.shuffleModeEnabled = false
}
PlayMode.RANDOM.value -> {
meController.repeatMode = Player.REPEAT_MODE_ALL
meController.shuffleModeEnabled = true
}
}
updatePlayModeUi()
LogD(TAG, "repeatMode->${meController.repeatMode}")
LogD(TAG, "shuffleModeEnabled->${meController.shuffleModeEnabled}")
}
}
binding.playLayoutBtn.setOnClickListener {
if (meController != null) {
if (meController.isPlaying) {
meController.pause()
updatePlayState(false)
} else {
meController.play()
updatePlayState(true)
}
updateProgressState()
}
}
binding.playSkipBackBtn.setOnClickListener {
if (meController != null) {
meController.seekToPreviousMediaItem()
updateProgressUi()
updateInfoUi(meController.currentMediaItem)
updateProgressState()
if (!meController.isPlaying) {
meController.prepare()
meController.play()
}
}
}
binding.playSkipForwardBtn.setOnClickListener {
if (meController != null) {
meController.seekToNextMediaItem()
updateProgressUi()
updateInfoUi(meController.currentMediaItem)
updateProgressState()
if (!meController.isPlaying) {
meController.prepare()
meController.play()
}
}
}
binding.listLayoutBtn.setOnClickListener {
toggleBottomLayout()
}
binding.bottomCloseBtn.setOnClickListener {
toggleBottomLayout()
}
binding.bottomBlankLayout.setOnClickListener {
toggleBottomLayout()
}
binding.progressBar.progress = 0
binding.sbProgress.value = 0f
binding.sbProgress.addOnChangeListener { slider, value, fromUser ->
if (fromUser) {
if (meController != null) {
meController.seekTo(value.toLong())
val ss = meController.isPlaying
if (!ss) {
meController.play()
}
}
}
}
binding.downloadBtn.setOnClickListener {
if (meController != null && meController.currentMediaItem != null) {
val currentMediaItem = meController.currentMediaItem
val contentId = currentMediaItem?.mediaId ?: ""
//如果已经存在就不进行下载
if (DownloadUtil.downloadResourceExist(contentId)) {
return@setOnClickListener
}
val downloadRequest = DownloadRequest
.Builder(contentId, contentId.toUri())
.setCustomCacheKey(contentId)
.build()
DownloadService.sendAddDownload(
this,
MyDownloadService::class.java,
downloadRequest,
false
)
}
}
}
private fun updatePlayModeUi() {
binding.modePlayImg.setImageResource(
when (AppStore(this).playMusicMode) {
PlayMode.LIST_LOOP.value -> {
R.drawable.mode_cycle_play_icon
}
PlayMode.SINGLE_LOOP.value -> {
R.drawable.mode_single_play_icon
}
PlayMode.RANDOM.value -> {
R.drawable.mode_random_play_icon
}
else -> {
R.drawable.mode_cycle_play_icon
}
}
)
}
@SuppressLint("NotifyDataSetChanged")
private fun initData(
videoId: String,
playlistId: String? = null,
playlistSetVideoId: String? = null,
parameters: String? = null
) {
SongRadio(
videoId,
playlistId,
playlistSetVideoId,
parameters
).let {
launch(Dispatchers.Main) {
val songRadioList = it.process()//获取到的资源集合
if (songRadioList.isEmpty()) {//集合为空则展示错误提示
binding.loadingView.visibility = View.GONE
binding.disableClicksLayout.visibility = View.GONE
binding.playbackErrorLayout.visibility = View.VISIBLE
}
if (isFinishing) {
return@launch
}
var mediaItem: MediaItem? = null
for (song: Innertube.SongItem in songRadioList) {
if (song.key == currentVideoID) {//判断当前ID得到一个mediaItem
mediaItem = song.asMediaItem
break
}
}
if (mediaItem != null) {
//数据请求完毕mediaItem不等于空就显示喜欢与下载按钮
binding.likeAndDownloadLayout.visibility = View.VISIBLE
updateInfoUi(mediaItem)
binding.playbackErrorLayout.visibility = View.GONE
binding.totalDurationTv.visibility = View.GONE
val newMediaItem =
MediaItem.Builder()
.setMediaId(videoId)
.setUri(videoId)
.setCustomCacheKey(videoId)
.setMediaMetadata(
mediaItem.mediaMetadata
)
.build()
meController?.let {
it.setMediaItem(newMediaItem, true)
it.prepare()
it.play()
//过滤掉进入页面的id得到集合addMediaItems
val mediaItems = songRadioList.map(Innertube.SongItem::asMediaItem)
.filter { filter -> filter.mediaId != videoId }
it.addMediaItems(mediaItems)
}
updatePlayListDataAndAdapter()
} else {
binding.playbackErrorLayout.visibility = View.VISIBLE
}
}
}
}
private val playerListener = object : Player.Listener {
@SuppressLint("NotifyDataSetChanged")
override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo,
newPosition: Player.PositionInfo,
reason: Int
) {
//保证自动播放完毕当前歌曲与通知切换歌曲可以更新UI信息
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION
|| reason == Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT
) {
updateProgressUi()
updateInfoUi(meController?.currentMediaItem)
if (playListAdapter != null) {
playListAdapter?.notifyDataSetChanged()
}
}
}
override fun onPlayerError(error: PlaybackException) {
LogD(TAG, "onPlayerError error= $error")
binding.playbackErrorLayout.visibility = View.VISIBLE//展示错误提示
updatePlayState(false)
}
override fun onPlayWhenReadyChanged(
playWhenReady: Boolean,
reason: Int
) {
updatePlayState(playWhenReady)
updateProgressState()
}
override fun onPlaybackStateChanged(playbackState: Int) {
when (playbackState) {
Player.STATE_BUFFERING -> {
binding.loadingView.visibility = View.VISIBLE
binding.disableClicksLayout.visibility = View.VISIBLE
binding.playbackErrorLayout.visibility = View.GONE
}
Player.STATE_READY -> {
binding.playbackErrorLayout.visibility = View.GONE
binding.loadingView.visibility = View.GONE
binding.disableClicksLayout.visibility = View.GONE
binding.totalDurationTv.visibility = View.VISIBLE
binding.totalDurationTv.text =
convertMillisToMinutesAndSecondsString(
MediaControllerManager.getDuration()
)
binding.sbProgress.valueTo =
MediaControllerManager.getDuration().toFloat()
binding.progressBar.max =
MediaControllerManager.getDuration().toInt()
updateProgressBufferingState()
}
else -> {
binding.loadingView.visibility = View.GONE
binding.disableClicksLayout.visibility = View.GONE
}
}
updateProgressState()
}
}
override fun onDestroy() {
super.onDestroy()
meController?.removeListener(playerListener)
}
private fun updateProgressUi() {
binding.sbProgress.value = 0f
binding.progressBar.progress = 0
binding.progressDurationTv.text = convertMillisToMinutesAndSecondsString(0L)
binding.totalDurationTv.visibility = View.GONE
}
private fun updateInfoUi(mediaItem: MediaItem?) {
if (mediaItem == null) {
binding.playbackErrorLayout.visibility = View.VISIBLE
return
}
// currentVideoID = mediaItem.mediaId
updateDownloadUi(mediaItem.mediaId)
Glide.with(this)
.asBitmap()
.load(mediaItem.mediaMetadata.artworkUri)
.placeholder(R.mipmap.app_logo)
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
binding.thumbnail.setImageBitmap(resource)
val blurredBitmap = applyGaussianBlur(resource, 25f, this@MoPlayDetailsActivity)
binding.imageView.setImageBitmap(blurredBitmap)
}
override fun onLoadCleared(placeholder: Drawable?) {
if (placeholder != null) {
binding.thumbnail.setImageDrawable(placeholder)
}
}
})
binding.nameTv.text = mediaItem.mediaMetadata.title
binding.descTv.text = mediaItem.mediaMetadata.artist
}
/**
* 更新播放进度
*/
private fun updateProgressState() {
//判断是否ready与播放中否则停止更新进度
if (meController != null && meController.playbackState == Player.STATE_READY && meController.isPlaying) {
updatePlayState(meController.isPlaying)
progressHandler.removeCallbacksAndMessages(null)
progressHandler.sendEmptyMessage(1)
} else {
progressHandler.removeCallbacksAndMessages(null)
}
}
/**
* 播放进度
*/
private val progressHandler = object : Handler(Looper.myLooper()!!) {
override fun handleMessage(msg: Message) {
//判断是否ready与播放中否则停止更新进度
if (meController != null && meController.playbackState == Player.STATE_READY && meController.isPlaying) {
val currentPosition = MediaControllerManager.getCurrentPosition()
val currentString = convertMillisToMinutesAndSecondsString(currentPosition)
binding.progressDurationTv.text = currentString
val currentBufferedPosition = MediaControllerManager.getBufferedPosition()
binding.progressBar.progress = currentBufferedPosition.toInt()
// 更新 SeekBar 的进度
binding.sbProgress.value = currentPosition.toFloat()
sendEmptyMessageDelayed(1, 50)
}
}
}
/**
* 更新缓冲进度
*/
private fun updateProgressBufferingState() {
if (meController != null && meController.isLoading) {
progressBufferingHandler.removeCallbacksAndMessages(null)
progressBufferingHandler.sendEmptyMessage(1)
} else {
progressBufferingHandler.removeCallbacksAndMessages(null)
}
}
/**
* 缓冲进度
*/
private val progressBufferingHandler = object : Handler(Looper.myLooper()!!) {
override fun handleMessage(msg: Message) {
if (meController != null && meController.isLoading) {
val currentBufferedPosition = MediaControllerManager.getBufferedPosition()
binding.progressBar.progress = currentBufferedPosition.toInt()
sendEmptyMessageDelayed(1, 50)
}
}
}
private fun updatePlayState(b: Boolean) {
if (b) {
binding.playImg.setImageResource(R.drawable.playing_green_icon)
} else {
binding.playImg.setImageResource(R.drawable.play_green_icon)
}
}
private fun toggleBottomLayout() {
if (binding.bottomLayout.visibility == View.VISIBLE) {
hideBottomLayout()
} else {
showBottomLayout()
}
}
private fun showBottomLayout() {
val slideUpAnimation = AnimationUtils.loadAnimation(this, R.anim.slide_up)
binding.bottomLayout.startAnimation(slideUpAnimation)
binding.bottomLayout.visibility = View.VISIBLE
}
private fun hideBottomLayout() {
val slideDownAnimation = AnimationUtils.loadAnimation(this, R.anim.slide_down)
binding.bottomLayout.startAnimation(slideDownAnimation)
binding.bottomLayout.visibility = View.GONE
}
}