This commit is contained in:
ocean 2024-05-07 16:06:37 +08:00
parent 830ad770eb
commit 2a0d2abfea
35 changed files with 1203 additions and 64 deletions

View File

@ -29,6 +29,13 @@ android {
"proguard-rules.pro"
)
}
// debug {
// isMinifyEnabled = true
// proguardFiles(
// getDefaultProguardFile("proguard-android-optimize.txt"),
// "proguard-rules.pro"
// )
// }
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8

View File

@ -18,4 +18,6 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#-renamesourcefileattribute SourceFile
-dontwarn org.slf4j.impl.StaticLoggerBinder

View File

@ -7,6 +7,9 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
@ -48,7 +51,10 @@
android:name=".activity.AboutActivity"
android:screenOrientation="portrait" />
<activity
android:name=".activity.DetailsActivity"
android:name=".activity.MoListDetailsActivity"
android:screenOrientation="portrait" />
<activity
android:name=".activity.MoPlayDetailsActivity"
android:screenOrientation="portrait" />
<service

View File

@ -9,7 +9,7 @@ import com.player.musicoo.databinding.ActivityDetailsBinding
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.requests.moPlaylistPage
class DetailsActivity : MoBaseActivity() {
class MoListDetailsActivity : MoBaseActivity() {
companion object {
const val PLAY_LIST_PAGE_BROWSE_ID = "play_list_page_browse_id"

View File

@ -0,0 +1,262 @@
package com.player.musicoo.activity
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.Log
import android.view.View
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import androidx.annotation.OptIn
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import com.bumptech.glide.Glide
import com.gyf.immersionbar.ktx.immersionBar
import com.player.musicoo.R
import com.player.musicoo.databinding.ActivityMoPlayDetailsBinding
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.bodies.PlayerBody
import com.player.musicoo.innertube.requests.moNextPage
import com.player.musicoo.innertube.requests.player
import com.player.musicoo.media.MediaControllerManager
import com.player.musicoo.media.SongRadio
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.util.asMediaItem
import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@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"
}
private lateinit var binding: ActivityMoPlayDetailsBinding
private var currentVideoID = ""
private fun initImmersionBar() {
immersionBar {
statusBarDarkFont(false)
statusBarView(binding.view)
}
}
override suspend fun main() {
binding = ActivityMoPlayDetailsBinding.inflate(layoutInflater)
setContentView(binding.root)
initImmersionBar()
initClick()
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)
binding.nameTv.text = intent.getStringExtra(PLAY_DETAILS_NAME)
binding.descTv.text = intent.getStringExtra(PLAY_DETAILS_DESC)
if (videoId.isNullOrEmpty()) {
finish()
return
}
currentVideoID = videoId
initData(
videoId,
playlistId,
playlistSetVideoId,
params
)
}
private fun initClick() {
binding.backBtn.setOnClickListener {
finish()
}
binding.playImg.setOnClickListener {
Log.d(TAG, "点击了播放按钮")
}
binding.sbProgress.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
}
})
}
private fun initData(
videoId: String,
playlistId: String? = null,
playlistSetVideoId: String? = null,
parameters: String? = null
) {
SongRadio(
videoId,
playlistId,
playlistSetVideoId,
parameters
).let {
launch(Dispatchers.Main) {
val ss = it.process()
if (ss.isEmpty()) {
binding.loadingView.visibility = View.GONE
binding.playbackErrorLayout.visibility = View.VISIBLE
binding.disableClicksLayout.visibility = View.VISIBLE
}
if (isFinishing) {
return@launch
}
var mediaItem: MediaItem? = null
Log.d(TAG, "size = ${ss.size}")
for (song: Innertube.SongItem in ss) {
if (song.key == currentVideoID) {
mediaItem = song.asMediaItem
break
}
}
if (mediaItem != null) {
binding.loadingView.visibility = View.GONE
Glide.with(this@MoPlayDetailsActivity)
.load(mediaItem.mediaMetadata.artworkUri)
.into(binding.thumbnail)
binding.nameTv.text = mediaItem.mediaMetadata.title
binding.descTv.text = mediaItem.mediaMetadata.artist
val urlResult = runBlocking(Dispatchers.IO) {
Innertube.player(PlayerBody(videoId = videoId))
}?.mapCatching { body ->
if (body.videoDetails?.videoId != videoId) {
throw VideoIdMismatchException()
}
when (val status = body.playabilityStatus?.status) {
"OK" -> body.streamingData?.highestQualityFormat?.let { format ->
format.url
} ?: throw PlayableFormatNotFoundException()
"UNPLAYABLE" -> throw UnplayableException()
"LOGIN_REQUIRED" -> throw LoginRequiredException()
else -> throw PlaybackException(
status,
null,
PlaybackException.ERROR_CODE_REMOTE_ERROR
)
}
}
val url = urlResult?.getOrNull()
if (url != null) {
binding.playbackErrorLayout.visibility = View.GONE
binding.disableClicksLayout.visibility = View.GONE
binding.totalDurationTv.visibility = View.GONE
val newMediaItem =
MediaItem.Builder()
.setUri(url)
.setMediaMetadata(mediaItem.mediaMetadata)
.build()
MediaControllerManager.getController()?.let {
it.addListener(object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
Log.d(TAG, "onPlayerError = $error")
}
override fun onPlayWhenReadyChanged(
playWhenReady: Boolean,
reason: Int
) {
Log.d(TAG, "onPlayWhenReadyChanged = $playWhenReady")
updateProgressState()
}
override fun onPlaybackStateChanged(playbackState: Int) {
Log.d(TAG, "onPlaybackStateChanged = $playbackState")
if (playbackState == Player.STATE_READY) {
binding.totalDurationTv.visibility = View.VISIBLE
binding.totalDurationTv.text =
convertMillisToMinutesAndSecondsString(
MediaControllerManager.getDuration()
)
binding.sbProgress.max =
MediaControllerManager.getDuration().toInt()
}
updateProgressState()
}
})
it.setMediaItem(newMediaItem)
it.repeatMode = Player.REPEAT_MODE_ALL
it.prepare()
it.play()
}
} else {
binding.playbackErrorLayout.visibility = View.VISIBLE
binding.disableClicksLayout.visibility = View.VISIBLE
}
}
}
}
}
private fun updateProgressState() {
val currentPlayer = MediaControllerManager.getController()
if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY && currentPlayer.isPlaying) {
// updatePlayState(currentPlayer.isPlaying)
progressHandler.removeCallbacksAndMessages(null)
progressHandler.sendEmptyMessage(1)
} else {
progressHandler.removeCallbacksAndMessages(null)
}
}
private val progressHandler = object : Handler(Looper.myLooper()!!) {
override fun handleMessage(msg: Message) {
val currentPlayer = MediaControllerManager.getController()
if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY && currentPlayer.isPlaying) {
val currentPosition = currentPlayer.currentPosition
val currentString = convertMillisToMinutesAndSecondsString(currentPosition)
binding.progressDurationTv.text = currentString
// 更新 SeekBar 的进度
binding.sbProgress.progress = currentPosition.toInt()
sendEmptyMessageDelayed(1, 1000)
}
}
}
private fun updatePlayState(b: Boolean) {
if (b) {
binding.playImg.setImageResource(R.drawable.playing_green_icon)
} else {
binding.playImg.setImageResource(R.drawable.play_green_icon)
}
}
}

View File

@ -3,20 +3,13 @@ package com.player.musicoo.adapter
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.player.musicoo.App
import com.player.musicoo.R
import com.player.musicoo.activity.PlayDetailsActivity
import com.player.musicoo.bean.Audio
import com.player.musicoo.activity.MoPlayDetailsActivity
import com.player.musicoo.databinding.MusicResponsiveItemBinding
import com.player.musicoo.databinding.SoundsOfAppliancesLayoutBinding
import com.player.musicoo.databinding.SoundsOfNatureLayoutBinding
import com.player.musicoo.innertube.models.MusicCarouselShelfRenderer
import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
import com.player.musicoo.util.getAudioDurationFromAssets
import com.player.musicoo.innertube.models.bodies.NextBody
class ResponsiveListAdapter(
private val context: Context,
@ -32,7 +25,61 @@ class ResponsiveListAdapter(
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val bean = list[position]
holder.bind(bean)
val url = bean.musicResponsiveListItemRenderer
?.thumbnail
?.musicThumbnailRenderer
?.thumbnail
?.thumbnails
?.let { it.getOrNull(1) ?: it.getOrNull(0) }
?.url
val name = bean.musicResponsiveListItemRenderer
?.flexColumns?.get(0)
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?.firstOrNull()
?.text
val desc = bean.musicResponsiveListItemRenderer
?.flexColumns?.get(1)
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?.firstOrNull()
?.text
val watchEndpoint = bean.musicResponsiveListItemRenderer
?.flexColumns
?.firstOrNull()
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?.firstOrNull()
?.navigationEndpoint
?.watchEndpoint
val videoId = watchEndpoint?.videoId
val playlistId = watchEndpoint?.playlistId
val playlistSetVideoId = watchEndpoint?.playlistSetVideoId
val params = watchEndpoint?.params
holder.bind(url, name, desc)
holder.itemView.setOnClickListener {
val intent = Intent(context, MoPlayDetailsActivity::class.java)
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_VIDEO_ID, videoId)
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_PLAY_LIST_ID, playlistId)
if (playlistSetVideoId != null) {
intent.putExtra(
MoPlayDetailsActivity.PLAY_DETAILS_PLAY_LIST_SET_VIDEO_ID,
playlistSetVideoId
)
}
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_PLAY_PARAMS, params)
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_NAME, name)
intent.putExtra(MoPlayDetailsActivity.PLAY_DETAILS_DESC, desc)
context.startActivity(intent)
}
}
override fun getItemCount(): Int = list.size
@ -40,28 +87,8 @@ class ResponsiveListAdapter(
inner class ViewHolder(private val binding: MusicResponsiveItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(content: MusicCarouselShelfRenderer.Content) {
val url = content.musicResponsiveListItemRenderer
?.thumbnail
?.musicThumbnailRenderer
?.thumbnail
?.thumbnails
?.let { it.getOrNull(1) ?: it.getOrNull(0) }
?.url
val name = content.musicResponsiveListItemRenderer
?.flexColumns?.get(0)
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?.firstOrNull()
?.text
val desc = content.musicResponsiveListItemRenderer
?.flexColumns?.get(1)
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?.firstOrNull()
?.text
fun bind(url: String?, name: String?, desc: String?) {
binding.apply {
Glide.with(context)
.load(url)

View File

@ -2,25 +2,13 @@ package com.player.musicoo.adapter
import android.content.Context
import android.content.Intent
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.player.musicoo.App
import com.player.musicoo.R
import com.player.musicoo.activity.DetailsActivity
import com.player.musicoo.activity.PlayDetailsActivity
import com.player.musicoo.bean.Audio
import com.player.musicoo.databinding.MusicResponsiveItemBinding
import com.player.musicoo.activity.MoListDetailsActivity
import com.player.musicoo.databinding.MusicTowRowItemBinding
import com.player.musicoo.databinding.SoundsOfAppliancesLayoutBinding
import com.player.musicoo.databinding.SoundsOfNatureLayoutBinding
import com.player.musicoo.innertube.models.MusicCarouselShelfRenderer
import com.player.musicoo.util.LogTag
import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
import com.player.musicoo.util.getAudioDurationFromAssets
class TowRowListAdapter(
private val context: Context,
@ -49,8 +37,8 @@ class TowRowListAdapter(
holder.bind(bean)
holder.itemView.setOnClickListener {
val intent = Intent(context, DetailsActivity::class.java)
intent.putExtra(DetailsActivity.PLAY_LIST_PAGE_BROWSE_ID, browseId)
val intent = Intent(context, MoListDetailsActivity::class.java)
intent.putExtra(MoListDetailsActivity.PLAY_LIST_PAGE_BROWSE_ID, browseId)
context.startActivity(intent)
}
}

View File

@ -99,6 +99,7 @@ object Innertube {
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
val album: Info<NavigationEndpoint.Endpoint.Browse>?,
val durationText: String?,
val bigThumbnail: Thumbnail?,
override val thumbnail: Thumbnail?
) : Item() {
override val key get() = info!!.endpoint!!.videoId!!

View File

@ -1,6 +1,5 @@
package com.player.musicoo.innertube.requests
import android.util.Log
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.BrowseResponse
import com.player.musicoo.innertube.models.Context

View File

@ -0,0 +1,58 @@
package com.player.musicoo.innertube.requests
import io.ktor.client.call.body
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.ContinuationResponse
import com.player.musicoo.innertube.models.NextResponse
import com.player.musicoo.innertube.models.bodies.ContinuationBody
import com.player.musicoo.innertube.models.bodies.NextBody
import com.player.musicoo.innertube.utils.from
import com.player.musicoo.innertube.utils.runCatchingNonCancellable
suspend fun Innertube.moNextPage(
videoId: String,
playlistId: String? = null,
params: String? = null,
playlistSetVideoId: String? = null
) =
runCatchingNonCancellable {
val response = client.post(next) {
setBody(
NextBody(
videoId = videoId,
playlistId = playlistId,
playlistSetVideoId = playlistSetVideoId,
params = params
)
)
}.body<NextResponse>()
val tabs = response
.contents
?.singleColumnMusicWatchNextResultsRenderer
?.tabbedRenderer
?.watchNextTabbedResultsRenderer
?.tabs
val playlistPanelRenderer = tabs
?.getOrNull(0)
?.tabRenderer
?.content
?.musicQueueRenderer
?.content
?.playlistPanelRenderer
val endpoint = playlistPanelRenderer
?.contents
?.lastOrNull()
?.automixPreviewVideoRenderer
?.content
?.automixPlaylistVideoRenderer
?.navigationEndpoint
?.watchPlaylistEndpoint
}

View File

@ -39,6 +39,16 @@ fun Innertube.SongItem.Companion.from(renderer: MusicResponsiveListItemRenderer)
?.runs
?.firstOrNull()
?.let(Innertube::Info),
bigThumbnail = renderer
.thumbnail
?.musicThumbnailRenderer
?.thumbnail
?.thumbnails
?.let {
it.getOrNull(5) ?: it.getOrNull(4)
?: it.getOrNull(3) ?: it.getOrNull(2)
?: it.getOrNull(1) ?: it.getOrNull(0)
},
thumbnail = renderer
.thumbnail
?.musicThumbnailRenderer

View File

@ -35,6 +35,8 @@ fun Innertube.SongItem.Companion.from(content: MusicShelfRenderer.Content): Inne
durationText = otherRuns
.lastOrNull()
?.firstOrNull()?.text,
bigThumbnail = content
.thumbnail,
thumbnail = content
.thumbnail
).takeIf { it.info?.endpoint?.videoId != null }

View File

@ -24,6 +24,14 @@ fun Innertube.SongItem.Companion.from(renderer: PlaylistPanelVideoRenderer): Inn
?.getOrNull(1)
?.getOrNull(0)
?.let(Innertube::Info),
bigThumbnail = renderer
.thumbnail
?.thumbnails
?.let {
it.getOrNull(5) ?: it.getOrNull(4)
?: it.getOrNull(3) ?: it.getOrNull(2)
?: it.getOrNull(1) ?: it.getOrNull(0)
},
thumbnail = renderer
.thumbnail
?.thumbnails

View File

@ -3,7 +3,6 @@ package com.player.musicoo.media
import android.content.ComponentName
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
@ -44,6 +43,28 @@ object MediaControllerManager {
}
}
fun setupMedia(id: String, listener: Player.Listener) {
val mediaItem =
MediaItem.Builder()
.setUri(id)
.setMediaMetadata(
MediaMetadata.Builder()
.setArtist("测试")
.setTitle("测试")
.build()
)
.build()
if (isConnected()) {
mediaController?.let {
it.addListener(listener)
it.setMediaItem(mediaItem)
it.repeatMode = Player.REPEAT_MODE_ALL
it.prepare()
it.play()
}
}
}
fun setupMedia(context: Context, audio: Audio, listener: Player.Listener) {
if (currentAudioFile != audio.file) {
currentAudioFile = audio.file
@ -77,7 +98,7 @@ object MediaControllerManager {
mediaController?.let {
it.addListener(listener)
it.setMediaItem(mediaItem)
it.repeatMode = Player.REPEAT_MODE_ONE
it.repeatMode = Player.REPEAT_MODE_ALL
it.prepare()
it.play()
val currentPlayingAudio =
@ -136,7 +157,7 @@ object MediaControllerManager {
mediaController?.let {
it.addListener(listener)
it.setMediaItem(mediaItem)
it.repeatMode = Player.REPEAT_MODE_ONE
it.repeatMode = Player.REPEAT_MODE_ALL
it.prepare()
it.play()
val currentPlayingAudio =
@ -174,4 +195,29 @@ object MediaControllerManager {
}
return false
}
fun play() {
mediaController?.play()
}
fun getMediaItemCount(): Int {
mediaController?.let {
return it.mediaItemCount
}
return 0
}
fun getCurrentMediaItem(): MediaItem? {
mediaController?.let {
return it.currentMediaItem
}
return null
}
fun getDuration(): Long {
mediaController?.let {
return it.duration
}
return 0
}
}

View File

@ -0,0 +1,10 @@
package com.player.musicoo.media
import androidx.media3.common.MediaItem
import androidx.media3.session.MediaController
fun MediaController.forcePlay(mediaItem: MediaItem){
setMediaItem(mediaItem)
prepare()
play()
}

View File

@ -0,0 +1,50 @@
package com.player.musicoo.media
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.bodies.ContinuationBody
import com.player.musicoo.innertube.models.bodies.NextBody
import com.player.musicoo.innertube.requests.nextPage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
data class SongRadio(
private val videoId: String? = null,
private var playlistId: String? = null,
private var playlistSetVideoId: String? = null,
private var parameters: String? = null
) {
private var nextContinuation: String? = null
suspend fun process(): List<Innertube.SongItem> {
var songItems: List<Innertube.SongItem>? = null
nextContinuation = withContext(Dispatchers.IO) {
val continuation = nextContinuation
if (continuation == null) {
Innertube.nextPage(
NextBody(
videoId = videoId,
playlistId = playlistId,
params = parameters,
playlistSetVideoId = playlistSetVideoId
)
)?.map { nextResult ->
playlistId = nextResult.playlistId
parameters = nextResult.params
playlistSetVideoId = nextResult.playlistSetVideoId
nextResult.itemsPage
}
} else {
Innertube.nextPage(ContinuationBody(continuation = continuation))
}?.getOrNull()?.let { songsPage ->
songItems = songsPage.items
songsPage.continuation?.takeUnless { nextContinuation == it }
}
}
return songItems ?: emptyList()
}
}

View File

@ -0,0 +1,43 @@
package com.player.musicoo.service
import android.content.Context
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.*
import com.google.common.collect.ImmutableList
import com.player.musicoo.util.LogTag
@UnstableApi
class CustomMediaNotificationProvider(context: Context) :
DefaultMediaNotificationProvider(context) {
override fun addNotificationActions(
mediaSession: MediaSession,
mediaButtons: ImmutableList<CommandButton>,
builder: NotificationCompat.Builder,
actionFactory: MediaNotification.ActionFactory
): IntArray {
Log.d(LogTag.VO_API_LOG, "mediaButtons->$mediaButtons")
for (com: CommandButton in mediaButtons) {
Log.d(LogTag.VO_API_LOG, "displayName->${com.displayName}")
Log.d(LogTag.VO_API_LOG, "playerCommand->${com.playerCommand}")
Log.d(LogTag.VO_API_LOG, "------------------------------------------")
}
val notificationMediaButtons = ImmutableList.builder<CommandButton>().apply {
add(NotificationCustomButton.SKIP_BACK.commandButton)
add(NotificationCustomButton.SKIP_FORWARD.commandButton)
}.build()
return super.addNotificationActions(
mediaSession,
notificationMediaButtons,
builder,
actionFactory
)
}
}

View File

@ -0,0 +1,30 @@
package com.player.musicoo.service
import android.os.Bundle
import androidx.media3.session.CommandButton
import androidx.media3.session.SessionCommand
import com.player.musicoo.R
private const val CUSTOM_COMMAND_SKIP_BACK_ACTION_ID = "Skip_back"
private const val CUSTOM_COMMAND_SKIP_FORWARD_ACTION_ID = "Skip_Forward"
enum class NotificationCustomButton(val customAction: String, val commandButton: CommandButton) {
SKIP_BACK(
customAction = CUSTOM_COMMAND_SKIP_BACK_ACTION_ID,
commandButton = CommandButton.Builder()
.setDisplayName("SkipBack")
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_SKIP_BACK_ACTION_ID, Bundle()))
.setIconResId(R.drawable.play_skip_back)
.build()
),
SKIP_FORWARD(
customAction = CUSTOM_COMMAND_SKIP_FORWARD_ACTION_ID,
commandButton = CommandButton.Builder()
.setDisplayName("SkipForward")
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_SKIP_FORWARD_ACTION_ID, Bundle()))
.setIconResId(R.drawable.play_skip_forward)
.build()
),
}

View File

@ -0,0 +1,17 @@
package com.player.musicoo.service
import androidx.annotation.OptIn
import androidx.media3.common.PlaybackException
import androidx.media3.common.util.UnstableApi
@OptIn(UnstableApi::class)
class PlayableFormatNotFoundException : PlaybackException(null, null, ERROR_CODE_REMOTE_ERROR)
@OptIn(UnstableApi::class)
class UnplayableException : PlaybackException(null, null, ERROR_CODE_REMOTE_ERROR)
@OptIn(UnstableApi::class)
class LoginRequiredException : PlaybackException(null, null, ERROR_CODE_REMOTE_ERROR)
@OptIn(UnstableApi::class)
class VideoIdMismatchException : PlaybackException(null, null, ERROR_CODE_REMOTE_ERROR)

View File

@ -2,36 +2,53 @@ package com.player.musicoo.service
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.media3.common.AudioAttributes
import androidx.media3.common.Player
import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT
import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM
import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS
import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.CommandButton
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import com.player.musicoo.R
import com.player.musicoo.util.LogTag
@UnstableApi
class PlaybackService : MediaSessionService() {
private val TAG = LogTag.VO_SERVICE_LOG
private var mediaSession: MediaSession? = null
private val notificationCustomButtons =
NotificationCustomButton.entries.map { command -> command.commandButton }
// Create your player and media session in the onCreate lifecycle event
override fun onCreate() {
super.onCreate()
val player = ExoPlayer.Builder(this).build()
mediaSession = MediaSession.Builder(this, player)
val player = ExoPlayer.Builder(this, createMediaSourceFactory())
.setAudioAttributes(AudioAttributes.DEFAULT, true)
.setHandleAudioBecomingNoisy(true)
.build()
mediaSession = MediaSession.Builder(this, player)
// .setCallback(MyCallback())
// .setCustomLayout(notificationCustomButtons)
.build()
// setMediaNotificationProvider(MyMediaNotificationProvider(this))
val customMediaNotificationProvider = CustomMediaNotificationProvider(this).apply {
setSmallIcon(R.mipmap.musicoo_logo_img)
}
// setMediaNotificationProvider(customMediaNotificationProvider)
}
private fun createMediaSourceFactory(): MediaSource.Factory {
Log.d(TAG, "createMediaSourceFactory")
return DefaultMediaSourceFactory(this)
}
// The user dismissed the app from the recent tasks
@ -59,4 +76,49 @@ class PlaybackService : MediaSessionService() {
}
super.onDestroy()
}
private inner class MyCallback : MediaSession.Callback {
override fun onConnect(
session: MediaSession,
controller: MediaSession.ControllerInfo
): MediaSession.ConnectionResult {
val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
if (session.isMediaNotificationController(controller)) {
val playerCommands =
MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
.removeAll()
val connectionResult = super.onConnect(session, controller)
val defaultPlayerCommands = connectionResult.availablePlayerCommands
Log.d(TAG, defaultPlayerCommands.toString())
notificationCustomButtons.forEach { commandButton ->
commandButton.sessionCommand?.let(sessionCommands::add)
}
return MediaSession.ConnectionResult.accept(
sessionCommands.build(),
playerCommands.build()
)
} else if (session.isAutoCompanionController(controller)) {
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
.setAvailableSessionCommands(sessionCommands.build())
.build()
}
return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build()
}
override fun onCustomCommand(
session: MediaSession,
controller: MediaSession.ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
when (customCommand.customAction) {
NotificationCustomButton.SKIP_BACK.customAction -> mediaSession?.player?.seekToPrevious()
NotificationCustomButton.SKIP_FORWARD.customAction -> mediaSession?.player?.seekToNext()
}
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
}
}

View File

@ -4,4 +4,5 @@ object LogTag {
const val VO_ACT_LOG = "vo-act—log"
const val VO_FRAGMENT_LOG = "vo-fragment-log"
const val VO_API_LOG = "vo-api—log"
const val VO_SERVICE_LOG = "vo-service—log"
}

View File

@ -0,0 +1,77 @@
package com.player.musicoo.util
import android.net.Uri
import android.os.Build
import android.text.format.DateUtils
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import com.player.musicoo.innertube.Innertube
import com.player.musicoo.innertube.models.bodies.ContinuationBody
import com.player.musicoo.innertube.requests.playlistPage
import com.player.musicoo.innertube.utils.plus
val Innertube.SongItem.asMediaItem: MediaItem
get() = MediaItem.Builder()
.setMediaId(key)
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle(info?.name)
.setArtist(authors?.joinToString("") { it.name ?: "" })
.setAlbumTitle(album?.name)
.setArtworkUri(bigThumbnail?.url?.toUri())
.setExtras(
bundleOf(
"albumId" to album?.endpoint?.browseId,
"durationText" to durationText,
"artistNames" to authors?.filter { it.endpoint != null }?.mapNotNull { it.name },
"artistIds" to authors?.mapNotNull { it.endpoint?.browseId },
)
)
.build()
)
.build()
fun String?.thumbnail(size: Int): String? {
return when {
this?.startsWith("https://lh3.googleusercontent.com") == true -> "$this-w$size-h$size"
this?.startsWith("https://yt3.ggpht.com") == true -> "$this-w$size-h$size-s$size"
else -> this
}
}
fun Uri?.thumbnail(size: Int): Uri? {
return toString().thumbnail(size)?.toUri()
}
fun formatAsDuration(millis: Long) = DateUtils.formatElapsedTime(millis / 1000).removePrefix("0")
suspend fun Result<Innertube.PlaylistOrAlbumPage>.completed(): Result<Innertube.PlaylistOrAlbumPage>? {
var playlistPage = getOrNull() ?: return null
while (playlistPage.songsPage?.continuation != null) {
val continuation = playlistPage.songsPage?.continuation!!
val otherPlaylistPageResult = Innertube.playlistPage(ContinuationBody(continuation = continuation)) ?: break
if (otherPlaylistPageResult.isFailure) break
otherPlaylistPageResult.getOrNull()?.let { otherSongsPage ->
playlistPage = playlistPage.copy(songsPage = playlistPage.songsPage + otherSongsPage)
}
}
return Result.success(playlistPage)
}
inline val isAtLeastAndroid6
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
inline val isAtLeastAndroid8
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
inline val isAtLeastAndroid12
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
inline val isAtLeastAndroid13
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="1dp" />
<solid android:color="@color/white_30" />
</shape>
</item>
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<corners android:radius="1dp" />
<solid android:color="@color/white_60" />
</shape>
</clip>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="1dp" />
<solid android:color="@color/white" />
</shape>
</clip>
</item>
</layer-list>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/white" />
<size
android:width="12dp"
android:height="12dp" />
</shape>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M18.522,10.432C18.622,10.432 18.718,10.461 18.801,10.516L22.564,13.033L21.458,14.696L20.017,13.733V18L20,17.977V18C20,18.638 19.796,19.259 19.419,19.773C19.042,20.288 18.511,20.668 17.902,20.86C17.294,21.052 16.64,21.045 16.036,20.84C15.432,20.635 14.909,20.243 14.543,19.721C14.177,19.199 13.987,18.573 14.001,17.935C14.014,17.298 14.231,16.681 14.619,16.175C15.007,15.668 15.546,15.299 16.158,15.12C16.77,14.941 17.423,14.962 18.023,15.179V10.932C18.023,10.866 18.036,10.801 18.061,10.74C18.086,10.679 18.123,10.624 18.169,10.578C18.216,10.531 18.271,10.495 18.332,10.47C18.392,10.444 18.457,10.432 18.523,10.432H18.522ZM10.476,18C10.542,18 10.607,18.013 10.668,18.038C10.729,18.063 10.784,18.1 10.83,18.146C10.877,18.193 10.913,18.248 10.938,18.309C10.964,18.369 10.976,18.434 10.976,18.5V19.5C10.976,19.566 10.964,19.631 10.938,19.691C10.913,19.752 10.877,19.807 10.83,19.854C10.784,19.9 10.729,19.937 10.668,19.962C10.607,19.987 10.542,20 10.476,20H3.5C3.434,20 3.369,19.987 3.309,19.962C3.248,19.937 3.193,19.9 3.146,19.854C3.1,19.807 3.063,19.752 3.038,19.691C3.013,19.631 3,19.566 3,19.5V18.5C3,18.434 3.013,18.369 3.038,18.309C3.063,18.248 3.1,18.193 3.146,18.146C3.193,18.1 3.248,18.063 3.309,18.038C3.369,18.013 3.434,18 3.5,18H10.476ZM17,17C16.735,17 16.48,17.105 16.293,17.293C16.105,17.48 16,17.735 16,18C16,18.265 16.105,18.52 16.293,18.707C16.48,18.895 16.735,19 17,19C17.265,19 17.52,18.895 17.707,18.707C17.895,18.52 18,18.265 18,18C18,17.735 17.895,17.48 17.707,17.293C17.52,17.105 17.265,17 17,17ZM14.465,11C14.531,11 14.596,11.013 14.656,11.038C14.717,11.063 14.772,11.1 14.819,11.146C14.865,11.193 14.902,11.248 14.927,11.309C14.952,11.369 14.965,11.434 14.965,11.5V12.5C14.965,12.566 14.952,12.631 14.927,12.691C14.902,12.752 14.865,12.807 14.819,12.854C14.772,12.9 14.717,12.937 14.656,12.962C14.596,12.987 14.531,13 14.465,13H3.5C3.434,13 3.369,12.987 3.309,12.962C3.248,12.937 3.193,12.9 3.146,12.854C3.1,12.807 3.063,12.752 3.038,12.691C3.013,12.631 3,12.566 3,12.5V11.5C3,11.434 3.013,11.369 3.038,11.309C3.063,11.248 3.1,11.193 3.146,11.146C3.193,11.1 3.248,11.063 3.309,11.038C3.369,11.013 3.434,11 3.5,11H14.465ZM19.5,4C19.633,4 19.76,4.053 19.854,4.146C19.947,4.24 20,4.367 20,4.5V5.5C20,5.633 19.947,5.76 19.854,5.854C19.76,5.947 19.633,6 19.5,6H3.5C3.434,6 3.369,5.987 3.309,5.962C3.248,5.937 3.193,5.9 3.146,5.854C3.1,5.807 3.063,5.752 3.038,5.691C3.013,5.631 3,5.566 3,5.5V4.5C3,4.434 3.013,4.369 3.038,4.309C3.063,4.248 3.1,4.193 3.146,4.146C3.193,4.1 3.248,4.063 3.309,4.038C3.369,4.013 3.434,4 3.5,4H19.5Z" />
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="M208,432H160a16,16 0,0 1,-16 -16V96a16,16 0,0 1,16 -16h48a16,16 0,0 1,16 16V416A16,16 0,0 1,208 432Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M352,432H304a16,16 0,0 1,-16 -16V96a16,16 0,0 1,16 -16h48a16,16 0,0 1,16 16V416A16,16 0,0 1,352 432Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="m171.2,440a35.37,35.37 0,0 1,-17.5 -4.67c-12,-6.8 -19.46,-20 -19.46,-34.33v-290c0,-14.37 7.46,-27.53 19.46,-34.33a35.13,35.13 0,0 1,35.77 0.45l247.85,148.36a36,36 0,0 1,0 61l-247.89,148.4a35.5,35.5 0,0 1,-18.23 5.12z"/>
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<clip-path android:pathData="M0,0h24v24h-24z" />
<path
android:fillColor="#ffffff"
android:pathData="M21.806,6.569C23.229,8.017 24.021,9.97 24.011,12C24.014,12.695 23.92,13.386 23.734,14.055C23.673,14.267 23.53,14.446 23.338,14.553C23.145,14.66 22.918,14.686 22.706,14.625C22.492,14.566 22.31,14.425 22.198,14.233C22.086,14.041 22.053,13.814 22.106,13.598C22.243,13.076 22.313,12.539 22.316,12C22.33,10.443 21.727,8.943 20.639,7.828C19.552,6.714 18.067,6.075 16.51,6.051H8.603V7.589C8.603,8.077 8.265,8.272 7.853,8.009L4.237,5.759C4.146,5.717 4.069,5.649 4.016,5.565C3.962,5.481 3.933,5.382 3.933,5.282C3.933,5.182 3.962,5.084 4.016,5C4.069,4.915 4.146,4.848 4.237,4.806L7.86,2.368C8.265,2.105 8.611,2.285 8.611,2.781V4.318H16.51C17.501,4.319 18.481,4.518 19.394,4.905C20.306,5.291 21.131,5.857 21.821,6.569H21.806ZM20.41,18.279C20.501,18.321 20.578,18.388 20.632,18.473C20.686,18.557 20.715,18.655 20.715,18.755C20.715,18.855 20.686,18.953 20.632,19.038C20.578,19.122 20.501,19.189 20.41,19.232L16.78,21.632C16.375,21.895 16.03,21.715 16.03,21.219V19.682H7.508C6.517,19.681 5.536,19.482 4.624,19.095C3.711,18.709 2.886,18.143 2.197,17.431C0.98,16.174 0.221,14.545 0.041,12.805C-0.138,11.065 0.272,9.315 1.207,7.837C1.265,7.742 1.343,7.659 1.434,7.595C1.525,7.53 1.628,7.485 1.738,7.461C1.847,7.437 1.96,7.435 2.069,7.456C2.179,7.477 2.284,7.52 2.377,7.582C2.567,7.707 2.701,7.903 2.748,8.126C2.796,8.349 2.754,8.582 2.632,8.774C2.023,9.74 1.701,10.858 1.702,12C1.688,13.557 2.29,15.057 3.378,16.172C4.466,17.286 5.951,17.925 7.508,17.949H16.052V16.411C16.052,15.923 16.382,15.728 16.802,15.991L20.418,18.279H20.41Z" />
</group>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="M112,64a16,16 0,0 1,16 16V216.43L360.77,77.11a35.13,35.13 0,0 1,35.77 -0.44c12,6.8 19.46,20 19.46,34.33V401c0,14.37 -7.46,27.53 -19.46,34.33a35.14,35.14 0,0 1,-35.77 -0.45L128,295.57V432a16,16 0,0 1,-32 0V80A16,16 0,0 1,112 64Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="#ffffff"
android:pathData="M2.813,17.857H2.12C2.029,17.86 1.939,17.844 1.854,17.81C1.769,17.776 1.692,17.726 1.627,17.662C1.563,17.597 1.512,17.521 1.478,17.436C1.444,17.352 1.427,17.261 1.429,17.17V2.732C1.429,2.437 1.725,2.143 2.121,2.143H2.813C3.208,2.143 3.505,2.438 3.505,2.83V17.17C3.406,17.563 3.11,17.857 2.813,17.857ZM18.432,17.17C18.037,17.759 17.246,17.956 16.653,17.563L5.877,11.277C5.68,11.179 5.581,10.982 5.482,10.786C5.087,10.196 5.284,9.411 5.877,9.018L16.554,2.634C16.752,2.536 16.949,2.438 17.246,2.438C17.937,2.438 18.531,3.027 18.531,3.714V16.482C18.63,16.777 18.531,16.974 18.432,17.17Z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="M400,64a16,16 0,0 0,-16 16V216.43L151.23,77.11a35.13,35.13 0,0 0,-35.77 -0.44C103.46,83.47 96,96.63 96,111V401c0,14.37 7.46,27.53 19.46,34.33a35.14,35.14 0,0 0,35.77 -0.45L384,295.57V432a16,16 0,0 0,32 0V80A16,16 0,0 0,400 64Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M17.187,17.857H17.88C17.971,17.86 18.062,17.844 18.146,17.81C18.231,17.776 18.308,17.726 18.372,17.662C18.437,17.597 18.488,17.521 18.522,17.436C18.556,17.352 18.573,17.261 18.571,17.17V2.732C18.571,2.437 18.275,2.143 17.879,2.143H17.187C16.792,2.143 16.495,2.438 16.495,2.83V17.17C16.594,17.563 16.89,17.857 17.187,17.857ZM1.568,17.17C1.963,17.759 2.754,17.956 3.347,17.563L14.123,11.277C14.32,11.179 14.419,10.982 14.518,10.786C14.913,10.196 14.716,9.411 14.123,9.018L3.446,2.634C3.248,2.536 3.051,2.438 2.754,2.438C2.063,2.438 1.469,3.027 1.469,3.714V16.482C1.37,16.777 1.469,16.974 1.568,17.17Z"
android:fillColor="#ffffff"/>
</vector>

View File

@ -0,0 +1,320 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="0dp" />
<RelativeLayout
android:id="@+id/relative"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="5"
android:scaleType="centerCrop" />
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="5"
android:background="@drawable/drw_details_bg"
android:visibility="gone" />
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@color/main_bg_color" />
</LinearLayout>
</RelativeLayout>
<LinearLayout
android:id="@+id/title_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/view"
android:layout_margin="16dp"
android:orientation="horizontal">
<RelativeLayout
android:id="@+id/back_btn"
android:layout_width="42dp"
android:layout_height="42dp"
android:background="@drawable/drw_back_bg">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/arrow_bottom_icon" />
</RelativeLayout>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="42dp"
android:fontFamily="@font/medium_font"
android:gravity="center"
android:text="@string/app_name"
android:textColor="@color/white"
android:textSize="18dp"
android:visibility="gone" />
</LinearLayout>
<androidx.cardview.widget.CardView
android:id="@+id/center_card_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/layout"
android:layout_below="@+id/title_layout"
android:layout_centerInParent="true"
android:layout_gravity="center"
android:layout_margin="20dp"
android:background="#00000000"
android:elevation="0dp"
app:cardBackgroundColor="#000000"
app:cardCornerRadius="16dp"
app:cardElevation="0dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/playback_error_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_margin="24dp"
android:fontFamily="@font/medium_font"
android:gravity="center"
android:text="@string/playback_error"
android:textColor="@color/white_80"
android:textSize="16dp"
android:visibility="gone" />
<ImageView
android:id="@+id/thumbnail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop" />
<ProgressBar
android:id="@+id/loading_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
<FrameLayout
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.player.musicoo.view.MarqueeTextView
android:id="@+id/name_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:ellipsize="marquee"
android:fontFamily="@font/medium_font"
android:gravity="center"
android:maxLines="2"
android:text="@string/app_name"
android:textColor="@color/white"
android:textSize="24dp" />
<com.player.musicoo.view.MarqueeTextView
android:id="@+id/desc_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:ellipsize="marquee"
android:fontFamily="@font/medium_font"
android:gravity="center"
android:maxLines="1"
android:text="@string/app_name"
android:textColor="@color/white_60"
android:textSize="14dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<SeekBar
android:id="@+id/sbProgress"
android:layout_width="match_parent"
android:layout_height="32dp"
android:maxHeight="4dp"
android:minHeight="4dp"
android:progressDrawable="@drawable/bg_playing_playback_progress"
android:thumb="@drawable/ic_playing_playback_progress_thumb" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="14dp"
android:orientation="horizontal">
<TextView
android:id="@+id/progress_duration_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/medium_font"
android:text="00:00"
android:textColor="#D9FFFFFF"
android:textSize="12dp" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/total_duration_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:fontFamily="@font/medium_font"
android:text="00:00"
android:textColor="#D9FFFFFF"
android:textSize="12dp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:layout_marginBottom="68dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/play_mode_icon" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/play_skip_back_white_icon" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="66dp"
android:layout_weight="1"
android:gravity="center">
<ImageView
android:id="@+id/play_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/play_green_icon" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/play_skip_forward_white_icon" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/music_list_icon" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/disable_clicks_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" />
</FrameLayout>
</RelativeLayout>

View File

@ -3,6 +3,7 @@
<color name="black">#FF000000</color>
<color name="black_60">#99000000</color>
<color name="white">#FFFFFFFF</color>
<color name="white_30">#4DFFFFFF</color>
<color name="white_60">#99FFFFFF</color>
<color name="white_80">#CCFFFFFF</color>
<color name="main_bg_color">#151718</color>

View File

@ -18,4 +18,5 @@
<string name="resource_loading">Resource Loading…</string>
<string name="expand">EXPAND</string>
<string name="description">Description</string>
<string name="playback_error">An unknown playback error has occurred</string>
</resources>