525 lines
22 KiB
Kotlin
525 lines
22 KiB
Kotlin
package melody.offline.music.activity
|
||
|
||
import android.content.Context
|
||
import android.graphics.Bitmap
|
||
import android.graphics.Color
|
||
import android.graphics.drawable.ColorDrawable
|
||
import android.os.Bundle
|
||
import android.renderscript.Allocation
|
||
import android.renderscript.Element
|
||
import android.renderscript.RenderScript
|
||
import android.renderscript.ScriptIntrinsicBlur
|
||
import android.view.LayoutInflater
|
||
import android.view.View
|
||
import android.widget.EditText
|
||
import android.widget.ImageView
|
||
import android.widget.LinearLayout
|
||
import android.widget.ProgressBar
|
||
import android.widget.RelativeLayout
|
||
import android.widget.TextView
|
||
import android.widget.Toast
|
||
import androidx.annotation.OptIn
|
||
import androidx.appcompat.app.AlertDialog
|
||
import androidx.appcompat.app.AppCompatActivity
|
||
import androidx.core.content.ContextCompat
|
||
import androidx.core.net.toUri
|
||
import androidx.lifecycle.LifecycleOwner
|
||
import androidx.media3.common.MediaItem
|
||
import androidx.media3.common.Player
|
||
import androidx.media3.common.util.UnstableApi
|
||
import androidx.media3.exoplayer.offline.Download
|
||
import androidx.media3.exoplayer.offline.DownloadRequest
|
||
import androidx.media3.exoplayer.offline.DownloadService
|
||
import androidx.recyclerview.widget.LinearLayoutManager
|
||
import androidx.recyclerview.widget.RecyclerView
|
||
import com.bumptech.glide.Glide
|
||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||
import com.ironsource.be
|
||
import com.ironsource.vi
|
||
import kotlinx.coroutines.CoroutineScope
|
||
import kotlinx.coroutines.Dispatchers
|
||
import kotlinx.coroutines.MainScope
|
||
import kotlinx.coroutines.NonCancellable
|
||
import kotlinx.coroutines.channels.Channel
|
||
import kotlinx.coroutines.isActive
|
||
import kotlinx.coroutines.launch
|
||
import kotlinx.coroutines.selects.select
|
||
import kotlinx.coroutines.withContext
|
||
import melody.offline.music.App
|
||
import melody.offline.music.R
|
||
import melody.offline.music.adapter.NewPlayListAdapter
|
||
import melody.offline.music.ads.AdPlacement
|
||
import melody.offline.music.ads.LolAdWrapper
|
||
import melody.offline.music.bean.FavoriteBean
|
||
import melody.offline.music.bean.OfflineBean
|
||
import melody.offline.music.bean.Playlist
|
||
import melody.offline.music.bean.PlaylistItem
|
||
import melody.offline.music.http.getAppVersionCode
|
||
import melody.offline.music.http.getCountryCode
|
||
import melody.offline.music.innertube.Innertube
|
||
import melody.offline.music.media.MediaControllerManager
|
||
import melody.offline.music.service.MyDownloadService
|
||
import melody.offline.music.service.ViewModelMain
|
||
import melody.offline.music.sp.AppStore
|
||
import melody.offline.music.util.AnalysisUtil
|
||
import melody.offline.music.util.DownloadUtil
|
||
import melody.offline.music.util.FileSizeConverter
|
||
import melody.offline.music.util.LogTag
|
||
import melody.offline.music.view.MusicPlayerView
|
||
import melody.offline.music.view.RatingDialog
|
||
import org.json.JSONObject
|
||
import java.util.concurrent.TimeUnit
|
||
|
||
|
||
@OptIn(UnstableApi::class)
|
||
abstract class MoBaseActivity : AppCompatActivity(), MusicPlayerView.PlaySkipForwardListener,
|
||
CoroutineScope by MainScope(), LifecycleOwner {
|
||
private var playerListener: Player.Listener? = null
|
||
|
||
enum class Event {
|
||
ActivityStart, ActivityStop, ActivityOnResume, AutomaticallySwitchSongs,
|
||
}
|
||
|
||
protected val TAG = LogTag.VO_ACT_LOG
|
||
protected val appStore by lazy { AppStore(this) }
|
||
protected val events = Channel<Event>(Channel.UNLIMITED)
|
||
protected val meController = MediaControllerManager.getController()
|
||
protected abstract suspend fun main()
|
||
private var defer: suspend () -> Unit = {}
|
||
private var deferRunning = false
|
||
private lateinit var musicPlayerView: MusicPlayerView
|
||
|
||
fun defer(operation: suspend () -> Unit) {
|
||
this.defer = operation
|
||
}
|
||
|
||
|
||
override fun onCreate(savedInstanceState: Bundle?) {
|
||
super.onCreate(savedInstanceState)
|
||
musicPlayerView = MusicPlayerView(this, meController, this)
|
||
initPlayerListener()
|
||
launch {
|
||
main()
|
||
}
|
||
}
|
||
|
||
override fun onResume() {
|
||
super.onResume()
|
||
events.trySend(Event.ActivityOnResume)
|
||
}
|
||
|
||
override fun onStart() {
|
||
super.onStart()
|
||
events.trySend(Event.ActivityStart)
|
||
}
|
||
|
||
override fun onStop() {
|
||
super.onStop()
|
||
events.trySend(Event.ActivityStop)
|
||
}
|
||
|
||
override fun finish() {
|
||
if (deferRunning) {
|
||
return
|
||
}
|
||
|
||
deferRunning = true
|
||
|
||
launch {
|
||
try {
|
||
defer()
|
||
} finally {
|
||
withContext(NonCancellable) {
|
||
super.finish()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fun addMusicPlayerViewToLayout(layoutId: LinearLayout) {
|
||
if (meController != null && meController.mediaItemCount > 0 && meController.duration > 0) {
|
||
if (layoutId.childCount <= 0) {//没有添加view才进行添加
|
||
layoutId.addView(musicPlayerView)
|
||
}
|
||
musicPlayerView.updateInfoUi(meController.currentMediaItem)
|
||
musicPlayerView.updateSetProgress(meController)
|
||
musicPlayerView.updateProgressState(meController)
|
||
layoutId.visibility = View.VISIBLE
|
||
} else {
|
||
layoutId.visibility = View.GONE
|
||
}
|
||
}
|
||
|
||
private fun initPlayerListener() {
|
||
if (this !is MoPlayDetailsActivity && this !is LaunchActivity) {
|
||
if (playerListener == null) {
|
||
LogTag.LogD(TAG, "MoBaseActivity initPlayerListener")
|
||
meController?.addListener(getPlayerListener())
|
||
}
|
||
}
|
||
}
|
||
|
||
override fun onDestroy() {
|
||
super.onDestroy()
|
||
if (meController != null && playerListener != null) {
|
||
meController.removeListener(playerListener!!)
|
||
}
|
||
}
|
||
|
||
private fun getPlayerListener(): Player.Listener {
|
||
if (playerListener == null) {
|
||
playerListener = object : Player.Listener {
|
||
|
||
override fun onPositionDiscontinuity(
|
||
oldPosition: Player.PositionInfo, newPosition: Player.PositionInfo, reason: Int
|
||
) {
|
||
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
|
||
if (meController != null) {
|
||
musicPlayerView.updateInfoUi(meController.currentMediaItem)
|
||
musicPlayerView.updateSetProgress(meController)
|
||
musicPlayerView.updateProgressState(meController)
|
||
}
|
||
|
||
events.trySend(Event.AutomaticallySwitchSongs)
|
||
}
|
||
}
|
||
|
||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||
val meController = MediaControllerManager.getController()
|
||
if (meController != null) {
|
||
musicPlayerView.updateProgressState(meController)
|
||
|
||
when (playbackState) {
|
||
Player.STATE_IDLE -> {
|
||
LogTag.LogD(TAG, "base STATE_IDLE")
|
||
}
|
||
|
||
Player.STATE_BUFFERING -> {
|
||
LogTag.LogD(TAG, "base STATE_BUFFERING")
|
||
}
|
||
|
||
Player.STATE_READY -> {
|
||
LogTag.LogD(TAG, "base STATE_READY")
|
||
musicPlayerView.updateSetProgress(meController)
|
||
}
|
||
|
||
else -> {}
|
||
}
|
||
}
|
||
}
|
||
|
||
override fun onPlayWhenReadyChanged(
|
||
playWhenReady: Boolean, reason: Int
|
||
) {
|
||
LogTag.LogD(TAG, "base onPlayWhenReadyChanged $playWhenReady")
|
||
musicPlayerView.updatePlayState(playWhenReady)
|
||
val meController = MediaControllerManager.getController()
|
||
if (meController != null) {
|
||
musicPlayerView.updateProgressState(meController)
|
||
}
|
||
|
||
}
|
||
}
|
||
}
|
||
return playerListener!!
|
||
}
|
||
|
||
fun applyGaussianBlur(inputBitmap: Bitmap, radius: Float, context: Context): Bitmap {
|
||
val rsContext = RenderScript.create(context)
|
||
val outputBitmap =
|
||
Bitmap.createBitmap(inputBitmap.width, inputBitmap.height, inputBitmap.config)
|
||
val blurScript = ScriptIntrinsicBlur.create(rsContext, Element.U8_4(rsContext))
|
||
val tmpIn = Allocation.createFromBitmap(rsContext, inputBitmap)
|
||
val tmpOut = Allocation.createFromBitmap(rsContext, outputBitmap)
|
||
blurScript.setRadius(radius)
|
||
blurScript.setInput(tmpIn)
|
||
blurScript.forEach(tmpOut)
|
||
tmpOut.copyTo(outputBitmap)
|
||
rsContext.finish()
|
||
return outputBitmap
|
||
}
|
||
|
||
fun showSongDescriptionDialog(description: String) {
|
||
val inflater = LayoutInflater.from(this)
|
||
val dialogView = inflater.inflate(R.layout.dialog_description, null)
|
||
val title = dialogView.findViewById<TextView>(R.id.dialog_title)
|
||
title.text = getString(R.string.description)
|
||
val content = dialogView.findViewById<TextView>(R.id.dialog_content)
|
||
content.text = description
|
||
val close = dialogView.findViewById<RelativeLayout>(R.id.closeBtn)
|
||
val dialogBuilder = AlertDialog.Builder(this).setView(dialogView)
|
||
val dialog = dialogBuilder.create()
|
||
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||
dialog.show()
|
||
close.setOnClickListener {
|
||
dialog.dismiss()
|
||
}
|
||
}
|
||
|
||
fun extractTextBeforeNewline(text: String): String {
|
||
// 用换行符分割文本,取第一个部分
|
||
return text.split("\n\n")[0]
|
||
}
|
||
|
||
|
||
suspend fun insertOfflineData(mediaItem: MediaItem) {
|
||
val currentDownload = DownloadUtil.getCurrentIdDownload(mediaItem.mediaId)
|
||
val favoriteBean = App.appFavoriteDBManager.getFavoriteBeanByID(mediaItem.mediaId)
|
||
val bean = OfflineBean(
|
||
videoId = mediaItem.mediaId,
|
||
title = mediaItem.mediaMetadata.title.toString(),
|
||
name = mediaItem.mediaMetadata.artist.toString(),
|
||
thumbnail = mediaItem.mediaMetadata.artworkUri.toString(),
|
||
isOffline = true,
|
||
isFavorite = favoriteBean?.isFavorite ?: false,
|
||
bytesDownloaded = currentDownload?.bytesDownloaded,
|
||
size = FileSizeConverter.formatSize(currentDownload?.bytesDownloaded ?: 0)
|
||
)
|
||
LogTag.LogD(TAG, "insertOfflineBean bean->${bean}")
|
||
App.appOfflineDBManager.insertOfflineBean(bean)
|
||
}
|
||
|
||
suspend fun insertOfflineData(favoriteBean: FavoriteBean) {
|
||
val currentDownload = DownloadUtil.getCurrentIdDownload(favoriteBean.videoId)
|
||
if (currentDownload != null) {
|
||
val bytesDownloaded = currentDownload.bytesDownloaded
|
||
val size = FileSizeConverter(currentDownload.bytesDownloaded).formattedSize()
|
||
val bean = OfflineBean(
|
||
videoId = favoriteBean.videoId,
|
||
title = favoriteBean.title,
|
||
name = favoriteBean.name,
|
||
thumbnail = favoriteBean.thumbnail,
|
||
isOffline = true,
|
||
isFavorite = favoriteBean.isFavorite,
|
||
bytesDownloaded = bytesDownloaded,
|
||
size = size
|
||
)
|
||
App.appOfflineDBManager.insertOfflineBean(bean)
|
||
} else {
|
||
val bean = OfflineBean(
|
||
videoId = favoriteBean.videoId,
|
||
title = favoriteBean.title,
|
||
name = favoriteBean.name,
|
||
thumbnail = favoriteBean.thumbnail,
|
||
isOffline = true,
|
||
isFavorite = favoriteBean.isFavorite,
|
||
)
|
||
App.appOfflineDBManager.insertOfflineBean(bean)
|
||
}
|
||
}
|
||
|
||
suspend fun insertFavoriteData(mediaItem: MediaItem) {
|
||
val bean = FavoriteBean(
|
||
videoId = mediaItem.mediaId,
|
||
title = mediaItem.mediaMetadata.title.toString(),
|
||
name = mediaItem.mediaMetadata.artist.toString(),
|
||
thumbnail = mediaItem.mediaMetadata.artworkUri.toString(),
|
||
isFavorite = true
|
||
)
|
||
LogTag.LogD(TAG, "insertFavoriteBean bean->${bean}")
|
||
App.appFavoriteDBManager.insertFavoriteBean(bean)
|
||
}
|
||
|
||
suspend fun insertFavoriteData(offlineBean: OfflineBean) {
|
||
val bean = FavoriteBean(
|
||
videoId = offlineBean.videoId,
|
||
title = offlineBean.title,
|
||
name = offlineBean.name,
|
||
thumbnail = offlineBean.thumbnail,
|
||
isFavorite = true
|
||
)
|
||
LogTag.LogD(TAG, "insertFavoriteBean bean->${bean}")
|
||
App.appFavoriteDBManager.insertFavoriteBean(bean)
|
||
}
|
||
|
||
fun withPermission(): Boolean {
|
||
//先判断当前配置的开关是否为true,为false的话就直接进入A
|
||
val jsonString = appStore.shouldEnterMusicPageJson//得到配置的json
|
||
LogTag.LogD(TAG, "withPermission jsonString->${jsonString}")
|
||
val json = JSONObject(jsonString)
|
||
val versionCode = json.optInt("versionCode")//得到配置的code
|
||
val enter = json.optBoolean("enter")//得到配置的开关
|
||
val currentVersionCode = getAppVersionCode(this)//得到当前应用的code
|
||
//如果配置的code 等于 当前app的code,则把配置赋值给shouldEnterMusicPage
|
||
if (versionCode.toLong() == currentVersionCode) {
|
||
appStore.shouldEnterMusicPage = enter
|
||
} else {
|
||
appStore.shouldEnterMusicPage = true
|
||
}
|
||
LogTag.LogD(TAG, "withPermission shouldEnterMusicPage->${appStore.shouldEnterMusicPage}")
|
||
if (!appStore.shouldEnterMusicPage) {
|
||
return false
|
||
}
|
||
// 不允许的国家代码
|
||
val restrictedCountries = setOf(
|
||
// "CN",
|
||
"HK", "TW", "JP", "KR", "GB", "CH", "BE", "MO", "SG"
|
||
)
|
||
// 检查是否包含当前的国家代码
|
||
LogTag.LogD(TAG, "withPermission ipCountryCode->${appStore.ipCountryCode}")
|
||
if (appStore.ipCountryCode in restrictedCountries) {
|
||
return false
|
||
}
|
||
// 如果不在受限国家代码中,则继续其他检查
|
||
return withIso()
|
||
}
|
||
|
||
private fun withIso(): Boolean {
|
||
//460 || 461 China (People's Republic of)
|
||
//454 "Hong Kong, China"
|
||
//466 "Taiwan, China"
|
||
//440 || 441 Japan
|
||
//450 Korea (Republic of)
|
||
//234 || 235 United Kingdom of Great Britain and Northern Ireland
|
||
//228 Switzerland (Confederation of)
|
||
//206 Belgium
|
||
//455 "Macao, China"
|
||
//525 Singapore (Republic of)
|
||
val restrictedCountryCodes = setOf(
|
||
// "460",
|
||
// "461",
|
||
"454", "466", "440", "441", "450", "234", "235", "228", "206", "455", "525"
|
||
)
|
||
val currentCountryCode = getCountryCode(this)
|
||
LogTag.LogD(TAG, "withPermission currentCountryCode->${currentCountryCode}")
|
||
return currentCountryCode !in restrictedCountryCodes
|
||
}
|
||
|
||
suspend fun showAddPlaylistBottomDialog(favoriteBean: FavoriteBean) {
|
||
val bottomAddPlaylistSheetDialog = BottomSheetDialog(this)
|
||
val view = layoutInflater.inflate(R.layout.add_playlist_layout, null)
|
||
bottomAddPlaylistSheetDialog.setContentView(view)
|
||
val newPlayListBtn = view.findViewById<LinearLayout>(R.id.newPlayListBtn)
|
||
val rv = view.findViewById<RecyclerView>(R.id.newPlayListRv)
|
||
newPlayListBtn.setOnClickListener {
|
||
bottomAddPlaylistSheetDialog.dismiss()
|
||
showNewPlaylistBottomDialog(favoriteBean)
|
||
}
|
||
// 设置对话框背景为透明以显示圆角
|
||
bottomAddPlaylistSheetDialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
|
||
bottomAddPlaylistSheetDialog.window?.navigationBarColor =
|
||
ContextCompat.getColor(this, R.color.main_bg_color)
|
||
bottomAddPlaylistSheetDialog.show()
|
||
|
||
val playlist = (App.appPlaylistDBManager.getAllPlaylists())
|
||
val adapter = NewPlayListAdapter(this, playlist)
|
||
adapter.setOnItemClickListener(object : NewPlayListAdapter.OnItemClickListener {
|
||
override fun onItemClick(position: Int) {
|
||
launch {
|
||
val playlistItem =
|
||
App.appPlaylistDBManager.getPlaylistItems(playlist[position].id)
|
||
val isAny = playlistItem.any { it.title == favoriteBean.title }
|
||
if (isAny) {//如何这首歌曲已经存在歌单则不添加
|
||
withContext(Dispatchers.Main) {
|
||
Toast.makeText(
|
||
this@MoBaseActivity,
|
||
getString(R.string.song_exists_playlist_hint),
|
||
Toast.LENGTH_LONG
|
||
).show()
|
||
}
|
||
} else {
|
||
val isOffline =
|
||
App.appOfflineDBManager.getOfflineBeanByID(favoriteBean.videoId) != null
|
||
val isFavorite =
|
||
App.appFavoriteDBManager.getFavoriteBeanByID(favoriteBean.videoId) != null
|
||
App.appPlaylistDBManager.insertOrUpdatePlaylistItem(
|
||
PlaylistItem(
|
||
playlistId = playlist[position].id,
|
||
videoId = favoriteBean.videoId,
|
||
title = favoriteBean.title,
|
||
name = favoriteBean.name,
|
||
thumbnail = favoriteBean.thumbnail,
|
||
isOffline = isOffline,
|
||
isFavorite = isFavorite
|
||
)
|
||
)
|
||
withContext(Dispatchers.Main) {
|
||
bottomAddPlaylistSheetDialog.dismiss()
|
||
Toast.makeText(
|
||
this@MoBaseActivity,
|
||
getString(R.string.added_playlist_success_Hint),
|
||
Toast.LENGTH_LONG
|
||
).show()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
})
|
||
rv.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
|
||
rv.adapter = adapter
|
||
}
|
||
|
||
private var bottomSheetDialog: BottomSheetDialog? = null
|
||
private fun showNewPlaylistBottomDialog(favoriteBean: FavoriteBean) {
|
||
bottomSheetDialog = BottomSheetDialog(this)
|
||
val view = layoutInflater.inflate(R.layout.new_playlist_layout, null)
|
||
bottomSheetDialog?.setContentView(view)
|
||
val edit = view.findViewById<EditText>(R.id.playlistEt)
|
||
val confirmBtn = view.findViewById<TextView>(R.id.confirmBtn)
|
||
confirmBtn.setOnClickListener {
|
||
val text = edit.text.toString().trim()
|
||
if (text.isNotEmpty()) {
|
||
launch {
|
||
val playlist = App.appPlaylistDBManager.getPlaylistByTitle(text)
|
||
if (playlist != null) {
|
||
withContext(Dispatchers.Main) {
|
||
Toast.makeText(
|
||
this@MoBaseActivity,
|
||
getString(R.string.new_playlist_duplicate_name_hint),
|
||
Toast.LENGTH_LONG
|
||
).show()
|
||
}
|
||
} else {
|
||
val newPlaylist = Playlist(title = text)
|
||
App.appPlaylistDBManager.insertOrUpdatePlaylist(newPlaylist)
|
||
withContext(Dispatchers.Main) {
|
||
if (bottomSheetDialog != null) {
|
||
bottomSheetDialog?.dismiss()
|
||
}
|
||
Toast.makeText(
|
||
this@MoBaseActivity,
|
||
getString(R.string.created_successfully),
|
||
Toast.LENGTH_LONG
|
||
).show()
|
||
}
|
||
val currentPlaylist = App.appPlaylistDBManager.getPlaylistByTitle(text)
|
||
if (currentPlaylist != null) {
|
||
val isOffline =
|
||
App.appOfflineDBManager.getOfflineBeanByID(favoriteBean.videoId) != null//返回非null则为true
|
||
val isFavorite =
|
||
App.appFavoriteDBManager.getFavoriteBeanByID(favoriteBean.videoId) != null
|
||
val playlistItem = PlaylistItem(
|
||
playlistId = currentPlaylist.id,
|
||
videoId = favoriteBean.videoId,
|
||
title = favoriteBean.title,
|
||
name = favoriteBean.name,
|
||
thumbnail = favoriteBean.thumbnail,
|
||
isOffline = isOffline,
|
||
isFavorite = isFavorite
|
||
)
|
||
App.appPlaylistDBManager.insertOrUpdatePlaylistItem(playlistItem)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// 设置对话框背景为透明以显示圆角
|
||
bottomSheetDialog?.window?.setBackgroundDrawableResource(android.R.color.transparent)
|
||
bottomSheetDialog?.window?.navigationBarColor =
|
||
ContextCompat.getColor(this, R.color.main_bg_color)
|
||
bottomSheetDialog?.show()
|
||
}
|
||
|
||
fun showRatingDialog() {
|
||
val intervalTime = 1000 * 60 * 5
|
||
val dialog = RatingDialog(this)
|
||
val installTime = appStore.showRateDialogTime
|
||
val currentTime = System.currentTimeMillis()
|
||
// 检测是否超过间隔时间
|
||
if (currentTime - installTime > intervalTime) {
|
||
dialog.show()
|
||
//更新为当前时间
|
||
appStore.showRateDialogTime = System.currentTimeMillis()
|
||
}
|
||
}
|
||
} |