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"?>
 | 
			
		||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
  xmlns:tools="http://schemas.android.com/tools"
 | 
			
		||||
  package="com.github.apognu.otter">
 | 
			
		||||
 | 
			
		||||
  <uses-permission android:name="android.permission.INTERNET" />
 | 
			
		||||
| 
						 | 
				
			
			@ -19,7 +20,7 @@
 | 
			
		|||
    android:usesCleartextTraffic="true">
 | 
			
		||||
 | 
			
		||||
    <activity
 | 
			
		||||
      android:name="com.github.apognu.otter.activities.SplashActivity"
 | 
			
		||||
      android:name=".activities.SplashActivity"
 | 
			
		||||
      android:launchMode="singleInstance"
 | 
			
		||||
      android:noHistory="true">
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -33,36 +34,48 @@
 | 
			
		|||
    </activity>
 | 
			
		||||
 | 
			
		||||
    <activity
 | 
			
		||||
      android:name="com.github.apognu.otter.activities.LoginActivity"
 | 
			
		||||
      android:name=".activities.LoginActivity"
 | 
			
		||||
      android:configChanges="screenSize|orientation"
 | 
			
		||||
      android:launchMode="singleInstance" />
 | 
			
		||||
 | 
			
		||||
    <activity android:name="com.github.apognu.otter.activities.MainActivity" />
 | 
			
		||||
    <activity android:name=".activities.MainActivity" />
 | 
			
		||||
 | 
			
		||||
    <activity
 | 
			
		||||
      android:name="com.github.apognu.otter.activities.SearchActivity"
 | 
			
		||||
      android:name=".activities.SearchActivity"
 | 
			
		||||
      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
 | 
			
		||||
      android:name="com.github.apognu.otter.playback.PlayerService"
 | 
			
		||||
      android:foregroundServiceType="mediaPlayback" />
 | 
			
		||||
      android:name=".playback.PlayerService"
 | 
			
		||||
      android:foregroundServiceType="mediaPlayback">
 | 
			
		||||
 | 
			
		||||
      <intent-filter>
 | 
			
		||||
        <action android:name="android.intent.action.MEDIA_BUTTON" />
 | 
			
		||||
      </intent-filter>
 | 
			
		||||
 | 
			
		||||
    </service>
 | 
			
		||||
 | 
			
		||||
    <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="androidx.media.session.MediaButtonReceiver">
 | 
			
		||||
      <intent-filter>
 | 
			
		||||
        <action android:name="android.intent.action.MEDIA_BUTTON" />
 | 
			
		||||
      </intent-filter>
 | 
			
		||||
    </receiver>
 | 
			
		||||
 | 
			
		||||
  </application>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,16 +3,19 @@ package com.github.apognu.otter.playback
 | 
			
		|||
import android.app.Notification
 | 
			
		||||
import android.app.PendingIntent
 | 
			
		||||
import android.app.Service
 | 
			
		||||
import android.content.BroadcastReceiver
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.support.v4.media.session.MediaSessionCompat
 | 
			
		||||
import android.support.v4.media.session.PlaybackStateCompat
 | 
			
		||||
import androidx.core.app.NotificationCompat
 | 
			
		||||
import androidx.core.app.NotificationManagerCompat
 | 
			
		||||
import androidx.media.app.NotificationCompat.MediaStyle
 | 
			
		||||
import androidx.media.session.MediaButtonReceiver
 | 
			
		||||
import com.github.apognu.otter.R
 | 
			
		||||
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 kotlinx.coroutines.CoroutineScope
 | 
			
		||||
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) {
 | 
			
		||||
  companion object {
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
  fun updateNotification(track: Track?, playing: Boolean) {
 | 
			
		||||
    "updateNotification".log()
 | 
			
		||||
 | 
			
		||||
    if (notification == null && !playing) return
 | 
			
		||||
 | 
			
		||||
    track?.let {
 | 
			
		||||
| 
						 | 
				
			
			@ -74,19 +76,19 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
 | 
			
		|||
          .addAction(
 | 
			
		||||
            action(
 | 
			
		||||
              R.drawable.previous, context.getString(R.string.control_previous),
 | 
			
		||||
              NOTIFICATION_ACTION_PREVIOUS
 | 
			
		||||
              PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
 | 
			
		||||
            )
 | 
			
		||||
          )
 | 
			
		||||
          .addAction(
 | 
			
		||||
            action(
 | 
			
		||||
              stateIcon, context.getString(R.string.control_toggle),
 | 
			
		||||
              NOTIFICATION_ACTION_TOGGLE
 | 
			
		||||
              PlaybackStateCompat.ACTION_PLAY_PAUSE
 | 
			
		||||
            )
 | 
			
		||||
          )
 | 
			
		||||
          .addAction(
 | 
			
		||||
            action(
 | 
			
		||||
              R.drawable.next, context.getString(R.string.control_next),
 | 
			
		||||
              NOTIFICATION_ACTION_NEXT
 | 
			
		||||
              PlaybackStateCompat.ACTION_SKIP_TO_NEXT
 | 
			
		||||
            )
 | 
			
		||||
          )
 | 
			
		||||
          .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 {
 | 
			
		||||
    val intent = Intent(context, MediaControlActionReceiver::class.java).apply { action = id.toString() }
 | 
			
		||||
    val pendingIntent = PendingIntent.getBroadcast(context, id, intent, 0)
 | 
			
		||||
 | 
			
		||||
    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))
 | 
			
		||||
  private fun action(icon: Int, title: String, id: Long): NotificationCompat.Action {
 | 
			
		||||
    return MediaButtonReceiver.buildMediaButtonPendingIntent(context, id).run {
 | 
			
		||||
      NotificationCompat.Action.Builder(icon, title, this).build()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,8 +6,6 @@ import android.os.Bundle
 | 
			
		|||
import android.os.ResultReceiver
 | 
			
		||||
import android.support.v4.media.session.MediaSessionCompat
 | 
			
		||||
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.CommandBus
 | 
			
		||||
import com.google.android.exoplayer2.ControlDispatcher
 | 
			
		||||
| 
						 | 
				
			
			@ -15,7 +13,7 @@ import com.google.android.exoplayer2.Player
 | 
			
		|||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
 | 
			
		||||
 | 
			
		||||
class MediaSession(private val context: Context) {
 | 
			
		||||
  var active: Boolean = false
 | 
			
		||||
  var active = false
 | 
			
		||||
 | 
			
		||||
  private val playbackStateBuilder = PlaybackStateCompat.Builder().apply {
 | 
			
		||||
    setActions(
 | 
			
		||||
| 
						 | 
				
			
			@ -30,41 +28,28 @@ class MediaSession(private val context: Context) {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  val session: MediaSessionCompat by lazy {
 | 
			
		||||
    active = true
 | 
			
		||||
 | 
			
		||||
    MediaSessionCompat(context, context.packageName).apply {
 | 
			
		||||
      setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
 | 
			
		||||
      setPlaybackState(playbackStateBuilder.build())
 | 
			
		||||
 | 
			
		||||
      isActive = true
 | 
			
		||||
      active = true
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  val connector: MediaSessionConnector by lazy {
 | 
			
		||||
    MediaSessionConnector(Otter.get().mediaSession.session).also {
 | 
			
		||||
    MediaSessionConnector(session).also {
 | 
			
		||||
      it.setQueueNavigator(OtterQueueNavigator())
 | 
			
		||||
 | 
			
		||||
      it.setMediaButtonEventHandler { _, _, event ->
 | 
			
		||||
      it.setMediaButtonEventHandler { _, _, intent ->
 | 
			
		||||
        if (!active) {
 | 
			
		||||
          event.extras?.getParcelable<KeyEvent>(Intent.EXTRA_KEY_EVENT)?.let { key ->
 | 
			
		||||
            if (key.action == KeyEvent.ACTION_UP) {
 | 
			
		||||
              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
 | 
			
		||||
              }
 | 
			
		||||
          context.startService(Intent(context, PlayerService::class.java).apply {
 | 
			
		||||
            action = intent.action
 | 
			
		||||
 | 
			
		||||
              command?.let {
 | 
			
		||||
                CommandBus.send(command)
 | 
			
		||||
                CommandBus.send(Command.StartService(command))
 | 
			
		||||
            intent.extras?.let { extras -> putExtras(extras) }
 | 
			
		||||
          })
 | 
			
		||||
 | 
			
		||||
                return@setMediaButtonEventHandler true
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          return@setMediaButtonEventHandler true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        false
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ import android.os.Build
 | 
			
		|||
import android.os.IBinder
 | 
			
		||||
import android.support.v4.media.MediaMetadataCompat
 | 
			
		||||
import androidx.core.app.NotificationManagerCompat
 | 
			
		||||
import androidx.media.session.MediaButtonReceiver
 | 
			
		||||
import com.github.apognu.otter.Otter
 | 
			
		||||
import com.github.apognu.otter.R
 | 
			
		||||
import com.github.apognu.otter.utils.*
 | 
			
		||||
| 
						 | 
				
			
			@ -55,18 +56,14 @@ class PlayerService : Service() {
 | 
			
		|||
  private lateinit var radioPlayer: RadioPlayer
 | 
			
		||||
 | 
			
		||||
  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) {
 | 
			
		||||
      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
 | 
			
		||||
| 
						 | 
				
			
			@ -98,8 +95,6 @@ class PlayerService : Service() {
 | 
			
		|||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Otter.get().mediaSession.active = true
 | 
			
		||||
 | 
			
		||||
    mediaControlsManager = MediaControlsManager(this, scope, Otter.get().mediaSession.session)
 | 
			
		||||
 | 
			
		||||
    player = SimpleExoPlayer.Builder(this).build().apply {
 | 
			
		||||
| 
						 | 
				
			
			@ -110,6 +105,8 @@ class PlayerService : Service() {
 | 
			
		|||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Otter.get().mediaSession.active = true
 | 
			
		||||
 | 
			
		||||
    Otter.get().mediaSession.connector.apply {
 | 
			
		||||
      setPlayer(player)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -247,12 +244,12 @@ class PlayerService : Service() {
 | 
			
		|||
        audioManager.abandonAudioFocus(audioFocusChangeListener)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    Otter.get().mediaSession.active = false
 | 
			
		||||
 | 
			
		||||
    player.removeListener(playerEventListener)
 | 
			
		||||
    setPlaybackState(false)
 | 
			
		||||
    player.release()
 | 
			
		||||
 | 
			
		||||
    Otter.get().mediaSession.active = false
 | 
			
		||||
 | 
			
		||||
    super.onDestroy()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -401,8 +398,10 @@ class PlayerService : Service() {
 | 
			
		|||
    override fun onTracksChanged(trackGroups: TrackGroupArray, trackSelections: TrackSelectionArray) {
 | 
			
		||||
      super.onTracksChanged(trackGroups, trackSelections)
 | 
			
		||||
 | 
			
		||||
      queue.current = player.currentWindowIndex
 | 
			
		||||
      mediaControlsManager.updateNotification(queue.current(), player.playWhenReady)
 | 
			
		||||
      if (queue.current != player.currentWindowIndex) {
 | 
			
		||||
        queue.current = player.currentWindowIndex
 | 
			
		||||
        mediaControlsManager.updateNotification(queue.current(), player.playWhenReady)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (queue.get().isNotEmpty() && queue.current() == queue.get().last() && radioPlayer.isActive()) {
 | 
			
		||||
        scope.launch(IO) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Ładowanie…
	
		Reference in New Issue