Better handling of download progress and event. Added an option to retry failed downloads. Performance improvement around downloads UI.
This commit is contained in:
parent
94fd3d51aa
commit
a2c35595c7
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
GlobalScope.launch(IO) {
|
||||||
|
EventBus.get().collect { event ->
|
||||||
|
if (event is Event.DownloadChanged) {
|
||||||
|
refreshTrack(event.download)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
GlobalScope.launch(Main) {
|
|
||||||
while (true) {
|
|
||||||
refresh()
|
refresh()
|
||||||
delay(1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
listener.refresh()
|
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,13 +81,17 @@ 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 ->
|
||||||
|
withContext(Main) {
|
||||||
adapter.currentTrack = response.track
|
adapter.currentTrack = response.track
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshDownloadedTracks()
|
||||||
|
}
|
||||||
|
|
||||||
play.setOnClickListener {
|
play.setOnClickListener {
|
||||||
CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled()))
|
CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled()))
|
||||||
|
|
||||||
|
@ -117,14 +124,21 @@ 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun refreshDownloadedTracks() {
|
||||||
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
||||||
|
|
||||||
|
withContext(Main) {
|
||||||
adapter.data = adapter.data.map {
|
adapter.data = adapter.data.map {
|
||||||
it.downloaded = downloaded.contains(it.id)
|
it.downloaded = downloaded.contains(it.id)
|
||||||
it
|
it
|
||||||
|
@ -133,13 +147,23 @@ class TracksFragment : FunkwhaleFragment<Track, TracksAdapter>() {
|
||||||
adapter.notifyDataSetChanged()
|
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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue