Initial handling of track downloads.

This commit is contained in:
Antoine POPINEAU 2020-06-10 16:25:20 +02:00
parent a0e201e68f
commit 2dfabf74e9
No known key found for this signature in database
GPG Key ID: A78AC64694F84063
10 changed files with 116 additions and 28 deletions

View File

@ -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>

View File

@ -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)

View File

@ -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

View File

@ -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))
} }

View File

@ -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()
}

View File

@ -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()

View File

@ -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) {

View File

@ -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)
}
}
}
} }
} }

View File

@ -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 {

View File

@ -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>