mirror of
https://github.com/readest/readest.git
synced 2026-04-28 19:42:21 +00:00
tts: improve media session control compatibility across more Android systems (#2185)
This commit is contained in:
parent
ad6a21a68f
commit
b69d9ed69f
9 changed files with 293 additions and 260 deletions
|
|
@ -33,9 +33,8 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.media3:media3-common:1.2.0")
|
||||
implementation("androidx.media3:media3-session:1.2.0")
|
||||
implementation("androidx.media3:media3-exoplayer:1.2.0")
|
||||
implementation("androidx.media:media:1.7.1")
|
||||
implementation("androidx.media3:media3-exoplayer:1.3.1")
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("com.google.android.material:material:1.7.0")
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -0,0 +1,232 @@
|
|||
package com.readest.native_tts
|
||||
|
||||
import com.readest.native_tts.R
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.graphics.Bitmap
|
||||
import android.media.AudioManager
|
||||
import android.media.AudioManager.OnAudioFocusChangeListener
|
||||
import android.support.v4.media.MediaBrowserCompat
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media.MediaBrowserServiceCompat
|
||||
import androidx.media.session.MediaButtonReceiver
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import app.tauri.plugin.JSObject
|
||||
|
||||
class MediaPlaybackService : MediaBrowserServiceCompat() {
|
||||
private var mediaSession: MediaSessionCompat? = null
|
||||
private lateinit var player: ExoPlayer
|
||||
private lateinit var stateBuilder: PlaybackStateCompat.Builder
|
||||
private lateinit var audioManager: AudioManager
|
||||
|
||||
private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
|
||||
Log.i("MediaPlaybackService", "Audio focus changed: $focusChange, $player.isPlaying")
|
||||
when (focusChange) {
|
||||
AudioManager.AUDIOFOCUS_GAIN -> {
|
||||
player.volume = 1.0f
|
||||
if (!player.isPlaying) player.play()
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||
player.volume = 0.3f
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
||||
if (player.isPlaying) player.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "media2_playback_channel"
|
||||
private const val NOTIFICATION_ID = 1002
|
||||
private const val MEDIA_ROOT_ID = "media_root_id"
|
||||
|
||||
var pluginEventTrigger: ((String, JSObject) -> Unit)? = null
|
||||
|
||||
var currentTitle: String = "Read Aloud"
|
||||
var currentArtist: String = "Reading your content"
|
||||
var currentArtwork: Bitmap? = null
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
val result = audioManager.requestAudioFocus(
|
||||
afChangeListener,
|
||||
AudioManager.STREAM_MUSIC,
|
||||
AudioManager.AUDIOFOCUS_GAIN
|
||||
)
|
||||
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||
Log.d("MediaPlaybackService", "Audio focus granted")
|
||||
} else {
|
||||
Log.w("MediaPlaybackService", "Failed to gain audio focus")
|
||||
}
|
||||
|
||||
player = ExoPlayer.Builder(this).build()
|
||||
|
||||
mediaSession = MediaSessionCompat(baseContext, "ReadestMediaSession").apply {
|
||||
stateBuilder = PlaybackStateCompat.Builder().setActions(
|
||||
PlaybackStateCompat.ACTION_PLAY or
|
||||
PlaybackStateCompat.ACTION_PLAY_PAUSE or
|
||||
PlaybackStateCompat.ACTION_PAUSE or
|
||||
PlaybackStateCompat.ACTION_STOP or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
|
||||
)
|
||||
setPlaybackState(stateBuilder.build())
|
||||
setCallback(SessionCallback())
|
||||
setSessionToken(sessionToken)
|
||||
isActive = true
|
||||
}
|
||||
|
||||
player.addListener(object : Player.Listener {
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
updatePlaybackState()
|
||||
}
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
updatePlaybackState()
|
||||
}
|
||||
})
|
||||
|
||||
val mediaItem = MediaItem.fromUri("asset:///silence.mp3")
|
||||
player.setMediaItem(mediaItem)
|
||||
player.repeatMode = Player.REPEAT_MODE_ONE
|
||||
player.prepare()
|
||||
player.playWhenReady = true
|
||||
|
||||
showNotification(PlaybackStateCompat.STATE_PLAYING)
|
||||
}
|
||||
|
||||
private inner class SessionCallback : MediaSessionCompat.Callback() {
|
||||
override fun onPlay() {
|
||||
player.play()
|
||||
pluginEventTrigger?.invoke("media-session-play", JSObject())
|
||||
updatePlaybackState()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
player.pause()
|
||||
pluginEventTrigger?.invoke("media-session-pause", JSObject())
|
||||
updatePlaybackState()
|
||||
}
|
||||
|
||||
override fun onSkipToNext() {
|
||||
player.seekTo(0)
|
||||
pluginEventTrigger?.invoke("media-session-next", JSObject())
|
||||
}
|
||||
|
||||
override fun onSkipToPrevious() {
|
||||
player.seekTo(0)
|
||||
pluginEventTrigger?.invoke("media-session-previous", JSObject())
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePlaybackState() {
|
||||
val state = if (player.isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED
|
||||
mediaSession?.setPlaybackState(
|
||||
stateBuilder.setState(state, player.currentPosition, 1f).build()
|
||||
)
|
||||
showNotification(state)
|
||||
}
|
||||
|
||||
private fun showNotification(playbackState: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(CHANNEL_ID, "Media Controls", NotificationManager.IMPORTANCE_LOW)
|
||||
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
|
||||
}
|
||||
startForeground(NOTIFICATION_ID, buildNotification(playbackState))
|
||||
}
|
||||
|
||||
private fun buildNotification(playbackState: Int): Notification {
|
||||
val builder = NotificationCompat.Builder(this, CHANNEL_ID).apply {
|
||||
setContentTitle(currentTitle)
|
||||
setContentText(currentArtist)
|
||||
setLargeIcon(currentArtwork)
|
||||
setContentIntent(mediaSession!!.controller.sessionActivity)
|
||||
setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(this@MediaPlaybackService, PlaybackStateCompat.ACTION_STOP))
|
||||
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
setSmallIcon(R.drawable.notification_icon)
|
||||
|
||||
setStyle(
|
||||
androidx.media.app.NotificationCompat.MediaStyle()
|
||||
.setMediaSession(mediaSession?.sessionToken)
|
||||
.setShowActionsInCompactView(0)
|
||||
)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
override fun onGetRoot(clientPackageName: String, clientUid: Int, rootHints: Bundle?): BrowserRoot? {
|
||||
return BrowserRoot(MEDIA_ROOT_ID, null)
|
||||
}
|
||||
|
||||
override fun onLoadChildren(parentId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||
result.sendResult(null)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
MediaButtonReceiver.handleIntent(mediaSession, intent)
|
||||
|
||||
if (intent?.action == "UPDATE_METADATA") {
|
||||
currentTitle = intent.getStringExtra("title") ?: currentTitle
|
||||
currentArtist = intent.getStringExtra("artist") ?: currentArtist
|
||||
val newArtwork = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra("artwork", Bitmap::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra("artwork")
|
||||
}
|
||||
if (newArtwork != null) {
|
||||
currentArtwork = newArtwork
|
||||
}
|
||||
|
||||
val metadataBuilder = MediaMetadataCompat.Builder()
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, currentTitle)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, currentArtist)
|
||||
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, currentArtwork)
|
||||
|
||||
mediaSession?.setMetadata(metadataBuilder.build())
|
||||
|
||||
showNotification(if (player.isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED)
|
||||
} else if (intent?.action == "UPDATE_PLAYBACK_STATE") {
|
||||
val isPlaying = intent.getBooleanExtra("playing", false)
|
||||
val position = intent.getLongExtra("position", 0L) // in milliseconds
|
||||
val duration = intent.getLongExtra("duration", 0L) // in milliseconds
|
||||
|
||||
if (isPlaying && !player.isPlaying) {
|
||||
player.play()
|
||||
} else if (!isPlaying && player.isPlaying) {
|
||||
player.pause()
|
||||
}
|
||||
player.seekTo(position)
|
||||
|
||||
val state = if (isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED
|
||||
mediaSession?.setPlaybackState(
|
||||
stateBuilder.setState(state, position, 1f).build()
|
||||
)
|
||||
showNotification(state)
|
||||
}
|
||||
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
player.release()
|
||||
mediaSession?.release()
|
||||
}
|
||||
}
|
||||
|
|
@ -11,13 +11,7 @@ import android.speech.tts.Voice
|
|||
import android.util.Log
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.common.ForwardingPlayer
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.core.content.ContextCompat
|
||||
import app.tauri.annotation.Command
|
||||
import app.tauri.annotation.InvokeArg
|
||||
import app.tauri.annotation.Permission
|
||||
|
|
@ -92,8 +86,8 @@ class UpdateMediaSessionMetadataArgs {
|
|||
@InvokeArg
|
||||
class UpdateMediaSessionStateArgs {
|
||||
var playing: Boolean? = null
|
||||
var position: Double? = null
|
||||
var duration: Double? = null
|
||||
var position: Int? = null // in milliseconds
|
||||
var duration: Int? = null // in milliseconds
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
|
|
@ -106,78 +100,6 @@ class SetMediaSessionActiveArgs {
|
|||
var foregroundServiceText: String? = null
|
||||
}
|
||||
|
||||
class MediaForegroundService : Service() {
|
||||
companion object {
|
||||
const val NOTIFICATION_ID = 1001
|
||||
const val CHANNEL_ID = "tts_media_playback_channel"
|
||||
const val ACTION_START = "START_FOREGROUND_SERVICE"
|
||||
const val ACTION_STOP = "STOP_FOREGROUND_SERVICE"
|
||||
|
||||
var pluginInstance: NativeTTSPlugin? = null
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createNotificationChannel()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.action) {
|
||||
ACTION_START -> {
|
||||
startForegroundService()
|
||||
}
|
||||
ACTION_STOP -> {
|
||||
stopForegroundService()
|
||||
}
|
||||
}
|
||||
return START_STICKY // Restart if killed
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
NativeTTSPlugin.NOTIFICATION_TITLE,
|
||||
NotificationManager.IMPORTANCE_MIN
|
||||
).apply {
|
||||
description = NativeTTSPlugin.NOTIFICATION_TEXT
|
||||
setShowBadge(false)
|
||||
setSound(null, null)
|
||||
}
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startForegroundService() {
|
||||
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(NativeTTSPlugin.FOREGROUND_SERVICE_TITLE)
|
||||
.setContentText(NativeTTSPlugin.FOREGROUND_SERVICE_TEXT)
|
||||
.setSmallIcon(android.R.drawable.ic_media_play)
|
||||
.setOngoing(true)
|
||||
.setSilent(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.build()
|
||||
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
Log.d("MediaForegroundService", "Foreground service started")
|
||||
}
|
||||
|
||||
private fun stopForegroundService() {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
Log.d("MediaForegroundService", "Foreground service stopped")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Log.d("MediaForegroundService", "Service destroyed")
|
||||
}
|
||||
}
|
||||
|
||||
@TauriPlugin(
|
||||
permissions = [
|
||||
Permission(strings = [Manifest.permission.POST_NOTIFICATIONS], alias = "postNotification")
|
||||
|
|
@ -203,18 +125,8 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
|||
|
||||
private val eventChannels = ConcurrentHashMap<String, Channel<TTSMessageEvent>>()
|
||||
private val speakingJobs = ConcurrentHashMap<String, Job>()
|
||||
|
||||
private var player: Player? = null
|
||||
private var mediaSession: MediaSession? = null
|
||||
private var isMediaSessionActive = false
|
||||
private var isForegroundServiceRunning = false
|
||||
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
init {
|
||||
MediaForegroundService.pluginInstance = this
|
||||
}
|
||||
|
||||
@Command
|
||||
fun init(invoke: Invoke) {
|
||||
coroutineScope.launch {
|
||||
|
|
@ -509,95 +421,6 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
|||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
private class WebViewPlayer(player: Player) : ForwardingPlayer(player) {
|
||||
var eventTrigger: ((String, JSObject) -> Unit)? = null
|
||||
|
||||
override fun play() {
|
||||
eventTrigger?.invoke("media-session-play", JSObject())
|
||||
if (!this.playWhenReady) {
|
||||
this.playWhenReady = true
|
||||
} else {
|
||||
this.playWhenReady = false
|
||||
}
|
||||
}
|
||||
override fun pause() {
|
||||
eventTrigger?.invoke("media-session-pause", JSObject())
|
||||
if (!this.playWhenReady) {
|
||||
this.playWhenReady = true
|
||||
} else {
|
||||
this.playWhenReady = false
|
||||
}
|
||||
}
|
||||
override fun seekToNext() {
|
||||
eventTrigger?.invoke("media-session-next", JSObject())
|
||||
}
|
||||
override fun seekToPrevious() {
|
||||
eventTrigger?.invoke("media-session-previous", JSObject())
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeMediaSession() {
|
||||
var basePlayer = ExoPlayer.Builder(activity).build()
|
||||
val webViewPlayer = WebViewPlayer(basePlayer)
|
||||
webViewPlayer.eventTrigger = { event, data -> trigger(event, data) }
|
||||
player = webViewPlayer
|
||||
|
||||
mediaSession = MediaSession.Builder(activity, player!!)
|
||||
.setCallback(object : MediaSession.Callback {
|
||||
override fun onConnect(
|
||||
session: MediaSession,
|
||||
controller: MediaSession.ControllerInfo
|
||||
): MediaSession.ConnectionResult {
|
||||
val builder = MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||
builder.setAvailablePlayerCommands(
|
||||
Player.Commands.Builder()
|
||||
.add(Player.COMMAND_PLAY_PAUSE)
|
||||
.add(Player.COMMAND_SEEK_TO_NEXT)
|
||||
.add(Player.COMMAND_SEEK_TO_PREVIOUS)
|
||||
.build()
|
||||
)
|
||||
return builder.build()
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun deinitializeMediaSession() {
|
||||
mediaSession?.release()
|
||||
mediaSession = null
|
||||
player?.release()
|
||||
player = null
|
||||
}
|
||||
|
||||
private fun startForegroundService() {
|
||||
try {
|
||||
val intent = Intent(activity, MediaForegroundService::class.java).apply {
|
||||
action = MediaForegroundService.ACTION_START
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
activity.startForegroundService(intent)
|
||||
} else {
|
||||
activity.startService(intent)
|
||||
}
|
||||
Log.d(TAG, "Foreground service started")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start foreground service", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopForegroundService() {
|
||||
try {
|
||||
val intent = Intent(activity, MediaForegroundService::class.java).apply {
|
||||
action = MediaForegroundService.ACTION_STOP
|
||||
}
|
||||
activity.stopService(intent)
|
||||
Log.d(TAG, "Foreground service stopped")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to stop foreground service", e)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadArtworkFromUrl(urlString: String): Bitmap? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
|
|
@ -624,50 +447,26 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
|||
}
|
||||
}
|
||||
|
||||
private var currentArtworkByteArray: ByteArray? = null
|
||||
|
||||
@Command
|
||||
fun update_media_session_metadata(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(UpdateMediaSessionMetadataArgs::class.java)
|
||||
val title = args.title ?: ""
|
||||
val artist = args.artist ?: ""
|
||||
val album = args.album ?: ""
|
||||
val artworkUrl = args.artwork ?: ""
|
||||
|
||||
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val metadataBuilder = MediaMetadata.Builder()
|
||||
.setTitle(title)
|
||||
.setArtist(artist)
|
||||
.setAlbumTitle(album)
|
||||
.apply {
|
||||
if (artworkUrl.isNotEmpty()) {
|
||||
loadArtworkFromUrl(artworkUrl)?.let { bitmap ->
|
||||
java.io.ByteArrayOutputStream().use { stream ->
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
|
||||
currentArtworkByteArray = stream.toByteArray()
|
||||
}
|
||||
}
|
||||
}
|
||||
currentArtworkByteArray?.let {
|
||||
setArtworkData(it, MediaMetadata.PICTURE_TYPE_FRONT_COVER)
|
||||
}
|
||||
}
|
||||
|
||||
val mediaItem: MediaItem = MediaItem.Builder()
|
||||
.setMediaId("silent_media")
|
||||
.setUri("data:audio/wav;base64,UklGRigAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQQAAAAAAA==")
|
||||
.setMediaMetadata(metadataBuilder.build())
|
||||
.build()
|
||||
|
||||
player?.setMediaItems(listOf(mediaItem, mediaItem))
|
||||
player?.prepare()
|
||||
player?.playWhenReady = true
|
||||
player?.setRepeatMode(Player.REPEAT_MODE_ALL)
|
||||
|
||||
val artworkBitmap = args.artwork?.let { loadArtworkFromUrl(it) }
|
||||
val intent = Intent(activity, MediaPlaybackService::class.java).apply {
|
||||
action = "UPDATE_METADATA"
|
||||
putExtra("title", title)
|
||||
putExtra("artist", artist)
|
||||
putExtra("album", album)
|
||||
putExtra("artwork", artworkBitmap)
|
||||
}
|
||||
activity.startService(intent)
|
||||
invoke.resolve()
|
||||
} catch (e: Exception) {
|
||||
Log.e("NativeBridgePlugin", "Failed to update metadata: ${e.message}")
|
||||
invoke.reject("Failed to update metadata: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
|
@ -677,15 +476,17 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
|||
fun update_media_session_state(invoke: Invoke) {
|
||||
var args = invoke.parseArgs(UpdateMediaSessionStateArgs::class.java)
|
||||
val isPlaying = args.playing ?: false
|
||||
val position = args.position ?: 0.0
|
||||
|
||||
val position = args.position ?: 0
|
||||
val duration = args.duration ?: 0
|
||||
|
||||
try {
|
||||
player?.let { player ->
|
||||
if (position > 0) {
|
||||
player.seekTo((position * 1000).toLong())
|
||||
}
|
||||
player.playWhenReady = isPlaying
|
||||
val intent = Intent(activity, MediaPlaybackService::class.java).apply {
|
||||
action = "UPDATE_PLAYBACK_STATE"
|
||||
putExtra("playing", isPlaying)
|
||||
putExtra("position", position)
|
||||
putExtra("duration", duration)
|
||||
}
|
||||
activity.startService(intent)
|
||||
invoke.resolve()
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Failed to update playback state: ${e.message}")
|
||||
|
|
@ -696,7 +497,6 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
|||
fun set_media_session_active(invoke: Invoke) {
|
||||
var args = invoke.parseArgs(SetMediaSessionActiveArgs::class.java)
|
||||
val active = args.active ?: true
|
||||
val keepAppInForeground = args.keepAppInForeground ?: false
|
||||
|
||||
args.notificationTitle?.let { NOTIFICATION_TITLE = it }
|
||||
args.notificationText?.let { NOTIFICATION_TEXT = it }
|
||||
|
|
@ -704,41 +504,26 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
|||
args.foregroundServiceText?.let { FOREGROUND_SERVICE_TEXT = it }
|
||||
|
||||
try {
|
||||
if (active && !isMediaSessionActive) {
|
||||
initializeMediaSession()
|
||||
if (keepAppInForeground) {
|
||||
startForegroundService()
|
||||
isForegroundServiceRunning = true
|
||||
}
|
||||
isMediaSessionActive = true
|
||||
Log.d(TAG, "Media session activated with foreground service")
|
||||
} else if (!active && isMediaSessionActive) {
|
||||
deinitializeMediaSession()
|
||||
if (isForegroundServiceRunning) {
|
||||
stopForegroundService()
|
||||
isForegroundServiceRunning = false
|
||||
}
|
||||
isMediaSessionActive = false
|
||||
Log.d(TAG, "Media session deactivated and foreground service stopped")
|
||||
val intent = Intent(activity, MediaPlaybackService::class.java)
|
||||
if (active) {
|
||||
MediaPlaybackService.pluginEventTrigger = { event, data -> trigger(event, data) }
|
||||
MediaPlaybackService.currentTitle = FOREGROUND_SERVICE_TITLE
|
||||
MediaPlaybackService.currentArtist = FOREGROUND_SERVICE_TEXT
|
||||
ContextCompat.startForegroundService(activity, intent)
|
||||
} else {
|
||||
activity.stopService(intent)
|
||||
MediaPlaybackService.pluginEventTrigger = null
|
||||
}
|
||||
invoke.resolve()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to set media session active state: ${e.message}")
|
||||
invoke.reject("Failed to set media session active state: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
try {
|
||||
if (isMediaSessionActive) {
|
||||
if (isForegroundServiceRunning) {
|
||||
stopForegroundService()
|
||||
isForegroundServiceRunning = false
|
||||
}
|
||||
isMediaSessionActive = false
|
||||
}
|
||||
|
||||
deinitializeMediaSession()
|
||||
val intent = Intent(activity, MediaPlaybackService::class.java)
|
||||
activity.stopService(intent)
|
||||
|
||||
coroutineScope.cancel()
|
||||
textToSpeech?.shutdown()
|
||||
|
|
@ -747,8 +532,6 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
|||
speakingJobs.values.forEach { it.cancel() }
|
||||
speakingJobs.clear()
|
||||
|
||||
MediaForegroundService.pluginInstance = null
|
||||
|
||||
Log.d(TAG, "Plugin destroyed successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error during plugin destruction", e)
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Loading…
Add table
Add a link
Reference in a new issue