Răsfoiți Sursa

feat: 添加安卓媒体控制通知

lingran7031 3 luni în urmă
părinte
comite
acd8233335

+ 1 - 0
app/src/main/java/io/legado/app/constant/NotificationId.kt

@@ -13,6 +13,7 @@ object NotificationId {
     const val WebService = 105
     const val WebService = 105
     const val DownloadService = 106
     const val DownloadService = 106
     const val BookSourceCheckService = 107
     const val BookSourceCheckService = 107
+    const val ReadAloudMediaControl = 108
     const val Download = 10000
     const val Download = 10000
     const val ExportBook = 201
     const val ExportBook = 201
 
 

+ 2 - 0
app/src/main/java/io/legado/app/constant/PreferKey.kt

@@ -324,7 +324,9 @@ object PreferKey {
     const val menuAlpha = "menuAlpha"
     const val menuAlpha = "menuAlpha"
     const val colorImage = "colorImage"
     const val colorImage = "colorImage"
     const val bookshelfRefreshingLimit = "bookshelfRefreshingLimit"
     const val bookshelfRefreshingLimit = "bookshelfRefreshingLimit"
+    const val mediaButtonPerNext = "mediaButtonPerNext"
     const val systemMediaControlCompatibilityChange = "systemMediaControlCompatibilityChange"
     const val systemMediaControlCompatibilityChange = "systemMediaControlCompatibilityChange"
+    const val readAloudAndroidMediaControl = "readAloudAndroidMediaControl"
     const val isPredictiveBackEnabled = "isPredictiveBackEnabled"
     const val isPredictiveBackEnabled = "isPredictiveBackEnabled"
     const val readRecordDisplayMode = "readRecordDisplayMode"
     const val readRecordDisplayMode = "readRecordDisplayMode"
     const val replaceSortMode = "desc"
     const val replaceSortMode = "desc"

+ 6 - 4
app/src/main/java/io/legado/app/data/repository/ReadAloudSettingsRepository.kt

@@ -57,6 +57,7 @@ internal fun Preferences.toReadAloudSettings(): ReadAloudSettings = ReadAloudSet
     capsuleOffsetY = compatDsValue(ReadAloudKeys.CapsuleOffsetY, 0f),
     capsuleOffsetY = compatDsValue(ReadAloudKeys.CapsuleOffsetY, 0f),
     mediaButtonPerNext = compatDsValue(ReadAloudKeys.MediaButtonPerNext, false),
     mediaButtonPerNext = compatDsValue(ReadAloudKeys.MediaButtonPerNext, false),
     readAloudByPage = compatDsValue(ReadAloudKeys.ReadAloudByPage, false),
     readAloudByPage = compatDsValue(ReadAloudKeys.ReadAloudByPage, false),
+    androidMediaControlEnabled = compatDsValue(ReadAloudKeys.AndroidMediaControlEnabled, false),
     systemMediaControlCompatibilityChange =
     systemMediaControlCompatibilityChange =
         compatDsValue(ReadAloudKeys.SystemMediaControlCompatibilityChange, true),
         compatDsValue(ReadAloudKeys.SystemMediaControlCompatibilityChange, true),
     streamReadAloudAudio = compatDsValue(ReadAloudKeys.StreamReadAloudAudio, false),
     streamReadAloudAudio = compatDsValue(ReadAloudKeys.StreamReadAloudAudio, false),
@@ -87,8 +88,9 @@ internal fun ReadAloudSettings.toPrefMap(): Map<String, Any?> = mapOf(
     PreferKey.capsuleAutoCollapse to capsuleAutoCollapse,
     PreferKey.capsuleAutoCollapse to capsuleAutoCollapse,
     ReadAloudKeys.CapsuleOffsetX.name to capsuleOffsetX,
     ReadAloudKeys.CapsuleOffsetX.name to capsuleOffsetX,
     ReadAloudKeys.CapsuleOffsetY.name to capsuleOffsetY,
     ReadAloudKeys.CapsuleOffsetY.name to capsuleOffsetY,
-    MEDIA_BUTTON_PER_NEXT to mediaButtonPerNext,
+    PreferKey.mediaButtonPerNext to mediaButtonPerNext,
     PreferKey.readAloudByPage to readAloudByPage,
     PreferKey.readAloudByPage to readAloudByPage,
+    PreferKey.readAloudAndroidMediaControl to androidMediaControlEnabled,
     PreferKey.systemMediaControlCompatibilityChange to systemMediaControlCompatibilityChange,
     PreferKey.systemMediaControlCompatibilityChange to systemMediaControlCompatibilityChange,
     PreferKey.streamReadAloudAudio to streamReadAloudAudio,
     PreferKey.streamReadAloudAudio to streamReadAloudAudio,
     PreferKey.ttsTimer to ttsTimer,
     PreferKey.ttsTimer to ttsTimer,
@@ -102,8 +104,6 @@ internal fun ReadAloudSettings.toPrefMap(): Map<String, Any?> = mapOf(
     PreferKey.ttsPreSynthesisConcurrency to ttsPreSynthesisConcurrency,
     PreferKey.ttsPreSynthesisConcurrency to ttsPreSynthesisConcurrency,
 )
 )
 
 
-private const val MEDIA_BUTTON_PER_NEXT = "mediaButtonPerNext"
-
 private object ReadAloudKeys {
 private object ReadAloudKeys {
     val TtsParagraphInterval = intPreferencesKey(PreferKey.ttsParagraphInterval)
     val TtsParagraphInterval = intPreferencesKey(PreferKey.ttsParagraphInterval)
     val AudioCacheCleanTime = intPreferencesKey(PreferKey.audioCacheCleanTime)
     val AudioCacheCleanTime = intPreferencesKey(PreferKey.audioCacheCleanTime)
@@ -117,8 +117,10 @@ private object ReadAloudKeys {
     val CapsuleAutoCollapse = booleanPreferencesKey(PreferKey.capsuleAutoCollapse)
     val CapsuleAutoCollapse = booleanPreferencesKey(PreferKey.capsuleAutoCollapse)
     val CapsuleOffsetX = floatPreferencesKey("read_aloud_capsule_offset_x")
     val CapsuleOffsetX = floatPreferencesKey("read_aloud_capsule_offset_x")
     val CapsuleOffsetY = floatPreferencesKey("read_aloud_capsule_offset_y")
     val CapsuleOffsetY = floatPreferencesKey("read_aloud_capsule_offset_y")
-    val MediaButtonPerNext = booleanPreferencesKey(MEDIA_BUTTON_PER_NEXT)
+    val MediaButtonPerNext = booleanPreferencesKey(PreferKey.mediaButtonPerNext)
     val ReadAloudByPage = booleanPreferencesKey(PreferKey.readAloudByPage)
     val ReadAloudByPage = booleanPreferencesKey(PreferKey.readAloudByPage)
+    val AndroidMediaControlEnabled =
+        booleanPreferencesKey(PreferKey.readAloudAndroidMediaControl)
     val SystemMediaControlCompatibilityChange =
     val SystemMediaControlCompatibilityChange =
         booleanPreferencesKey(PreferKey.systemMediaControlCompatibilityChange)
         booleanPreferencesKey(PreferKey.systemMediaControlCompatibilityChange)
     val StreamReadAloudAudio = booleanPreferencesKey(PreferKey.streamReadAloudAudio)
     val StreamReadAloudAudio = booleanPreferencesKey(PreferKey.streamReadAloudAudio)

+ 1 - 0
app/src/main/java/io/legado/app/domain/model/settings/ReadAloudSettings.kt

@@ -15,6 +15,7 @@ data class ReadAloudSettings(
     val capsuleOffsetY: Float = 0f,
     val capsuleOffsetY: Float = 0f,
     val mediaButtonPerNext: Boolean = false,
     val mediaButtonPerNext: Boolean = false,
     val readAloudByPage: Boolean = false,
     val readAloudByPage: Boolean = false,
+    val androidMediaControlEnabled: Boolean = false,
     val systemMediaControlCompatibilityChange: Boolean = true,
     val systemMediaControlCompatibilityChange: Boolean = true,
     val streamReadAloudAudio: Boolean = false,
     val streamReadAloudAudio: Boolean = false,
     val ttsTimer: Int = 0,
     val ttsTimer: Int = 0,

+ 3 - 2
app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt

@@ -5,6 +5,7 @@ import android.content.Context
 import android.content.Intent
 import android.content.Intent
 import android.view.KeyEvent
 import android.view.KeyEvent
 import io.legado.app.constant.EventBus
 import io.legado.app.constant.EventBus
+import io.legado.app.constant.PreferKey
 import io.legado.app.data.appDb
 import io.legado.app.data.appDb
 import io.legado.app.help.LifecycleHelp
 import io.legado.app.help.LifecycleHelp
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.AppConfig
@@ -48,7 +49,7 @@ class MediaButtonReceiver : BroadcastReceiver() {
                     LogUtils.d(TAG, "Receive mediaButton event, keycode:$keycode")
                     LogUtils.d(TAG, "Receive mediaButton event, keycode:$keycode")
                     when (keycode) {
                     when (keycode) {
                         KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
                         KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
-                            if (context.getPrefBoolean("mediaButtonPerNext", false)) {
+                            if (context.getPrefBoolean(PreferKey.mediaButtonPerNext, false)) {
                                 ReadBook.moveToPrevChapter(true)
                                 ReadBook.moveToPrevChapter(true)
                             } else {
                             } else {
                                 ReadAloud.prevParagraph(context)
                                 ReadAloud.prevParagraph(context)
@@ -56,7 +57,7 @@ class MediaButtonReceiver : BroadcastReceiver() {
                         }
                         }
 
 
                         KeyEvent.KEYCODE_MEDIA_NEXT -> {
                         KeyEvent.KEYCODE_MEDIA_NEXT -> {
-                            if (context.getPrefBoolean("mediaButtonPerNext", false)) {
+                            if (context.getPrefBoolean(PreferKey.mediaButtonPerNext, false)) {
                                 ReadBook.moveToNextChapter(true)
                                 ReadBook.moveToNextChapter(true)
                             } else {
                             } else {
                                 ReadAloud.nextParagraph(context)
                                 ReadAloud.nextParagraph(context)

+ 184 - 59
app/src/main/java/io/legado/app/service/BaseReadAloudService.kt

@@ -116,6 +116,14 @@ abstract class BaseReadAloudService : BaseService(),
 
 
         private const val TAG = "BaseReadAloudService"
         private const val TAG = "BaseReadAloudService"
 
 
+        private const val READ_ALOUD_MEDIA_SESSION_ACTIONS =
+            (PlaybackStateCompat.ACTION_PLAY
+                    or PlaybackStateCompat.ACTION_PAUSE
+                    or PlaybackStateCompat.ACTION_PLAY_PAUSE
+                    or PlaybackStateCompat.ACTION_STOP
+                    or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
+                    or PlaybackStateCompat.ACTION_SKIP_TO_NEXT)
+
     }
     }
 
 
     private val sessionStore: ReadAloudSessionStore by lazy {
     private val sessionStore: ReadAloudSessionStore by lazy {
@@ -158,7 +166,12 @@ abstract class BaseReadAloudService : BaseService(),
     private var needResumeOnCallStateIdle = false
     private var needResumeOnCallStateIdle = false
     private var registeredPhoneStateListener = false
     private var registeredPhoneStateListener = false
     private var dsJob: Job? = null
     private var dsJob: Job? = null
-    private var upNotificationJob: Coroutine<*>? = null
+    private var upNotificationJob: Job? = null
+    @Volatile
+    private var systemMediaCompatibilityEnabled =
+        ReadConfig.systemMediaControlCompatibilityChange
+    @Volatile
+    private var androidMediaControlEnabled = ReadConfig.androidMediaControlEnabled
     private var prepareReadAloudJob: Coroutine<*>? = null
     private var prepareReadAloudJob: Coroutine<*>? = null
     private var prepareReadAloudGeneration = 0L
     private var prepareReadAloudGeneration = 0L
     private var cover: Bitmap =
     private var cover: Bitmap =
@@ -187,6 +200,7 @@ abstract class BaseReadAloudService : BaseService(),
         pause = false
         pause = false
         observeLiveBus()
         observeLiveBus()
         initMediaSession()
         initMediaSession()
+        observeMediaControlSettings()
         initBroadcastReceiver()
         initBroadcastReceiver()
         initPhoneStateListener()
         initPhoneStateListener()
         upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
         upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
@@ -243,7 +257,11 @@ abstract class BaseReadAloudService : BaseService(),
         unregisterReceiver(broadcastReceiver)
         unregisterReceiver(broadcastReceiver)
         postEvent(EventBus.ALOUD_STATE, Status.STOP)
         postEvent(EventBus.ALOUD_STATE, Status.STOP)
         notificationManager.cancel(NotificationId.ReadAloudService)
         notificationManager.cancel(NotificationId.ReadAloudService)
+        notificationManager.cancel(NotificationId.ReadAloudMediaControl)
         upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED)
         upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED)
+        systemMediaCompatibilityEnabled = false
+        androidMediaControlEnabled = false
+        mediaSessionCompat.isActive = false
         mediaSessionCompat.release()
         mediaSessionCompat.release()
         ReadBook.uploadProgress()
         ReadBook.uploadProgress()
         unregisterPhoneStateListener(phoneStateListener)
         unregisterPhoneStateListener(phoneStateListener)
@@ -253,6 +271,7 @@ abstract class BaseReadAloudService : BaseService(),
         }
         }
         upNotificationJob?.invokeOnCompletion {
         upNotificationJob?.invokeOnCompletion {
             notificationManager.cancel(NotificationId.ReadAloudService)
             notificationManager.cancel(NotificationId.ReadAloudService)
+            notificationManager.cancel(NotificationId.ReadAloudMediaControl)
         }
         }
     }
     }
 
 
@@ -403,6 +422,7 @@ abstract class BaseReadAloudService : BaseService(),
         pause = false
         pause = false
         needResumeOnAudioFocusGain = false
         needResumeOnAudioFocusGain = false
         needResumeOnCallStateIdle = false
         needResumeOnCallStateIdle = false
+        upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
         upReadAloudNotification()
         upReadAloudNotification()
         sessionStore.setStatus(ReadAloudSessionStatus.Playing)
         sessionStore.setStatus(ReadAloudSessionStatus.Playing)
         postEvent(EventBus.ALOUD_STATE, Status.PLAY)
         postEvent(EventBus.ALOUD_STATE, Status.PLAY)
@@ -704,18 +724,11 @@ abstract class BaseReadAloudService : BaseService(),
      * 更新媒体状态
      * 更新媒体状态
      */
      */
     private fun upMediaSessionPlaybackState(state: Int) {
     private fun upMediaSessionPlaybackState(state: Int) {
+        val playbackSpeed = if (state == PlaybackStateCompat.STATE_PLAYING) 1f else 0f
         mediaSessionCompat.setPlaybackState(
         mediaSessionCompat.setPlaybackState(
             PlaybackStateCompat.Builder()
             PlaybackStateCompat.Builder()
-                .setActions(MediaHelp.MEDIA_SESSION_ACTIONS)
-                .setState(state, nowSpeak.toLong(), 1f)
-                // 为系统媒体控件添加定时按钮
-//                .addCustomAction(
-//                    PlaybackStateCompat.CustomAction.Builder(
-//                        "ACTION_ADD_TIMER",
-//                        getString(R.string.set_timer),
-//                        R.drawable.ic_time_add_24dp
-//                    ).build()
-//                )
+                .setActions(READ_ALOUD_MEDIA_SESSION_ACTIONS)
+                .setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, playbackSpeed)
                 .build()
                 .build()
         )
         )
     }
     }
@@ -748,55 +761,102 @@ abstract class BaseReadAloudService : BaseService(),
      */
      */
     @SuppressLint("UnspecifiedImmutableFlag")
     @SuppressLint("UnspecifiedImmutableFlag")
     private fun initMediaSession() {
     private fun initMediaSession() {
-        if (ReadConfig.systemMediaControlCompatibilityChange) {
-            mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() {
-                override fun onPlay() {
-                    resumeReadAloud()
-                }
+        mediaSessionCompat.setSessionActivity(readAloudActivityPendingIntent())
+        mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() {
+            override fun onPlay() {
+                resumeReadAloud()
+            }
 
 
-                override fun onPause() {
-                    pauseReadAloud()
-                }
+            override fun onPause() {
+                pauseReadAloud()
+            }
 
 
-                override fun onSkipToNext() {
-                    if (ReadConfig.mediaButtonPerNext) {
-                        nextChapter()
-                    } else {
-                        nextP()
-                    }
+            override fun onSkipToNext() {
+                if (ReadConfig.mediaButtonPerNext) {
+                    nextChapter()
+                } else {
+                    nextP()
                 }
                 }
+            }
 
 
-                override fun onSkipToPrevious() {
-                    if (ReadConfig.mediaButtonPerNext) {
-                        prevChapter()
-                    } else {
-                        prevP()
-                    }
+            override fun onSkipToPrevious() {
+                if (ReadConfig.mediaButtonPerNext) {
+                    prevChapter()
+                } else {
+                    prevP()
                 }
                 }
+            }
 
 
-                override fun onStop() {
-                    stopSelf()
-                }
+            override fun onStop() {
+                stopSelf()
+            }
 
 
-                override fun onCustomAction(action: String, extras: Bundle?) {
-                    if (action == "ACTION_ADD_TIMER") addTimer()
-                }
+            override fun onCustomAction(action: String, extras: Bundle?) {
+                if (action == "ACTION_ADD_TIMER") addTimer()
+            }
 
 
-                override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
-                    return MediaButtonReceiver.handleIntent(
-                        this@BaseReadAloudService, mediaButtonEvent
-                    )
-                }
-            })
+            override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
+                return MediaButtonReceiver.handleIntent(
+                    this@BaseReadAloudService, mediaButtonEvent
+                )
+            }
+        })
+        updateMediaSessionActivation()
+    }
+
+    private fun observeMediaControlSettings() {
+        lifecycleScope.launch {
+            AppConfigStore.observeBoolean(PreferKey.readAloudAndroidMediaControl).collect {
+                setAndroidMediaControlEnabled(it == true)
+            }
+        }
+        lifecycleScope.launch {
+            AppConfigStore.observeBoolean(
+                PreferKey.systemMediaControlCompatibilityChange
+            ).collect {
+                setSystemMediaCompatibilityEnabled(it ?: true)
+            }
+        }
+        lifecycleScope.launch {
+            AppConfigStore.observeBoolean(PreferKey.mediaButtonPerNext)
+                .drop(1)
+                .collect { upAndroidMediaControlNotification() }
+        }
+    }
+
+    private fun setAndroidMediaControlEnabled(enabled: Boolean) {
+        val changed = androidMediaControlEnabled != enabled
+        androidMediaControlEnabled = enabled
+        updateMediaSessionActivation()
+        if (enabled) {
+            upMediaMetadata()
+            upMediaSessionPlaybackState(
+                if (pause) PlaybackStateCompat.STATE_PAUSED
+                else PlaybackStateCompat.STATE_PLAYING
+            )
         } else {
         } else {
-            mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() {
-                override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
-                    return MediaButtonReceiver.handleIntent(
-                        this@BaseReadAloudService, mediaButtonEvent
-                    )
-                }
-            })
+            notificationManager.cancel(NotificationId.ReadAloudMediaControl)
         }
         }
+        if (changed) {
+            upReadAloudNotification()
+        } else if (enabled) {
+            upAndroidMediaControlNotification()
+        }
+    }
+
+    private fun setSystemMediaCompatibilityEnabled(enabled: Boolean) {
+        if (systemMediaCompatibilityEnabled == enabled) {
+            updateMediaSessionActivation()
+            return
+        }
+        systemMediaCompatibilityEnabled = enabled
+        updateMediaSessionActivation()
+        upReadAloudNotification()
+    }
+
+    private fun updateMediaSessionActivation() {
+        mediaSessionCompat.isActive =
+            systemMediaCompatibilityEnabled || androidMediaControlEnabled
     }
     }
 
 
     /**
     /**
@@ -846,7 +906,8 @@ abstract class BaseReadAloudService : BaseService(),
     }
     }
 
 
     private fun upReadAloudNotification() {
     private fun upReadAloudNotification() {
-        upNotificationJob = execute {
+        upAndroidMediaControlNotification()
+        upNotificationJob = lifecycleScope.launch(Main.immediate) {
             try {
             try {
                 val notification = createNotification()
                 val notification = createNotification()
                 notificationManager.notify(NotificationId.ReadAloudService, notification.build())
                 notificationManager.notify(NotificationId.ReadAloudService, notification.build())
@@ -859,8 +920,7 @@ abstract class BaseReadAloudService : BaseService(),
     private fun choiceMediaStyle(): androidx.media.app.NotificationCompat.MediaStyle {
     private fun choiceMediaStyle(): androidx.media.app.NotificationCompat.MediaStyle {
         val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle()
         val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle()
             .setShowActionsInCompactView(1, 2, 4)
             .setShowActionsInCompactView(1, 2, 4)
-        if (ReadConfig.systemMediaControlCompatibilityChange) {
-            //fix #4090 android 14 can not show play control in lock screen
+        if (systemMediaCompatibilityEnabled && !androidMediaControlEnabled) {
             mediaStyle.setMediaSession(mediaSessionCompat.sessionToken)
             mediaStyle.setMediaSession(mediaSessionCompat.sessionToken)
         }
         }
         return mediaStyle
         return mediaStyle
@@ -891,12 +951,7 @@ abstract class BaseReadAloudService : BaseService(),
             .setOnlyAlertOnce(true)
             .setOnlyAlertOnce(true)
             .setContentTitle(nTitle)
             .setContentTitle(nTitle)
             .setContentText(nSubtitle)
             .setContentText(nSubtitle)
-            .setContentIntent(
-                activityPendingIntent(
-                    MainActivity.createReadBookIntent(this, readAloud = true),
-                    "activity"
-                )
-            )
+            .setContentIntent(readAloudActivityPendingIntent())
             .setVibrate(null)
             .setVibrate(null)
             .setSound(null)
             .setSound(null)
             .setLights(0, 0, 0)
             .setLights(0, 0, 0)
@@ -939,6 +994,76 @@ abstract class BaseReadAloudService : BaseService(),
         return builder
         return builder
     }
     }
 
 
+    private fun upAndroidMediaControlNotification() {
+        lifecycleScope.launch(Main.immediate) {
+            if (!androidMediaControlEnabled) return@launch
+            notificationManager.notify(
+                NotificationId.ReadAloudMediaControl,
+                createAndroidMediaControlNotification().build(),
+            )
+        }
+    }
+
+    private fun createAndroidMediaControlNotification(): NotificationCompat.Builder {
+        val navigateByChapter = ReadConfig.mediaButtonPerNext
+        val previousAction = if (navigateByChapter) {
+            IntentAction.prev
+        } else {
+            IntentAction.prevParagraph
+        }
+        val nextAction = if (navigateByChapter) IntentAction.next else IntentAction.nextParagraph
+        val previousLabel = getString(
+            if (navigateByChapter) R.string.previous_chapter else R.string.prev_sentence
+        )
+        val nextLabel = getString(
+            if (navigateByChapter) R.string.next_chapter else R.string.next_sentence
+        )
+        val chapterTitle = ReadBook.curTextChapter?.title
+            ?.takeIf { it.isNotBlank() }
+            ?: getString(R.string.read_aloud_s)
+        return NotificationCompat.Builder(this, AppConst.channelIdReadAloud)
+            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+            .setCategory(NotificationCompat.CATEGORY_TRANSPORT)
+            .setSmallIcon(R.drawable.ic_volume_up)
+            .setSubText(ReadBook.book?.author ?: getString(R.string.read_aloud))
+            .setOngoing(true)
+            .setOnlyAlertOnce(true)
+            .setContentTitle(ReadBook.book?.name ?: getString(R.string.read_aloud))
+            .setContentText(chapterTitle)
+            .setContentIntent(readAloudActivityPendingIntent())
+            .setLargeIcon(cover)
+            .setVibrate(null)
+            .setSound(null)
+            .setLights(0, 0, 0)
+            .addAction(
+                R.drawable.ic_skip_previous,
+                previousLabel,
+                aloudServicePendingIntent(previousAction),
+            )
+            .addAction(
+                if (pause) R.drawable.ic_play else R.drawable.ic_pause,
+                getString(if (pause) R.string.resume else R.string.pause),
+                aloudServicePendingIntent(
+                    if (pause) IntentAction.resume else IntentAction.pause
+                ),
+            )
+            .addAction(
+                R.drawable.ic_skip_next,
+                nextLabel,
+                aloudServicePendingIntent(nextAction),
+            )
+            .setStyle(
+                androidx.media.app.NotificationCompat.MediaStyle()
+                    .setMediaSession(mediaSessionCompat.sessionToken)
+                    .setShowActionsInCompactView(0, 1, 2)
+            )
+    }
+
+    private fun readAloudActivityPendingIntent(): PendingIntent? = activityPendingIntent(
+        MainActivity.createReadBookIntent(this, readAloud = true),
+        "activity",
+    )
+
     /**
     /**
      * 更新通知
      * 更新通知
      */
      */

+ 6 - 1
app/src/main/java/io/legado/app/ui/book/read/ReadAloudDelegate.kt

@@ -86,7 +86,9 @@ class ReadAloudDelegate(
                         readAloudCapsuleOffsetY = prefs.capsuleOffsetY,
                         readAloudCapsuleOffsetY = prefs.capsuleOffsetY,
                         readAloudMediaButtonPerNext = prefs.mediaButtonPerNext,
                         readAloudMediaButtonPerNext = prefs.mediaButtonPerNext,
                         readAloudByPage = prefs.readAloudByPage,
                         readAloudByPage = prefs.readAloudByPage,
-                        readAloudSystemMediaCompat = prefs.systemMediaControlCompatibilityChange,
+                        readAloudSystemMediaCompat =
+                            prefs.systemMediaControlCompatibilityChange,
+                        readAloudAndroidMediaControl = prefs.androidMediaControlEnabled,
                         readAloudStreamAudio = prefs.streamReadAloudAudio,
                         readAloudStreamAudio = prefs.streamReadAloudAudio,
                         readAloudTtsFollowSys = prefs.ttsFollowSys,
                         readAloudTtsFollowSys = prefs.ttsFollowSys,
                         readAloudTtsSpeechRate = prefs.ttsSpeechRate,
                         readAloudTtsSpeechRate = prefs.ttsSpeechRate,
@@ -324,6 +326,9 @@ class ReadAloudDelegate(
     fun setSystemMediaCompat(value: Boolean) =
     fun setSystemMediaCompat(value: Boolean) =
         updateSettings { it.copy(systemMediaControlCompatibilityChange = value) }
         updateSettings { it.copy(systemMediaControlCompatibilityChange = value) }
 
 
+    fun setAndroidMediaControl(value: Boolean) =
+        updateSettings { it.copy(androidMediaControlEnabled = value) }
+
     fun setByPage(value: Boolean) {
     fun setByPage(value: Boolean) {
         updateSettings { it.copy(readAloudByPage = value) }
         updateSettings { it.copy(readAloudByPage = value) }
         if (value) postEvent(EventBus.MEDIA_BUTTON, false)
         if (value) postEvent(EventBus.MEDIA_BUTTON, false)

+ 2 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt

@@ -283,6 +283,7 @@ data class ReadBookUiState(
     val readAloudMediaButtonPerNext: Boolean = false,
     val readAloudMediaButtonPerNext: Boolean = false,
     val readAloudByPage: Boolean = false,
     val readAloudByPage: Boolean = false,
     val readAloudSystemMediaCompat: Boolean = true,
     val readAloudSystemMediaCompat: Boolean = true,
+    val readAloudAndroidMediaControl: Boolean = false,
     val readAloudStreamAudio: Boolean = false,
     val readAloudStreamAudio: Boolean = false,
     val readAloudTtsFollowSys: Boolean = false,
     val readAloudTtsFollowSys: Boolean = false,
     val readAloudTtsSpeechRate: Int = 10,
     val readAloudTtsSpeechRate: Int = 10,
@@ -765,6 +766,7 @@ sealed interface ReadBookIntent {
     data class SetReadAloudMediaButtonPerNext(val value: Boolean) : ReadBookIntent
     data class SetReadAloudMediaButtonPerNext(val value: Boolean) : ReadBookIntent
     data class SetReadAloudByPage(val value: Boolean) : ReadBookIntent
     data class SetReadAloudByPage(val value: Boolean) : ReadBookIntent
     data class SetReadAloudSystemMediaCompat(val value: Boolean) : ReadBookIntent
     data class SetReadAloudSystemMediaCompat(val value: Boolean) : ReadBookIntent
+    data class SetReadAloudAndroidMediaControl(val value: Boolean) : ReadBookIntent
     data class SetReadAloudStreamAudio(val value: Boolean) : ReadBookIntent
     data class SetReadAloudStreamAudio(val value: Boolean) : ReadBookIntent
     data object ReadAloudPrevParagraph : ReadBookIntent
     data object ReadAloudPrevParagraph : ReadBookIntent
     data object ReadAloudTogglePause : ReadBookIntent
     data object ReadAloudTogglePause : ReadBookIntent

+ 2 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

@@ -1320,6 +1320,8 @@ class ReadBookViewModel(
             is ReadBookIntent.SetReadAloudByPage -> readAloudDelegate.setByPage(intent.value)
             is ReadBookIntent.SetReadAloudByPage -> readAloudDelegate.setByPage(intent.value)
             is ReadBookIntent.SetReadAloudSystemMediaCompat ->
             is ReadBookIntent.SetReadAloudSystemMediaCompat ->
                 readAloudDelegate.setSystemMediaCompat(intent.value)
                 readAloudDelegate.setSystemMediaCompat(intent.value)
+            is ReadBookIntent.SetReadAloudAndroidMediaControl ->
+                readAloudDelegate.setAndroidMediaControl(intent.value)
             is ReadBookIntent.SetReadAloudStreamAudio ->
             is ReadBookIntent.SetReadAloudStreamAudio ->
                 readAloudDelegate.setStreamAudio(intent.value)
                 readAloudDelegate.setStreamAudio(intent.value)
             is ReadBookIntent.ReadAloudPrevParagraph -> readAloudDelegate.prevParagraph()
             is ReadBookIntent.ReadAloudPrevParagraph -> readAloudDelegate.prevParagraph()

+ 8 - 0
app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudConfigContent.kt

@@ -154,6 +154,14 @@ fun ReadAloudConfigContent(
                             onIntent(ReadBookIntent.SetReadAloudByPage(it))
                             onIntent(ReadBookIntent.SetReadAloudByPage(it))
                         },
                         },
                     )
                     )
+                    TinySwitchSettingItem(
+                        title = stringResource(R.string.read_aloud_android_media_control),
+                        description = stringResource(R.string.read_aloud_android_media_control_summary),
+                        checked = state.readAloudAndroidMediaControl,
+                        onCheckedChange = {
+                            onIntent(ReadBookIntent.SetReadAloudAndroidMediaControl(it))
+                        },
+                    )
                     TinySwitchSettingItem(
                     TinySwitchSettingItem(
                         title = stringResource(R.string.system_media_control_compatibility_change),
                         title = stringResource(R.string.system_media_control_compatibility_change),
                         description = stringResource(R.string.system_media_control_compatibility_change_summary),
                         description = stringResource(R.string.system_media_control_compatibility_change_summary),

+ 1 - 0
app/src/main/java/io/legado/app/ui/config/readConfig/ReadConfig.kt

@@ -91,6 +91,7 @@ object ReadConfig {
     val readAloudWakeLock get() = aloud.readAloudWakeLock
     val readAloudWakeLock get() = aloud.readAloudWakeLock
     val mediaButtonPerNext get() = aloud.mediaButtonPerNext
     val mediaButtonPerNext get() = aloud.mediaButtonPerNext
     val readAloudByPage get() = aloud.readAloudByPage
     val readAloudByPage get() = aloud.readAloudByPage
+    val androidMediaControlEnabled get() = aloud.androidMediaControlEnabled
     val systemMediaControlCompatibilityChange get() = aloud.systemMediaControlCompatibilityChange
     val systemMediaControlCompatibilityChange get() = aloud.systemMediaControlCompatibilityChange
     val streamReadAloudAudio get() = aloud.streamReadAloudAudio
     val streamReadAloudAudio get() = aloud.streamReadAloudAudio
     val contentSelectSpeakMod get() = aloud.contentSelectSpeakMode
     val contentSelectSpeakMod get() = aloud.contentSelectSpeakMode

+ 2 - 0
app/src/main/res/values-zh-rCN/strings.xml

@@ -1636,6 +1636,8 @@
     <string name="firebase_enable_summary">允许应用使用Google Firebase进行崩溃和性能统计</string>
     <string name="firebase_enable_summary">允许应用使用Google Firebase进行崩溃和性能统计</string>
     <string name="locate_current">定位至当前</string>
     <string name="locate_current">定位至当前</string>
     <string name="padding_display_cutouts">填充刘海区域</string>
     <string name="padding_display_cutouts">填充刘海区域</string>
+    <string name="read_aloud_android_media_control">安卓媒体控制</string>
+    <string name="read_aloud_android_media_control_summary">在保留原朗读通知的同时显示系统媒体控制</string>
     <string name="system_media_control_compatibility_change">使用媒体通道</string>
     <string name="system_media_control_compatibility_change">使用媒体通道</string>
     <string name="system_media_control_compatibility_change_summary">使用OEM厂商提供的媒体通道,可能解决一些通知无法显示的问题</string>
     <string name="system_media_control_compatibility_change_summary">使用OEM厂商提供的媒体通道,可能解决一些通知无法显示的问题</string>
     <string name="read_aloud_pause_resume">朗读暂停/继续</string>
     <string name="read_aloud_pause_resume">朗读暂停/继续</string>

+ 2 - 0
app/src/main/res/values-zh-rHK/strings.xml

@@ -2210,6 +2210,8 @@
     <string name="system_contrast">系統對比度</string>
     <string name="system_contrast">系統對比度</string>
     <string name="system_contrast_value">系統對比度:%1$s</string>
     <string name="system_contrast_value">系統對比度:%1$s</string>
     <string name="system_engine">系統引擎</string>
     <string name="system_engine">系統引擎</string>
+    <string name="read_aloud_android_media_control">Android 媒體控制</string>
+    <string name="read_aloud_android_media_control_summary">保留原有朗讀通知,並同時顯示系統媒體控制</string>
     <string name="system_media_control_compatibility_change">系統媒體控制相容性</string>
     <string name="system_media_control_compatibility_change">系統媒體控制相容性</string>
     <string name="system_media_control_compatibility_change_summary">若鎖定螢幕缺少系統媒體控制項時啟用(例如 OneUI 7.0、OriginOS)</string>
     <string name="system_media_control_compatibility_change_summary">若鎖定螢幕缺少系統媒體控制項時啟用(例如 OneUI 7.0、OriginOS)</string>
     <string name="tabletInterface">平板介面</string>
     <string name="tabletInterface">平板介面</string>

+ 2 - 0
app/src/main/res/values-zh-rTW/strings.xml

@@ -1777,6 +1777,8 @@
     <string name="firebase_enable_title">匿名統計</string>
     <string name="firebase_enable_title">匿名統計</string>
     <string name="firebase_enable_summary">允許應用使用 Google Firebase 進行崩潰和效能報告</string>
     <string name="firebase_enable_summary">允許應用使用 Google Firebase 進行崩潰和效能報告</string>
     <string name="locate_current">定位目前位置</string>
     <string name="locate_current">定位目前位置</string>
+    <string name="read_aloud_android_media_control">Android 媒體控制</string>
+    <string name="read_aloud_android_media_control_summary">在保留原朗讀通知的同時顯示系統媒體控制</string>
     <string name="system_media_control_compatibility_change">系統媒體控制相容性</string>
     <string name="system_media_control_compatibility_change">系統媒體控制相容性</string>
     <string name="system_media_control_compatibility_change_summary">如果鎖定畫面上缺少系統媒體控制項,請啟用此選項(例如 OneUI 7.0、OriginOS)</string>
     <string name="system_media_control_compatibility_change_summary">如果鎖定畫面上缺少系統媒體控制項,請啟用此選項(例如 OneUI 7.0、OriginOS)</string>
     <string name="read_aloud_pause_resume">暫停/繼續朗讀</string>
     <string name="read_aloud_pause_resume">暫停/繼續朗讀</string>

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -1691,6 +1691,8 @@
     <string name="firebase_enable_summary">Allow the app to use Google Firebase for crash and performance reports</string>
     <string name="firebase_enable_summary">Allow the app to use Google Firebase for crash and performance reports</string>
     <string name="locate_current">Locate Current</string>
     <string name="locate_current">Locate Current</string>
     <string name="padding_display_cutouts">Fill Display Cutout</string>
     <string name="padding_display_cutouts">Fill Display Cutout</string>
+    <string name="read_aloud_android_media_control">Android media controls</string>
+    <string name="read_aloud_android_media_control_summary">Show Android system media controls while keeping the original read-aloud notification</string>
     <string name="system_media_control_compatibility_change">System Media Control Compatibility</string>
     <string name="system_media_control_compatibility_change">System Media Control Compatibility</string>
     <string name="system_media_control_compatibility_change_summary">Enable if system media controls are missing on lock screen (e.g. OneUI 7.0, OriginOS)</string>
     <string name="system_media_control_compatibility_change_summary">Enable if system media controls are missing on lock screen (e.g. OneUI 7.0, OriginOS)</string>
     <string name="read_aloud_pause_resume">Pause/Resume Reading Aloud</string>
     <string name="read_aloud_pause_resume">Pause/Resume Reading Aloud</string>

+ 11 - 2
app/src/test/java/io/legado/app/data/repository/ReadAloudSettingsMappingTest.kt

@@ -8,14 +8,14 @@ import org.junit.Test
 class ReadAloudSettingsMappingTest {
 class ReadAloudSettingsMappingTest {
 
 
     @Test
     @Test
-    fun `朗读设置 23 键写映射逐字段对应`() {
+    fun `朗读设置 26 键写映射逐字段对应`() {
         readAloudMappingSamples().forEach { settings ->
         readAloudMappingSamples().forEach { settings ->
             assertEquals(settings.expectedPrefMap(), settings.toPrefMap())
             assertEquals(settings.expectedPrefMap(), settings.toPrefMap())
         }
         }
     }
     }
 
 
     @Test
     @Test
-    fun `朗读设置 23 键读映射逐字段对应`() {
+    fun `朗读设置 26 键读映射逐字段对应`() {
         readAloudMappingSamples().forEach { expected ->
         readAloudMappingSamples().forEach { expected ->
             assertEquals(expected, expected.expectedPrefMap().toTestPreferences().toReadAloudSettings())
             assertEquals(expected, expected.expectedPrefMap().toTestPreferences().toReadAloudSettings())
         }
         }
@@ -45,6 +45,13 @@ class ReadAloudSettingsMappingTest {
             values,
             values,
         )
         )
     }
     }
+
+    @Test
+    fun `安卓媒体控制默认关闭`() {
+        val settings = emptyMap<String, Any?>().toTestPreferences().toReadAloudSettings()
+
+        assertEquals(false, settings.androidMediaControlEnabled)
+    }
 }
 }
 
 
 private const val CAPSULE_OFFSET_X = "read_aloud_capsule_offset_x"
 private const val CAPSULE_OFFSET_X = "read_aloud_capsule_offset_x"
@@ -77,6 +84,7 @@ private fun readAloudMappingSamples(): List<ReadAloudSettings> {
         base.copy(showReadAloudCapsule = false),
         base.copy(showReadAloudCapsule = false),
         base.copy(mediaButtonPerNext = true),
         base.copy(mediaButtonPerNext = true),
         base.copy(readAloudByPage = true),
         base.copy(readAloudByPage = true),
+        base.copy(androidMediaControlEnabled = true),
         base.copy(systemMediaControlCompatibilityChange = false),
         base.copy(systemMediaControlCompatibilityChange = false),
         base.copy(streamReadAloudAudio = true),
         base.copy(streamReadAloudAudio = true),
         base.copy(ttsFollowSys = false),
         base.copy(ttsFollowSys = false),
@@ -99,6 +107,7 @@ private fun ReadAloudSettings.expectedPrefMap(): Map<String, Any?> = mapOf(
     CAPSULE_OFFSET_Y to capsuleOffsetY,
     CAPSULE_OFFSET_Y to capsuleOffsetY,
     MEDIA_BUTTON_PER_NEXT to mediaButtonPerNext,
     MEDIA_BUTTON_PER_NEXT to mediaButtonPerNext,
     PreferKey.readAloudByPage to readAloudByPage,
     PreferKey.readAloudByPage to readAloudByPage,
+    PreferKey.readAloudAndroidMediaControl to androidMediaControlEnabled,
     PreferKey.systemMediaControlCompatibilityChange to systemMediaControlCompatibilityChange,
     PreferKey.systemMediaControlCompatibilityChange to systemMediaControlCompatibilityChange,
     PreferKey.streamReadAloudAudio to streamReadAloudAudio,
     PreferKey.streamReadAloudAudio to streamReadAloudAudio,
     PreferKey.ttsTimer to ttsTimer,
     PreferKey.ttsTimer to ttsTimer,