Initial handling of track downloads.
This commit is contained in:
parent
a0e201e68f
commit
2dfabf74e9
|
@ -45,6 +45,15 @@
|
||||||
|
|
||||||
<service android:name="com.github.apognu.otter.playback.PlayerService" />
|
<service android:name="com.github.apognu.otter.playback.PlayerService" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".playback.PinService"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.google.android.exoplayer.downloadService.action.RESTART" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
<receiver android:name="com.github.apognu.otter.playback.MediaControlActionReceiver" />
|
<receiver android:name="com.github.apognu.otter.playback.MediaControlActionReceiver" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
|
@ -6,6 +6,9 @@ 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.upstream.cache.LeastRecentlyUsedCacheEvictor
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
||||||
import com.preference.PowerPreference
|
import com.preference.PowerPreference
|
||||||
import kotlinx.coroutines.channels.BroadcastChannel
|
import kotlinx.coroutines.channels.BroadcastChannel
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
@ -27,6 +30,9 @@ 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
|
||||||
|
var exoDatabase: ExoDatabaseProvider? = null
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
@ -35,6 +41,15 @@ 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)
|
||||||
|
|
|
@ -22,6 +22,7 @@ import androidx.fragment.app.FragmentManager
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.fragments.*
|
import com.github.apognu.otter.fragments.*
|
||||||
import com.github.apognu.otter.playback.MediaControlsManager
|
import com.github.apognu.otter.playback.MediaControlsManager
|
||||||
|
import com.github.apognu.otter.playback.PinService
|
||||||
import com.github.apognu.otter.playback.PlayerService
|
import com.github.apognu.otter.playback.PlayerService
|
||||||
import com.github.apognu.otter.repositories.FavoritedRepository
|
import com.github.apognu.otter.repositories.FavoritedRepository
|
||||||
import com.github.apognu.otter.repositories.FavoritesRepository
|
import com.github.apognu.otter.repositories.FavoritesRepository
|
||||||
|
|
|
@ -105,6 +105,7 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
|
||||||
when (it.itemId) {
|
when (it.itemId) {
|
||||||
R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track)))
|
R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track)))
|
||||||
R.id.track_play_next -> CommandBus.send(Command.PlayNext(track))
|
R.id.track_play_next -> CommandBus.send(Command.PlayNext(track))
|
||||||
|
R.id.track_pin -> CommandBus.send(Command.PinTrack(track))
|
||||||
R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(track))
|
R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(track))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package com.github.apognu.otter.playback
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import com.github.apognu.otter.Otter
|
||||||
|
import com.github.apognu.otter.R
|
||||||
|
import com.github.apognu.otter.utils.AppContext
|
||||||
|
import com.google.android.exoplayer2.offline.*
|
||||||
|
import com.google.android.exoplayer2.scheduler.Scheduler
|
||||||
|
import com.google.android.exoplayer2.ui.DownloadNotificationHelper
|
||||||
|
|
||||||
|
class PinService : DownloadService(AppContext.NOTIFICATION_DOWNLOADS) {
|
||||||
|
private val manager by lazy {
|
||||||
|
val database = Otter.get().exoDatabase
|
||||||
|
val cache = Otter.get().exoCache
|
||||||
|
val helper = DownloaderConstructorHelper(cache, QueueManager.factory(this))
|
||||||
|
|
||||||
|
DownloadManager(this, DefaultDownloadIndex(database), DefaultDownloaderFactory(helper))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDownloadManager() = manager
|
||||||
|
|
||||||
|
override fun getScheduler(): Scheduler? = null
|
||||||
|
|
||||||
|
override fun getForegroundNotification(downloads: MutableList<Download>?): Notification {
|
||||||
|
return DownloadNotificationHelper(this, AppContext.NOTIFICATION_CHANNEL_DOWNLOADS).buildProgressNotification(R.drawable.ottershape, null, null, downloads)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDownloads() = manager.downloadIndex.getDownloads()
|
||||||
|
}
|
|
@ -8,14 +8,19 @@ 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
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
|
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.*
|
||||||
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
|
||||||
|
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 kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
@ -25,6 +30,7 @@ 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
|
||||||
|
@ -186,6 +192,16 @@ class PlayerService : Service() {
|
||||||
}
|
}
|
||||||
|
|
||||||
is Command.SetRepeatMode -> player.repeatMode = message.mode
|
is Command.SetRepeatMode -> player.repeatMode = message.mode
|
||||||
|
|
||||||
|
is Command.PinTrack -> {
|
||||||
|
message.track.bestUpload()?.let { upload ->
|
||||||
|
val url = mustNormalizeUrl(upload.listen_url)
|
||||||
|
|
||||||
|
DownloadRequest(url, DownloadRequest.TYPE_PROGRESSIVE, Uri.parse(url), Collections.emptyList(), null, null).also {
|
||||||
|
DownloadService.sendAddDownload(this@PlayerService, PinService::class.java, it, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player.playWhenReady) {
|
if (player.playWhenReady) {
|
||||||
|
@ -246,7 +262,7 @@ class PlayerService : Service() {
|
||||||
state(false)
|
state(false)
|
||||||
player.release()
|
player.release()
|
||||||
|
|
||||||
queue.cache.release()
|
Otter.get().exoCache?.release()
|
||||||
|
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.github.apognu.otter.playback
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
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.github.kittinunf.fuel.gson.gsonDeserializerOf
|
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||||
|
@ -9,31 +10,34 @@ import com.google.android.exoplayer2.source.ConcatenatingMediaSource
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
|
||||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory
|
||||||
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
|
|
||||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
|
||||||
import com.google.android.exoplayer2.util.Util
|
import com.google.android.exoplayer2.util.Util
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.preference.PowerPreference
|
|
||||||
|
|
||||||
class QueueManager(val context: Context) {
|
class QueueManager(val context: Context) {
|
||||||
var cache: SimpleCache
|
|
||||||
var metadata: MutableList<Track> = mutableListOf()
|
var metadata: MutableList<Track> = mutableListOf()
|
||||||
val datasources = ConcatenatingMediaSource()
|
val datasources = ConcatenatingMediaSource()
|
||||||
var current = -1
|
var current = -1
|
||||||
|
|
||||||
init {
|
companion object {
|
||||||
PowerPreference.getDefaultFile().getInt("media_cache_size", 1).toLong().also {
|
fun factory(context: Context): CacheDataSourceFactory {
|
||||||
cache = SimpleCache(
|
val http = DefaultHttpDataSourceFactory(Util.getUserAgent(context, context.getString(R.string.app_name))).apply {
|
||||||
context.cacheDir.resolve("media"),
|
defaultRequestProperties.apply {
|
||||||
LeastRecentlyUsedCacheEvictor(it * 1024 * 1024 * 1024)
|
if (!Settings.isAnonymous()) {
|
||||||
)
|
set("Authorization", "Bearer ${Settings.getAccessToken()}")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CacheDataSourceFactory(Otter.get().exoCache, http)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
Cache.get(context, "queue")?.let { json ->
|
Cache.get(context, "queue")?.let { json ->
|
||||||
gsonDeserializerOf(QueueCache::class.java).deserialize(json)?.let { cache ->
|
gsonDeserializerOf(QueueCache::class.java).deserialize(json)?.let { cache ->
|
||||||
metadata = cache.data.toMutableList()
|
metadata = cache.data.toMutableList()
|
||||||
|
|
||||||
val factory = factory()
|
val factory = factory(context)
|
||||||
|
|
||||||
datasources.addMediaSources(metadata.map { track ->
|
datasources.addMediaSources(metadata.map { track ->
|
||||||
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
||||||
|
@ -56,20 +60,8 @@ class QueueManager(val context: Context) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun factory(): CacheDataSourceFactory {
|
|
||||||
val http = DefaultHttpDataSourceFactory(Util.getUserAgent(context, context.getString(R.string.app_name))).apply {
|
|
||||||
defaultRequestProperties.apply {
|
|
||||||
if (!Settings.isAnonymous()) {
|
|
||||||
set("Authorization", "Bearer ${Settings.getAccessToken()}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return CacheDataSourceFactory(cache, http)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun replace(tracks: List<Track>) {
|
fun replace(tracks: List<Track>) {
|
||||||
val factory = factory()
|
val factory = factory(context)
|
||||||
|
|
||||||
val sources = tracks.map { track ->
|
val sources = tracks.map { track ->
|
||||||
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
||||||
|
@ -87,7 +79,7 @@ class QueueManager(val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun append(tracks: List<Track>) {
|
fun append(tracks: List<Track>) {
|
||||||
val factory = factory()
|
val factory = factory(context)
|
||||||
val missingTracks = tracks.filter { metadata.indexOf(it) == -1 }
|
val missingTracks = tracks.filter { metadata.indexOf(it) == -1 }
|
||||||
|
|
||||||
val sources = missingTracks.map { track ->
|
val sources = missingTracks.map { track ->
|
||||||
|
@ -105,7 +97,7 @@ class QueueManager(val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insertNext(track: Track) {
|
fun insertNext(track: Track) {
|
||||||
val factory = factory()
|
val factory = factory(context)
|
||||||
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
||||||
|
|
||||||
if (metadata.indexOf(track) == -1) {
|
if (metadata.indexOf(track) == -1) {
|
||||||
|
|
|
@ -16,7 +16,9 @@ object AppContext {
|
||||||
const val PREFS_CREDENTIALS = "credentials"
|
const val PREFS_CREDENTIALS = "credentials"
|
||||||
|
|
||||||
const val NOTIFICATION_MEDIA_CONTROL = 1
|
const val NOTIFICATION_MEDIA_CONTROL = 1
|
||||||
|
const val NOTIFICATION_DOWNLOADS = 2
|
||||||
const val NOTIFICATION_CHANNEL_MEDIA_CONTROL = "mediacontrols"
|
const val NOTIFICATION_CHANNEL_MEDIA_CONTROL = "mediacontrols"
|
||||||
|
const val NOTIFICATION_CHANNEL_DOWNLOADS = "downloads"
|
||||||
|
|
||||||
const val PAGE_SIZE = 50
|
const val PAGE_SIZE = 50
|
||||||
const val TRANSITION_DURATION = 300L
|
const val TRANSITION_DURATION = 300L
|
||||||
|
@ -62,6 +64,24 @@ object AppContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Build.VERSION_CODES.O.onApi {
|
||||||
|
(context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).let { manager ->
|
||||||
|
NotificationChannel(
|
||||||
|
NOTIFICATION_CHANNEL_DOWNLOADS,
|
||||||
|
"Downloads",
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
).run {
|
||||||
|
description = "Downloads"
|
||||||
|
|
||||||
|
enableLights(false)
|
||||||
|
enableVibration(false)
|
||||||
|
setSound(null, null)
|
||||||
|
|
||||||
|
manager.createNotificationChannel(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ sealed class Command {
|
||||||
class SetRepeatMode(val mode: Int) : Command()
|
class SetRepeatMode(val mode: Int) : Command()
|
||||||
|
|
||||||
class PlayTrack(val index: Int) : Command()
|
class PlayTrack(val index: Int) : Command()
|
||||||
|
class PinTrack(val track: Track) : Command()
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Event {
|
sealed class Event {
|
||||||
|
|
|
@ -9,4 +9,8 @@
|
||||||
android:id="@+id/track_play_next"
|
android:id="@+id/track_play_next"
|
||||||
android:title="@string/playback_queue_play_next" />
|
android:title="@string/playback_queue_play_next" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/track_pin"
|
||||||
|
android:title="Pin to cache" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
Loading…
Reference in New Issue