diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5ac8e8f..2c509b3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("org.jetbrains.kotlin.android") id("kotlin-kapt") id("org.jetbrains.kotlin.plugin.serialization") + id("kotlin-android") } android { @@ -58,6 +59,9 @@ dependencies { implementation("com.google.android.material:material:1.11.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.media3:media3-session:1.3.1") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0") +// implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d462a8d..7d9e94d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -66,6 +66,9 @@ + - - + + diff --git a/app/src/main/java/com/player/musicoo/App.kt b/app/src/main/java/com/player/musicoo/App.kt index 1c4bdc6..481179f 100644 --- a/app/src/main/java/com/player/musicoo/App.kt +++ b/app/src/main/java/com/player/musicoo/App.kt @@ -7,11 +7,13 @@ import com.player.musicoo.bean.Audio import com.player.musicoo.bean.CurrentPlayingAudio import com.player.musicoo.bean.ResourcesList import com.player.musicoo.database.AppDatabase +import com.player.musicoo.database.AppOfflineDBManager import com.player.musicoo.database.CurrentAudioDatabase import com.player.musicoo.database.CurrentAudioManager import com.player.musicoo.database.DatabaseManager import com.player.musicoo.media.MediaControllerManager import com.player.musicoo.util.CacheManager +import com.player.musicoo.util.DownloadUtil import com.player.musicoo.util.parseResources import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -24,6 +26,8 @@ class App : Application() { companion object { lateinit var app: App private set + lateinit var appOfflineDBManager: AppOfflineDBManager + private set lateinit var currentAudioManager: CurrentAudioManager private set lateinit var databaseManager: DatabaseManager @@ -102,11 +106,13 @@ class App : Application() { app = this initialize(this) MediaControllerManager.init(this) + appOfflineDBManager = AppOfflineDBManager.getInstance(this) currentAudioManager = CurrentAudioManager.getInstance(this) databaseManager = DatabaseManager.getInstance(this) initCurrentPlayingAudio() initImportAudio() CacheManager.initializeCaches(this) + DownloadUtil.getDownloadManager(this) } } \ No newline at end of file diff --git a/app/src/main/java/com/player/musicoo/activity/MoBaseActivity.kt b/app/src/main/java/com/player/musicoo/activity/MoBaseActivity.kt index 75fc331..1735d8d 100644 --- a/app/src/main/java/com/player/musicoo/activity/MoBaseActivity.kt +++ b/app/src/main/java/com/player/musicoo/activity/MoBaseActivity.kt @@ -2,12 +2,9 @@ package com.player.musicoo.activity import android.content.Context import android.graphics.Bitmap -import android.graphics.BitmapFactory import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.os.Bundle -import android.os.Handler -import android.os.Looper import android.renderscript.Allocation import android.renderscript.Element import android.renderscript.RenderScript @@ -19,16 +16,19 @@ import android.widget.RelativeLayout import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.LifecycleOwner import androidx.media3.common.Player -import androidx.media3.session.MediaController -import com.bumptech.glide.Glide +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.offline.Download +import androidx.media3.exoplayer.offline.DownloadManager import com.player.musicoo.App import com.player.musicoo.R +import com.player.musicoo.bean.OfflineBean import com.player.musicoo.innertube.Innertube -import com.player.musicoo.innertube.Innertube.TAG import com.player.musicoo.media.MediaControllerManager import com.player.musicoo.sp.AppStore import com.player.musicoo.util.DownloadUtil +import com.player.musicoo.util.FileSizeConverter import com.player.musicoo.util.LogTag import com.player.musicoo.view.MusicPlayerView import kotlinx.coroutines.CoroutineScope @@ -38,11 +38,12 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.io.IOException -import java.io.InputStream -abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope() { +@UnstableApi +abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope(), + LifecycleOwner { private var playerListener: Player.Listener? = null + private var downloadManagerListener: DownloadManager.Listener? = null enum class Event { ActivityStart, @@ -64,6 +65,7 @@ abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope this.defer = operation } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) musicPlayerView = MusicPlayerView(this, meController) @@ -131,7 +133,6 @@ abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope override fun onDestroy() { super.onDestroy() - LogTag.LogD(TAG, "MoBaseActivity onDestroy") if (meController != null && playerListener != null) { meController.removeListener(playerListener!!) } @@ -147,10 +148,6 @@ abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope reason: Int ) { if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) { - LogTag.LogD( - Innertube.TAG, - "MoBaseActivity DISCONTINUITY_REASON_AUTO_TRANSITION" - ) if (meController != null) { musicPlayerView.updateInfoUi(meController.currentMediaItem) musicPlayerView.updateSetProgress(meController) @@ -162,7 +159,6 @@ abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope } override fun onPlaybackStateChanged(playbackState: Int) { - LogTag.LogD(Innertube.TAG, "MoBaseActivity playbackState->$playbackState") val meController = MediaControllerManager.getController() if (meController != null) { musicPlayerView.updateProgressState(meController) @@ -181,10 +177,6 @@ abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope playWhenReady: Boolean, reason: Int ) { - LogTag.LogD( - Innertube.TAG, - "MoBaseActivity onPlayWhenReadyChanged->$playWhenReady" - ) musicPlayerView.updatePlayState(playWhenReady) val meController = MediaControllerManager.getController() if (meController != null) { diff --git a/app/src/main/java/com/player/musicoo/activity/MoListDetailsActivity.kt b/app/src/main/java/com/player/musicoo/activity/MoListDetailsActivity.kt index 2458a62..7106ca8 100644 --- a/app/src/main/java/com/player/musicoo/activity/MoListDetailsActivity.kt +++ b/app/src/main/java/com/player/musicoo/activity/MoListDetailsActivity.kt @@ -103,6 +103,10 @@ class MoListDetailsActivity : MoBaseActivity() { showLoadingUi() Innertube.moPlaylistPage(browseId) ?.onSuccess { + if (this.isDestroyed || this.isFinishing) { + return + } + showDataUi() Glide.with(this) .load(it.thumbnail) diff --git a/app/src/main/java/com/player/musicoo/activity/MoOfflineSongsActivity.kt b/app/src/main/java/com/player/musicoo/activity/MoOfflineSongsActivity.kt new file mode 100644 index 0000000..c79eb47 --- /dev/null +++ b/app/src/main/java/com/player/musicoo/activity/MoOfflineSongsActivity.kt @@ -0,0 +1,143 @@ +package com.player.musicoo.activity + +import android.annotation.SuppressLint +import android.view.View +import androidx.media3.common.util.UnstableApi +import androidx.recyclerview.widget.LinearLayoutManager +import com.bumptech.glide.Glide +import com.gyf.immersionbar.ktx.immersionBar +import com.player.musicoo.App +import com.player.musicoo.adapter.DetailsListAdapter +import com.player.musicoo.adapter.OfflineSongsAdapter +import com.player.musicoo.bean.OfflineBean +import com.player.musicoo.databinding.ActivityDetailsBinding +import com.player.musicoo.databinding.ActivityOfflineSongsBinding +import com.player.musicoo.innertube.Innertube +import com.player.musicoo.innertube.requests.moPlaylistPage +import com.player.musicoo.util.DownloadUtil +import com.player.musicoo.util.LogTag.LogD +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.isActive +import kotlinx.coroutines.selects.select + +@UnstableApi +class MoOfflineSongsActivity : MoBaseActivity() { + private val requests: Channel = Channel(Channel.UNLIMITED) + + enum class Request { + TryAgain, + } + + private lateinit var binding: ActivityOfflineSongsBinding + private var adapter: OfflineSongsAdapter? = null + private var offlineList: MutableList = mutableListOf() + + override suspend fun main() { + binding = ActivityOfflineSongsBinding.inflate(layoutInflater) + setContentView(binding.root) + initImmersionBar() + initView() + initAdapter() + initData() + onReceive() + } + + private fun initImmersionBar() { + immersionBar { + statusBarDarkFont(false) + statusBarView(binding.view) + } + } + + @SuppressLint("NotifyDataSetChanged") + private suspend fun onReceive() { + while (isActive) { + select { + requests.onReceive { + when (it) { + Request.TryAgain -> { + initData() + } + } + } + events.onReceive { + when (it) { + Event.ActivityOnResume -> { + activityOnResume() + } + + Event.AutomaticallySwitchSongs -> { + if (adapter != null) { + adapter?.notifyDataSetChanged() + } + } + + else -> {} + } + } + } + } + } + + @SuppressLint("NotifyDataSetChanged") + private fun activityOnResume() { + addMusicPlayerViewToLayout(binding.playMusicLayout) + + if (adapter != null) { + adapter?.notifyDataSetChanged() + } + } + + private fun initView() { + binding.backBtn.setOnClickListener { + finish() + } + binding.tryAgainBtn.setOnClickListener { + requests.trySend(Request.TryAgain) + } + } + + private fun initAdapter() { + adapter = OfflineSongsAdapter(this, offlineList) + binding.rv.layoutManager = + LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) + binding.rv.adapter = adapter + } + + @SuppressLint("NotifyDataSetChanged") + private suspend fun initData() { + showLoadingUi() + + offlineList.clear() + offlineList.addAll(App.appOfflineDBManager.getAllOfflineBeans()) + + for (offline in offlineList){ + LogD(TAG,"offline id->${offline.videoId}") + } + + if (offlineList.size > 0) { + showDataUi() + } else { + showNoContentUi() + } + if (adapter != null) { + adapter?.notifyDataSetChanged() + } + + } + + private fun showDataUi() { + binding.loadingLayout.visibility = View.GONE + binding.noContentLayout.visibility = View.GONE + } + + private fun showLoadingUi() { + binding.loadingLayout.visibility = View.VISIBLE + binding.noContentLayout.visibility = View.GONE + } + + private fun showNoContentUi() { + binding.loadingLayout.visibility = View.GONE + binding.noContentLayout.visibility = View.VISIBLE + } +} \ No newline at end of file diff --git a/app/src/main/java/com/player/musicoo/activity/MoPlayDetailsActivity.kt b/app/src/main/java/com/player/musicoo/activity/MoPlayDetailsActivity.kt index d25d11f..be8fab6 100644 --- a/app/src/main/java/com/player/musicoo/activity/MoPlayDetailsActivity.kt +++ b/app/src/main/java/com/player/musicoo/activity/MoPlayDetailsActivity.kt @@ -10,6 +10,7 @@ import android.view.View import android.view.animation.AnimationUtils import androidx.annotation.OptIn import androidx.core.net.toUri +import androidx.lifecycle.LifecycleOwner import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException import androidx.media3.common.Player @@ -23,25 +24,29 @@ 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 com.player.musicoo.App import com.player.musicoo.R import com.player.musicoo.adapter.PlayListAdapter +import com.player.musicoo.bean.OfflineBean import com.player.musicoo.databinding.ActivityMoPlayDetailsBinding import com.player.musicoo.innertube.Innertube import com.player.musicoo.media.MediaControllerManager import com.player.musicoo.media.SongRadio import com.player.musicoo.service.MyDownloadService +import com.player.musicoo.service.ViewModelMain import com.player.musicoo.sp.AppStore import com.player.musicoo.util.DownloadUtil import com.player.musicoo.util.FileSizeConverter +import com.player.musicoo.util.LogTag import com.player.musicoo.util.LogTag.LogD import com.player.musicoo.util.PlayMode import com.player.musicoo.util.asMediaItem import com.player.musicoo.util.convertMillisToMinutesAndSecondsString +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.selects.select -import java.lang.Exception @OptIn(UnstableApi::class) class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { @@ -77,7 +82,6 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { initImmersionBar() initClick() initPlayerListener() - initDownloadListener() initPlayListAdapter() updatePlayModeUi() val videoId = intent.getStringExtra(PLAY_DETAILS_VIDEO_ID) @@ -87,33 +91,41 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { comeFrom = intent.getSerializableExtra(PLAY_DETAILS_COME_FROM) as Class<*>? if (comeFrom != null && comeFrom == PrimaryActivity::class.java) { + LogD(TAG, "从当前播放的悬浮layout进入") // 处理来自 PrimaryActivity 的情况 updateCurrentMediaItemInfo() - } else { - if (meController != null && meController.currentMediaItem != null && videoId == meController.currentMediaItem?.mediaId) { - //进入的id与当前的id一样就不重新去获取播放 - updateCurrentMediaItemInfo() + if (meController != null && meController.currentMediaItem != null) { 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 - } - //传入进来的ID,就是进入此界面的当前ID - currentVideoID = videoId - //根据进来界面的当前ID来获取资源。 - initData( - videoId, - playlistId, - playlistSetVideoId, - params - ) } + } 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() } @@ -134,25 +146,72 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { } private fun activityOnResume() { - if (comeFrom != null && comeFrom == PrimaryActivity::class.java) { +// if (meController != null && meController.currentMediaItem != null) { +// updateInfoUi(meController.currentMediaItem) +// } + } + + private fun initDownloadFlow() { + ViewModelMain.modelDownloadsFlow.observe(this) { downloads -> if (meController != null && meController.currentMediaItem != null) { - updateInfoUi(meController.currentMediaItem) + val id = meController.currentMediaItem?.mediaId + LogD(TAG, "initDownloadFlow id ->${id}") + val currentScreenDownloads = downloads[id] + LogD(TAG, "currentScreenDownloads->${currentScreenDownloads}") + if (currentScreenDownloads != null) { + updateDownloadUI(currentScreenDownloads) + } } + } - if (meController != null && meController.currentMediaItem != null) { - LogD(TAG,"meController.currentMediaItem != null->${meController.currentMediaItem?.mediaId!!}") - updateDownloadUi(meController.currentMediaItem?.mediaId!!) - } else { - LogD(TAG,"currentVideoID->${currentVideoID}") - updateDownloadUi(currentVideoID) + } + + 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)) { + 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 } } @@ -161,40 +220,6 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { meController?.addListener(playerListener) } - private fun initDownloadListener() { - downloadManager = DownloadUtil.getDownloadManager(this) - if (downloadManager != null) { - downloadManager?.addListener(object : DownloadManager.Listener { - override fun onDownloadChanged( - downloadManager: DownloadManager, - download: Download, - finalException: Exception? - ) { - when (download.state) { - Download.STATE_DOWNLOADING -> { - binding.downloadLoading.visibility = View.VISIBLE - binding.downloadImg.setImageResource(R.drawable.download_icon) - binding.downloadImg.visibility = View.GONE - } - - Download.STATE_COMPLETED -> { - binding.downloadLoading.visibility = View.GONE - binding.downloadImg.setImageResource(R.drawable.download_done_icon) - binding.downloadImg.visibility = View.VISIBLE - } - - else -> { - binding.downloadLoading.visibility = View.GONE - binding.downloadImg.setImageResource(R.drawable.error) - binding.downloadImg.visibility = View.VISIBLE - } - } - - } - }) - } - } - private fun updateCurrentMediaItemInfo() { if (meController != null && meController.currentMediaItem != null) { binding.playbackErrorLayout.visibility = View.GONE @@ -289,11 +314,6 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { PlayMode.RANDOM.value -> { meController.repeatMode = Player.REPEAT_MODE_ALL -// val availableCommands = meController.availableCommands -// //控制器支持设置随机播放模式的命令 -// if (availableCommands.contains(Player.COMMAND_SET_SHUFFLE_MODE)) { -// meController.shuffleModeEnabled = true -// } meController.shuffleModeEnabled = true } } @@ -363,35 +383,24 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { binding.downloadBtn.setOnClickListener { if (meController != null && meController.currentMediaItem != null) { - val contentId = meController.currentMediaItem?.mediaId!! + val currentMediaItem = meController.currentMediaItem + val contentId = currentMediaItem?.mediaId ?: "" //如果已经存在就不进行下载 if (DownloadUtil.downloadResourceExist(contentId)) { return@setOnClickListener } - val currentDownload = DownloadUtil.currentDownload(contentId) - if (currentDownload != null) { - if (currentDownload.state == Download.STATE_DOWNLOADING) { - DownloadService.sendRemoveDownload( - this, - MyDownloadService::class.java, - contentId, - false - ) - } - } else { - val downloadRequest = DownloadRequest - .Builder(contentId, contentId.toUri()) - .setCustomCacheKey(contentId) - .build() - DownloadService.sendAddDownload( - this, - MyDownloadService::class.java, - downloadRequest, - false - ) - } - + val downloadRequest = DownloadRequest + .Builder(contentId, contentId.toUri()) + .setCustomCacheKey(contentId) + .build() + DownloadService.sendAddDownload( + this, + MyDownloadService::class.java, + downloadRequest, + false + ) } + } } @@ -453,6 +462,8 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { } if (mediaItem != null) { + //数据请求完毕mediaItem不等于空就显示喜欢与下载按钮 + binding.likeAndDownloadLayout.visibility = View.VISIBLE updateInfoUi(mediaItem) binding.playbackErrorLayout.visibility = View.GONE @@ -573,6 +584,8 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { binding.playbackErrorLayout.visibility = View.VISIBLE return } +// currentVideoID = mediaItem.mediaId + updateDownloadUi(mediaItem.mediaId) Glide.with(this) .asBitmap() diff --git a/app/src/main/java/com/player/musicoo/adapter/DetailsListAdapter.kt b/app/src/main/java/com/player/musicoo/adapter/DetailsListAdapter.kt index 3b7e066..82ffa17 100644 --- a/app/src/main/java/com/player/musicoo/adapter/DetailsListAdapter.kt +++ b/app/src/main/java/com/player/musicoo/adapter/DetailsListAdapter.kt @@ -51,8 +51,8 @@ class DetailsListAdapter( ) intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_PLAY_PARAMS, bean.params) - intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_NAME, bean.name) - intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_DESC, bean.title) + intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_NAME, bean.title) + intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_DESC, bean.name) context.startActivity(intent) } } diff --git a/app/src/main/java/com/player/musicoo/adapter/OfflineSongsAdapter.kt b/app/src/main/java/com/player/musicoo/adapter/OfflineSongsAdapter.kt new file mode 100644 index 0000000..1decd0e --- /dev/null +++ b/app/src/main/java/com/player/musicoo/adapter/OfflineSongsAdapter.kt @@ -0,0 +1,99 @@ +package com.player.musicoo.adapter + +import android.annotation.SuppressLint +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.player.musicoo.R +import com.player.musicoo.bean.OfflineBean +import com.player.musicoo.databinding.OfflineListItemBinding +import com.player.musicoo.databinding.PlayListItemBinding +import com.player.musicoo.media.MediaControllerManager + +class OfflineSongsAdapter( + private val context: Context, + private val list: List, +) : + RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = OfflineListItemBinding.inflate(LayoutInflater.from(context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val bean = list[position] + holder.bind(bean) + + holder.itemView.setOnClickListener { +// val meController = MediaControllerManager.getController() +// if (meController != null && meController.currentMediaItem != null) { +// var index = holder.bindingAdapterPosition +// if (index > meController.mediaItemCount) { +// index = 1 +// } +// meController.seekTo(index, C.TIME_UNSET) +// if (!meController.isPlaying) { +// meController.prepare() +// meController.play() +// } +// } + + } + } + + override fun getItemCount(): Int = list.size + + inner class ViewHolder(private val binding: OfflineListItemBinding) : + RecyclerView.ViewHolder(binding.root) { + + @SuppressLint("SetTextI18n") + fun bind(bean: OfflineBean) { + + binding.apply { + Glide.with(context) + .load(bean.thumbnail) + .into(image) + title.text = bean.title + if (bean.name.isEmpty()) { + name.visibility = View.GONE + } else { + name.visibility = View.VISIBLE + name.text = bean.name + } + size.text = bean.size + + val meController = MediaControllerManager.getController() + if (meController != null && meController.currentMediaItem != null) { + if (meController.currentMediaItem?.mediaId == bean.videoId) { + binding.listPlayView.visibility = View.VISIBLE + binding.title.setTextColor(context.getColor(R.color.green)) + binding.name.setTextColor(context.getColor(R.color.green_60)) + binding.size.setTextColor(context.getColor(R.color.green_60)) + } else { + binding.title.setTextColor(context.getColor(R.color.white)) + binding.name.setTextColor(context.getColor(R.color.white_60)) + binding.size.setTextColor(context.getColor(R.color.white_60)) + binding.listPlayView.visibility = View.GONE + } + } + } + } + } + + private var itemClickListener: OnItemClickListener? = null + + fun setOnItemClickListener(listener: OnItemClickListener) { + itemClickListener = listener + } + + interface OnItemClickListener { + fun onItemClick(position: Int) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/player/musicoo/bean/OfflineBean.kt b/app/src/main/java/com/player/musicoo/bean/OfflineBean.kt new file mode 100644 index 0000000..5d3646f --- /dev/null +++ b/app/src/main/java/com/player/musicoo/bean/OfflineBean.kt @@ -0,0 +1,22 @@ +package com.player.musicoo.bean + +import android.net.Uri +import androidx.annotation.Keep +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.io.Serializable + +@Keep +@Entity +data class OfflineBean( + @ColumnInfo(name = "videoId") var videoId: String, + @ColumnInfo(name = "title") var title: String, + @ColumnInfo(name = "name") var name: String, + @ColumnInfo(name = "thumbnail") var thumbnail: String? = null, + @ColumnInfo(name = "size") var size: String? = null, + @ColumnInfo(name = "isOffline") var isOffline: Boolean +) : Serializable { + @PrimaryKey(autoGenerate = true) + var id: Long = 0 +} \ No newline at end of file diff --git a/app/src/main/java/com/player/musicoo/database/AppOfflineDBManager.kt b/app/src/main/java/com/player/musicoo/database/AppOfflineDBManager.kt new file mode 100644 index 0000000..075f74b --- /dev/null +++ b/app/src/main/java/com/player/musicoo/database/AppOfflineDBManager.kt @@ -0,0 +1,83 @@ +package com.player.musicoo.database + +import android.content.Context +import androidx.room.Room +import com.player.musicoo.bean.OfflineBean +import com.player.musicoo.util.LogTag +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class AppOfflineDBManager private constructor(context: Context) { + + companion object { + @Volatile + private var instance: AppOfflineDBManager? = null + + fun getInstance(context: Context): AppOfflineDBManager { + return instance ?: synchronized(this) { + instance ?: AppOfflineDBManager(context).also { instance = it } + } + } + } + + private val database = Room.databaseBuilder( + context.applicationContext, + AppOfflineDatabase::class.java, "offline_data_base" + ).build() + + private val dao = database.localOfflineDao() + + suspend fun insertOfflineBean(bean: OfflineBean) { + withContext(Dispatchers.IO) { + val offlineBean = getOfflineBeanByID(bean.videoId) + if (offlineBean == null) { + LogTag.LogD(LogTag.VO_TEST_ONLY,"insertOfflineBean") + dao.insertOfflineBean(bean) + } else { + LogTag.LogD(LogTag.VO_TEST_ONLY,"updateOfflineBean") + dao.updateOfflineBean(bean) + } + } + } + + suspend fun insertOfflineListBean(list: List) { + withContext(Dispatchers.IO) { + for (bean in list) { + val offlineBean = getOfflineBeanByID(bean.videoId) + if (offlineBean == null) { + dao.insertOfflineBean(bean) + } else { + dao.updateOfflineBean(bean) + } + } + } + } + + suspend fun getAllOfflineBeans(): List { + return withContext(Dispatchers.IO) { + dao.getAllOfflineBeans() + } + } + + suspend fun deleteOfflineBean(bean: OfflineBean) { + withContext(Dispatchers.IO) { + dao.deleteOfflineBean(bean) + } + } + + suspend fun deleteAllOfflineBean() { + withContext(Dispatchers.IO) { + dao.deleteAllOfflineBean() + } + } + + suspend fun updateOfflineBean(bean: OfflineBean) { + withContext(Dispatchers.IO) { + dao.updateOfflineBean(bean) + } + } + + private suspend fun getOfflineBeanByID(id: String): OfflineBean? { + return dao.getOfflineBeanByID(id) + } +} diff --git a/app/src/main/java/com/player/musicoo/database/AppOfflineDatabase.kt b/app/src/main/java/com/player/musicoo/database/AppOfflineDatabase.kt new file mode 100644 index 0000000..23f633e --- /dev/null +++ b/app/src/main/java/com/player/musicoo/database/AppOfflineDatabase.kt @@ -0,0 +1,11 @@ + +package com.player.musicoo.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.player.musicoo.bean.OfflineBean + +@Database(entities = [OfflineBean::class], version = 1, exportSchema = false) +abstract class AppOfflineDatabase : RoomDatabase() { + abstract fun localOfflineDao(): OfflineDao +} \ No newline at end of file diff --git a/app/src/main/java/com/player/musicoo/database/OfflineDao.kt b/app/src/main/java/com/player/musicoo/database/OfflineDao.kt new file mode 100644 index 0000000..4aee0b7 --- /dev/null +++ b/app/src/main/java/com/player/musicoo/database/OfflineDao.kt @@ -0,0 +1,33 @@ +package com.player.musicoo.database + +import androidx.room.* +import com.player.musicoo.bean.OfflineBean + +@Dao +interface OfflineDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertOfflineBean(bean: OfflineBean) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertOfflineBeans(beans: List) + + @Query("SELECT * FROM OfflineBean") + suspend fun getAllOfflineBeans(): List + + @Delete + suspend fun deleteOfflineBean(bean: OfflineBean) + + @Query("DELETE FROM OfflineBean") + suspend fun deleteAllOfflineBean() + + @Update + suspend fun updateOfflineBean(bean: OfflineBean) + + @Query("SELECT * FROM OfflineBean WHERE videoId = :id LIMIT 1") + suspend fun getOfflineBeanByID(id: String): OfflineBean? + + @Query("SELECT * FROM OfflineBean WHERE isOffline = 1") + suspend fun getOfflineBeanByIsOffline(): List + +} diff --git a/app/src/main/java/com/player/musicoo/fragment/MoMeFragment.kt b/app/src/main/java/com/player/musicoo/fragment/MoMeFragment.kt index 0d81ded..57f18c8 100644 --- a/app/src/main/java/com/player/musicoo/fragment/MoMeFragment.kt +++ b/app/src/main/java/com/player/musicoo/fragment/MoMeFragment.kt @@ -1,10 +1,15 @@ package com.player.musicoo.fragment +import android.content.Intent import android.view.LayoutInflater import android.view.ViewGroup +import android.widget.Toast import com.gyf.immersionbar.ktx.immersionBar -import com.player.musicoo.databinding.FragmentMoHomeBinding +import com.player.musicoo.R +import com.player.musicoo.activity.MoOfflineSongsActivity import com.player.musicoo.databinding.FragmentMoMeBinding +import com.player.musicoo.innertube.utils.BrotliEncoder.decode +import com.player.musicoo.util.DownloadUtil import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.isActive import kotlinx.coroutines.selects.select @@ -38,13 +43,39 @@ class MoMeFragment : MoBaseFragment() { requests.onReceive { } + + events.onReceive { + when (it) { + Event.FragmentOnResume -> { + fragmentOnResume() + } + } + } } } } private fun initView() { + binding.likedSongsBtn.setOnClickListener { + + } + binding.offlineSongsBtn.setOnClickListener { + if (DownloadUtil.getDownloadCount() > 0) { + val intent = Intent(context, MoOfflineSongsActivity::class.java) + startActivity(intent) + } else { + Toast.makeText( + activity, + getString(R.string.offline_songs_no_data_prompt), + Toast.LENGTH_LONG + ).show() + } + } } + private fun fragmentOnResume() { + binding.offlineSongsTv.text = "${DownloadUtil.getDownloadCount()}" + } override fun onResume() { super.onResume() diff --git a/app/src/main/java/com/player/musicoo/service/MyDownloadService.kt b/app/src/main/java/com/player/musicoo/service/MyDownloadService.kt index 4fd2df9..0b746f9 100644 --- a/app/src/main/java/com/player/musicoo/service/MyDownloadService.kt +++ b/app/src/main/java/com/player/musicoo/service/MyDownloadService.kt @@ -12,8 +12,13 @@ import androidx.media3.exoplayer.offline.DownloadManager import androidx.media3.exoplayer.offline.DownloadNotificationHelper import androidx.media3.exoplayer.offline.DownloadService import androidx.media3.exoplayer.scheduler.PlatformScheduler +import com.player.musicoo.App import com.player.musicoo.R +import com.player.musicoo.bean.OfflineBean import com.player.musicoo.util.DownloadUtil +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch @OptIn(UnstableApi::class) @@ -37,8 +42,9 @@ class MyDownloadService : DownloadService( val downloadManager = DownloadUtil.getDownloadManager(this) downloadManager!!.maxParallelDownloads = 3 val downloadNotificationHelper = DownloadUtil.getDownloadNotificationHelper(this) + downloadManager.addListener( - TerminalStateNotificationHelper( + DownloadUtil.TerminalStateNotificationHelper( this, downloadNotificationHelper!!, FOREGROUND_NOTIFICATION_ID + 1 ) ) @@ -63,43 +69,4 @@ class MyDownloadService : DownloadService( notMetRequirements ) } - - - private class TerminalStateNotificationHelper( - private val context: Context, - private val notificationHelper: DownloadNotificationHelper, - private var nextNotificationId: Int - ) : - DownloadManager.Listener { - - override fun onDownloadChanged( - downloadManager: DownloadManager, - download: Download, - finalException: Exception? - ) { - val notification: Notification = when (download.state) { - Download.STATE_COMPLETED -> { - notificationHelper.buildDownloadCompletedNotification( - context, - R.drawable.download_done_icon, - null, - Util.fromUtf8Bytes(download.request.data) - ) - } - Download.STATE_FAILED -> { - notificationHelper.buildDownloadFailedNotification( - context, - R.drawable.error, - null, - Util.fromUtf8Bytes(download.request.data) - ) - } - else -> { - return - } - } - NotificationUtil.setNotification(context, nextNotificationId++, notification) - } - } - } \ No newline at end of file diff --git a/app/src/main/java/com/player/musicoo/service/ViewModelMain.kt b/app/src/main/java/com/player/musicoo/service/ViewModelMain.kt new file mode 100644 index 0000000..4d1ad74 --- /dev/null +++ b/app/src/main/java/com/player/musicoo/service/ViewModelMain.kt @@ -0,0 +1,9 @@ +package com.player.musicoo.service + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.media3.exoplayer.offline.Download + +object ViewModelMain : ViewModel() { + var modelDownloadsFlow = MutableLiveData>() +} \ No newline at end of file diff --git a/app/src/main/java/com/player/musicoo/util/DownloadUtil.kt b/app/src/main/java/com/player/musicoo/util/DownloadUtil.kt index 983cbab..419d16c 100644 --- a/app/src/main/java/com/player/musicoo/util/DownloadUtil.kt +++ b/app/src/main/java/com/player/musicoo/util/DownloadUtil.kt @@ -1,11 +1,16 @@ package com.player.musicoo.util +import android.app.Notification import android.content.Context import android.net.Uri +import android.util.Log import androidx.annotation.OptIn import androidx.core.net.toUri +import androidx.lifecycle.MutableLiveData import androidx.media3.common.PlaybackException +import androidx.media3.common.util.NotificationUtil import androidx.media3.common.util.UnstableApi +import androidx.media3.common.util.Util import androidx.media3.database.DatabaseProvider import androidx.media3.database.StandaloneDatabaseProvider import androidx.media3.datasource.DataSource @@ -23,15 +28,28 @@ import androidx.media3.datasource.cronet.CronetUtil import androidx.media3.exoplayer.offline.Download import androidx.media3.exoplayer.offline.DownloadManager import androidx.media3.exoplayer.offline.DownloadNotificationHelper +import com.player.musicoo.App +import com.player.musicoo.R +import com.player.musicoo.bean.OfflineBean import com.player.musicoo.innertube.Innertube import com.player.musicoo.innertube.Innertube.TAG import com.player.musicoo.innertube.models.bodies.PlayerBody import com.player.musicoo.innertube.requests.player +import com.player.musicoo.media.MediaControllerManager import com.player.musicoo.service.LoginRequiredException import com.player.musicoo.service.PlayableFormatNotFoundException import com.player.musicoo.service.UnplayableException import com.player.musicoo.service.VideoIdMismatchException +import com.player.musicoo.service.ViewModelMain +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import java.net.CookieHandler import java.net.CookieManager @@ -84,7 +102,6 @@ object DownloadUtil { context.applicationContext, getHttpDataSourceFactory(context.applicationContext)!! ) - val chunkLength = 512 * 1024L val ringBuffer = RingBuffer?>(2) { null } return ResolvingDataSource.Factory( CacheDataSource.Factory() @@ -158,31 +175,43 @@ object DownloadUtil { .use { cursor -> while (cursor.moveToNext()) { val download = cursor.download - LogTag.LogD(TAG, "download.request.id->${download.request.id}") - LogTag.LogD( - TAG, - "download formattedSize->${FileSizeConverter(download.bytesDownloaded).formattedSize()}" - ) if (download.request.id == id) { isExist = true } } } } - LogTag.LogD(TAG, "isExist->$isExist") return isExist } - fun currentDownload(id: String): Download? { - var download: Download? = null + fun getCurrentIdDownloadState(id: String): Int { if (downloadManager != null) { - downloadManager?.currentDownloads?.map { - if (it.request.id === id) { - download = it + val downloadIndex = downloadManager!!.downloadIndex + downloadIndex.getDownloads() + .use { cursor -> + while (cursor.moveToNext()) { + val download = cursor.download + if (download.request.id == id) { + return download.state + } + } } - } } - return download + return -1 + } + + fun getDownloadCount(): Int { + var count = 0 + if (downloadManager != null) { + val downloadIndex = downloadManager!!.downloadIndex + downloadIndex.getDownloads() + .use { cursor -> + while (cursor.moveToNext()) { + count++ + } + } + } + return count } @Synchronized @@ -212,13 +241,68 @@ object DownloadUtil { return databaseProvider } - private fun buildReadOnlyCacheDataSource( - upstreamFactory: DataSource.Factory, cache: Cache? - ): CacheDataSource.Factory { - return CacheDataSource.Factory() - .setCache(cache!!) - .setUpstreamDataSourceFactory(upstreamFactory) - .setCacheWriteDataSinkFactory(null) - .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR) + class TerminalStateNotificationHelper( + private val context: Context, + private val notificationHelper: DownloadNotificationHelper, + private var nextNotificationId: Int + ) : + DownloadManager.Listener { + + override fun onDownloadChanged( + downloadManager: DownloadManager, + download: Download, + finalException: Exception? + ) { + val downloadsMap = mutableMapOf() + downloadsMap[download.request.id] = download + ViewModelMain.modelDownloadsFlow.postValue(downloadsMap) + + val notification: Notification = when (download.state) { + Download.STATE_COMPLETED -> { + insertOfflineData(download) + + notificationHelper.buildDownloadCompletedNotification( + context, + R.drawable.download_done_icon, + null, + Util.fromUtf8Bytes(download.request.data) + ) + } + + Download.STATE_FAILED -> { + notificationHelper.buildDownloadFailedNotification( + context, + R.drawable.error, + null, + Util.fromUtf8Bytes(download.request.data) + ) + } + + else -> { + return + } + } + NotificationUtil.setNotification(context, nextNotificationId++, notification) + } + } + + private fun insertOfflineData(download: Download) { + val meController = MediaControllerManager.getController() + if (meController != null && meController.currentMediaItem != null) { + val currentMediaItem = meController.currentMediaItem + val mediaMetadata = currentMediaItem?.mediaMetadata + CoroutineScope(Dispatchers.IO).launch { + val bean = OfflineBean( + videoId = download.request.id, + title = mediaMetadata?.title.toString(), + name = mediaMetadata?.artist.toString(), + thumbnail = mediaMetadata?.artworkUri.toString(), + size = FileSizeConverter(download.bytesDownloaded).formattedSize(), + isOffline = true + ) + LogTag.LogD(TAG, "insertOfflineBean bean->${bean}") + App.appOfflineDBManager.insertOfflineBean(bean) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/player/musicoo/util/LogTag.kt b/app/src/main/java/com/player/musicoo/util/LogTag.kt index 6b7a6e6..60e83c9 100644 --- a/app/src/main/java/com/player/musicoo/util/LogTag.kt +++ b/app/src/main/java/com/player/musicoo/util/LogTag.kt @@ -8,6 +8,8 @@ object LogTag { const val VO_API_LOG = "vo-api—log" const val VO_SERVICE_LOG = "vo-service—log" + const val VO_TEST_ONLY = "vo-only—log" + fun LogD(tag: String, message: String) { Log.d(tag, message) } diff --git a/app/src/main/res/drawable/download_green_done_icon.xml b/app/src/main/res/drawable/download_green_done_icon.xml new file mode 100644 index 0000000..1debda6 --- /dev/null +++ b/app/src/main/res/drawable/download_green_done_icon.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/library_liked_icon.xml b/app/src/main/res/drawable/library_liked_icon.xml new file mode 100644 index 0000000..07d39f5 --- /dev/null +++ b/app/src/main/res/drawable/library_liked_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/library_offline_icon.xml b/app/src/main/res/drawable/library_offline_icon.xml new file mode 100644 index 0000000..85887bb --- /dev/null +++ b/app/src/main/res/drawable/library_offline_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_mo_play_details.xml b/app/src/main/res/layout/activity_mo_play_details.xml index 0aec8f4..d2d0a79 100644 --- a/app/src/main/res/layout/activity_mo_play_details.xml +++ b/app/src/main/res/layout/activity_mo_play_details.xml @@ -212,51 +212,57 @@ android:textSize="12dp" /> - - - + - + + - + - + + + + + + - diff --git a/app/src/main/res/layout/activity_offline_songs.xml b/app/src/main/res/layout/activity_offline_songs.xml new file mode 100644 index 0000000..f99b656 --- /dev/null +++ b/app/src/main/res/layout/activity_offline_songs.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/details_list_item.xml b/app/src/main/res/layout/details_list_item.xml index c486d1e..84b33bb 100644 --- a/app/src/main/res/layout/details_list_item.xml +++ b/app/src/main/res/layout/details_list_item.xml @@ -63,8 +63,9 @@ @@ -90,6 +91,17 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_mo_me.xml b/app/src/main/res/layout/fragment_mo_me.xml index 819756d..d023bb1 100644 --- a/app/src/main/res/layout/fragment_mo_me.xml +++ b/app/src/main/res/layout/fragment_mo_me.xml @@ -1,5 +1,6 @@ @@ -15,7 +16,161 @@ android:orientation="vertical"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/offline_list_item.xml b/app/src/main/res/layout/offline_list_item.xml new file mode 100644 index 0000000..68fc1a4 --- /dev/null +++ b/app/src/main/res/layout/offline_list_item.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index d38b82f..863b9f3 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -9,6 +9,8 @@ #151718 #FF80F988 #9980F988 + #FFFC746F + #99FC746F #1A1A1A #00000000 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 135f4e1..47d03d0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,4 +29,11 @@ More Download Downloads + Library + Liked songs + Offline songs + Songs + New playlist + You haven\'t liked any songs yet. + You haven\'t saved any songs for offline listening yet. \ No newline at end of file