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=".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" />
|
||||
|
||||
</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.Event
|
||||
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 kotlinx.coroutines.channels.BroadcastChannel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
|
@ -27,6 +30,9 @@ class Otter : Application() {
|
|||
val requestBus: BroadcastChannel<Request> = BroadcastChannel(10)
|
||||
val progressBus: BroadcastChannel<Triple<Int, Int, Int>> = ConflatedBroadcastChannel()
|
||||
|
||||
var exoCache: SimpleCache? = null
|
||||
var exoDatabase: ExoDatabaseProvider? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
|
@ -35,6 +41,15 @@ class Otter : Application() {
|
|||
Thread.setDefaultUncaughtExceptionHandler(CrashReportHandler())
|
||||
|
||||
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")) {
|
||||
"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.fragments.*
|
||||
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.repositories.FavoritedRepository
|
||||
import com.github.apognu.otter.repositories.FavoritesRepository
|
||||
|
|
|
@ -105,6 +105,7 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
|
|||
when (it.itemId) {
|
||||
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_pin -> CommandBus.send(Command.PinTrack(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.AudioFocusRequest
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.view.KeyEvent
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.google.android.exoplayer2.*
|
||||
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.trackselection.TrackSelectionArray
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
|
@ -25,6 +30,7 @@ import kotlinx.coroutines.Job
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
|
||||
class PlayerService : Service() {
|
||||
private lateinit var queue: QueueManager
|
||||
|
@ -186,6 +192,16 @@ class PlayerService : Service() {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
@ -246,7 +262,7 @@ class PlayerService : Service() {
|
|||
state(false)
|
||||
player.release()
|
||||
|
||||
queue.cache.release()
|
||||
Otter.get().exoCache?.release()
|
||||
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.github.apognu.otter.playback
|
|||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.utils.*
|
||||
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.upstream.DefaultHttpDataSourceFactory
|
||||
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.gson.Gson
|
||||
import com.preference.PowerPreference
|
||||
|
||||
class QueueManager(val context: Context) {
|
||||
var cache: SimpleCache
|
||||
var metadata: MutableList<Track> = mutableListOf()
|
||||
val datasources = ConcatenatingMediaSource()
|
||||
var current = -1
|
||||
|
||||
init {
|
||||
PowerPreference.getDefaultFile().getInt("media_cache_size", 1).toLong().also {
|
||||
cache = SimpleCache(
|
||||
context.cacheDir.resolve("media"),
|
||||
LeastRecentlyUsedCacheEvictor(it * 1024 * 1024 * 1024)
|
||||
)
|
||||
}
|
||||
companion object {
|
||||
fun factory(context: Context): 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(Otter.get().exoCache, http)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
Cache.get(context, "queue")?.let { json ->
|
||||
gsonDeserializerOf(QueueCache::class.java).deserialize(json)?.let { cache ->
|
||||
metadata = cache.data.toMutableList()
|
||||
|
||||
val factory = factory()
|
||||
val factory = factory(context)
|
||||
|
||||
datasources.addMediaSources(metadata.map { track ->
|
||||
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>) {
|
||||
val factory = factory()
|
||||
val factory = factory(context)
|
||||
|
||||
val sources = tracks.map { track ->
|
||||
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
||||
|
@ -87,7 +79,7 @@ class QueueManager(val context: Context) {
|
|||
}
|
||||
|
||||
fun append(tracks: List<Track>) {
|
||||
val factory = factory()
|
||||
val factory = factory(context)
|
||||
val missingTracks = tracks.filter { metadata.indexOf(it) == -1 }
|
||||
|
||||
val sources = missingTracks.map { track ->
|
||||
|
@ -105,7 +97,7 @@ class QueueManager(val context: Context) {
|
|||
}
|
||||
|
||||
fun insertNext(track: Track) {
|
||||
val factory = factory()
|
||||
val factory = factory(context)
|
||||
val url = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "")
|
||||
|
||||
if (metadata.indexOf(track) == -1) {
|
||||
|
|
|
@ -16,7 +16,9 @@ object AppContext {
|
|||
const val PREFS_CREDENTIALS = "credentials"
|
||||
|
||||
const val NOTIFICATION_MEDIA_CONTROL = 1
|
||||
const val NOTIFICATION_DOWNLOADS = 2
|
||||
const val NOTIFICATION_CHANNEL_MEDIA_CONTROL = "mediacontrols"
|
||||
const val NOTIFICATION_CHANNEL_DOWNLOADS = "downloads"
|
||||
|
||||
const val PAGE_SIZE = 50
|
||||
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 PlayTrack(val index: Int) : Command()
|
||||
class PinTrack(val track: Track) : Command()
|
||||
}
|
||||
|
||||
sealed class Event {
|
||||
|
|
|
@ -9,4 +9,8 @@
|
|||
android:id="@+id/track_play_next"
|
||||
android:title="@string/playback_queue_play_next" />
|
||||
|
||||
<item
|
||||
android:id="@+id/track_pin"
|
||||
android:title="Pin to cache" />
|
||||
|
||||
</menu>
|
Loading…
Reference in New Issue