diff --git a/app/src/foss/java/com/github/apognu/otter/utils/Cast.kt b/app/src/foss/java/com/github/apognu/otter/utils/Cast.kt index eadcf46..805f81d 100644 --- a/app/src/foss/java/com/github/apognu/otter/utils/Cast.kt +++ b/app/src/foss/java/com/github/apognu/otter/utils/Cast.kt @@ -2,10 +2,18 @@ package com.github.apognu.otter import android.content.Context import android.view.Menu -import com.github.apognu.otter.utils.log +import com.github.apognu.otter.playback.PlayerService +import com.github.apognu.otter.utils.CastInterface -object Cast { - fun init(context: Context) {} - fun setupButton(context: Context, menu: Menu?) {} +class Cast(val context: Context, val switchListener: PlayerService.OnPlayerSwitchListener, playerEventListener: PlayerService.PlayerEventListener) : CastInterface { + companion object { + fun init(context: Context) {} + fun setupButton(context: Context, menu: Menu?) {} + + fun get( + context: Context, + playerSwitchListener: PlayerService.OnPlayerSwitchListener, + playerEventListener: PlayerService.PlayerEventListener + ): Cast? = null + } } - diff --git a/app/src/foss/res/menu-land/toolbar.xml b/app/src/foss/res/menu-land/toolbar.xml new file mode 100644 index 0000000..54a89f4 --- /dev/null +++ b/app/src/foss/res/menu-land/toolbar.xml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/app/src/foss/res/menu/toolbar.xml b/app/src/foss/res/menu/toolbar.xml new file mode 100644 index 0000000..e0428d7 --- /dev/null +++ b/app/src/foss/res/menu/toolbar.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/app/src/full/java/com/github/apognu/otter/utils/Cast.kt b/app/src/full/java/com/github/apognu/otter/utils/Cast.kt index 94e2a70..7e9e44d 100644 --- a/app/src/full/java/com/github/apognu/otter/utils/Cast.kt +++ b/app/src/full/java/com/github/apognu/otter/utils/Cast.kt @@ -1,17 +1,142 @@ package com.github.apognu.otter import android.content.Context +import android.net.Uri import android.view.Menu +import com.github.apognu.otter.playback.PlayerService +import com.github.apognu.otter.utils.AppContext +import com.github.apognu.otter.utils.CastInterface +import com.github.apognu.otter.utils.Track +import com.github.apognu.otter.utils.mustNormalizeUrl +import com.google.android.exoplayer2.Player +import com.google.android.exoplayer2.Timeline +import com.google.android.exoplayer2.ext.cast.CastPlayer +import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener +import com.google.android.gms.cast.MediaInfo +import com.google.android.gms.cast.MediaMetadata +import com.google.android.gms.cast.MediaQueueItem import com.google.android.gms.cast.framework.CastButtonFactory import com.google.android.gms.cast.framework.CastContext +import com.google.android.gms.common.images.WebImage +import com.preference.PowerPreference -object Cast { - fun init(context: Context) { - CastContext.getSharedInstance(context) +fun Player.onCast(): CastPlayer? { + return if (this is CastPlayer) this + else null +} + +class Cast(val context: Context, val switchListener: PlayerService.OnPlayerSwitchListener, playerEventListener: PlayerService.PlayerEventListener) : CastInterface { + companion object { + fun init(context: Context) { + CastContext.getSharedInstance(context) + } + + fun setupButton(context: Context, menu: Menu?) { + CastButtonFactory.setUpMediaRouteButton(context, menu, R.id.cast) + } + + fun get( + context: Context, + playerSwitchListener: PlayerService.OnPlayerSwitchListener, + playerEventListener: PlayerService.PlayerEventListener + ): Cast = Cast(context, playerSwitchListener, playerEventListener) } - fun setupButton(context: Context, menu: Menu?) { - CastButtonFactory.setUpMediaRouteButton(context, menu, R.id.cast) + private val player: Player + + init { + player = CastPlayer(CastContext.getSharedInstance(context)).apply { + addListener(playerEventListener) + setSessionAvailabilityListener(CastSessionListener()) + } + } + + override fun getPlayer(context: Context): Player = player + + override fun replaceQueue(tracks: List) { + player.onCast()?.let { castPlayer -> + tracks + .map { track -> buildMediaQueueItem(track) } + .apply { + castPlayer.loadItems(this.toTypedArray(), 0, 0, Player.REPEAT_MODE_OFF) + castPlayer.playWhenReady = true + } + } + } + + override fun addToQueue(tracks: List) { + player.onCast()?.let { castPlayer -> + tracks + .map { track -> buildMediaQueueItem(track) } + .forEach { + castPlayer.addItems(it) + } + } + } + + override fun insertNext(track: Track, current: Int) { + player.onCast()?.let { castPlayer -> + val period = Timeline.Period().run { + player.currentTimeline.getPeriod(current + 1, this) + } + + castPlayer.addItems(period.id.toString().toInt(), buildMediaQueueItem(track)) + } + } + + override fun remove(index: Int) { + player.onCast()?.let { castPlayer -> + val period = Timeline.Period().run { + player.currentTimeline.getPeriod(index, this) + } + + castPlayer.removeItem(period.id.toString().toInt()) + } + } + + override fun move(oldPosition: Int, newPosition: Int) { + player.onCast()?.let { castPlayer -> + val period = Timeline.Period().run { + player.currentTimeline.getPeriod(oldPosition, this) + } + + castPlayer.moveItem(period.id.toString().toInt(), newPosition) + } + } + + private fun buildMediaQueueItem(track: Track): MediaQueueItem { + val listenUrl = mustNormalizeUrl(track.bestUpload()?.listen_url ?: "") + val token = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("listen_token", "") + val metadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK).apply { + putString(MediaMetadata.KEY_ARTIST, track.artist.name) + putString(MediaMetadata.KEY_ALBUM_TITLE, track.album.title) + putString(MediaMetadata.KEY_TITLE, track.title) + + addImage(WebImage(Uri.parse(mustNormalizeUrl(track.album.cover())))) + } + + val url = Uri.parse(listenUrl) + .buildUpon() + .appendQueryParameter("token", token) + .build() + .toString() + + val mediaInfo = MediaInfo.Builder(url) + .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) + .setMetadata(metadata) + .build() + + return MediaQueueItem.Builder(mediaInfo).build() + } + + inner class CastSessionListener : SessionAvailabilityListener { + override fun onCastSessionAvailable() { + switchListener.switchToRemote() + } + + override fun onCastSessionUnavailable() { + switchListener.switchToLocal() + } } } diff --git a/app/src/main/res/menu-land/toolbar.xml b/app/src/full/res/menu-land/toolbar.xml similarity index 76% rename from app/src/main/res/menu-land/toolbar.xml rename to app/src/full/res/menu-land/toolbar.xml index 794f648..01386de 100644 --- a/app/src/main/res/menu-land/toolbar.xml +++ b/app/src/full/res/menu-land/toolbar.xml @@ -2,18 +2,17 @@ - + app:showAsAction="always" /> + app:showAsAction="always" /> - \ No newline at end of file + diff --git a/app/src/main/res/menu/toolbar.xml b/app/src/full/res/menu/toolbar.xml similarity index 100% rename from app/src/main/res/menu/toolbar.xml rename to app/src/full/res/menu/toolbar.xml diff --git a/app/src/full/res/values/styles.xml b/app/src/full/res/values/styles.xml new file mode 100644 index 0000000..595549b --- /dev/null +++ b/app/src/full/res/values/styles.xml @@ -0,0 +1,13 @@ + + + + + + + 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 f395d31..9cf2dd0 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 @@ -12,6 +12,7 @@ import android.os.Build import android.os.IBinder import android.support.v4.media.session.MediaSessionCompat import android.view.KeyEvent +import com.github.apognu.otter.Cast import com.github.apognu.otter.R import com.github.apognu.otter.utils.* import com.google.android.exoplayer2.C @@ -30,6 +31,11 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch class PlayerService : Service() { + interface OnPlayerSwitchListener { + fun switchToLocal() + fun switchToRemote() + } + private lateinit var queue: QueueManager private val jobs = mutableListOf() @@ -40,7 +46,10 @@ class PlayerService : Service() { private lateinit var mediaControlsManager: MediaControlsManager private lateinit var mediaSession: MediaSessionCompat - private lateinit var player: SimpleExoPlayer + + private lateinit var player: Player + private lateinit var localPlayer: SimpleExoPlayer + private var cast: Cast? = null private lateinit var playerEventListener: PlayerEventListener private val headphonesUnpluggedReceiver = HeadphonesUnpluggedReceiver() @@ -58,7 +67,6 @@ class PlayerService : Service() { override fun onCreate() { super.onCreate() - queue = QueueManager(this) radioPlayer = RadioPlayer(this) audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager @@ -85,13 +93,15 @@ class PlayerService : Service() { mediaControlsManager = MediaControlsManager(this, mediaSession) - player = SimpleExoPlayer.Builder(this).build().apply { + localPlayer = SimpleExoPlayer.Builder(this).build().apply { playWhenReady = false playerEventListener = PlayerEventListener().also { addListener(it) } + cast = Cast.get(this@PlayerService, PlayerSwitchListener(), playerEventListener) + MediaSessionConnector(mediaSession).also { it.setPlayer(this) it.setMediaButtonEventHandler { player, _, mediaButtonEvent -> @@ -111,8 +121,11 @@ class PlayerService : Service() { } } + player = if (cast?.isCastSessionAvailable() == true) cast!!.getPlayer(this) else localPlayer + queue = QueueManager(this, cast) + if (queue.current > -1) { - player.prepare(queue.datasources, true, true) + player.onLocal()?.prepare(queue.datasources, true, true) Cache.get(this, "progress")?.let { progress -> player.seekTo(queue.current, progress.readLine().toLong()) @@ -143,7 +156,7 @@ class PlayerService : Service() { if (!command.fromRadio) radioPlayer.stop() queue.replace(command.queue) - player.prepare(queue.datasources, true, true) + player.onLocal()?.prepare(queue.datasources, true, true) state(true) @@ -262,7 +275,7 @@ class PlayerService : Service() { } if (state && player.playbackState == Player.STATE_IDLE) { - player.prepare(queue.datasources) + player.onLocal()?.prepare(queue.datasources) } var allowed = !state @@ -394,7 +407,7 @@ class PlayerService : Service() { EventBus.send(Event.PlaybackError(getString(R.string.error_playback))) queue.current++ - player.prepare(queue.datasources, true, true) + player.onLocal()?.prepare(queue.datasources, true, true) player.seekTo(queue.current, 0) player.playWhenReady = true @@ -406,7 +419,7 @@ class PlayerService : Service() { override fun onAudioFocusChange(focus: Int) { when (focus) { AudioManager.AUDIOFOCUS_GAIN -> { - player.volume = 1f + player.onLocal()?.volume = 1f state(stateWhenLostFocus) stateWhenLostFocus = false @@ -424,9 +437,21 @@ class PlayerService : Service() { AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { stateWhenLostFocus = player.playWhenReady - player.volume = 0.3f + player.onLocal()?.volume = 0.3f } } } } + + inner class PlayerSwitchListener : OnPlayerSwitchListener { + override fun switchToLocal() { + player = localPlayer + } + + override fun switchToRemote() { + cast?.let { cast -> + player = cast.getPlayer(this@PlayerService) + } + } + } } 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 96187c3..1e5b7a0 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 @@ -15,7 +15,7 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory import com.google.android.exoplayer2.util.Util import com.google.gson.Gson -class QueueManager(val context: Context) { +class QueueManager(val context: Context, val cast: CastInterface?) { var metadata: MutableList = mutableListOf() val datasources = ConcatenatingMediaSource() var current = -1 @@ -84,6 +84,8 @@ class QueueManager(val context: Context) { datasources.clear() datasources.addMediaSources(sources) + cast?.replaceQueue(tracks) + persist() EventBus.send(Event.QueueChanged) @@ -102,6 +104,8 @@ class QueueManager(val context: Context) { metadata.addAll(tracks) datasources.addMediaSources(sources) + cast?.addToQueue(tracks) + persist() EventBus.send(Event.QueueChanged) @@ -120,25 +124,29 @@ class QueueManager(val context: Context) { move(metadata.indexOf(track), current + 1) } + cast?.insertNext(track, current) + persist() EventBus.send(Event.QueueChanged) } fun remove(track: Track) { - metadata.indexOf(track).let { - if (it < 0) { + metadata.indexOf(track).let { trackIndex -> + if (trackIndex < 0) { return } - datasources.removeMediaSource(it) - metadata.removeAt(it) + datasources.removeMediaSource(trackIndex) + metadata.removeAt(trackIndex) - if (it == current) { + cast?.remove(trackIndex) + + if (trackIndex == current) { CommandBus.send(Command.NextTrack) } - if (it < current) { + if (trackIndex < current) { current-- } } @@ -156,6 +164,8 @@ class QueueManager(val context: Context) { datasources.moveMediaSource(oldPosition, newPosition) metadata.add(newPosition, metadata.removeAt(oldPosition)) + cast?.move(oldPosition, newPosition) + persist() } diff --git a/app/src/main/java/com/github/apognu/otter/utils/Bus.kt b/app/src/main/java/com/github/apognu/otter/utils/Bus.kt index 6a8ad84..1c7d36d 100644 --- a/app/src/main/java/com/github/apognu/otter/utils/Bus.kt +++ b/app/src/main/java/com/github/apognu/otter/utils/Bus.kt @@ -68,7 +68,6 @@ sealed class Response { object EventBus { fun send(event: Event) { GlobalScope.launch(IO) { - Otter.get().eventBus.log() Otter.get().eventBus.offer(event) } } diff --git a/app/src/main/java/com/github/apognu/otter/utils/CastInterface.kt b/app/src/main/java/com/github/apognu/otter/utils/CastInterface.kt new file mode 100644 index 0000000..0740ad6 --- /dev/null +++ b/app/src/main/java/com/github/apognu/otter/utils/CastInterface.kt @@ -0,0 +1,16 @@ +package com.github.apognu.otter.utils + +import android.content.Context +import com.google.android.exoplayer2.Player +import com.google.android.exoplayer2.SimpleExoPlayer + +interface CastInterface { + fun isCastSessionAvailable(): Boolean = false + fun getPlayer(context: Context): Player = SimpleExoPlayer.Builder(context).build() + + fun replaceQueue(tracks: List) {} + fun addToQueue(tracks: List) {} + fun insertNext(track: Track, current: Int) {} + fun remove(index: Int) {} + fun move(oldPosition: Int, newPosition: Int) {} +} \ No newline at end of file diff --git a/app/src/main/java/com/github/apognu/otter/utils/Extensions.kt b/app/src/main/java/com/github/apognu/otter/utils/Extensions.kt index 1b5b601..8f6c4e3 100644 --- a/app/src/main/java/com/github/apognu/otter/utils/Extensions.kt +++ b/app/src/main/java/com/github/apognu/otter/utils/Extensions.kt @@ -6,6 +6,8 @@ import com.github.apognu.otter.R import com.github.apognu.otter.fragments.BrowseFragment import com.github.apognu.otter.repositories.Repository import com.github.kittinunf.fuel.core.Request +import com.google.android.exoplayer2.Player +import com.google.android.exoplayer2.SimpleExoPlayer import com.google.android.exoplayer2.offline.Download import com.google.gson.Gson import com.squareup.picasso.Picasso @@ -77,3 +79,8 @@ fun Request.authorize(): Request { } fun Download.getMetadata(): DownloadInfo? = Gson().fromJson(String(this.request.data), DownloadInfo::class.java) + +fun Player.onLocal(): SimpleExoPlayer? { + return if (this is SimpleExoPlayer) this + else null +} diff --git a/app/src/main/java/com/github/apognu/otter/utils/Models.kt b/app/src/main/java/com/github/apognu/otter/utils/Models.kt index 1dab613..ba09ba6 100644 --- a/app/src/main/java/com/github/apognu/otter/utils/Models.kt +++ b/app/src/main/java/com/github/apognu/otter/utils/Models.kt @@ -4,9 +4,12 @@ import com.google.android.exoplayer2.offline.Download import com.preference.PowerPreference data class User( - val full_username: String + val full_username: String, + val tokens: UserTokens ) +data class UserTokens(val listen: String) + sealed class CacheItem(val data: List) class ArtistsCache(data: List) : CacheItem(data) class AlbumsCache(data: List) : CacheItem(data) diff --git a/app/src/main/java/com/github/apognu/otter/utils/Userinfo.kt b/app/src/main/java/com/github/apognu/otter/utils/Userinfo.kt index 823a069..dc9904d 100644 --- a/app/src/main/java/com/github/apognu/otter/utils/Userinfo.kt +++ b/app/src/main/java/com/github/apognu/otter/utils/Userinfo.kt @@ -20,6 +20,7 @@ object Userinfo { PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).apply { setString("actor_username", user.full_username) + setString("listen_token", user.tokens.listen) } user diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 57d165d..e68c6c8 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,6 +1,8 @@ - + + @@ -101,12 +101,4 @@ @android:color/white - - - -