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(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(R.id.dialog_title) title.text = getString(R.string.description) val content = dialogView.findViewById(R.id.dialog_content) content.text = description val close = dialogView.findViewById(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(R.id.newPlayListBtn) val rv = view.findViewById(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(R.id.playlistEt) val confirmBtn = view.findViewById(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() } } }