From 2dfabf74e9e5047ccbfd7f24d4f0d3c5521cba10 Mon Sep 17 00:00:00 2001 From: Antoine POPINEAU Date: Wed, 10 Jun 2020 16:25:20 +0200 Subject: [PATCH] Initial handling of track downloads. --- app/src/main/AndroidManifest.xml | 9 ++++ .../java/com/github/apognu/otter/Otter.kt | 15 ++++++ .../apognu/otter/activities/MainActivity.kt | 1 + .../apognu/otter/adapters/TracksAdapter.kt | 1 + .../apognu/otter/playback/PinService.kt | 29 ++++++++++++ .../apognu/otter/playback/PlayerService.kt | 18 +++++++- .../apognu/otter/playback/QueueManager.kt | 46 ++++++++----------- .../github/apognu/otter/utils/AppContext.kt | 20 ++++++++ .../com/github/apognu/otter/utils/EventBus.kt | 1 + app/src/main/res/menu/row_track.xml | 4 ++ 10 files changed, 116 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/com/github/apognu/otter/playback/PinService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b8660be..3c6e2e1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -45,6 +45,15 @@ + + + + + + + diff --git a/app/src/main/java/com/github/apognu/otter/Otter.kt b/app/src/main/java/com/github/apognu/otter/Otter.kt index 2edeb0b..81af4ac 100644 --- a/app/src/main/java/com/github/apognu/otter/Otter.kt +++ b/app/src/main/java/com/github/apognu/otter/Otter.kt @@ -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 = BroadcastChannel(10) val progressBus: BroadcastChannel> = 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) diff --git a/app/src/main/java/com/github/apognu/otter/activities/MainActivity.kt b/app/src/main/java/com/github/apognu/otter/activities/MainActivity.kt index f471156..2913230 100644 --- a/app/src/main/java/com/github/apognu/otter/activities/MainActivity.kt +++ b/app/src/main/java/com/github/apognu/otter/activities/MainActivity.kt @@ -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 diff --git a/app/src/main/java/com/github/apognu/otter/adapters/TracksAdapter.kt b/app/src/main/java/com/github/apognu/otter/adapters/TracksAdapter.kt index c43b48c..1969c9b 100644 --- a/app/src/main/java/com/github/apognu/otter/adapters/TracksAdapter.kt +++ b/app/src/main/java/com/github/apognu/otter/adapters/TracksAdapter.kt @@ -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)) } diff --git a/app/src/main/java/com/github/apognu/otter/playback/PinService.kt b/app/src/main/java/com/github/apognu/otter/playback/PinService.kt new file mode 100644 index 0000000..3568423 --- /dev/null +++ b/app/src/main/java/com/github/apognu/otter/playback/PinService.kt @@ -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?): Notification { + return DownloadNotificationHelper(this, AppContext.NOTIFICATION_CHANNEL_DOWNLOADS).buildProgressNotification(R.drawable.ottershape, null, null, downloads) + } + + fun getDownloads() = manager.downloadIndex.getDownloads() +} \ No newline at end of file diff --git a/app/src/main/java/com/github/apognu/otter/playback/PlayerService.kt b/app/src/main/java/com/github/apognu/otter/playback/PlayerService.kt index 44dd550..568d6dc 100644 --- a/app/src/main/java/com/github/apognu/otter/playback/PlayerService.kt +++ b/app/src/main/java/com/github/apognu/otter/playback/PlayerService.kt @@ -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() diff --git a/app/src/main/java/com/github/apognu/otter/playback/QueueManager.kt b/app/src/main/java/com/github/apognu/otter/playback/QueueManager.kt index 066f602..dbd6cd8 100644 --- a/app/src/main/java/com/github/apognu/otter/playback/QueueManager.kt +++ b/app/src/main/java/com/github/apognu/otter/playback/QueueManager.kt @@ -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 = 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) { - 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) { - 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) { diff --git a/app/src/main/java/com/github/apognu/otter/utils/AppContext.kt b/app/src/main/java/com/github/apognu/otter/utils/AppContext.kt index e180dac..99459db 100644 --- a/app/src/main/java/com/github/apognu/otter/utils/AppContext.kt +++ b/app/src/main/java/com/github/apognu/otter/utils/AppContext.kt @@ -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) + } + } + } } } diff --git a/app/src/main/java/com/github/apognu/otter/utils/EventBus.kt b/app/src/main/java/com/github/apognu/otter/utils/EventBus.kt index f57eace..b617e90 100644 --- a/app/src/main/java/com/github/apognu/otter/utils/EventBus.kt +++ b/app/src/main/java/com/github/apognu/otter/utils/EventBus.kt @@ -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 { diff --git a/app/src/main/res/menu/row_track.xml b/app/src/main/res/menu/row_track.xml index 9b4dce0..6f1c2f4 100644 --- a/app/src/main/res/menu/row_track.xml +++ b/app/src/main/res/menu/row_track.xml @@ -9,4 +9,8 @@ android:id="@+id/track_play_next" android:title="@string/playback_queue_play_next" /> + + \ No newline at end of file