pull/34/head
kunfei 5 years ago
parent 7c38657397
commit 7f6302d80d
  1. 2
      app/src/main/AndroidManifest.xml
  2. 4
      app/src/main/java/io/legado/app/help/CrashHandler.kt
  3. 4
      app/src/main/java/io/legado/app/help/PendingIntentHelp.kt
  4. 368
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  5. 218
      app/src/main/java/io/legado/app/service/TTSReadAloudService.kt
  6. 68
      app/src/main/java/io/legado/app/service/notification/ReadAloudNotification.kt
  7. 14
      app/src/main/java/io/legado/app/ui/readbook/ReadBookActivity.kt
  8. 4
      app/src/main/java/io/legado/app/ui/readbook/ReadBookViewModel.kt
  9. 21
      app/src/main/java/io/legado/app/ui/readbook/config/ReadAloudDialog.kt

@ -68,7 +68,7 @@
<service android:name=".service.CheckSourceService" /> <service android:name=".service.CheckSourceService" />
<service android:name=".service.DownloadService" /> <service android:name=".service.DownloadService" />
<service android:name=".service.ReadAloudService" /> <service android:name=".service.TTSReadAloudService" />
<service android:name=".service.UpdateService" /> <service android:name=".service.UpdateService" />
<service android:name=".service.WebService" /> <service android:name=".service.WebService" />

@ -8,7 +8,7 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import io.legado.app.service.ReadAloudService import io.legado.app.service.TTSReadAloudService
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.PrintWriter import java.io.PrintWriter
@ -54,7 +54,7 @@ class CrashHandler : Thread.UncaughtExceptionHandler {
* uncaughtException 回调函数 * uncaughtException 回调函数
*/ */
override fun uncaughtException(thread: Thread, ex: Throwable) { override fun uncaughtException(thread: Thread, ex: Throwable) {
ReadAloudService.clearTTS() TTSReadAloudService.clearTTS()
handleException(ex) handleException(ex)
mDefaultHandler?.uncaughtException(thread, ex) mDefaultHandler?.uncaughtException(thread, ex)
} }

@ -3,7 +3,7 @@ package io.legado.app.help
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import io.legado.app.service.ReadAloudService import io.legado.app.service.TTSReadAloudService
import io.legado.app.ui.readbook.ReadBookActivity import io.legado.app.ui.readbook.ReadBookActivity
object PendingIntentHelp { object PendingIntentHelp {
@ -15,7 +15,7 @@ object PendingIntentHelp {
} }
fun aloudServicePendingIntent(context: Context, actionStr: String): PendingIntent { fun aloudServicePendingIntent(context: Context, actionStr: String): PendingIntent {
val intent = Intent(context, ReadAloudService::class.java) val intent = Intent(context, TTSReadAloudService::class.java)
intent.action = actionStr intent.action = actionStr
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
} }

@ -2,40 +2,32 @@ package io.legado.app.service
import android.app.PendingIntent import android.app.PendingIntent
import android.content.* import android.content.*
import android.graphics.BitmapFactory
import android.media.AudioFocusRequest import android.media.AudioFocusRequest
import android.media.AudioManager import android.media.AudioManager
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat import android.support.v4.media.session.PlaybackStateCompat
import androidx.annotation.CallSuper
import androidx.core.app.NotificationCompat
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseService import io.legado.app.base.BaseService
import io.legado.app.constant.Action import io.legado.app.constant.Action
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.Bus import io.legado.app.constant.Bus
import io.legado.app.constant.Status import io.legado.app.constant.Status
import io.legado.app.help.IntentDataHelp
import io.legado.app.help.IntentHelp
import io.legado.app.help.MediaHelp import io.legado.app.help.MediaHelp
import io.legado.app.help.PendingIntentHelp
import io.legado.app.receiver.MediaButtonReceiver import io.legado.app.receiver.MediaButtonReceiver
import io.legado.app.service.notification.ReadAloudNotification
import io.legado.app.ui.widget.page.TextChapter import io.legado.app.ui.widget.page.TextChapter
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import kotlinx.coroutines.launch
import org.jetbrains.anko.toast
import java.util.*
class ReadAloudService : BaseService(), TextToSpeech.OnInitListener, abstract class BaseReadAloudService : BaseService(),
AudioManager.OnAudioFocusChangeListener { AudioManager.OnAudioFocusChangeListener {
companion object { companion object {
val tag: String = ReadAloudService::class.java.simpleName
var isRun = false var isRun = false
var textToSpeech: TextToSpeech? = null
var timeMinute: Int = 0 var timeMinute: Int = 0
fun play( fun play(
@ -46,7 +38,7 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener,
dataKey: String, dataKey: String,
play: Boolean = true play: Boolean = true
) { ) {
val readAloudIntent = Intent(context, ReadAloudService::class.java) val readAloudIntent = Intent(context, TTSReadAloudService::class.java)
readAloudIntent.action = Action.play readAloudIntent.action = Action.play
readAloudIntent.putExtra("title", title) readAloudIntent.putExtra("title", title)
readAloudIntent.putExtra("subtitle", subtitle) readAloudIntent.putExtra("subtitle", subtitle)
@ -58,7 +50,7 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener,
fun pause(context: Context) { fun pause(context: Context) {
if (isRun) { if (isRun) {
val intent = Intent(context, ReadAloudService::class.java) val intent = Intent(context, TTSReadAloudService::class.java)
intent.action = Action.pause intent.action = Action.pause
context.startService(intent) context.startService(intent)
} }
@ -66,7 +58,7 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener,
fun resume(context: Context) { fun resume(context: Context) {
if (isRun) { if (isRun) {
val intent = Intent(context, ReadAloudService::class.java) val intent = Intent(context, TTSReadAloudService::class.java)
intent.action = Action.resume intent.action = Action.resume
context.startService(intent) context.startService(intent)
} }
@ -74,7 +66,7 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener,
fun stop(context: Context) { fun stop(context: Context) {
if (isRun) { if (isRun) {
val intent = Intent(context, ReadAloudService::class.java) val intent = Intent(context, TTSReadAloudService::class.java)
intent.action = Action.stop intent.action = Action.stop
context.startService(intent) context.startService(intent)
} }
@ -82,7 +74,7 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener,
fun prevParagraph(context: Context) { fun prevParagraph(context: Context) {
if (isRun) { if (isRun) {
val intent = Intent(context, ReadAloudService::class.java) val intent = Intent(context, TTSReadAloudService::class.java)
intent.action = Action.prevParagraph intent.action = Action.prevParagraph
context.startService(intent) context.startService(intent)
} }
@ -90,7 +82,7 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener,
fun nextParagraph(context: Context) { fun nextParagraph(context: Context) {
if (isRun) { if (isRun) {
val intent = Intent(context, ReadAloudService::class.java) val intent = Intent(context, TTSReadAloudService::class.java)
intent.action = Action.nextParagraph intent.action = Action.nextParagraph
context.startService(intent) context.startService(intent)
} }
@ -98,7 +90,7 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener,
fun upTtsSpeechRate(context: Context) { fun upTtsSpeechRate(context: Context) {
if (isRun) { if (isRun) {
val intent = Intent(context, ReadAloudService::class.java) val intent = Intent(context, TTSReadAloudService::class.java)
intent.action = Action.upTtsSpeechRate intent.action = Action.upTtsSpeechRate
context.startService(intent) context.startService(intent)
} }
@ -106,57 +98,46 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener,
fun setTimer(context: Context, minute: Int) { fun setTimer(context: Context, minute: Int) {
if (isRun) { if (isRun) {
val intent = Intent(context, ReadAloudService::class.java) val intent = Intent(context, TTSReadAloudService::class.java)
intent.action = Action.setTimer intent.action = Action.setTimer
intent.putExtra("minute", minute) intent.putExtra("minute", minute)
context.startService(intent) context.startService(intent)
} }
} }
fun clearTTS() {
textToSpeech?.stop()
textToSpeech?.shutdown()
textToSpeech = null
}
} }
private val handler = Handler() private val handler = Handler()
private var ttsIsSuccess: Boolean = false
private lateinit var audioManager: AudioManager private lateinit var audioManager: AudioManager
private lateinit var mFocusRequest: AudioFocusRequest private lateinit var mFocusRequest: AudioFocusRequest
private var broadcastReceiver: BroadcastReceiver? = null private var broadcastReceiver: BroadcastReceiver? = null
private var nowSpeak: Int = 0 private var mediaSessionCompat: MediaSessionCompat? = null
private val contentList = arrayListOf<String>() private var pause = false
private var readAloudNumber: Int = 0
private var textChapter: TextChapter? = null
private var pageIndex = 0
private val dsRunnable: Runnable? = Runnable { doDs() }
var mediaSessionCompat: MediaSessionCompat? = null
var pause = false
var title: String = "" var title: String = ""
var subtitle: String = "" private var subtitle: String = ""
val contentList = arrayListOf<String>()
var nowSpeak: Int = 0
var readAloudNumber: Int = 0
var textChapter: TextChapter? = null
var pageIndex = 0
private val dsRunnable: Runnable? = Runnable { doDs() }
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
isRun = true isRun = true
textToSpeech = TextToSpeech(this, this)
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mFocusRequest = MediaHelp.getFocusRequest(this) mFocusRequest = MediaHelp.getFocusRequest(this)
} }
initMediaSession() initMediaSession()
initBroadcastReceiver() initBroadcastReceiver()
upSpeechRate() upNotification()
ReadAloudNotification.upNotification(this)
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED) isRun = false
clearTTS()
unregisterReceiver(broadcastReceiver) unregisterReceiver(broadcastReceiver)
postEvent(Bus.ALOUD_STATE, Status.STOP) postEvent(Bus.ALOUD_STATE, Status.STOP)
isRun = false
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -184,77 +165,24 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener,
return super.onStartCommand(intent, flags, startId) return super.onStartCommand(intent, flags, startId)
} }
override fun onInit(status: Int) { abstract fun newReadAloud(dataKey: String?, play: Boolean)
launch {
if (status == TextToSpeech.SUCCESS) {
val result = textToSpeech?.setLanguage(Locale.CHINA)
if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
toast(R.string.tts_fix)
IntentHelp.toTTSSetting(this@ReadAloudService)
stopSelf()
} else {
textToSpeech?.setOnUtteranceProgressListener(TTSUtteranceListener())
ttsIsSuccess = true
playTTS()
}
} else {
toast(R.string.tts_init_failed)
}
}
}
private fun newReadAloud(dataKey: String?, play: Boolean) { @CallSuper
dataKey?.let { open fun pauseReadAloud(pause: Boolean) {
textChapter = IntentDataHelp.getData(dataKey) as? TextChapter postEvent(Bus.ALOUD_STATE, Status.PAUSE)
textChapter?.let { textChapter -> this.pause = pause
nowSpeak = 0
readAloudNumber = textChapter.getReadLength(pageIndex)
contentList.clear()
if (getPrefBoolean("readAloudByPage")) {
for (index in pageIndex..textChapter.lastIndex()) {
textChapter.page(index)?.text?.split("\n")?.let {
contentList.addAll(it)
}
}
} else {
contentList.addAll(textChapter.getUnRead(pageIndex).split("\n"))
}
if (play) playTTS()
} ?: stopSelf()
} ?: stopSelf()
} }
@Suppress("DEPRECATION") @CallSuper
private fun playTTS() { open fun resumeReadAloud() {
if (contentList.size < 1 || !ttsIsSuccess) { pause = false
return
}
if (requestFocus()) {
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
postEvent(Bus.ALOUD_STATE, Status.PLAY)
ReadAloudNotification.upNotification(this)
for (i in nowSpeak until contentList.size) {
if (i == 0) {
speak(contentList[i], TextToSpeech.QUEUE_FLUSH, AppConst.APP_TAG + i)
} else {
speak(contentList[i], TextToSpeech.QUEUE_ADD, AppConst.APP_TAG + i)
}
}
}
} }
private fun speak(content: String, queueMode: Int, utteranceId: String) { abstract fun upSpeechRate(reset: Boolean = false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
textToSpeech?.speak(content, queueMode, null, utteranceId) abstract fun prevP()
} else {
@Suppress("DEPRECATION") abstract fun nextP()
textToSpeech?.speak(
content,
queueMode,
hashMapOf(Pair(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId))
)
}
}
private fun setTimer(minute: Int) { private fun setTimer(minute: Int) {
timeMinute = minute timeMinute = minute
@ -262,7 +190,7 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener,
handler.removeCallbacks(dsRunnable) handler.removeCallbacks(dsRunnable)
handler.postDelayed(dsRunnable, 60000) handler.postDelayed(dsRunnable, 60000)
} }
ReadAloudNotification.upNotification(this) upNotification()
} }
private fun addTimer() { private fun addTimer() {
@ -276,7 +204,7 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener,
handler.postDelayed(dsRunnable, 60000) handler.postDelayed(dsRunnable, 60000)
} }
postEvent(Bus.TTS_DS, timeMinute) postEvent(Bus.TTS_DS, timeMinute)
ReadAloudNotification.upNotification(this) upNotification()
} }
/** /**
@ -292,47 +220,57 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener,
} }
} }
postEvent(Bus.TTS_DS, timeMinute) postEvent(Bus.TTS_DS, timeMinute)
ReadAloudNotification.upNotification(this) upNotification()
} }
/**
* 更新朗读速度 override fun onAudioFocusChange(focusChange: Int) {
*/ when (focusChange) {
private fun upSpeechRate(reset: Boolean = false) { AudioManager.AUDIOFOCUS_GAIN -> {
if (this.getPrefBoolean("ttsFollowSys", true)) { // 重新获得焦点, 可做恢复播放,恢复后台音量的操作
if (reset) { if (!pause) resumeReadAloud()
clearTTS()
textToSpeech = TextToSpeech(this, this)
} }
} else { AudioManager.AUDIOFOCUS_LOSS -> {
textToSpeech?.setSpeechRate((this.getPrefInt("ttsSpeechRate", 5) + 5) / 10f) // 永久丢失焦点除非重新主动获取,这种情况是被其他播放器抢去了焦点, 为避免与其他播放器混音,可将音乐暂停
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
// 暂时丢失焦点,这种情况是被其他应用申请了短暂的焦点,可压低后台音量
if (!pause) pauseReadAloud(false)
} }
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
// 短暂丢失焦点,这种情况是被其他应用申请了短暂的焦点希望其他声音能压低音量(或者关闭声音)凸显这个声音(比如短信提示音),
} }
/**
* 上一段
*/
private fun prevP() {
if (nowSpeak > 0) {
textToSpeech?.stop()
nowSpeak--
readAloudNumber -= contentList[nowSpeak].length.minus(1)
playTTS()
} }
} }
/** /**
* 下一段 * @return 音频焦点
*/ */
private fun nextP() { fun requestFocus(): Boolean {
if (nowSpeak < contentList.size - 1) { MediaHelp.playSilentSound(this)
textToSpeech?.stop() val request: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
readAloudNumber += contentList[nowSpeak].length.plus(1) audioManager.requestAudioFocus(mFocusRequest)
nowSpeak++ } else {
playTTS() @Suppress("DEPRECATION")
audioManager.requestAudioFocus(
this,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN
)
} }
return request == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
}
fun upMediaSessionPlaybackState(state: Int) {
mediaSessionCompat?.setPlaybackState(
PlaybackStateCompat.Builder()
.setActions(MediaHelp.MEDIA_SESSION_ACTIONS)
.setState(state, nowSpeak.toLong(), 1f)
.build()
)
} }
/** /**
* 初始化MediaSession, 注册多媒体按钮 * 初始化MediaSession, 注册多媒体按钮
*/ */
@ -347,10 +285,10 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener,
PendingIntent.FLAG_CANCEL_CURRENT PendingIntent.FLAG_CANCEL_CURRENT
) )
mediaSessionCompat = MediaSessionCompat(this, tag) mediaSessionCompat = MediaSessionCompat(this, TTSReadAloudService.tag)
mediaSessionCompat?.setCallback(object : MediaSessionCompat.Callback() { mediaSessionCompat?.setCallback(object : MediaSessionCompat.Callback() {
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
return MediaButtonReceiver.handleIntent(this@ReadAloudService, mediaButtonEvent) return MediaButtonReceiver.handleIntent(this@BaseReadAloudService, mediaButtonEvent)
} }
}) })
mediaSessionCompat?.setMediaButtonReceiver(mediaButtonReceiverPendingIntent) mediaSessionCompat?.setMediaButtonReceiver(mediaButtonReceiverPendingIntent)
@ -373,110 +311,58 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener,
} }
/** /**
* 暂停朗读 * 更新通知
*/
private fun pauseReadAloud(pause: Boolean) {
postEvent(Bus.ALOUD_STATE, Status.PAUSE)
this.pause = pause
textToSpeech?.stop()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED)
ReadAloudNotification.upNotification(this)
}
/**
* 恢复朗读
*/
private fun resumeReadAloud() {
pause = false
playTTS()
}
/**
* @return 音频焦点
*/ */
private fun requestFocus(): Boolean { fun upNotification() {
MediaHelp.playSilentSound(this) var nTitle: String = when {
val request: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { pause -> getString(R.string.read_aloud_pause)
audioManager.requestAudioFocus(mFocusRequest) timeMinute in 1..60 -> getString(
R.string.read_aloud_timer,
timeMinute
)
else -> getString(R.string.read_aloud_t)
}
nTitle += ": $title"
var nSubtitle = subtitle
if (subtitle.isEmpty())
nSubtitle = getString(R.string.read_aloud_s)
val builder = NotificationCompat.Builder(this, AppConst.channelIdReadAloud)
.setSmallIcon(R.drawable.ic_volume_up)
.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.icon_read_book))
.setOngoing(true)
.setContentTitle(nTitle)
.setContentText(nSubtitle)
.setContentIntent(PendingIntentHelp.readBookActivityPendingIntent(this))
if (pause) {
builder.addAction(
R.drawable.ic_play_24dp,
getString(R.string.resume),
PendingIntentHelp.aloudServicePendingIntent(this, Action.resume)
)
} else { } else {
@Suppress("DEPRECATION") builder.addAction(
audioManager.requestAudioFocus( R.drawable.ic_pause_24dp,
this, getString(R.string.pause),
AudioManager.STREAM_MUSIC, PendingIntentHelp.aloudServicePendingIntent(this, Action.pause)
AudioManager.AUDIOFOCUS_GAIN
) )
} }
return request == AudioManager.AUDIOFOCUS_REQUEST_GRANTED builder.addAction(
} R.drawable.ic_stop_black_24dp,
getString(R.string.stop),
private fun upMediaSessionPlaybackState(state: Int) { PendingIntentHelp.aloudServicePendingIntent(this, Action.stop)
mediaSessionCompat?.setPlaybackState(
PlaybackStateCompat.Builder()
.setActions(MediaHelp.MEDIA_SESSION_ACTIONS)
.setState(state, nowSpeak.toLong(), 1f)
.build()
) )
builder.addAction(
R.drawable.ic_time_add_24dp,
getString(R.string.set_timer),
PendingIntentHelp.aloudServicePendingIntent(this, Action.addTimer)
)
builder.setStyle(
androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSessionCompat?.sessionToken)
.setShowActionsInCompactView(0, 1, 2)
)
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
val notification = builder.build()
startForeground(112201, notification)
} }
override fun onAudioFocusChange(focusChange: Int) {
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> {
// 重新获得焦点, 可做恢复播放,恢复后台音量的操作
if (!pause) resumeReadAloud()
}
AudioManager.AUDIOFOCUS_LOSS -> {
// 永久丢失焦点除非重新主动获取,这种情况是被其他播放器抢去了焦点, 为避免与其他播放器混音,可将音乐暂停
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
// 暂时丢失焦点,这种情况是被其他应用申请了短暂的焦点,可压低后台音量
if (!pause) pauseReadAloud(false)
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
// 短暂丢失焦点,这种情况是被其他应用申请了短暂的焦点希望其他声音能压低音量(或者关闭声音)凸显这个声音(比如短信提示音),
}
}
}
/**
* 朗读监听
*/
private inner class TTSUtteranceListener : UtteranceProgressListener() {
override fun onStart(s: String) {
textChapter?.let {
if (readAloudNumber + 1 > it.getReadLength(pageIndex + 1)) {
pageIndex++
postEvent(Bus.TTS_TURN_PAGE, 1)
}
}
postEvent(Bus.TTS_START, readAloudNumber + 1)
}
override fun onDone(s: String) {
readAloudNumber += contentList[nowSpeak].length + 1
nowSpeak += 1
if (nowSpeak >= contentList.size) {
postEvent(Bus.TTS_TURN_PAGE, 2)
}
}
override fun onRangeStart(utteranceId: String?, start: Int, end: Int, frame: Int) {
super.onRangeStart(utteranceId, start, end, frame)
textChapter?.let {
if (readAloudNumber + start > it.getReadLength(pageIndex + 1)) {
pageIndex++
postEvent(Bus.TTS_TURN_PAGE, 1)
postEvent(Bus.TTS_START, readAloudNumber + start)
}
}
}
override fun onError(s: String) {
launch {
toast(s)
}
}
}
} }

@ -0,0 +1,218 @@
package io.legado.app.service
import android.os.Build
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import android.support.v4.media.session.PlaybackStateCompat
import io.legado.app.R
import io.legado.app.constant.AppConst
import io.legado.app.constant.Bus
import io.legado.app.constant.Status
import io.legado.app.help.IntentDataHelp
import io.legado.app.help.IntentHelp
import io.legado.app.ui.widget.page.TextChapter
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.postEvent
import kotlinx.coroutines.launch
import org.jetbrains.anko.toast
import java.util.*
class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener {
companion object {
val tag: String = TTSReadAloudService::class.java.simpleName
var textToSpeech: TextToSpeech? = null
fun clearTTS() {
textToSpeech?.stop()
textToSpeech?.shutdown()
textToSpeech = null
}
}
private var ttsIsSuccess: Boolean = false
override fun onCreate() {
super.onCreate()
textToSpeech = TextToSpeech(this, this)
upSpeechRate()
}
override fun onDestroy() {
super.onDestroy()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED)
clearTTS()
}
override fun onInit(status: Int) {
launch {
if (status == TextToSpeech.SUCCESS) {
val result = textToSpeech?.setLanguage(Locale.CHINA)
if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
toast(R.string.tts_fix)
IntentHelp.toTTSSetting(this@TTSReadAloudService)
stopSelf()
} else {
textToSpeech?.setOnUtteranceProgressListener(TTSUtteranceListener())
ttsIsSuccess = true
playTTS()
}
} else {
toast(R.string.tts_init_failed)
}
}
}
override fun newReadAloud(dataKey: String?, play: Boolean) {
dataKey?.let {
textChapter = IntentDataHelp.getData(dataKey) as? TextChapter
textChapter?.let { textChapter ->
nowSpeak = 0
readAloudNumber = textChapter.getReadLength(pageIndex)
contentList.clear()
if (getPrefBoolean("readAloudByPage")) {
for (index in pageIndex..textChapter.lastIndex()) {
textChapter.page(index)?.text?.split("\n")?.let {
contentList.addAll(it)
}
}
} else {
contentList.addAll(textChapter.getUnRead(pageIndex).split("\n"))
}
if (play) playTTS()
} ?: stopSelf()
} ?: stopSelf()
}
@Suppress("DEPRECATION")
private fun playTTS() {
if (contentList.size < 1 || !ttsIsSuccess) {
return
}
if (requestFocus()) {
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
postEvent(Bus.ALOUD_STATE, Status.PLAY)
upNotification()
for (i in nowSpeak until contentList.size) {
if (i == 0) {
speak(contentList[i], TextToSpeech.QUEUE_FLUSH, AppConst.APP_TAG + i)
} else {
speak(contentList[i], TextToSpeech.QUEUE_ADD, AppConst.APP_TAG + i)
}
}
}
}
private fun speak(content: String, queueMode: Int, utteranceId: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
textToSpeech?.speak(content, queueMode, null, utteranceId)
} else {
@Suppress("DEPRECATION")
textToSpeech?.speak(
content,
queueMode,
hashMapOf(Pair(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId))
)
}
}
/**
* 更新朗读速度
*/
override fun upSpeechRate(reset: Boolean) {
if (this.getPrefBoolean("ttsFollowSys", true)) {
if (reset) {
clearTTS()
textToSpeech = TextToSpeech(this, this)
}
} else {
textToSpeech?.setSpeechRate((this.getPrefInt("ttsSpeechRate", 5) + 5) / 10f)
}
}
/**
* 上一段
*/
override fun prevP() {
if (nowSpeak > 0) {
textToSpeech?.stop()
nowSpeak--
readAloudNumber -= contentList[nowSpeak].length.minus(1)
playTTS()
}
}
/**
* 下一段
*/
override fun nextP() {
if (nowSpeak < contentList.size - 1) {
textToSpeech?.stop()
readAloudNumber += contentList[nowSpeak].length.plus(1)
nowSpeak++
playTTS()
}
}
/**
* 暂停朗读
*/
override fun pauseReadAloud(pause: Boolean) {
super.pauseReadAloud(pause)
textToSpeech?.stop()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED)
upNotification()
}
/**
* 恢复朗读
*/
override fun resumeReadAloud() {
super.resumeReadAloud()
playTTS()
}
/**
* 朗读监听
*/
private inner class TTSUtteranceListener : UtteranceProgressListener() {
override fun onStart(s: String) {
textChapter?.let {
if (readAloudNumber + 1 > it.getReadLength(pageIndex + 1)) {
pageIndex++
postEvent(Bus.TTS_TURN_PAGE, 1)
}
}
postEvent(Bus.TTS_START, readAloudNumber + 1)
}
override fun onDone(s: String) {
readAloudNumber += contentList[nowSpeak].length + 1
nowSpeak += 1
if (nowSpeak >= contentList.size) {
postEvent(Bus.TTS_TURN_PAGE, 2)
}
}
override fun onRangeStart(utteranceId: String?, start: Int, end: Int, frame: Int) {
super.onRangeStart(utteranceId, start, end, frame)
textChapter?.let {
if (readAloudNumber + start > it.getReadLength(pageIndex + 1)) {
pageIndex++
postEvent(Bus.TTS_TURN_PAGE, 1)
postEvent(Bus.TTS_START, readAloudNumber + start)
}
}
}
override fun onError(s: String) {
launch {
toast(s)
}
}
}
}

@ -1,68 +0,0 @@
package io.legado.app.service.notification
import android.graphics.BitmapFactory
import androidx.core.app.NotificationCompat
import io.legado.app.R
import io.legado.app.constant.Action
import io.legado.app.constant.AppConst
import io.legado.app.help.PendingIntentHelp
import io.legado.app.service.ReadAloudService
object ReadAloudNotification {
/**
* 更新通知
*/
fun upNotification(service: ReadAloudService) {
var nTitle: String = when {
service.pause -> service.getString(R.string.read_aloud_pause)
ReadAloudService.timeMinute in 1..60 -> service.getString(
R.string.read_aloud_timer,
ReadAloudService.timeMinute
)
else -> service.getString(R.string.read_aloud_t)
}
nTitle += ": ${service.title}"
var nSubtitle = service.subtitle
if (service.subtitle.isEmpty())
nSubtitle = service.getString(R.string.read_aloud_s)
val builder = NotificationCompat.Builder(service, AppConst.channelIdReadAloud)
.setSmallIcon(R.drawable.ic_volume_up)
.setLargeIcon(BitmapFactory.decodeResource(service.resources, R.drawable.icon_read_book))
.setOngoing(true)
.setContentTitle(nTitle)
.setContentText(nSubtitle)
.setContentIntent(PendingIntentHelp.readBookActivityPendingIntent(service))
if (service.pause) {
builder.addAction(
R.drawable.ic_play_24dp,
service.getString(R.string.resume),
PendingIntentHelp.aloudServicePendingIntent(service, Action.resume)
)
} else {
builder.addAction(
R.drawable.ic_pause_24dp,
service.getString(R.string.pause),
PendingIntentHelp.aloudServicePendingIntent(service, Action.pause)
)
}
builder.addAction(
R.drawable.ic_stop_black_24dp,
service.getString(R.string.stop),
PendingIntentHelp.aloudServicePendingIntent(service, Action.stop)
)
builder.addAction(
R.drawable.ic_time_add_24dp,
service.getString(R.string.set_timer),
PendingIntentHelp.aloudServicePendingIntent(service, Action.addTimer)
)
builder.setStyle(
androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(service.mediaSessionCompat?.sessionToken)
.setShowActionsInCompactView(0, 1, 2)
)
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
val notification = builder.build()
service.startForeground(112201, notification)
}
}

@ -25,7 +25,7 @@ import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.okButton import io.legado.app.lib.dialogs.okButton
import io.legado.app.receiver.TimeElectricityReceiver import io.legado.app.receiver.TimeElectricityReceiver
import io.legado.app.service.ReadAloudService import io.legado.app.service.BaseReadAloudService
import io.legado.app.ui.changesource.ChangeSourceDialog import io.legado.app.ui.changesource.ChangeSourceDialog
import io.legado.app.ui.chapterlist.ChapterListActivity import io.legado.app.ui.chapterlist.ChapterListActivity
import io.legado.app.ui.readbook.config.* import io.legado.app.ui.readbook.config.*
@ -190,7 +190,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_rea
when (keyCode) { when (keyCode) {
KeyEvent.KEYCODE_BACK -> { KeyEvent.KEYCODE_BACK -> {
if (readAloudStatus == Status.PLAY) { if (readAloudStatus == Status.PLAY) {
ReadAloudService.pause(this) BaseReadAloudService.pause(this)
toast(R.string.read_aloud_pause) toast(R.string.read_aloud_pause)
return true return true
} }
@ -444,14 +444,14 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_rea
* 朗读按钮 * 朗读按钮
*/ */
private fun onClickReadAloud() { private fun onClickReadAloud() {
if (!ReadAloudService.isRun) { if (!BaseReadAloudService.isRun) {
readAloudStatus = Status.STOP readAloudStatus = Status.STOP
SystemUtils.ignoreBatteryOptimization(this) SystemUtils.ignoreBatteryOptimization(this)
} }
when (readAloudStatus) { when (readAloudStatus) {
Status.STOP -> readAloud() Status.STOP -> readAloud()
Status.PLAY -> ReadAloudService.pause(this) Status.PLAY -> BaseReadAloudService.pause(this)
Status.PAUSE -> ReadAloudService.resume(this) Status.PAUSE -> BaseReadAloudService.resume(this)
} }
} }
@ -464,7 +464,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_rea
if (book != null && textChapter != null) { if (book != null && textChapter != null) {
val key = System.currentTimeMillis().toString() val key = System.currentTimeMillis().toString()
IntentDataHelp.putData(key, textChapter) IntentDataHelp.putData(key, textChapter)
ReadAloudService.play( BaseReadAloudService.play(
this, book.name, textChapter.title, this, book.name, textChapter.title,
viewModel.durPageIndex, key, play viewModel.durPageIndex, key, play
) )
@ -569,7 +569,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_rea
page_view.upContent() page_view.upContent()
viewModel.saveRead() viewModel.saveRead()
} }
2 -> if (!moveToNextChapter()) ReadAloudService.stop(this) 2 -> if (!moveToNextChapter()) BaseReadAloudService.stop(this)
-1 -> { -1 -> {
if (viewModel.durPageIndex > 0) { if (viewModel.durPageIndex > 0) {
viewModel.durPageIndex = viewModel.durPageIndex - 1 viewModel.durPageIndex = viewModel.durPageIndex - 1

@ -11,7 +11,7 @@ import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.model.WebBook import io.legado.app.model.WebBook
import io.legado.app.service.ReadAloudService import io.legado.app.service.BaseReadAloudService
import io.legado.app.ui.widget.page.TextChapter import io.legado.app.ui.widget.page.TextChapter
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
@ -332,7 +332,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
ReadAloudService.stop(context) BaseReadAloudService.stop(context)
} }
interface CallBack { interface CallBack {

@ -11,7 +11,7 @@ import androidx.fragment.app.DialogFragment
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.Bus import io.legado.app.constant.Bus
import io.legado.app.constant.Status import io.legado.app.constant.Status
import io.legado.app.service.ReadAloudService import io.legado.app.service.BaseReadAloudService
import io.legado.app.ui.readbook.Help import io.legado.app.ui.readbook.Help
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.android.synthetic.main.dialog_read_aloud.* import kotlinx.android.synthetic.main.dialog_read_aloud.*
@ -61,8 +61,9 @@ class ReadAloudDialog : DialogFragment() {
callBack?.readAloudStatus?.let { callBack?.readAloudStatus?.let {
upPlayState(it) upPlayState(it)
} }
seek_timer.progress = ReadAloudService.timeMinute seek_timer.progress = BaseReadAloudService.timeMinute
tv_timer.text = requireContext().getString(R.string.timer_m, ReadAloudService.timeMinute) tv_timer.text =
requireContext().getString(R.string.timer_m, BaseReadAloudService.timeMinute)
cb_by_page.isChecked = requireContext().getPrefBoolean("readAloudByPage") cb_by_page.isChecked = requireContext().getPrefBoolean("readAloudByPage")
cb_tts_follow_sys.isChecked = requireContext().getPrefBoolean("ttsFollowSys", true) cb_tts_follow_sys.isChecked = requireContext().getPrefBoolean("ttsFollowSys", true)
seek_tts_SpeechRate.isEnabled = !cb_tts_follow_sys.isChecked seek_tts_SpeechRate.isEnabled = !cb_tts_follow_sys.isChecked
@ -103,7 +104,7 @@ class ReadAloudDialog : DialogFragment() {
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
override fun onStopTrackingTouch(seekBar: SeekBar?) { override fun onStopTrackingTouch(seekBar: SeekBar?) {
ReadAloudService.setTimer(requireContext(), seek_timer.progress) BaseReadAloudService.setTimer(requireContext(), seek_timer.progress)
} }
}) })
} }
@ -111,11 +112,11 @@ class ReadAloudDialog : DialogFragment() {
private fun initOnClick() { private fun initOnClick() {
iv_menu.onClick { callBack?.showMenu(); dismiss() } iv_menu.onClick { callBack?.showMenu(); dismiss() }
iv_menu.onLongClick { callBack?.openChapterList(); true } iv_menu.onLongClick { callBack?.openChapterList(); true }
iv_stop.onClick { ReadAloudService.stop(requireContext()); dismiss() } iv_stop.onClick { BaseReadAloudService.stop(requireContext()); dismiss() }
iv_play_pause.onClick { postEvent(Bus.READ_ALOUD_BUTTON, true) } iv_play_pause.onClick { postEvent(Bus.READ_ALOUD_BUTTON, true) }
iv_play_prev.onClick { ReadAloudService.prevParagraph(requireContext()) } iv_play_prev.onClick { BaseReadAloudService.prevParagraph(requireContext()) }
iv_play_prev.onLongClick { postEvent(Bus.TTS_TURN_PAGE, -2); true } iv_play_prev.onLongClick { postEvent(Bus.TTS_TURN_PAGE, -2); true }
iv_play_next.onClick { ReadAloudService.nextParagraph(requireContext()) } iv_play_next.onClick { BaseReadAloudService.nextParagraph(requireContext()) }
iv_play_next.onLongClick { postEvent(Bus.TTS_TURN_PAGE, 2); true } iv_play_next.onLongClick { postEvent(Bus.TTS_TURN_PAGE, 2); true }
} }
@ -128,10 +129,10 @@ class ReadAloudDialog : DialogFragment() {
} }
private fun upTtsSpeechRate() { private fun upTtsSpeechRate() {
ReadAloudService.upTtsSpeechRate(requireContext()) BaseReadAloudService.upTtsSpeechRate(requireContext())
if (callBack?.readAloudStatus == Status.PLAY) { if (callBack?.readAloudStatus == Status.PLAY) {
ReadAloudService.pause(requireContext()) BaseReadAloudService.pause(requireContext())
ReadAloudService.resume(requireContext()) BaseReadAloudService.resume(requireContext())
} }
} }

Loading…
Cancel
Save