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 { 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 = 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 = 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() { override fun onResourceReady(resource: Bitmap, transition: Transition?) { 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 } }