Files
QieKan-3.0/app/src/main/java/io/legato/kazusa/service/BaseReadAloudService.kt
T
2025-10-24 12:58:44 +08:00

766 lines
26 KiB
Kotlin

@file:Suppress("DEPRECATION")
package io.legato.kazusa.service
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.AudioManager
import android.net.wifi.WifiManager
import android.os.Bundle
import android.os.PowerManager
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.telephony.PhoneStateListener
import android.telephony.TelephonyManager
import androidx.annotation.CallSuper
import androidx.core.app.NotificationCompat
import androidx.lifecycle.lifecycleScope
import androidx.media.AudioFocusRequestCompat
import androidx.media.AudioManagerCompat
import io.legato.kazusa.R
import io.legato.kazusa.base.BaseService
import io.legato.kazusa.constant.AppConst
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.constant.AppPattern
import io.legato.kazusa.constant.EventBus
import io.legato.kazusa.constant.IntentAction
import io.legato.kazusa.constant.NotificationId
import io.legato.kazusa.constant.PreferKey
import io.legato.kazusa.constant.Status
import io.legato.kazusa.help.MediaHelp
import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.help.coroutine.Coroutine
import io.legato.kazusa.help.glide.ImageLoader
import io.legato.kazusa.lib.permission.Permissions
import io.legato.kazusa.lib.permission.PermissionsCompat
import io.legato.kazusa.model.ReadAloud
import io.legato.kazusa.model.ReadBook
import io.legato.kazusa.receiver.MediaButtonReceiver
import io.legato.kazusa.ui.book.read.ReadBookActivity
import io.legato.kazusa.ui.book.read.page.entities.TextChapter
import io.legato.kazusa.utils.LogUtils
import io.legato.kazusa.utils.activityPendingIntent
import io.legato.kazusa.utils.getPrefBoolean
import io.legato.kazusa.utils.observeEvent
import io.legato.kazusa.utils.observeSharedPreferences
import io.legato.kazusa.utils.postEvent
import io.legato.kazusa.utils.toastOnUi
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import splitties.init.appCtx
import splitties.systemservices.audioManager
import splitties.systemservices.notificationManager
import splitties.systemservices.powerManager
import splitties.systemservices.telephonyManager
import splitties.systemservices.wifiManager
/**
* 朗读服务
*/
abstract class BaseReadAloudService : BaseService(),
AudioManager.OnAudioFocusChangeListener {
companion object {
@JvmStatic
var isRun = false
private set
@JvmStatic
var pause = true
private set
@JvmStatic
var timeMinute: Int = 0
private set
fun isPlay(): Boolean {
return isRun && !pause
}
private const val TAG = "BaseReadAloudService"
}
private val useWakeLock = appCtx.getPrefBoolean(PreferKey.readAloudWakeLock, false)
private val wakeLock by lazy {
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "legado:ReadAloudService")
.apply {
this.setReferenceCounted(false)
}
}
private val wifiLock by lazy {
@Suppress("DEPRECATION")
wifiManager?.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "legado:AudioPlayService")
?.apply {
setReferenceCounted(false)
}
}
private val mFocusRequest: AudioFocusRequestCompat by lazy {
MediaHelp.buildAudioFocusRequestCompat(this)
}
private val mediaSessionCompat: MediaSessionCompat by lazy {
MediaSessionCompat(this, "readAloud")
}
private val phoneStateListener by lazy {
ReadAloudPhoneStateListener()
}
internal var contentList = emptyList<String>()
internal var nowSpeak: Int = 0
internal var readAloudNumber: Int = 0
internal var textChapter: TextChapter? = null
internal var pageIndex = 0
private var needResumeOnAudioFocusGain = false
private var needResumeOnCallStateIdle = false
private var registeredPhoneStateListener = false
private var dsJob: Job? = null
private var upNotificationJob: Coroutine<*>? = null
private var cover: Bitmap =
BitmapFactory.decodeResource(appCtx.resources, R.drawable.ic_launcher)
var pageChanged = false
private var toLast = false
var paragraphStartPos = 0
var readAloudByPage = false
private set
private val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY == intent.action) {
pauseReadAloud()
}
}
}
@SuppressLint("WakelockTimeout")
override fun onCreate() {
super.onCreate()
isRun = true
pause = false
observeLiveBus()
initMediaSession()
initBroadcastReceiver()
initPhoneStateListener()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
setTimer(AppConfig.ttsTimer)
if (AppConfig.ttsTimer > 0) {
toastOnUi("朗读定时 ${AppConfig.ttsTimer} 分钟")
}
execute {
ImageLoader
.loadBitmap(this@BaseReadAloudService, ReadBook.book?.getDisplayCover())
.submit()
.get()
}.onSuccess {
if (it.width > 16 && it.height > 16) {
cover = it
upReadAloudNotification()
}
}
}
fun observeLiveBus() {
observeEvent<Bundle>(EventBus.READ_ALOUD_PLAY) {
val play = it.getBoolean("play")
val pageIndex = it.getInt("pageIndex")
val startPos = it.getInt("startPos")
newReadAloud(play, pageIndex, startPos)
}
observeSharedPreferences { _, key ->
when (key) {
PreferKey.ignoreAudioFocus,
PreferKey.pauseReadAloudWhilePhoneCalls -> {
initPhoneStateListener()
}
}
}
}
override fun onDestroy() {
super.onDestroy()
if (useWakeLock) {
wakeLock.release()
wifiLock?.release()
}
isRun = false
pause = true
abandonFocus()
unregisterReceiver(broadcastReceiver)
postEvent(EventBus.ALOUD_STATE, Status.STOP)
notificationManager.cancel(NotificationId.ReadAloudService)
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED)
mediaSessionCompat.release()
ReadBook.uploadProgress()
unregisterPhoneStateListener(phoneStateListener)
upNotificationJob?.invokeOnCompletion {
notificationManager.cancel(NotificationId.ReadAloudService)
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
IntentAction.play -> newReadAloud(
intent.getBooleanExtra("play", true),
intent.getIntExtra("pageIndex", ReadBook.durPageIndex),
intent.getIntExtra("startPos", 0)
)
IntentAction.pause -> pauseReadAloud()
IntentAction.resume -> resumeReadAloud()
IntentAction.upTtsSpeechRate -> upSpeechRate(true)
IntentAction.prevParagraph -> prevP()
IntentAction.nextParagraph -> nextP()
IntentAction.prev -> prevChapter()
IntentAction.next -> nextChapter()
IntentAction.addTimer -> addTimer()
IntentAction.setTimer -> setTimer(intent.getIntExtra("minute", 0))
IntentAction.stop -> stopSelf()
}
return super.onStartCommand(intent, flags, startId)
}
private fun newReadAloud(play: Boolean, pageIndex: Int, startPos: Int) {
execute(executeContext = IO) {
this@BaseReadAloudService.pageIndex = pageIndex
textChapter = ReadBook.curTextChapter
val textChapter = textChapter ?: return@execute
if (!textChapter.isCompleted) {
return@execute
}
readAloudNumber = textChapter.getReadLength(pageIndex) + startPos
readAloudByPage = getPrefBoolean(PreferKey.readAloudByPage)
contentList = textChapter.getNeedReadAloud(0, readAloudByPage, 0)
.split("\n")
.filter { it.isNotEmpty() }
var pos = startPos
val page = textChapter.getPage(pageIndex)!!
if (pos > 0) {
for (paragraph in page.paragraphs) {
val tmp = pos - paragraph.length - 1
if (tmp < 0) break
pos = tmp
}
}
nowSpeak = textChapter.getParagraphNum(readAloudNumber + 1, readAloudByPage) - 1
if (!readAloudByPage && startPos == 0 && !toLast) {
pos = page.chapterPosition -
textChapter.paragraphs[nowSpeak].chapterPosition
}
if (toLast) {
toLast = false
readAloudNumber = textChapter.getLastParagraphPosition()
nowSpeak = contentList.lastIndex
if (page.paragraphs.size == 1) {
pos = page.chapterPosition -
textChapter.paragraphs[nowSpeak].chapterPosition
}
}
paragraphStartPos = pos
launch(Main) {
if (play) play() else pageChanged = true
}
}.onError {
AppLog.put("启动朗读出错\n${it.localizedMessage}", it, true)
}
}
@SuppressLint("WakelockTimeout")
open fun play() {
if (useWakeLock) {
wakeLock.acquire()
wifiLock?.acquire()
}
isRun = true
pause = false
needResumeOnAudioFocusGain = false
needResumeOnCallStateIdle = false
upReadAloudNotification()
postEvent(EventBus.ALOUD_STATE, Status.PLAY)
}
abstract fun playStop()
@CallSuper
open fun pauseReadAloud(abandonFocus: Boolean = true) {
if (useWakeLock) {
wakeLock.release()
wifiLock?.release()
}
pause = true
if (abandonFocus) {
abandonFocus()
}
upReadAloudNotification()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED)
postEvent(EventBus.ALOUD_STATE, Status.PAUSE)
ReadBook.uploadProgress()
doDs()
}
@SuppressLint("WakelockTimeout")
@CallSuper
open fun resumeReadAloud() {
pause = false
needResumeOnAudioFocusGain = false
needResumeOnCallStateIdle = false
upReadAloudNotification()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
postEvent(EventBus.ALOUD_STATE, Status.PLAY)
}
abstract fun upSpeechRate(reset: Boolean = false)
fun upTtsProgress(progress: Int) {
postEvent(EventBus.TTS_PROGRESS, progress)
}
private fun prevP() {
if (nowSpeak > 0) {
playStop()
do {
nowSpeak--
readAloudNumber -= contentList[nowSpeak].length + 1 + paragraphStartPos
paragraphStartPos = 0
} while (contentList[nowSpeak].matches(AppPattern.notReadAloudRegex))
textChapter?.let {
if (readAloudByPage) {
val paragraphs = it.getParagraphs(true)
if (!paragraphs[nowSpeak].isParagraphEnd) readAloudNumber++
}
if (readAloudNumber < it.getReadLength(pageIndex)) {
pageIndex--
ReadBook.moveToPrevPage()
}
}
upTtsProgress(readAloudNumber + 1)
play()
} else {
toLast = true
ReadBook.moveToPrevChapter(true)
}
}
private fun nextP() {
if (nowSpeak < contentList.size - 1) {
playStop()
readAloudNumber += contentList[nowSpeak].length.plus(1) - paragraphStartPos
paragraphStartPos = 0
nowSpeak++
textChapter?.let {
if (readAloudByPage) {
val paragraphs = it.getParagraphs(true)
if (!paragraphs[nowSpeak].isParagraphEnd) readAloudNumber--
}
if (pageIndex + 1 < it.pageSize
&& readAloudNumber >= it.getReadLength(pageIndex + 1)
) {
pageIndex++
ReadBook.moveToNextPage()
}
}
upTtsProgress(readAloudNumber + 1)
play()
} else {
nextChapter()
}
}
private fun setTimer(minute: Int) {
timeMinute = minute
doDs()
}
private fun addTimer() {
if (timeMinute == 180) {
timeMinute = 0
} else {
timeMinute += 10
if (timeMinute > 180) timeMinute = 180
}
doDs()
}
/**
* 定时
*/
@Synchronized
private fun doDs() {
postEvent(EventBus.READ_ALOUD_DS, timeMinute)
upReadAloudNotification()
dsJob?.cancel()
dsJob = lifecycleScope.launch {
while (isActive) {
delay(60000)
if (!pause) {
if (timeMinute >= 0) {
timeMinute--
}
if (timeMinute == 0) {
ReadAloud.stop(this@BaseReadAloudService)
postEvent(EventBus.READ_ALOUD_DS, timeMinute)
break
}
}
postEvent(EventBus.READ_ALOUD_DS, timeMinute)
upReadAloudNotification()
}
}
}
/**
* 请求音频焦点
* @return 音频焦点
*/
fun requestFocus(): Boolean {
if (AppConfig.ignoreAudioFocus) {
return true
}
val requestFocus = MediaHelp.requestFocus(mFocusRequest)
if (!requestFocus) {
pauseReadAloud(false)
toastOnUi("未获取到音频焦点")
}
return requestFocus
}
/**
* 放弃音频焦点
*/
private fun abandonFocus() {
AudioManagerCompat.abandonAudioFocusRequest(audioManager, mFocusRequest)
}
/**
* 更新媒体状态
*/
private fun upMediaSessionPlaybackState(state: Int) {
mediaSessionCompat.setPlaybackState(
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()
// )
.build()
)
}
/**
* 初始化MediaSession, 注册多媒体按钮
*/
/**
* 初始化MediaSession, 注册多媒体按钮
*/
@SuppressLint("UnspecifiedImmutableFlag")
private fun initMediaSession() {
if (getPrefBoolean("systemMediaControlCompatibilityChange")) {
mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() {
override fun onPlay() {
resumeReadAloud()
}
override fun onPause() {
pauseReadAloud()
}
override fun onSkipToNext() {
if (getPrefBoolean("mediaButtonPerNext", false)) {
nextChapter()
} else {
nextP()
}
}
override fun onSkipToPrevious() {
if (getPrefBoolean("mediaButtonPerNext", false)) {
prevChapter()
} else {
prevP()
}
}
override fun onStop() {
stopSelf()
}
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
)
}
})
} else {
mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() {
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
return MediaButtonReceiver.handleIntent(
this@BaseReadAloudService, mediaButtonEvent
)
}
})
}
}
/**
* 注册多媒体按钮监听
*/
private fun initBroadcastReceiver() {
val intentFilter = IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
registerReceiver(broadcastReceiver, intentFilter)
}
/**
* 音频焦点变化
*/
override fun onAudioFocusChange(focusChange: Int) {
if (AppConfig.ignoreAudioFocus) {
AppLog.put("忽略音频焦点处理(TTS)")
return
}
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> {
if (needResumeOnAudioFocusGain) {
AppLog.put("音频焦点获得,继续朗读")
resumeReadAloud()
} else {
AppLog.put("音频焦点获得")
}
}
AudioManager.AUDIOFOCUS_LOSS -> {
AppLog.put("音频焦点丢失,暂停朗读")
pauseReadAloud()
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
AppLog.put("音频焦点暂时丢失并会很快再次获得,暂停朗读")
if (!pause) {
needResumeOnAudioFocusGain = true
pauseReadAloud(false)
}
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
// 短暂丢失焦点,这种情况是被其他应用申请了短暂的焦点希望其他声音能压低音量(或者关闭声音)凸显这个声音(比如短信提示音),
AppLog.put("音频焦点短暂丢失,不做处理")
}
}
}
private fun upReadAloudNotification() {
upNotificationJob = execute {
try {
val notification = createNotification()
notificationManager.notify(NotificationId.ReadAloudService, notification.build())
} catch (e: Exception) {
AppLog.put("创建朗读通知出错,${e.localizedMessage}", e, true)
}
}
}
private fun choiceMediaStyle(): androidx.media.app.NotificationCompat.MediaStyle {
val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle()
.setShowActionsInCompactView(1, 2, 4)
if (getPrefBoolean("systemMediaControlCompatibilityChange")) {
//fix #4090 android 14 can not show play control in lock screen
mediaStyle.setMediaSession(mediaSessionCompat.sessionToken)
}
return mediaStyle
}
private fun createNotification(): NotificationCompat.Builder {
var nTitle: String = when {
pause -> getString(R.string.read_aloud_pause)
timeMinute > 0 -> getString(
R.string.read_aloud_timer,
timeMinute
)
else -> getString(R.string.read_aloud_t)
}
nTitle += ": ${ReadBook.book?.name}"
var nSubtitle = ReadBook.curTextChapter?.title
if (nSubtitle.isNullOrBlank())
nSubtitle = getString(R.string.read_aloud_s)
val builder = NotificationCompat
.Builder(this, AppConst.channelIdReadAloud)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
.setSmallIcon(R.drawable.ic_volume_up)
.setSubText(getString(R.string.read_aloud))
.setOngoing(true)
.setOnlyAlertOnce(true)
.setContentTitle(nTitle)
.setContentText(nSubtitle)
.setContentIntent(
activityPendingIntent<ReadBookActivity>("activity")
)
.setVibrate(null)
.setSound(null)
.setLights(0, 0, 0)
builder.setLargeIcon(cover)
// 按钮定义:上一章、播放、停止、下一章、定时
builder.addAction(
R.drawable.ic_skip_previous,
getString(R.string.previous_chapter),
aloudServicePendingIntent(IntentAction.prev)
)
if (pause) {
builder.addAction(
R.drawable.ic_play,
getString(R.string.resume),
aloudServicePendingIntent(IntentAction.resume)
)
} else {
builder.addAction(
R.drawable.ic_pause,
getString(R.string.pause),
aloudServicePendingIntent(IntentAction.pause)
)
}
builder.addAction(
R.drawable.ic_stop_black_24dp,
getString(R.string.stop),
aloudServicePendingIntent(IntentAction.stop)
)
builder.addAction(
R.drawable.ic_skip_next,
getString(R.string.next_chapter),
aloudServicePendingIntent(IntentAction.next)
)
builder.addAction(
R.drawable.ic_time_add_24dp,
getString(R.string.set_timer),
aloudServicePendingIntent(IntentAction.addTimer)
)
builder.setStyle(choiceMediaStyle())
return builder
}
/**
* 更新通知
*/
override fun startForegroundNotification() {
execute {
try {
val notification = createNotification()
startForeground(NotificationId.ReadAloudService, notification.build())
} catch (e: Exception) {
AppLog.put("创建朗读通知出错,${e.localizedMessage}", e, true)
//创建通知出错不结束服务就会崩溃,服务必须绑定通知
stopSelf()
}
}
}
abstract fun aloudServicePendingIntent(actionStr: String): PendingIntent?
open fun prevChapter() {
toLast = false
ReadBook.moveToPrevChapter(true, toLast = false)
}
open fun nextChapter() {
ReadBook.upReadTime()
AppLog.putDebug("${ReadBook.curTextChapter?.chapter?.title} 朗读结束跳转下一章并朗读")
if (!ReadBook.moveToNextChapter(true)) {
stopSelf()
}
}
private fun initPhoneStateListener() {
val needRegister = AppConfig.ignoreAudioFocus && AppConfig.pauseReadAloudWhilePhoneCalls
if (needRegister && registeredPhoneStateListener) {
return
}
if (needRegister) {
registerPhoneStateListener(phoneStateListener)
} else {
unregisterPhoneStateListener(phoneStateListener)
}
}
private fun unregisterPhoneStateListener(l: PhoneStateListener) {
if (registeredPhoneStateListener) {
withReadPhoneStatePermission {
telephonyManager.listen(l, PhoneStateListener.LISTEN_NONE)
registeredPhoneStateListener = false
}
}
}
private fun registerPhoneStateListener(l: PhoneStateListener) {
withReadPhoneStatePermission {
telephonyManager.listen(l, PhoneStateListener.LISTEN_CALL_STATE)
registeredPhoneStateListener = true
}
}
private fun withReadPhoneStatePermission(block: () -> Unit) {
try {
block.invoke()
} catch (_: SecurityException) {
PermissionsCompat.Builder()
.addPermissions(Permissions.READ_PHONE_STATE)
.rationale(R.string.read_aloud_read_phone_state_permission_rationale)
.onGranted {
try {
block.invoke()
} catch (_: SecurityException) {
LogUtils.d(TAG, "Grant read phone state permission fail.")
}
}
.request()
}
}
@Suppress("OVERRIDE_DEPRECATION")
inner class ReadAloudPhoneStateListener : PhoneStateListener() {
override fun onCallStateChanged(state: Int, phoneNumber: String?) {
super.onCallStateChanged(state, phoneNumber)
when (state) {
TelephonyManager.CALL_STATE_IDLE -> {
if (needResumeOnCallStateIdle) {
AppLog.put("来电结束,继续朗读")
resumeReadAloud()
} else {
AppLog.put("来电结束")
}
}
TelephonyManager.CALL_STATE_RINGING -> {
if (!pause) {
AppLog.put("来电响铃,暂停朗读")
needResumeOnCallStateIdle = true
pauseReadAloud()
} else {
AppLog.put("来电响铃")
}
}
TelephonyManager.CALL_STATE_OFFHOOK -> {
AppLog.put("来电接听,不做处理")
}
}
}
}
}