kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale-android
Streamline the way the media session is controled across devices.
rodzic
e7cb5e4c6e
commit
b0640cf1b2
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.github.apognu.otter">
|
package="com.github.apognu.otter">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
@ -19,7 +20,7 @@
|
||||||
android:usesCleartextTraffic="true">
|
android:usesCleartextTraffic="true">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.github.apognu.otter.activities.SplashActivity"
|
android:name=".activities.SplashActivity"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
android:noHistory="true">
|
android:noHistory="true">
|
||||||
|
|
||||||
|
@ -33,36 +34,48 @@
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.github.apognu.otter.activities.LoginActivity"
|
android:name=".activities.LoginActivity"
|
||||||
android:configChanges="screenSize|orientation"
|
android:configChanges="screenSize|orientation"
|
||||||
android:launchMode="singleInstance" />
|
android:launchMode="singleInstance" />
|
||||||
|
|
||||||
<activity android:name="com.github.apognu.otter.activities.MainActivity" />
|
<activity android:name=".activities.MainActivity" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.github.apognu.otter.activities.SearchActivity"
|
android:name=".activities.SearchActivity"
|
||||||
android:launchMode="singleTop" />
|
android:launchMode="singleTop" />
|
||||||
|
|
||||||
<activity android:name="com.github.apognu.otter.activities.DownloadsActivity" />
|
<activity android:name=".activities.DownloadsActivity" />
|
||||||
|
|
||||||
<activity android:name="com.github.apognu.otter.activities.SettingsActivity" />
|
<activity android:name=".activities.SettingsActivity" />
|
||||||
|
|
||||||
<activity android:name="com.github.apognu.otter.activities.LicencesActivity" />
|
<activity android:name=".activities.LicencesActivity" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="com.github.apognu.otter.playback.PlayerService"
|
android:name=".playback.PlayerService"
|
||||||
android:foregroundServiceType="mediaPlayback" />
|
android:foregroundServiceType="mediaPlayback">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</service>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".playback.PinService"
|
android:name=".playback.PinService"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.google.android.exoplayer.downloadService.action.RESTART" />
|
<action android:name="com.google.android.exoplayer.downloadService.action.RESTART" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<receiver android:name="com.github.apognu.otter.playback.MediaControlActionReceiver" />
|
<receiver android:name="androidx.media.session.MediaButtonReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|
|
@ -3,16 +3,19 @@ package com.github.apognu.otter.playback
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.media.app.NotificationCompat.MediaStyle
|
import androidx.media.app.NotificationCompat.MediaStyle
|
||||||
|
import androidx.media.session.MediaButtonReceiver
|
||||||
import com.github.apognu.otter.R
|
import com.github.apognu.otter.R
|
||||||
import com.github.apognu.otter.activities.MainActivity
|
import com.github.apognu.otter.activities.MainActivity
|
||||||
import com.github.apognu.otter.utils.*
|
import com.github.apognu.otter.utils.AppContext
|
||||||
|
import com.github.apognu.otter.utils.Track
|
||||||
|
import com.github.apognu.otter.utils.log
|
||||||
|
import com.github.apognu.otter.utils.maybeNormalizeUrl
|
||||||
import com.squareup.picasso.Picasso
|
import com.squareup.picasso.Picasso
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers.Default
|
import kotlinx.coroutines.Dispatchers.Default
|
||||||
|
@ -21,14 +24,13 @@ import kotlinx.coroutines.launch
|
||||||
class MediaControlsManager(val context: Service, private val scope: CoroutineScope, private val mediaSession: MediaSessionCompat) {
|
class MediaControlsManager(val context: Service, private val scope: CoroutineScope, private val mediaSession: MediaSessionCompat) {
|
||||||
companion object {
|
companion object {
|
||||||
const val NOTIFICATION_ACTION_OPEN_QUEUE = 0
|
const val NOTIFICATION_ACTION_OPEN_QUEUE = 0
|
||||||
const val NOTIFICATION_ACTION_PREVIOUS = 1
|
|
||||||
const val NOTIFICATION_ACTION_TOGGLE = 2
|
|
||||||
const val NOTIFICATION_ACTION_NEXT = 3
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var notification: Notification? = null
|
private var notification: Notification? = null
|
||||||
|
|
||||||
fun updateNotification(track: Track?, playing: Boolean) {
|
fun updateNotification(track: Track?, playing: Boolean) {
|
||||||
|
"updateNotification".log()
|
||||||
|
|
||||||
if (notification == null && !playing) return
|
if (notification == null && !playing) return
|
||||||
|
|
||||||
track?.let {
|
track?.let {
|
||||||
|
@ -74,19 +76,19 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
|
||||||
.addAction(
|
.addAction(
|
||||||
action(
|
action(
|
||||||
R.drawable.previous, context.getString(R.string.control_previous),
|
R.drawable.previous, context.getString(R.string.control_previous),
|
||||||
NOTIFICATION_ACTION_PREVIOUS
|
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.addAction(
|
.addAction(
|
||||||
action(
|
action(
|
||||||
stateIcon, context.getString(R.string.control_toggle),
|
stateIcon, context.getString(R.string.control_toggle),
|
||||||
NOTIFICATION_ACTION_TOGGLE
|
PlaybackStateCompat.ACTION_PLAY_PAUSE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.addAction(
|
.addAction(
|
||||||
action(
|
action(
|
||||||
R.drawable.next, context.getString(R.string.control_next),
|
R.drawable.next, context.getString(R.string.control_next),
|
||||||
NOTIFICATION_ACTION_NEXT
|
PlaybackStateCompat.ACTION_SKIP_TO_NEXT
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
@ -102,26 +104,9 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun action(icon: Int, title: String, id: Int): NotificationCompat.Action {
|
private fun action(icon: Int, title: String, id: Long): NotificationCompat.Action {
|
||||||
val intent = Intent(context, MediaControlActionReceiver::class.java).apply { action = id.toString() }
|
return MediaButtonReceiver.buildMediaButtonPendingIntent(context, id).run {
|
||||||
val pendingIntent = PendingIntent.getBroadcast(context, id, intent, 0)
|
NotificationCompat.Action.Builder(icon, title, this).build()
|
||||||
|
|
||||||
return NotificationCompat.Action.Builder(icon, title, pendingIntent).build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MediaControlActionReceiver : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
|
||||||
val command = when (intent?.action) {
|
|
||||||
MediaControlsManager.NOTIFICATION_ACTION_PREVIOUS.toString() -> Command.PreviousTrack
|
|
||||||
MediaControlsManager.NOTIFICATION_ACTION_TOGGLE.toString() -> Command.ToggleState
|
|
||||||
MediaControlsManager.NOTIFICATION_ACTION_NEXT.toString() -> Command.NextTrack
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
command?.let {
|
|
||||||
CommandBus.send(command)
|
|
||||||
CommandBus.send(Command.StartService(command))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,6 @@ import android.os.Bundle
|
||||||
import android.os.ResultReceiver
|
import android.os.ResultReceiver
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import android.view.KeyEvent
|
|
||||||
import com.github.apognu.otter.Otter
|
|
||||||
import com.github.apognu.otter.utils.Command
|
import com.github.apognu.otter.utils.Command
|
||||||
import com.github.apognu.otter.utils.CommandBus
|
import com.github.apognu.otter.utils.CommandBus
|
||||||
import com.google.android.exoplayer2.ControlDispatcher
|
import com.google.android.exoplayer2.ControlDispatcher
|
||||||
|
@ -15,7 +13,7 @@ import com.google.android.exoplayer2.Player
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||||
|
|
||||||
class MediaSession(private val context: Context) {
|
class MediaSession(private val context: Context) {
|
||||||
var active: Boolean = false
|
var active = false
|
||||||
|
|
||||||
private val playbackStateBuilder = PlaybackStateCompat.Builder().apply {
|
private val playbackStateBuilder = PlaybackStateCompat.Builder().apply {
|
||||||
setActions(
|
setActions(
|
||||||
|
@ -30,41 +28,28 @@ class MediaSession(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
val session: MediaSessionCompat by lazy {
|
val session: MediaSessionCompat by lazy {
|
||||||
active = true
|
|
||||||
|
|
||||||
MediaSessionCompat(context, context.packageName).apply {
|
MediaSessionCompat(context, context.packageName).apply {
|
||||||
setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
|
setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
|
||||||
setPlaybackState(playbackStateBuilder.build())
|
setPlaybackState(playbackStateBuilder.build())
|
||||||
|
|
||||||
isActive = true
|
isActive = true
|
||||||
|
active = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val connector: MediaSessionConnector by lazy {
|
val connector: MediaSessionConnector by lazy {
|
||||||
MediaSessionConnector(Otter.get().mediaSession.session).also {
|
MediaSessionConnector(session).also {
|
||||||
it.setQueueNavigator(OtterQueueNavigator())
|
it.setQueueNavigator(OtterQueueNavigator())
|
||||||
|
|
||||||
it.setMediaButtonEventHandler { _, _, event ->
|
it.setMediaButtonEventHandler { _, _, intent ->
|
||||||
if (!active) {
|
if (!active) {
|
||||||
event.extras?.getParcelable<KeyEvent>(Intent.EXTRA_KEY_EVENT)?.let { key ->
|
context.startService(Intent(context, PlayerService::class.java).apply {
|
||||||
if (key.action == KeyEvent.ACTION_UP) {
|
action = intent.action
|
||||||
val command = when (key.keyCode) {
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY -> Command.ToggleState
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> Command.ToggleState
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> Command.ToggleState
|
|
||||||
KeyEvent.KEYCODE_MEDIA_NEXT -> Command.NextTrack
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> Command.PreviousTrack
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
command?.let {
|
intent.extras?.let { extras -> putExtras(extras) }
|
||||||
CommandBus.send(command)
|
})
|
||||||
CommandBus.send(Command.StartService(command))
|
|
||||||
|
|
||||||
return@setMediaButtonEventHandler true
|
return@setMediaButtonEventHandler true
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
|
|
|
@ -13,6 +13,7 @@ import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.media.session.MediaButtonReceiver
|
||||||
import com.github.apognu.otter.Otter
|
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.*
|
||||||
|
@ -55,18 +56,14 @@ class PlayerService : Service() {
|
||||||
private lateinit var radioPlayer: RadioPlayer
|
private lateinit var radioPlayer: RadioPlayer
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
intent?.action?.let {
|
||||||
|
if (it == Intent.ACTION_MEDIA_BUTTON) {
|
||||||
|
MediaButtonReceiver.handleIntent(Otter.get().mediaSession.session, intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!started) {
|
if (!started) {
|
||||||
watchEventBus()
|
watchEventBus()
|
||||||
|
|
||||||
intent?.extras?.getString(INITIAL_COMMAND_KEY)?.let {
|
|
||||||
when (it) {
|
|
||||||
Command.SetState(true).toString() -> setPlaybackState(true)
|
|
||||||
Command.SetState(false).toString() -> setPlaybackState(false)
|
|
||||||
Command.ToggleState.toString() -> togglePlayback()
|
|
||||||
Command.NextTrack.toString() -> skipToNextTrack()
|
|
||||||
Command.PreviousTrack.toString() -> skipToPreviousTrack()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
started = true
|
started = true
|
||||||
|
@ -98,8 +95,6 @@ class PlayerService : Service() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Otter.get().mediaSession.active = true
|
|
||||||
|
|
||||||
mediaControlsManager = MediaControlsManager(this, scope, Otter.get().mediaSession.session)
|
mediaControlsManager = MediaControlsManager(this, scope, Otter.get().mediaSession.session)
|
||||||
|
|
||||||
player = SimpleExoPlayer.Builder(this).build().apply {
|
player = SimpleExoPlayer.Builder(this).build().apply {
|
||||||
|
@ -110,6 +105,8 @@ class PlayerService : Service() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Otter.get().mediaSession.active = true
|
||||||
|
|
||||||
Otter.get().mediaSession.connector.apply {
|
Otter.get().mediaSession.connector.apply {
|
||||||
setPlayer(player)
|
setPlayer(player)
|
||||||
|
|
||||||
|
@ -247,12 +244,12 @@ class PlayerService : Service() {
|
||||||
audioManager.abandonAudioFocus(audioFocusChangeListener)
|
audioManager.abandonAudioFocus(audioFocusChangeListener)
|
||||||
})
|
})
|
||||||
|
|
||||||
Otter.get().mediaSession.active = false
|
|
||||||
|
|
||||||
player.removeListener(playerEventListener)
|
player.removeListener(playerEventListener)
|
||||||
setPlaybackState(false)
|
setPlaybackState(false)
|
||||||
player.release()
|
player.release()
|
||||||
|
|
||||||
|
Otter.get().mediaSession.active = false
|
||||||
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -401,8 +398,10 @@ class PlayerService : Service() {
|
||||||
override fun onTracksChanged(trackGroups: TrackGroupArray, trackSelections: TrackSelectionArray) {
|
override fun onTracksChanged(trackGroups: TrackGroupArray, trackSelections: TrackSelectionArray) {
|
||||||
super.onTracksChanged(trackGroups, trackSelections)
|
super.onTracksChanged(trackGroups, trackSelections)
|
||||||
|
|
||||||
queue.current = player.currentWindowIndex
|
if (queue.current != player.currentWindowIndex) {
|
||||||
mediaControlsManager.updateNotification(queue.current(), player.playWhenReady)
|
queue.current = player.currentWindowIndex
|
||||||
|
mediaControlsManager.updateNotification(queue.current(), player.playWhenReady)
|
||||||
|
}
|
||||||
|
|
||||||
if (queue.get().isNotEmpty() && queue.current() == queue.get().last() && radioPlayer.isActive()) {
|
if (queue.get().isNotEmpty() && queue.current() == queue.get().last() && radioPlayer.isActive()) {
|
||||||
scope.launch(IO) {
|
scope.launch(IO) {
|
||||||
|
|
Ładowanie…
Reference in New Issue