add close command

This commit is contained in:
Christian Schabesberger 2024-09-11 12:39:16 +02:00
parent e965528011
commit 2e50634b50
5 changed files with 171 additions and 10 deletions

View File

@ -55,6 +55,8 @@ class NewPlayerImpl(
private val repository: MediaRepository private val repository: MediaRepository
) : NewPlayer { ) : NewPlayer {
private var playerScope = CoroutineScope(Dispatchers.Main + Job())
private var uniqueIdToIdLookup = HashMap<Long, String>() private var uniqueIdToIdLookup = HashMap<Long, String>()
// this is used to take care of the NewPlayerService // this is used to take care of the NewPlayerService
@ -74,8 +76,6 @@ class NewPlayerImpl(
override var fastSeekAmountSec: Int = 10 override var fastSeekAmountSec: Int = 10
private var playerScope = CoroutineScope(Dispatchers.Main + Job())
override var playBackMode = MutableStateFlow(PlayMode.IDLE) override var playBackMode = MutableStateFlow(PlayMode.IDLE)
override var shuffle: Boolean override var shuffle: Boolean
@ -251,7 +251,11 @@ class NewPlayerImpl(
} }
override fun release() { override fun release() {
mediaController?.release()
internalPlayer.release() internalPlayer.release()
playBackMode.update {
PlayMode.IDLE
}
} }
private fun internalPlayStream(mediaItem: MediaItem, playMode: PlayMode) { private fun internalPlayStream(mediaItem: MediaItem, playMode: PlayMode) {

View File

@ -0,0 +1,56 @@
/* NewPlayer
*
* @author Christian Schabesberger
*
* Copyright (C) NewPipe e.V. 2024 <code(at)newpipe-ev.de>
*
* NewPlayer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPlayer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPlayer. If not, see <http://www.gnu.org/licenses/>.
*
*/
package net.newpipe.newplayer.service
import android.content.Context
import android.os.Bundle
import androidx.media3.session.CommandButton
import androidx.media3.session.SessionCommand
import net.newpipe.newplayer.R
data class CustomCommand(
val action: String,
val commandButton: CommandButton
) {
companion object {
const val NEW_PLAYER_NOTIFICATION_COMMAND_CLOSE_PLAYBACK = "NEW_PLAYER_CLOSE_PLAYBACK"
}
}
fun buildCustomCommandList(context: Context) = listOf(
CustomCommand(
CustomCommand.NEW_PLAYER_NOTIFICATION_COMMAND_CLOSE_PLAYBACK,
CommandButton.Builder()
.setDisplayName(context.getString(R.string.close))
.setDisplayName("Close")
.setSessionCommand(
SessionCommand(
CustomCommand.NEW_PLAYER_NOTIFICATION_COMMAND_CLOSE_PLAYBACK,
Bundle()
)
)
.setIconResId(R.drawable.close_24px)
.build()
)
)

View File

@ -21,32 +21,122 @@
package net.newpipe.newplayer.service package net.newpipe.newplayer.service
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService import androidx.media3.session.MediaSessionService
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionError
import androidx.media3.session.SessionResult
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import net.newpipe.newplayer.NewPlayer import net.newpipe.newplayer.NewPlayer
import net.newpipe.newplayer.PlayMode
import javax.inject.Inject import javax.inject.Inject
private const val TAG = "NewPlayerService"
@AndroidEntryPoint @AndroidEntryPoint
class NewPlayerService : MediaSessionService() { class NewPlayerService : MediaSessionService() {
private var mediaSession: MediaSession? = null private lateinit var mediaSession: MediaSession
private lateinit var customCommands: List<CustomCommand>
@Inject @Inject
lateinit var newPlayer: NewPlayer lateinit var newPlayer: NewPlayer
private var serviceScope = CoroutineScope(Dispatchers.Main + Job())
@OptIn(UnstableApi::class)
override fun onCreate() {
super.onCreate()
customCommands = buildCustomCommandList(this)
mediaSession = MediaSession.Builder(this, newPlayer.internalPlayer)
.setCallback(object : MediaSession.Callback {
override fun onConnect(
session: MediaSession,
controller: MediaSession.ControllerInfo
): MediaSession.ConnectionResult {
val connectionResult = super.onConnect(session, controller)
val availableSessionCommands =
connectionResult.availableSessionCommands.buildUpon()
customCommands.forEach { command ->
command.commandButton.sessionCommand?.let {
availableSessionCommands.add(it)
}
}
return MediaSession.ConnectionResult.accept(
availableSessionCommands.build(),
connectionResult.availablePlayerCommands
)
}
override fun onPostConnect(
session: MediaSession,
controller: MediaSession.ControllerInfo
) {
super.onPostConnect(session, controller)
mediaSession.setCustomLayout(customCommands.map{it.commandButton})
}
override fun onCustomCommand(
session: MediaSession,
controller: MediaSession.ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
when(customCommand.customAction) {
CustomCommand.NEW_PLAYER_NOTIFICATION_COMMAND_CLOSE_PLAYBACK -> {
newPlayer.release()
}
else -> {
Log.e(TAG, "Unknown custom command: ${customCommand.customAction}")
return Futures.immediateFuture(SessionResult(SessionError.ERROR_NOT_SUPPORTED))
}
}
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
})
.build()
serviceScope.launch {
newPlayer.playBackMode.collect { mode ->
if(mode == PlayMode.IDLE) {
stopSelf()
}
}
}
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
newPlayer.release() newPlayer.release()
mediaSession?.release() mediaSession.release()
mediaSession = null
} }
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? { override fun onTaskRemoved(rootIntent: Intent?) {
println("gurken get session") // Check if the player is not ready to play or there are no items in the media queue
if (mediaSession == null) { if (!newPlayer.internalPlayer.playWhenReady || newPlayer.playlist.value.size == 0) {
mediaSession = MediaSession.Builder(this, newPlayer.internalPlayer).build() // Stop the service
} stopSelf()
return mediaSession
} }
} }
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) = mediaSession
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M256,760L200,704L424,480L200,256L256,200L480,424L704,200L760,256L536,480L760,704L704,760L480,536L256,760Z"/>
</vector>

View File

@ -52,4 +52,5 @@
<string name="shuffle_on">Shuffle playlist enabled</string> <string name="shuffle_on">Shuffle playlist enabled</string>
<string name="shuffle_off">Shuffle playlist disabled</string> <string name="shuffle_off">Shuffle playlist disabled</string>
<string name="store_playlist">Save current playlist</string> <string name="store_playlist">Save current playlist</string>
<string name="close">Close</string>
</resources> </resources>