Better handling of download progress and event. Added an option to retry failed downloads. Performance improvement around downloads UI.

This commit is contained in:
Antoine POPINEAU 2020-06-14 14:59:50 +02:00
parent 94fd3d51aa
commit a2c35595c7
No known key found for this signature in database
GPG Key ID: A78AC64694F84063
11 changed files with 187 additions and 100 deletions

View File

@ -111,6 +111,7 @@ dependencies {
implementation("com.android.support.constraint:constraint-layout:1.1.3") implementation("com.android.support.constraint:constraint-layout:1.1.3")
implementation("com.google.android.exoplayer:exoplayer-core:2.11.5") implementation("com.google.android.exoplayer:exoplayer-core:2.11.5")
implementation("com.google.android.exoplayer:exoplayer-ui:2.11.5")
implementation("com.google.android.exoplayer:extension-mediasession:2.11.5") implementation("com.google.android.exoplayer:extension-mediasession:2.11.5")
implementation("com.aliassadi:power-preference-lib:1.4.1") implementation("com.aliassadi:power-preference-lib:1.4.1")

View File

@ -2,11 +2,16 @@ package com.github.apognu.otter
import android.app.Application import android.app.Application
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import com.github.apognu.otter.playback.QueueManager
import com.github.apognu.otter.utils.Cache import com.github.apognu.otter.utils.Cache
import com.github.apognu.otter.utils.Command import com.github.apognu.otter.utils.Command
import com.github.apognu.otter.utils.Event import com.github.apognu.otter.utils.Event
import com.github.apognu.otter.utils.Request import com.github.apognu.otter.utils.Request
import com.google.android.exoplayer2.database.ExoDatabaseProvider import com.google.android.exoplayer2.database.ExoDatabaseProvider
import com.google.android.exoplayer2.offline.DefaultDownloadIndex
import com.google.android.exoplayer2.offline.DefaultDownloaderFactory
import com.google.android.exoplayer2.offline.DownloadManager
import com.google.android.exoplayer2.offline.DownloaderConstructorHelper
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
import com.google.android.exoplayer2.upstream.cache.SimpleCache import com.google.android.exoplayer2.upstream.cache.SimpleCache
import com.preference.PowerPreference import com.preference.PowerPreference
@ -30,8 +35,21 @@ class Otter : Application() {
val requestBus: BroadcastChannel<Request> = BroadcastChannel(10) val requestBus: BroadcastChannel<Request> = BroadcastChannel(10)
val progressBus: BroadcastChannel<Triple<Int, Int, Int>> = ConflatedBroadcastChannel() val progressBus: BroadcastChannel<Triple<Int, Int, Int>> = ConflatedBroadcastChannel()
var exoCache: SimpleCache? = null private val exoDatabase: ExoDatabaseProvider by lazy { ExoDatabaseProvider(this) }
var exoDatabase: ExoDatabaseProvider? = null val exoCache: SimpleCache by lazy {
PowerPreference.getDefaultFile().getInt("media_cache_size", 1).toLong().let {
SimpleCache(
cacheDir.resolve("media"),
LeastRecentlyUsedCacheEvictor(it * 1024 * 1024 * 1024),
exoDatabase
)
}
}
val exoDownloadManager: DownloadManager by lazy {
DownloaderConstructorHelper(exoCache, QueueManager.factory(this)).run {
DownloadManager(this@Otter, DefaultDownloadIndex(exoDatabase), DefaultDownloaderFactory(this))
}
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -41,15 +59,6 @@ class Otter : Application() {
Thread.setDefaultUncaughtExceptionHandler(CrashReportHandler()) Thread.setDefaultUncaughtExceptionHandler(CrashReportHandler())
instance = this instance = this
exoDatabase = ExoDatabaseProvider(this)
PowerPreference.getDefaultFile().getInt("media_cache_size", 1).toLong().also {
exoCache = SimpleCache(
cacheDir.resolve("media"),
LeastRecentlyUsedCacheEvictor(it * 1024 * 1024 * 1024),
exoDatabase
)
}
when (PowerPreference.getDefaultFile().getString("night_mode")) { when (PowerPreference.getDefaultFile().getString("night_mode")) {
"on" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) "on" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)

View File

@ -1,17 +1,19 @@
package com.github.apognu.otter.activities package com.github.apognu.otter.activities
import android.os.Bundle import android.os.Bundle
import kotlinx.coroutines.flow.collect
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.github.apognu.otter.R import com.github.apognu.otter.R
import com.github.apognu.otter.adapters.DownloadsAdapter import com.github.apognu.otter.adapters.DownloadsAdapter
import com.github.apognu.otter.utils.* import com.github.apognu.otter.utils.*
import com.google.gson.Gson import com.google.android.exoplayer2.offline.Download
import kotlinx.android.synthetic.main.activity_downloads.* import kotlinx.android.synthetic.main.activity_downloads.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class DownloadsActivity : AppCompatActivity() { class DownloadsActivity : AppCompatActivity() {
lateinit var adapter: DownloadsAdapter lateinit var adapter: DownloadsAdapter
@ -21,17 +23,24 @@ class DownloadsActivity : AppCompatActivity() {
setContentView(R.layout.activity_downloads) setContentView(R.layout.activity_downloads)
adapter = DownloadsAdapter(this, RefreshListener()).also { adapter = DownloadsAdapter(this, DownloadChangedListener()).also {
downloads.layoutManager = LinearLayoutManager(this) downloads.layoutManager = LinearLayoutManager(this)
downloads.adapter = it downloads.adapter = it
} }
}
GlobalScope.launch(Main) { override fun onResume() {
while (true) { super.onResume()
refresh()
delay(1000) GlobalScope.launch(IO) {
EventBus.get().collect { event ->
if (event is Event.DownloadChanged) {
refreshTrack(event.download)
}
} }
} }
refresh()
} }
private fun refresh() { private fun refresh() {
@ -42,7 +51,7 @@ class DownloadsActivity : AppCompatActivity() {
while (response.cursor.moveToNext()) { while (response.cursor.moveToNext()) {
val download = response.cursor.download val download = response.cursor.download
Gson().fromJson(String(download.request.data), DownloadInfo::class.java)?.let { info -> download.getMetadata()?.let { info ->
adapter.downloads.add(info.apply { adapter.downloads.add(info.apply {
this.download = download this.download = download
}) })
@ -54,9 +63,26 @@ class DownloadsActivity : AppCompatActivity() {
} }
} }
inner class RefreshListener : DownloadsAdapter.OnRefreshListener { private suspend fun refreshTrack(download: Download) {
override fun refresh() { if (download.state == Download.STATE_COMPLETED) {
this@DownloadsActivity.refresh() download.getMetadata()?.let { info ->
adapter.downloads.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
withContext(Main) {
adapter.downloads[match.second] = info.apply {
this.download = download
}
adapter.notifyItemChanged(match.second)
}
}
}
}
}
inner class DownloadChangedListener : DownloadsAdapter.OnDownloadChangedListener {
override fun onItemRemoved(index: Int) {
adapter.downloads.removeAt(index)
adapter.notifyDataSetChanged()
} }
} }
} }

View File

@ -8,14 +8,14 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R import com.github.apognu.otter.R
import com.github.apognu.otter.playback.PinService import com.github.apognu.otter.playback.PinService
import com.github.apognu.otter.utils.DownloadInfo import com.github.apognu.otter.utils.*
import com.google.android.exoplayer2.offline.Download import com.google.android.exoplayer2.offline.Download
import com.google.android.exoplayer2.offline.DownloadService import com.google.android.exoplayer2.offline.DownloadService
import kotlinx.android.synthetic.main.row_download.view.* import kotlinx.android.synthetic.main.row_download.view.*
class DownloadsAdapter(private val context: Context, private val listener: OnRefreshListener) : RecyclerView.Adapter<DownloadsAdapter.ViewHolder>() { class DownloadsAdapter(private val context: Context, private val listener: OnDownloadChangedListener) : RecyclerView.Adapter<DownloadsAdapter.ViewHolder>() {
interface OnRefreshListener { interface OnDownloadChangedListener {
fun refresh() fun onItemRemoved(index: Int)
} }
var downloads: MutableList<DownloadInfo> = mutableListOf() var downloads: MutableList<DownloadInfo> = mutableListOf()
@ -38,7 +38,15 @@ class DownloadsAdapter(private val context: Context, private val listener: OnRef
when (state.isTerminalState) { when (state.isTerminalState) {
true -> { true -> {
holder.progress.visibility = View.GONE holder.progress.visibility = View.GONE
holder.toggle.visibility = View.GONE
when (state.state) {
Download.STATE_FAILED -> {
holder.toggle.setImageDrawable(context.getDrawable(R.drawable.retry))
holder.progress.visibility = View.GONE
}
else -> holder.toggle.visibility = View.GONE
}
} }
false -> { false -> {
@ -53,25 +61,29 @@ class DownloadsAdapter(private val context: Context, private val listener: OnRef
} }
Download.STATE_STOPPED -> holder.toggle.setImageIcon(Icon.createWithResource(context, R.drawable.play)) Download.STATE_STOPPED -> holder.toggle.setImageIcon(Icon.createWithResource(context, R.drawable.play))
else -> holder.toggle.setImageIcon(Icon.createWithResource(context, R.drawable.pause)) else -> holder.toggle.setImageIcon(Icon.createWithResource(context, R.drawable.pause))
} }
} }
} }
holder.toggle.setOnClickListener { holder.toggle.setOnClickListener {
if (state.state == Download.STATE_DOWNLOADING) { when (state.state) {
DownloadService.sendSetStopReason(context, PinService::class.java, download.contentId, 1, false) Download.STATE_DOWNLOADING -> DownloadService.sendSetStopReason(context, PinService::class.java, download.contentId, 1, false)
} else {
DownloadService.sendSetStopReason(context, PinService::class.java, download.contentId, Download.STOP_REASON_NONE, false)
}
listener.refresh() Download.STATE_FAILED -> {
Track(download.id, download.title, Artist(0, download.artist, listOf()),Album(0, Album.Artist(""), "", Covers("")), 0, listOf(Track.Upload(download.contentId, 0, 0))).also {
PinService.download(context, it)
}
}
else -> DownloadService.sendSetStopReason(context, PinService::class.java, download.contentId, Download.STOP_REASON_NONE, false)
}
} }
holder.delete.setOnClickListener { holder.delete.setOnClickListener {
listener.onItemRemoved(position)
DownloadService.sendRemoveDownload(context, PinService::class.java, download.contentId, false) DownloadService.sendRemoveDownload(context, PinService::class.java, download.contentId, false)
listener.refresh()
} }
} }
} }

View File

@ -11,13 +11,16 @@ import com.github.apognu.otter.adapters.TracksAdapter
import com.github.apognu.otter.repositories.FavoritesRepository import com.github.apognu.otter.repositories.FavoritesRepository
import com.github.apognu.otter.repositories.TracksRepository import com.github.apognu.otter.repositories.TracksRepository
import com.github.apognu.otter.utils.* import com.github.apognu.otter.utils.*
import com.google.android.exoplayer2.offline.Download
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.fragment_tracks.* import kotlinx.android.synthetic.main.fragment_tracks.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class TracksFragment : FunkwhaleFragment<Track, TracksAdapter>() { class TracksFragment : FunkwhaleFragment<Track, TracksAdapter>() {
override val viewRes = R.layout.fragment_tracks override val viewRes = R.layout.fragment_tracks
@ -78,11 +81,15 @@ class TracksFragment : FunkwhaleFragment<Track, TracksAdapter>() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
GlobalScope.launch(Main) { GlobalScope.launch(IO) {
RequestBus.send(Request.GetCurrentTrack).wait<Response.CurrentTrack>()?.let { response -> RequestBus.send(Request.GetCurrentTrack).wait<Response.CurrentTrack>()?.let { response ->
adapter.currentTrack = response.track withContext(Main) {
adapter.notifyDataSetChanged() adapter.currentTrack = response.track
adapter.notifyDataSetChanged()
}
} }
refreshDownloadedTracks()
} }
play.setOnClickListener { play.setOnClickListener {
@ -117,29 +124,46 @@ class TracksFragment : FunkwhaleFragment<Track, TracksAdapter>() {
} }
private fun watchEventBus() { private fun watchEventBus() {
GlobalScope.launch(Main) { GlobalScope.launch(IO) {
EventBus.get().collect { message -> EventBus.get().collect { message ->
when (message) { when (message) {
is Event.TrackPlayed -> refreshCurrentTrack() is Event.TrackPlayed -> refreshCurrentTrack()
is Event.RefreshTrack -> refreshCurrentTrack() is Event.RefreshTrack -> refreshCurrentTrack()
is Event.DownloadChanged -> { is Event.DownloadChanged -> refreshDownloadedTrack(message.download)
val downloaded = TracksRepository.getDownloadedIds() ?: listOf() }
}
}
}
adapter.data = adapter.data.map { private suspend fun refreshDownloadedTracks() {
it.downloaded = downloaded.contains(it.id) val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
it
}.toMutableList()
adapter.notifyDataSetChanged() withContext(Main) {
adapter.data = adapter.data.map {
it.downloaded = downloaded.contains(it.id)
it
}.toMutableList()
adapter.notifyDataSetChanged()
}
}
private suspend fun refreshDownloadedTrack(download: Download) {
if (download.state == Download.STATE_COMPLETED) {
download.getMetadata()?.let { info ->
adapter.data.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
withContext(Main) {
adapter.data[match.second].downloaded = true
adapter.notifyItemChanged(match.second)
} }
} }
} }
} }
} }
private fun refreshCurrentTrack() { private suspend fun refreshCurrentTrack() {
GlobalScope.launch(Main) { RequestBus.send(Request.GetCurrentTrack).wait<Response.CurrentTrack>()?.let { response ->
RequestBus.send(Request.GetCurrentTrack).wait<Response.CurrentTrack>()?.let { response -> withContext(Main) {
adapter.currentTrack = response.track adapter.currentTrack = response.track
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }

View File

@ -1,25 +1,45 @@
package com.github.apognu.otter.playback package com.github.apognu.otter.playback
import android.app.Notification import android.app.Notification
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import com.github.apognu.otter.Otter import com.github.apognu.otter.Otter
import com.github.apognu.otter.R import com.github.apognu.otter.R
import com.github.apognu.otter.utils.* import com.github.apognu.otter.utils.*
import com.google.android.exoplayer2.offline.* import com.google.android.exoplayer2.offline.Download
import com.google.android.exoplayer2.offline.DownloadManager
import com.google.android.exoplayer2.offline.DownloadRequest
import com.google.android.exoplayer2.offline.DownloadService
import com.google.android.exoplayer2.scheduler.Scheduler import com.google.android.exoplayer2.scheduler.Scheduler
import com.google.android.exoplayer2.ui.DownloadNotificationHelper import com.google.android.exoplayer2.ui.DownloadNotificationHelper
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.*
class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) { class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
private val manager by lazy { companion object {
val database = Otter.get().exoDatabase fun download(context: Context, track: Track) {
val cache = Otter.get().exoCache track.bestUpload()?.let { upload ->
val helper = DownloaderConstructorHelper(cache, QueueManager.factory(this)) val url = mustNormalizeUrl(upload.listen_url)
val data = Gson().toJson(
DownloadInfo(
track.id,
url,
track.title,
track.artist.name,
null
)
).toByteArray()
DownloadManager(this, DefaultDownloadIndex(database), DefaultDownloaderFactory(helper)) DownloadRequest(url, DownloadRequest.TYPE_PROGRESSIVE, Uri.parse(url), Collections.emptyList(), null, data).also {
sendAddDownload(context, PinService::class.java, it, false)
}
}
}
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -36,22 +56,25 @@ class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
return super.onStartCommand(intent, flags, startId) return super.onStartCommand(intent, flags, startId)
} }
override fun getDownloadManager() = manager override fun getDownloadManager() = Otter.get().exoDownloadManager.apply {
addListener(DownloadListener())
}
override fun getScheduler(): Scheduler? = null override fun getScheduler(): Scheduler? = null
override fun getForegroundNotification(downloads: MutableList<Download>?): Notification { override fun getForegroundNotification(downloads: MutableList<Download>): Notification {
val quantity = downloads?.size ?: 0 val description = resources.getQuantityString(R.plurals.downloads_description, downloads.size, downloads.size)
val description = resources.getQuantityString(R.plurals.downloads_description, quantity, quantity)
return DownloadNotificationHelper(this, AppContext.NOTIFICATION_CHANNEL_DOWNLOADS).buildProgressNotification(R.drawable.downloads, null, description, downloads) return DownloadNotificationHelper(this, AppContext.NOTIFICATION_CHANNEL_DOWNLOADS).buildProgressNotification(R.drawable.downloads, null, description, downloads)
} }
override fun onDownloadChanged(download: Download?) { private fun getDownloads() = downloadManager.downloadIndex.getDownloads()
super.onDownloadChanged(download)
EventBus.send(Event.DownloadChanged) inner class DownloadListener : DownloadManager.Listener {
override fun onDownloadChanged(downloadManager: DownloadManager, download: Download) {
super.onDownloadChanged(downloadManager, download)
EventBus.send(Event.DownloadChanged(download))
}
} }
private fun getDownloads() = manager.downloadIndex.getDownloads()
} }

View File

@ -8,7 +8,6 @@ import android.content.IntentFilter
import android.media.AudioAttributes import android.media.AudioAttributes
import android.media.AudioFocusRequest import android.media.AudioFocusRequest
import android.media.AudioManager import android.media.AudioManager
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
@ -16,13 +15,13 @@ import android.view.KeyEvent
import com.github.apognu.otter.Otter import com.github.apognu.otter.Otter
import com.github.apognu.otter.R import com.github.apognu.otter.R
import com.github.apognu.otter.utils.* import com.github.apognu.otter.utils.*
import com.google.android.exoplayer2.* import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlaybackException
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.offline.DownloadRequest
import com.google.android.exoplayer2.offline.DownloadService.sendAddDownload
import com.google.android.exoplayer2.source.TrackGroupArray import com.google.android.exoplayer2.source.TrackGroupArray
import com.google.android.exoplayer2.trackselection.TrackSelectionArray import com.google.android.exoplayer2.trackselection.TrackSelectionArray
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -30,7 +29,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.*
class PlayerService : Service() { class PlayerService : Service() {
private lateinit var queue: QueueManager private lateinit var queue: QueueManager
@ -88,7 +86,7 @@ class PlayerService : Service() {
mediaControlsManager = MediaControlsManager(this, mediaSession) mediaControlsManager = MediaControlsManager(this, mediaSession)
player = ExoPlayerFactory.newSimpleInstance(this).apply { player = SimpleExoPlayer.Builder(this).build().apply {
playWhenReady = false playWhenReady = false
playerEventListener = PlayerEventListener().also { playerEventListener = PlayerEventListener().also {
@ -98,12 +96,12 @@ class PlayerService : Service() {
MediaSessionConnector(mediaSession).also { MediaSessionConnector(mediaSession).also {
it.setPlayer(this) it.setPlayer(this)
it.setMediaButtonEventHandler { player, _, mediaButtonEvent -> it.setMediaButtonEventHandler { player, _, mediaButtonEvent ->
mediaButtonEvent?.extras?.getParcelable<KeyEvent>(Intent.EXTRA_KEY_EVENT)?.let { key -> mediaButtonEvent.extras?.getParcelable<KeyEvent>(Intent.EXTRA_KEY_EVENT)?.let { key ->
if (key.action == KeyEvent.ACTION_UP) { if (key.action == KeyEvent.ACTION_UP) {
when (key.keyCode) { when (key.keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY -> state(true) KeyEvent.KEYCODE_MEDIA_PLAY -> state(true)
KeyEvent.KEYCODE_MEDIA_PAUSE -> state(false) KeyEvent.KEYCODE_MEDIA_PAUSE -> state(false)
KeyEvent.KEYCODE_MEDIA_NEXT -> player?.next() KeyEvent.KEYCODE_MEDIA_NEXT -> player.next()
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> previousTrack() KeyEvent.KEYCODE_MEDIA_PREVIOUS -> previousTrack()
} }
} }
@ -193,8 +191,8 @@ class PlayerService : Service() {
is Command.SetRepeatMode -> player.repeatMode = message.mode is Command.SetRepeatMode -> player.repeatMode = message.mode
is Command.PinTrack -> download(message.track) is Command.PinTrack -> PinService.download(this@PlayerService, message.track)
is Command.PinTracks -> message.tracks.forEach { download(it) } is Command.PinTracks -> message.tracks.forEach { PinService.download(this@PlayerService, it) }
} }
if (player.playWhenReady) { if (player.playWhenReady) {
@ -255,7 +253,7 @@ class PlayerService : Service() {
state(false) state(false)
player.release() player.release()
Otter.get().exoCache?.release() Otter.get().exoCache.release()
stopForeground(true) stopForeground(true)
stopSelf() stopSelf()
@ -340,25 +338,6 @@ class PlayerService : Service() {
player.seekTo(duration.toLong()) player.seekTo(duration.toLong())
} }
private fun download(track: Track) {
track.bestUpload()?.let { upload ->
val url = mustNormalizeUrl(upload.listen_url)
val data = Gson().toJson(
DownloadInfo(
track.id,
url,
track.title,
track.artist.name,
null
)
).toByteArray()
DownloadRequest(url, DownloadRequest.TYPE_PROGRESSIVE, Uri.parse(url), Collections.emptyList(), null, data).also {
sendAddDownload(this@PlayerService, PinService::class.java, it, false)
}
}
}
inner class PlayerEventListener : Player.EventListener { inner class PlayerEventListener : Player.EventListener {
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
super.onPlayerStateChanged(playWhenReady, playbackState) super.onPlayerStateChanged(playWhenReady, playbackState)

View File

@ -4,7 +4,6 @@ import android.content.Context
import com.github.apognu.otter.utils.* import com.github.apognu.otter.utils.*
import com.github.kittinunf.fuel.gson.gsonDeserializerOf import com.github.kittinunf.fuel.gson.gsonDeserializerOf
import com.google.android.exoplayer2.offline.Download import com.google.android.exoplayer2.offline.Download
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
@ -26,7 +25,7 @@ class TracksRepository(override val context: Context?, albumId: Int) : Repositor
while (response.cursor.moveToNext()) { while (response.cursor.moveToNext()) {
val download = response.cursor.download val download = response.cursor.download
Gson().fromJson(String(download.request.data), DownloadInfo::class.java)?.let { download.getMetadata()?.let {
if (download.state == Download.STATE_COMPLETED) { if (download.state == Download.STATE_COMPLETED) {
ids.add(it.id) ids.add(it.id)
} }

View File

@ -1,6 +1,7 @@
package com.github.apognu.otter.utils package com.github.apognu.otter.utils
import com.github.apognu.otter.Otter import com.github.apognu.otter.Otter
import com.google.android.exoplayer2.offline.Download
import com.google.android.exoplayer2.offline.DownloadCursor import com.google.android.exoplayer2.offline.DownloadCursor
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -47,7 +48,7 @@ sealed class Event {
object QueueChanged : Event() object QueueChanged : Event()
object RadioStarted : Event() object RadioStarted : Event()
object ListingsChanged : Event() object ListingsChanged : Event()
object DownloadChanged : Event() class DownloadChanged(val download: Download) : Event()
} }
sealed class Request(var channel: Channel<Response>? = null) { sealed class Request(var channel: Channel<Response>? = null) {

View File

@ -6,6 +6,8 @@ import com.github.apognu.otter.R
import com.github.apognu.otter.fragments.BrowseFragment import com.github.apognu.otter.fragments.BrowseFragment
import com.github.apognu.otter.repositories.Repository import com.github.apognu.otter.repositories.Repository
import com.github.kittinunf.fuel.core.Request import com.github.kittinunf.fuel.core.Request
import com.google.android.exoplayer2.offline.Download
import com.google.gson.Gson
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import com.squareup.picasso.RequestCreator import com.squareup.picasso.RequestCreator
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
@ -62,8 +64,8 @@ fun <T> T.applyOnApi(api: Int, block: T.() -> T): T {
} }
fun Picasso.maybeLoad(url: String?): RequestCreator { fun Picasso.maybeLoad(url: String?): RequestCreator {
if (url == null) return load(R.drawable.cover) return if (url == null) load(R.drawable.cover)
else return load(url) else load(url)
} }
fun Request.authorize(): Request { fun Request.authorize(): Request {
@ -73,3 +75,5 @@ fun Request.authorize(): Request {
} }
} }
} }
fun Download.getMetadata(): DownloadInfo? = Gson().fromJson(String(this.request.data), DownloadInfo::class.java)

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.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</vector>