update
This commit is contained in:
parent
058416facd
commit
0fa6e9156c
@ -11,6 +11,7 @@ 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.parseResources
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -105,6 +106,7 @@ class App : Application() {
|
||||
databaseManager = DatabaseManager.getInstance(this)
|
||||
initCurrentPlayingAudio()
|
||||
initImportAudio()
|
||||
CacheManager.initializeCaches(this)
|
||||
}
|
||||
|
||||
}
|
||||
@ -22,11 +22,13 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.session.MediaController
|
||||
import com.bumptech.glide.Glide
|
||||
import com.player.musicoo.App
|
||||
import com.player.musicoo.R
|
||||
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.LogTag
|
||||
import com.player.musicoo.view.MusicPlayerView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@ -14,6 +14,8 @@ import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.media3.exoplayer.offline.DownloadManager
|
||||
import androidx.media3.exoplayer.offline.DownloadRequest
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@ -25,17 +27,12 @@ import com.player.musicoo.R
|
||||
import com.player.musicoo.adapter.PlayListAdapter
|
||||
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.player
|
||||
import com.player.musicoo.media.MediaControllerManager
|
||||
import com.player.musicoo.media.SongRadio
|
||||
import com.player.musicoo.service.LoginRequiredException
|
||||
import com.player.musicoo.service.MyDownloadService
|
||||
import com.player.musicoo.service.PlayableFormatNotFoundException
|
||||
import com.player.musicoo.service.UnplayableException
|
||||
import com.player.musicoo.service.VideoIdMismatchException
|
||||
import com.player.musicoo.sp.AppStore
|
||||
import com.player.musicoo.util.DemoUtil
|
||||
import com.player.musicoo.util.DownloadUtil
|
||||
import com.player.musicoo.util.FileSizeConverter
|
||||
import com.player.musicoo.util.LogTag.LogD
|
||||
import com.player.musicoo.util.PlayMode
|
||||
import com.player.musicoo.util.asMediaItem
|
||||
@ -43,8 +40,8 @@ import com.player.musicoo.util.convertMillisToMinutesAndSecondsString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.selects.select
|
||||
import java.lang.Exception
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
|
||||
@ -65,6 +62,7 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
|
||||
|
||||
private var comeFrom: Class<*>? = null
|
||||
private var playListAdapter: PlayListAdapter? = null
|
||||
private var downloadManager: DownloadManager? = null
|
||||
|
||||
private fun initImmersionBar() {
|
||||
immersionBar {
|
||||
@ -79,6 +77,7 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
|
||||
initImmersionBar()
|
||||
initClick()
|
||||
initPlayerListener()
|
||||
initDownloadListener()
|
||||
initPlayListAdapter()
|
||||
updatePlayModeUi()
|
||||
val videoId = intent.getStringExtra(PLAY_DETAILS_VIDEO_ID)
|
||||
@ -140,12 +139,62 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
|
||||
updateInfoUi(meController.currentMediaItem)
|
||||
}
|
||||
}
|
||||
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(id: String) {
|
||||
if (DownloadUtil.downloadResourceExist(id)) {
|
||||
binding.downloadImg.setImageResource(R.drawable.download_done_icon)
|
||||
} else {
|
||||
binding.downloadImg.setImageResource(R.drawable.download_icon)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun initPlayerListener() {
|
||||
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
|
||||
@ -315,46 +364,24 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
|
||||
binding.downloadBtn.setOnClickListener {
|
||||
if (meController != null && meController.currentMediaItem != null) {
|
||||
val contentId = meController.currentMediaItem?.mediaId!!
|
||||
|
||||
val downloadManager = DemoUtil.getDownloadManager(this)
|
||||
val downloadIndex = downloadManager.downloadIndex
|
||||
downloadIndex.getDownloads()
|
||||
.use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val download = cursor.download
|
||||
if(download.request.id == contentId){
|
||||
return@setOnClickListener
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogD(TAG, "download get contentUrl")
|
||||
val urlResult = runBlocking(Dispatchers.IO) {
|
||||
Innertube.player(PlayerBody(videoId = contentId))
|
||||
}?.mapCatching { body ->
|
||||
if (body.videoDetails?.videoId != contentId) {
|
||||
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
|
||||
//如果已经存在就不进行下载
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
urlResult?.getOrThrow()?.let { url ->
|
||||
val contentUrl = url.toUri()
|
||||
LogD(TAG, "download contentUrl->${contentUrl}")
|
||||
val downloadRequest = DownloadRequest.Builder(contentId, contentUrl)
|
||||
} else {
|
||||
val downloadRequest = DownloadRequest
|
||||
.Builder(contentId, contentId.toUri())
|
||||
.setCustomCacheKey(contentId)
|
||||
.build()
|
||||
DownloadService.sendAddDownload(
|
||||
this,
|
||||
@ -363,8 +390,8 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ import androidx.media3.exoplayer.offline.DownloadNotificationHelper
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import androidx.media3.exoplayer.scheduler.PlatformScheduler
|
||||
import com.player.musicoo.R
|
||||
import com.player.musicoo.util.DemoUtil
|
||||
import com.player.musicoo.util.DownloadUtil
|
||||
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@ -34,11 +34,12 @@ class MyDownloadService : DownloadService(
|
||||
|
||||
@SuppressLint("ServiceCast")
|
||||
override fun getDownloadManager(): DownloadManager {
|
||||
val downloadManager = DemoUtil.getDownloadManager(this)
|
||||
val downloadNotificationHelper = DemoUtil.getDownloadNotificationHelper(this)
|
||||
val downloadManager = DownloadUtil.getDownloadManager(this)
|
||||
downloadManager!!.maxParallelDownloads = 3
|
||||
val downloadNotificationHelper = DownloadUtil.getDownloadNotificationHelper(this)
|
||||
downloadManager.addListener(
|
||||
TerminalStateNotificationHelper(
|
||||
this, downloadNotificationHelper, FOREGROUND_NOTIFICATION_ID + 1
|
||||
this, downloadNotificationHelper!!, FOREGROUND_NOTIFICATION_ID + 1
|
||||
)
|
||||
)
|
||||
return downloadManager
|
||||
@ -55,7 +56,7 @@ class MyDownloadService : DownloadService(
|
||||
return DownloadNotificationHelper(this, DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
||||
.buildProgressNotification(
|
||||
this,
|
||||
R.mipmap.musicoo_logo_img,
|
||||
R.drawable.download_icon,
|
||||
null,
|
||||
null,
|
||||
downloads,
|
||||
@ -76,22 +77,26 @@ class MyDownloadService : DownloadService(
|
||||
download: Download,
|
||||
finalException: Exception?
|
||||
) {
|
||||
val notification: Notification = if (download.state == Download.STATE_COMPLETED) {
|
||||
notificationHelper.buildDownloadCompletedNotification(
|
||||
context,
|
||||
R.mipmap.ic_download_done,
|
||||
null,
|
||||
Util.fromUtf8Bytes(download.request.data)
|
||||
)
|
||||
} else if (download.state == Download.STATE_FAILED) {
|
||||
notificationHelper.buildDownloadFailedNotification(
|
||||
context,
|
||||
R.mipmap.ic_download_done,
|
||||
null,
|
||||
Util.fromUtf8Bytes(download.request.data)
|
||||
)
|
||||
} else {
|
||||
return
|
||||
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)
|
||||
}
|
||||
|
||||
@ -3,21 +3,16 @@ package com.player.musicoo.service
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.database.StandaloneDatabaseProvider
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.DefaultHttpDataSource
|
||||
import androidx.media3.datasource.ResolvingDataSource
|
||||
import androidx.media3.datasource.cache.CacheDataSource
|
||||
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
|
||||
import androidx.media3.datasource.cache.NoOpCacheEvictor
|
||||
import androidx.media3.datasource.cache.SimpleCache
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.RenderersFactory
|
||||
@ -40,7 +35,8 @@ import com.player.musicoo.innertube.Innertube
|
||||
import com.player.musicoo.innertube.models.bodies.PlayerBody
|
||||
import com.player.musicoo.innertube.requests.player
|
||||
import com.player.musicoo.sp.AppStore
|
||||
import com.player.musicoo.util.ExoPlayerDiskCacheMaxSize
|
||||
import com.player.musicoo.util.CacheManager
|
||||
import com.player.musicoo.util.DownloadUtil
|
||||
import com.player.musicoo.util.LogTag
|
||||
import com.player.musicoo.util.LogTag.LogD
|
||||
import com.player.musicoo.util.PlayMode
|
||||
@ -53,34 +49,16 @@ import kotlinx.coroutines.runBlocking
|
||||
class PlaybackService : MediaSessionService(), Player.Listener {
|
||||
private val TAG = LogTag.VO_SERVICE_LOG
|
||||
private var mediaSession: MediaSession? = null
|
||||
private lateinit var cache: SimpleCache
|
||||
|
||||
private lateinit var player: ExoPlayer
|
||||
private val playerCache = CacheManager.getPlayerCache()
|
||||
private val downloadCache = CacheManager.getDownloadCache()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
val cacheEvictor = when (val size = ExoPlayerDiskCacheMaxSize.`2GB`) {
|
||||
ExoPlayerDiskCacheMaxSize.Unlimited -> NoOpCacheEvictor()
|
||||
else -> LeastRecentlyUsedCacheEvictor(size.bytes)
|
||||
}
|
||||
val directory = cacheDir.resolve("exoplayer").also { directory ->
|
||||
if (directory.exists()) return@also
|
||||
|
||||
directory.mkdir()
|
||||
|
||||
cacheDir.listFiles()?.forEach { file ->
|
||||
if (file.isDirectory && file.name.length == 1 && file.name.isDigitsOnly() || file.extension == "uid") {
|
||||
if (!file.renameTo(directory.resolve(file.name))) {
|
||||
file.deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filesDir.resolve("coil").deleteRecursively()
|
||||
}
|
||||
cache = SimpleCache(directory, cacheEvictor, StandaloneDatabaseProvider(this))
|
||||
|
||||
player = ExoPlayer.Builder(this, createRendersFactory(), createMediaSourceFactory())
|
||||
player = ExoPlayer.Builder(this)
|
||||
.setMediaSourceFactory(createMediaSourceFactory())
|
||||
.setRenderersFactory(createRendersFactory())
|
||||
.setHandleAudioBecomingNoisy(true)
|
||||
.setWakeMode(C.WAKE_MODE_LOCAL)
|
||||
.setAudioAttributes(
|
||||
@ -98,10 +76,12 @@ class PlaybackService : MediaSessionService(), Player.Listener {
|
||||
player.repeatMode = Player.REPEAT_MODE_ALL
|
||||
player.shuffleModeEnabled = false
|
||||
}
|
||||
|
||||
PlayMode.SINGLE_LOOP.value -> {
|
||||
player.repeatMode = Player.REPEAT_MODE_ONE
|
||||
player.shuffleModeEnabled = false
|
||||
}
|
||||
|
||||
PlayMode.RANDOM.value -> {
|
||||
player.repeatMode = Player.REPEAT_MODE_ALL
|
||||
player.shuffleModeEnabled = true
|
||||
@ -146,21 +126,24 @@ class PlaybackService : MediaSessionService(), Player.Listener {
|
||||
release()
|
||||
mediaSession = null
|
||||
}
|
||||
cache.release()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
||||
private fun createCacheDataSource(): DataSource.Factory {
|
||||
return CacheDataSource.Factory().setCache(cache).apply {
|
||||
setUpstreamDataSourceFactory(
|
||||
DefaultHttpDataSource.Factory()
|
||||
.setConnectTimeoutMs(16000)
|
||||
.setReadTimeoutMs(8000)
|
||||
.setUserAgent("Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0")
|
||||
private fun createCacheDataSource(): CacheDataSource.Factory =
|
||||
CacheDataSource.Factory()
|
||||
.setCache(downloadCache!!)
|
||||
.setUpstreamDataSourceFactory(
|
||||
CacheDataSource.Factory()
|
||||
.setCache(playerCache!!)
|
||||
.setUpstreamDataSourceFactory(
|
||||
DefaultHttpDataSource.Factory()
|
||||
.setConnectTimeoutMs(16000)
|
||||
.setReadTimeoutMs(8000)
|
||||
.setUserAgent("Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.setCacheWriteDataSinkFactory(null)
|
||||
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
|
||||
|
||||
private fun createDataSourceFactory(): DataSource.Factory {
|
||||
val chunkLength = 512 * 1024L
|
||||
@ -168,51 +151,62 @@ class PlaybackService : MediaSessionService(), Player.Listener {
|
||||
|
||||
return ResolvingDataSource.Factory(createCacheDataSource()) { dataSpec ->
|
||||
val videoId = dataSpec.key ?: error("A key must be set")
|
||||
if (cache.isCached(videoId, dataSpec.position, chunkLength)) {
|
||||
dataSpec
|
||||
} else {
|
||||
when (videoId) {
|
||||
ringBuffer.getOrNull(0)?.first -> dataSpec.withUri(ringBuffer.getOrNull(0)!!.second)
|
||||
ringBuffer.getOrNull(1)?.first -> dataSpec.withUri(ringBuffer.getOrNull(1)!!.second)
|
||||
else -> {
|
||||
val urlResult = runBlocking(Dispatchers.IO) {
|
||||
Innertube.player(PlayerBody(videoId = videoId))
|
||||
}?.mapCatching { body ->
|
||||
if (body.videoDetails?.videoId != videoId) {
|
||||
throw VideoIdMismatchException()
|
||||
}
|
||||
val position = dataSpec.position
|
||||
val length = if (dataSpec.length >= 0) dataSpec.length else 1
|
||||
|
||||
when (val status = body.playabilityStatus?.status) {
|
||||
"OK" -> body.streamingData?.highestQualityFormat?.let { format ->
|
||||
format.url
|
||||
} ?: throw PlayableFormatNotFoundException()
|
||||
if (downloadCache!!.isCached(videoId, position, length)) {
|
||||
LogD(TAG, "playbackService downloadCache contains data for $videoId at position $position")
|
||||
return@Factory dataSpec
|
||||
}
|
||||
|
||||
"UNPLAYABLE" -> throw UnplayableException()
|
||||
"LOGIN_REQUIRED" -> throw LoginRequiredException()
|
||||
else -> throw PlaybackException(
|
||||
status,
|
||||
null,
|
||||
PlaybackException.ERROR_CODE_REMOTE_ERROR
|
||||
)
|
||||
}
|
||||
if (playerCache!!.isCached(videoId, position, chunkLength)) {
|
||||
LogD(TAG, "playbackService playerCache contains data for $videoId at position $position")
|
||||
return@Factory dataSpec
|
||||
}
|
||||
|
||||
when (videoId) {
|
||||
ringBuffer.getOrNull(0)?.first -> return@Factory dataSpec.withUri(ringBuffer.getOrNull(0)!!.second)
|
||||
ringBuffer.getOrNull(1)?.first -> return@Factory dataSpec.withUri(ringBuffer.getOrNull(1)!!.second)
|
||||
else -> {
|
||||
val urlResult = runBlocking(Dispatchers.IO) {
|
||||
Innertube.player(PlayerBody(videoId = videoId))
|
||||
}?.mapCatching { body ->
|
||||
if (body.videoDetails?.videoId != videoId) {
|
||||
throw VideoIdMismatchException()
|
||||
}
|
||||
LogD(TAG, "service urlResult->$urlResult")
|
||||
|
||||
urlResult?.getOrThrow()?.let { url ->
|
||||
ringBuffer.append(videoId to url.toUri())
|
||||
dataSpec.withUri(url.toUri())
|
||||
.subrange(dataSpec.uriPositionOffset, chunkLength)
|
||||
} ?: throw PlaybackException(
|
||||
null,
|
||||
urlResult?.exceptionOrNull(),
|
||||
PlaybackException.ERROR_CODE_REMOTE_ERROR
|
||||
)
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LogD(TAG, "playbackService urlResult->$urlResult")
|
||||
|
||||
urlResult?.getOrThrow()?.let { url ->
|
||||
ringBuffer.append(videoId to url.toUri())
|
||||
return@Factory dataSpec.withUri(url.toUri())
|
||||
.subrange(dataSpec.uriPositionOffset, chunkLength)
|
||||
} ?: throw PlaybackException(
|
||||
null,
|
||||
urlResult?.exceptionOrNull(),
|
||||
PlaybackException.ERROR_CODE_REMOTE_ERROR
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun createMediaSourceFactory(): MediaSource.Factory {
|
||||
return DefaultMediaSourceFactory(createDataSourceFactory(), createExtractorsFactory())
|
||||
}
|
||||
@ -227,7 +221,6 @@ class PlaybackService : MediaSessionService(), Player.Listener {
|
||||
val audioSink = DefaultAudioSink.Builder()
|
||||
.setEnableFloatOutput(false)
|
||||
.setEnableAudioTrackPlaybackParams(false)
|
||||
// .setOffloadMode(DefaultAudioSink.OFFLOAD_MODE_DISABLED)
|
||||
.setAudioProcessorChain(
|
||||
DefaultAudioSink.DefaultAudioProcessorChain(
|
||||
emptyArray(),
|
||||
|
||||
50
app/src/main/java/com/player/musicoo/util/CacheManager.kt
Normal file
50
app/src/main/java/com/player/musicoo/util/CacheManager.kt
Normal file
@ -0,0 +1,50 @@
|
||||
package com.player.musicoo.util
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.database.DatabaseProvider
|
||||
import androidx.media3.database.StandaloneDatabaseProvider
|
||||
import androidx.media3.datasource.cache.CacheEvictor
|
||||
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
|
||||
import androidx.media3.datasource.cache.NoOpCacheEvictor
|
||||
import androidx.media3.datasource.cache.SimpleCache
|
||||
import java.io.File
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
object CacheManager {
|
||||
private var playerCache: SimpleCache? = null
|
||||
private var downloadCache: SimpleCache? = null
|
||||
private var databaseProvider: DatabaseProvider? = null
|
||||
private const val DOWNLOAD_CONTENT_DIRECTORY = "DownloadOO"
|
||||
private const val PLAYER_CONTENT_DIRECTORY = "PlayerOO"
|
||||
|
||||
fun initializeCaches(context: Context) {
|
||||
databaseProvider = StandaloneDatabaseProvider(context)
|
||||
|
||||
// Initialize player cache
|
||||
val playerCacheDir = File(context.filesDir, PLAYER_CONTENT_DIRECTORY)
|
||||
val cacheEvictor: CacheEvictor = when (val size = ExoPlayerDiskCacheMaxSize.`1GB`) {
|
||||
ExoPlayerDiskCacheMaxSize.Unlimited -> NoOpCacheEvictor()
|
||||
else -> LeastRecentlyUsedCacheEvictor(size.bytes)
|
||||
}
|
||||
|
||||
playerCache = SimpleCache(
|
||||
playerCacheDir,
|
||||
cacheEvictor,
|
||||
databaseProvider!!
|
||||
)
|
||||
|
||||
// Initialize download cache
|
||||
val downloadCacheDir = File(context.filesDir, DOWNLOAD_CONTENT_DIRECTORY)
|
||||
downloadCache = SimpleCache(downloadCacheDir, NoOpCacheEvictor(), databaseProvider!!)
|
||||
}
|
||||
|
||||
fun getPlayerCache(): SimpleCache? {
|
||||
return playerCache
|
||||
}
|
||||
|
||||
fun getDownloadCache(): SimpleCache? {
|
||||
return downloadCache
|
||||
}
|
||||
}
|
||||
@ -1,182 +0,0 @@
|
||||
package com.player.musicoo.util;
|
||||
import android.content.Context;
|
||||
|
||||
import org.chromium.net.CronetEngine;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.media3.database.DatabaseProvider;
|
||||
import androidx.media3.database.StandaloneDatabaseProvider;
|
||||
import androidx.media3.datasource.DataSource;
|
||||
import androidx.media3.datasource.DefaultDataSource;
|
||||
import androidx.media3.datasource.DefaultHttpDataSource;
|
||||
import androidx.media3.datasource.cache.Cache;
|
||||
import androidx.media3.datasource.cache.CacheDataSource;
|
||||
import androidx.media3.datasource.cache.NoOpCacheEvictor;
|
||||
import androidx.media3.datasource.cache.SimpleCache;
|
||||
import androidx.media3.datasource.cronet.CronetDataSource;
|
||||
import androidx.media3.datasource.cronet.CronetUtil;
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory;
|
||||
import androidx.media3.exoplayer.RenderersFactory;
|
||||
import androidx.media3.exoplayer.offline.DownloadManager;
|
||||
import androidx.media3.exoplayer.offline.DownloadNotificationHelper;
|
||||
import java.io.File;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/** Utility methods for the demo app. */
|
||||
public final class DemoUtil {
|
||||
|
||||
public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel";
|
||||
|
||||
/**
|
||||
* Whether the demo application uses Cronet for networking. Note that Cronet does not provide
|
||||
* automatic support for cookies (https://github.com/google/ExoPlayer/issues/5975).
|
||||
*
|
||||
* <p>If set to false, the platform's default network stack is used with a {@link CookieManager}
|
||||
* configured in {@link #getHttpDataSourceFactory}.
|
||||
*/
|
||||
private static final boolean USE_CRONET_FOR_NETWORKING = true;
|
||||
|
||||
private static final String TAG = "DemoUtil";
|
||||
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
|
||||
|
||||
private static DataSource.Factory dataSourceFactory;
|
||||
private static DataSource.Factory httpDataSourceFactory;
|
||||
private static DatabaseProvider databaseProvider;
|
||||
private static File downloadDirectory;
|
||||
private static Cache downloadCache;
|
||||
private static DownloadManager downloadManager;
|
||||
// private static @MonotonicNonNull DownloadTracker downloadTracker;
|
||||
private static DownloadNotificationHelper downloadNotificationHelper;
|
||||
|
||||
/** Returns whether extension renderers should be used. */
|
||||
public static boolean useExtensionRenderers() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
|
||||
public static RenderersFactory buildRenderersFactory(
|
||||
Context context, boolean preferExtensionRenderer) {
|
||||
@DefaultRenderersFactory.ExtensionRendererMode
|
||||
int extensionRendererMode =
|
||||
useExtensionRenderers()
|
||||
? (preferExtensionRenderer
|
||||
? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
|
||||
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
|
||||
return new DefaultRenderersFactory(context.getApplicationContext())
|
||||
.setExtensionRendererMode(extensionRendererMode);
|
||||
}
|
||||
|
||||
public static synchronized DataSource.Factory getHttpDataSourceFactory(Context context) {
|
||||
if (httpDataSourceFactory == null) {
|
||||
if (USE_CRONET_FOR_NETWORKING) {
|
||||
context = context.getApplicationContext();
|
||||
@Nullable CronetEngine cronetEngine = CronetUtil.buildCronetEngine(context);
|
||||
if (cronetEngine != null) {
|
||||
httpDataSourceFactory =
|
||||
new CronetDataSource.Factory(cronetEngine, Executors.newSingleThreadExecutor());
|
||||
}
|
||||
}
|
||||
if (httpDataSourceFactory == null) {
|
||||
// We don't want to use Cronet, or we failed to instantiate a CronetEngine.
|
||||
CookieManager cookieManager = new CookieManager();
|
||||
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||
CookieHandler.setDefault(cookieManager);
|
||||
httpDataSourceFactory = new DefaultHttpDataSource.Factory();
|
||||
}
|
||||
}
|
||||
return httpDataSourceFactory;
|
||||
}
|
||||
|
||||
/** Returns a {@link DataSource.Factory}. */
|
||||
public static synchronized DataSource.Factory getDataSourceFactory(Context context) {
|
||||
if (dataSourceFactory == null) {
|
||||
context = context.getApplicationContext();
|
||||
DefaultDataSource.Factory upstreamFactory =
|
||||
new DefaultDataSource.Factory(context, getHttpDataSourceFactory(context));
|
||||
dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
|
||||
}
|
||||
return dataSourceFactory;
|
||||
}
|
||||
|
||||
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
|
||||
public static synchronized DownloadNotificationHelper getDownloadNotificationHelper(
|
||||
Context context) {
|
||||
if (downloadNotificationHelper == null) {
|
||||
downloadNotificationHelper =
|
||||
new DownloadNotificationHelper(context, DOWNLOAD_NOTIFICATION_CHANNEL_ID);
|
||||
}
|
||||
return downloadNotificationHelper;
|
||||
}
|
||||
|
||||
public static synchronized DownloadManager getDownloadManager(Context context) {
|
||||
ensureDownloadManagerInitialized(context);
|
||||
return downloadManager;
|
||||
}
|
||||
|
||||
// public static synchronized DownloadTracker getDownloadTracker(Context context) {
|
||||
// ensureDownloadManagerInitialized(context);
|
||||
// return downloadTracker;
|
||||
// }
|
||||
|
||||
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
|
||||
private static synchronized Cache getDownloadCache(Context context) {
|
||||
if (downloadCache == null) {
|
||||
File downloadContentDirectory =
|
||||
new File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY);
|
||||
downloadCache =
|
||||
new SimpleCache(
|
||||
downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider(context));
|
||||
}
|
||||
return downloadCache;
|
||||
}
|
||||
|
||||
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
|
||||
private static synchronized void ensureDownloadManagerInitialized(Context context) {
|
||||
if (downloadManager == null) {
|
||||
downloadManager =
|
||||
new DownloadManager(
|
||||
context,
|
||||
getDatabaseProvider(context),
|
||||
getDownloadCache(context),
|
||||
getHttpDataSourceFactory(context),
|
||||
Executors.newFixedThreadPool(/* nThreads= */ 6));
|
||||
// downloadTracker =
|
||||
// new DownloadTracker(context, getHttpDataSourceFactory(context), downloadManager);
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
|
||||
private static synchronized DatabaseProvider getDatabaseProvider(Context context) {
|
||||
if (databaseProvider == null) {
|
||||
databaseProvider = new StandaloneDatabaseProvider(context);
|
||||
}
|
||||
return databaseProvider;
|
||||
}
|
||||
|
||||
private static synchronized File getDownloadDirectory(Context context) {
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.getExternalFilesDir(/* type= */ null);
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.getFilesDir();
|
||||
}
|
||||
}
|
||||
return downloadDirectory;
|
||||
}
|
||||
|
||||
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
|
||||
private static CacheDataSource.Factory buildReadOnlyCacheDataSource(
|
||||
DataSource.Factory upstreamFactory, Cache cache) {
|
||||
return new CacheDataSource.Factory()
|
||||
.setCache(cache)
|
||||
.setUpstreamDataSourceFactory(upstreamFactory)
|
||||
.setCacheWriteDataSinkFactory(null)
|
||||
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR);
|
||||
}
|
||||
|
||||
private DemoUtil() {}
|
||||
}
|
||||
224
app/src/main/java/com/player/musicoo/util/DownloadUtil.kt
Normal file
224
app/src/main/java/com/player/musicoo/util/DownloadUtil.kt
Normal file
@ -0,0 +1,224 @@
|
||||
package com.player.musicoo.util
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.net.toUri
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.database.DatabaseProvider
|
||||
import androidx.media3.database.StandaloneDatabaseProvider
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.datasource.DefaultHttpDataSource
|
||||
import androidx.media3.datasource.ResolvingDataSource
|
||||
import androidx.media3.datasource.cache.Cache
|
||||
import androidx.media3.datasource.cache.CacheDataSource
|
||||
import androidx.media3.datasource.cache.CacheEvictor
|
||||
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
|
||||
import androidx.media3.datasource.cache.NoOpCacheEvictor
|
||||
import androidx.media3.datasource.cache.SimpleCache
|
||||
import androidx.media3.datasource.cronet.CronetDataSource
|
||||
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.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.service.LoginRequiredException
|
||||
import com.player.musicoo.service.PlayableFormatNotFoundException
|
||||
import com.player.musicoo.service.UnplayableException
|
||||
import com.player.musicoo.service.VideoIdMismatchException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.net.CookieHandler
|
||||
import java.net.CookieManager
|
||||
import java.net.CookiePolicy
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
||||
@OptIn(markerClass = [UnstableApi::class])
|
||||
object DownloadUtil {
|
||||
private const val DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel"
|
||||
|
||||
private const val USE_CRONET_FOR_NETWORKING = true
|
||||
|
||||
private var httpDataSourceFactory: DataSource.Factory? = null
|
||||
private var databaseProvider: DatabaseProvider? = null
|
||||
private val playerCache = CacheManager.getPlayerCache()
|
||||
private val downloadCache = CacheManager.getDownloadCache()
|
||||
private var downloadManager: DownloadManager? = null
|
||||
|
||||
private var downloadNotificationHelper: DownloadNotificationHelper? = null
|
||||
|
||||
@Synchronized
|
||||
fun getHttpDataSourceFactory(context: Context): DataSource.Factory? {
|
||||
if (httpDataSourceFactory == null) {
|
||||
if (USE_CRONET_FOR_NETWORKING) {
|
||||
val cronetEngine = CronetUtil.buildCronetEngine(context.applicationContext)
|
||||
if (cronetEngine != null) {
|
||||
httpDataSourceFactory =
|
||||
CronetDataSource.Factory(cronetEngine, Executors.newSingleThreadExecutor())
|
||||
}
|
||||
}
|
||||
if (httpDataSourceFactory == null) {
|
||||
// We don't want to use Cronet, or we failed to instantiate a CronetEngine.
|
||||
val cookieManager = CookieManager()
|
||||
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER)
|
||||
CookieHandler.setDefault(cookieManager)
|
||||
httpDataSourceFactory = DefaultHttpDataSource.Factory()
|
||||
}
|
||||
}
|
||||
return httpDataSourceFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [DataSource.Factory].
|
||||
*/
|
||||
@Synchronized
|
||||
fun getDataSourceFactory(context: Context): DataSource.Factory {
|
||||
val upstreamFactory = DefaultDataSource.Factory(
|
||||
context.applicationContext,
|
||||
getHttpDataSourceFactory(context.applicationContext)!!
|
||||
)
|
||||
val chunkLength = 512 * 1024L
|
||||
val ringBuffer = RingBuffer<Pair<String, Uri>?>(2) { null }
|
||||
return ResolvingDataSource.Factory(
|
||||
CacheDataSource.Factory()
|
||||
.setCache(playerCache!!)
|
||||
.setUpstreamDataSourceFactory(upstreamFactory)
|
||||
) { dataSpec ->
|
||||
val videoId = dataSpec.key ?: error("A key must be set")
|
||||
val length = if (dataSpec.length >= 0) dataSpec.length else 1
|
||||
|
||||
if (playerCache.isCached(videoId, dataSpec.position, length)) {
|
||||
LogTag.LogD(TAG, "下载 getDataSourceFactory playerCache")
|
||||
return@Factory dataSpec
|
||||
}
|
||||
when (videoId) {
|
||||
ringBuffer.getOrNull(0)?.first -> dataSpec.withUri(ringBuffer.getOrNull(0)!!.second)
|
||||
ringBuffer.getOrNull(1)?.first -> dataSpec.withUri(ringBuffer.getOrNull(1)!!.second)
|
||||
else -> {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
LogTag.LogD(TAG, "DownloadUtil urlResult->$urlResult")
|
||||
|
||||
urlResult?.getOrThrow()?.let { url ->
|
||||
ringBuffer.append(videoId to url.toUri())
|
||||
dataSpec.withUri(url.toUri())
|
||||
} ?: throw PlaybackException(
|
||||
null,
|
||||
urlResult?.exceptionOrNull(),
|
||||
PlaybackException.ERROR_CODE_REMOTE_ERROR
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getDownloadNotificationHelper(
|
||||
context: Context?
|
||||
): DownloadNotificationHelper? {
|
||||
if (downloadNotificationHelper == null) {
|
||||
downloadNotificationHelper =
|
||||
DownloadNotificationHelper(context!!, DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
||||
}
|
||||
return downloadNotificationHelper
|
||||
}
|
||||
|
||||
fun downloadResourceExist(id: String): Boolean {
|
||||
var isExist = false
|
||||
if (downloadManager != null) {
|
||||
val downloadIndex = downloadManager!!.downloadIndex
|
||||
downloadIndex.getDownloads()
|
||||
.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
|
||||
if (downloadManager != null) {
|
||||
downloadManager?.currentDownloads?.map {
|
||||
if (it.request.id === id) {
|
||||
download = it
|
||||
}
|
||||
}
|
||||
}
|
||||
return download
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getDownloadManager(context: Context): DownloadManager? {
|
||||
ensureDownloadManagerInitialized(context)
|
||||
return downloadManager
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun ensureDownloadManagerInitialized(context: Context) {
|
||||
if (downloadManager == null) {
|
||||
downloadManager = DownloadManager(
|
||||
context,
|
||||
getDatabaseProvider(context)!!,
|
||||
downloadCache!!,
|
||||
getDataSourceFactory(context)!!,
|
||||
Executor(Runnable::run)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun getDatabaseProvider(context: Context): DatabaseProvider? {
|
||||
if (databaseProvider == null) {
|
||||
databaseProvider = StandaloneDatabaseProvider(context)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package com.player.musicoo.util
|
||||
|
||||
class FileSizeConverter(private val sizeInBytes: Long) {
|
||||
|
||||
companion object {
|
||||
private const val BYTE: Long = 1
|
||||
private const val KILOBYTE: Long = 1024
|
||||
private const val MEGABYTE: Long = 1024 * KILOBYTE
|
||||
private const val GIGABYTE: Long = 1024 * MEGABYTE
|
||||
private const val TERABYTE: Long = 1024 * GIGABYTE
|
||||
|
||||
fun formatSize(sizeInBytes: Long): String {
|
||||
return when {
|
||||
sizeInBytes >= TERABYTE -> String.format("%.2f TB", sizeInBytes / TERABYTE.toDouble())
|
||||
sizeInBytes >= GIGABYTE -> String.format("%.2f GB", sizeInBytes / GIGABYTE.toDouble())
|
||||
sizeInBytes >= MEGABYTE -> String.format("%.2f MB", sizeInBytes / MEGABYTE.toDouble())
|
||||
sizeInBytes >= KILOBYTE -> String.format("%.2f KB", sizeInBytes / KILOBYTE.toDouble())
|
||||
else -> String.format("%d B", sizeInBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toBytes(): Long {
|
||||
return sizeInBytes
|
||||
}
|
||||
|
||||
fun toKilobytes(): Double {
|
||||
return sizeInBytes / KILOBYTE.toDouble()
|
||||
}
|
||||
|
||||
fun toMegabytes(): Double {
|
||||
return sizeInBytes / MEGABYTE.toDouble()
|
||||
}
|
||||
|
||||
fun toGigabytes(): Double {
|
||||
return sizeInBytes / GIGABYTE.toDouble()
|
||||
}
|
||||
|
||||
fun toTerabytes(): Double {
|
||||
return sizeInBytes / TERABYTE.toDouble()
|
||||
}
|
||||
|
||||
fun formattedSize(): String {
|
||||
return formatSize(sizeInBytes)
|
||||
}
|
||||
}
|
||||
16
app/src/main/res/drawable/download_done_icon.xml
Normal file
16
app/src/main/res/drawable/download_done_icon.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<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="#ff666666"
|
||||
android:pathData="M2.623,8.779C3.339,5.724 5.724,3.339 8.779,2.623C10.898,2.126 13.102,2.126 15.221,2.623C18.276,3.339 20.661,5.724 21.377,8.779C21.874,10.898 21.874,13.102 21.377,15.221C20.661,18.276 18.276,20.661 15.221,21.377C13.102,21.874 10.898,21.874 8.779,21.377C5.724,20.661 3.339,18.276 2.623,15.221C2.126,13.102 2.126,10.898 2.623,8.779L4.083,9.122C3.639,11.015 3.639,12.985 4.083,14.878C4.669,17.378 6.622,19.33 9.122,19.917C11.015,20.361 12.985,20.361 14.878,19.917C17.378,19.33 19.33,17.378 19.917,14.878C20.361,12.985 20.361,11.015 19.917,9.122C19.33,6.622 17.378,4.669 14.878,4.083C12.985,3.639 11.015,3.639 9.122,4.083C6.622,4.669 4.669,6.622 4.083,9.122L2.623,8.779Z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M8.5,12L11,14.5L16,9.5"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#ff666666"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
</vector>
|
||||
@ -3,16 +3,14 @@
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M2.623,8.779C3.339,5.724 5.724,3.339 8.779,2.623C10.898,2.126 13.102,2.126 15.221,2.623C18.276,3.339 20.661,5.724 21.377,8.779C21.874,10.898 21.874,13.102 21.377,15.221C20.661,18.276 18.276,20.661 15.221,21.377C13.102,21.874 10.898,21.874 8.779,21.377C5.724,20.661 3.339,18.276 2.623,15.221C2.126,13.102 2.126,10.898 2.623,8.779L4.083,9.122C3.639,11.015 3.639,12.985 4.083,14.878C4.669,17.378 6.622,19.33 9.122,19.917C11.015,20.361 12.985,20.361 14.878,19.917C17.378,19.33 19.33,17.378 19.917,14.878C20.361,12.985 20.361,11.015 19.917,9.122C19.33,6.622 17.378,4.669 14.878,4.083C12.985,3.639 11.015,3.639 9.122,4.083C6.622,4.669 4.669,6.622 4.083,9.122L2.623,8.779Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillAlpha="0.85"/>
|
||||
<path
|
||||
android:pathData="M12,8.5V15.5M12,15.5L14.5,13M12,15.5L9.5,13"
|
||||
android:strokeAlpha="0.85"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M2.623,8.779C3.339,5.724 5.724,3.339 8.779,2.623C10.898,2.126 13.102,2.126 15.221,2.623C18.276,3.339 20.661,5.724 21.377,8.779C21.874,10.898 21.874,13.102 21.377,15.221C20.661,18.276 18.276,20.661 15.221,21.377C13.102,21.874 10.898,21.874 8.779,21.377C5.724,20.661 3.339,18.276 2.623,15.221C2.126,13.102 2.126,10.898 2.623,8.779L4.083,9.122C3.639,11.015 3.639,12.985 4.083,14.878C4.669,17.378 6.622,19.33 9.122,19.917C11.015,20.361 12.985,20.361 14.878,19.917C17.378,19.33 19.33,17.378 19.917,14.878C20.361,12.985 20.361,11.015 19.917,9.122C19.33,6.622 17.378,4.669 14.878,4.083C12.985,3.639 11.015,3.639 9.122,4.083C6.622,4.669 4.669,6.622 4.083,9.122L2.623,8.779Z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M12,8.5V15.5M12,15.5L14.5,13M12,15.5L9.5,13"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
</vector>
|
||||
|
||||
10
app/src/main/res/drawable/error.xml
Normal file
10
app/src/main/res/drawable/error.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="#c62828">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,680Q497,680 508.5,668.5Q520,657 520,640Q520,623 508.5,611.5Q497,600 480,600Q463,600 451.5,611.5Q440,623 440,640Q440,657 451.5,668.5Q463,680 480,680ZM440,520L520,520L520,280L440,280L440,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880Z" />
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/favorited_icon.xml
Normal file
9
app/src/main/res/drawable/favorited_icon.xml
Normal 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:pathData="M15.698,3.065C17.377,2.805 18.953,3.333 20.229,4.577C21.526,5.841 22.147,7.507 21.97,9.254C21.795,10.979 20.879,12.684 19.338,14.186C18.908,14.606 17.752,15.738 16.093,17.365C15.347,18.097 14.553,18.876 13.753,19.662L12.975,20.425L12.666,20.729C12.488,20.903 12.249,21.001 12,21.001C11.75,21.001 11.511,20.903 11.334,20.729L8.648,18.088L8.08,17.532C6.941,16.417 5.801,15.302 4.662,14.186C3.12,12.684 2.205,10.98 2.03,9.254C1.853,7.507 2.474,5.841 3.771,4.577C5.047,3.333 6.623,2.805 8.302,3.065C9.548,3.257 10.812,3.878 12,4.868C13.189,3.878 14.452,3.257 15.698,3.065H15.698Z"
|
||||
android:fillColor="#80F988"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/not_favorited_icon.xml
Normal file
9
app/src/main/res/drawable/not_favorited_icon.xml
Normal 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:pathData="M11.995,21C11.775,21.001 11.557,20.959 11.353,20.875C11.15,20.791 10.965,20.668 10.81,20.513L3.9,13.646C2.72,12.505 2.036,10.945 1.997,9.304C1.958,7.663 2.566,6.072 3.69,4.876C4.764,3.777 6.206,3.115 7.739,3.016C9.272,2.917 10.788,3.388 11.995,4.339C13.2,3.388 14.715,2.917 16.247,3.016C17.78,3.115 19.221,3.778 20.295,4.876C21.419,6.072 22.027,7.663 21.987,9.304C21.948,10.945 21.264,12.505 20.085,13.646L13.175,20.513C13.02,20.668 12.836,20.791 12.633,20.874C12.431,20.958 12.214,21.001 11.995,21ZM8.14,4.482H8.095C7.472,4.486 6.857,4.614 6.284,4.858C5.712,5.102 5.193,5.458 4.76,5.905C3.907,6.823 3.45,8.039 3.485,9.291C3.521,10.544 4.047,11.732 4.95,12.601L11.86,19.466C11.875,19.487 11.895,19.504 11.918,19.516C11.941,19.528 11.966,19.534 11.992,19.534C12.018,19.534 12.043,19.528 12.066,19.516C12.089,19.504 12.109,19.487 12.125,19.466L19.035,12.601C19.939,11.733 20.465,10.544 20.501,9.291C20.536,8.039 20.078,6.822 19.225,5.905C18.791,5.457 18.273,5.101 17.701,4.857C17.128,4.612 16.512,4.485 15.89,4.482C15.268,4.477 14.651,4.594 14.074,4.827C13.498,5.061 12.973,5.406 12.53,5.842C12.386,5.98 12.194,6.057 11.995,6.057C11.795,6.057 11.603,5.98 11.46,5.842C10.577,4.966 9.383,4.477 8.14,4.482V4.482Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
@ -176,52 +176,88 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/downloadBtn"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:gravity="center">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/download_icon" />
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.player.musicoo.view.MarqueeTextView
|
||||
android:id="@+id/name_tv"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:fontFamily="@font/medium_font"
|
||||
android:maxLines="1"
|
||||
android:text="@string/app_name"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="22dp" />
|
||||
|
||||
<com.player.musicoo.view.MarqueeTextView
|
||||
android:id="@+id/desc_tv"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:fontFamily="@font/medium_font"
|
||||
android:maxLines="1"
|
||||
android:text="@string/app_name"
|
||||
android:textColor="@color/white_60"
|
||||
android:textSize="12dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/favoritesBtn"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/favoritesImg"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/not_favorited_icon" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/downloadBtn"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/downloadImg"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/download_icon" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/downloadLoading"
|
||||
android:visibility="gone"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:indeterminateTint="@color/green"
|
||||
android:progressBackgroundTint="@color/green"
|
||||
android:progressTint="@color/green" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<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:layout_marginTop="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
@ -264,8 +300,8 @@
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginEnd="14dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
|
||||
Loading…
Reference in New Issue
Block a user