diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d0689f9eb..7bd159503 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -68,7 +68,7 @@ - + diff --git a/app/src/main/java/io/legado/app/help/CrashHandler.kt b/app/src/main/java/io/legado/app/help/CrashHandler.kt index 35c29304c..a8fe0d05f 100644 --- a/app/src/main/java/io/legado/app/help/CrashHandler.kt +++ b/app/src/main/java/io/legado/app/help/CrashHandler.kt @@ -8,7 +8,7 @@ import android.os.Handler import android.os.Looper import android.util.Log import android.widget.Toast -import io.legado.app.service.ReadAloudService +import io.legado.app.service.TTSReadAloudService import java.io.File import java.io.FileOutputStream import java.io.PrintWriter @@ -54,7 +54,7 @@ class CrashHandler : Thread.UncaughtExceptionHandler { * uncaughtException 回调函数 */ override fun uncaughtException(thread: Thread, ex: Throwable) { - ReadAloudService.clearTTS() + TTSReadAloudService.clearTTS() handleException(ex) mDefaultHandler?.uncaughtException(thread, ex) } diff --git a/app/src/main/java/io/legado/app/help/PendingIntentHelp.kt b/app/src/main/java/io/legado/app/help/PendingIntentHelp.kt index e0725cb27..280469f3e 100644 --- a/app/src/main/java/io/legado/app/help/PendingIntentHelp.kt +++ b/app/src/main/java/io/legado/app/help/PendingIntentHelp.kt @@ -3,7 +3,7 @@ package io.legado.app.help import android.app.PendingIntent import android.content.Context import android.content.Intent -import io.legado.app.service.ReadAloudService +import io.legado.app.service.TTSReadAloudService import io.legado.app.ui.readbook.ReadBookActivity object PendingIntentHelp { @@ -15,7 +15,7 @@ object PendingIntentHelp { } fun aloudServicePendingIntent(context: Context, actionStr: String): PendingIntent { - val intent = Intent(context, ReadAloudService::class.java) + val intent = Intent(context, TTSReadAloudService::class.java) intent.action = actionStr return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } diff --git a/app/src/main/java/io/legado/app/service/ReadAloudService.kt b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt similarity index 54% rename from app/src/main/java/io/legado/app/service/ReadAloudService.kt rename to app/src/main/java/io/legado/app/service/BaseReadAloudService.kt index b3c864a80..35e3ad6c1 100644 --- a/app/src/main/java/io/legado/app/service/ReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt @@ -2,40 +2,32 @@ package io.legado.app.service import android.app.PendingIntent import android.content.* +import android.graphics.BitmapFactory import android.media.AudioFocusRequest import android.media.AudioManager import android.os.Build 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.PlaybackStateCompat +import androidx.annotation.CallSuper +import androidx.core.app.NotificationCompat import io.legado.app.R import io.legado.app.base.BaseService import io.legado.app.constant.Action 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.help.MediaHelp +import io.legado.app.help.PendingIntentHelp 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.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 ReadAloudService : BaseService(), TextToSpeech.OnInitListener, +abstract class BaseReadAloudService : BaseService(), AudioManager.OnAudioFocusChangeListener { companion object { - val tag: String = ReadAloudService::class.java.simpleName var isRun = false - var textToSpeech: TextToSpeech? = null var timeMinute: Int = 0 fun play( @@ -46,7 +38,7 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener, dataKey: String, play: Boolean = true ) { - val readAloudIntent = Intent(context, ReadAloudService::class.java) + val readAloudIntent = Intent(context, TTSReadAloudService::class.java) readAloudIntent.action = Action.play readAloudIntent.putExtra("title", title) readAloudIntent.putExtra("subtitle", subtitle) @@ -58,7 +50,7 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener, fun pause(context: Context) { if (isRun) { - val intent = Intent(context, ReadAloudService::class.java) + val intent = Intent(context, TTSReadAloudService::class.java) intent.action = Action.pause context.startService(intent) } @@ -66,7 +58,7 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener, fun resume(context: Context) { if (isRun) { - val intent = Intent(context, ReadAloudService::class.java) + val intent = Intent(context, TTSReadAloudService::class.java) intent.action = Action.resume context.startService(intent) } @@ -74,7 +66,7 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener, fun stop(context: Context) { if (isRun) { - val intent = Intent(context, ReadAloudService::class.java) + val intent = Intent(context, TTSReadAloudService::class.java) intent.action = Action.stop context.startService(intent) } @@ -82,7 +74,7 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener, fun prevParagraph(context: Context) { if (isRun) { - val intent = Intent(context, ReadAloudService::class.java) + val intent = Intent(context, TTSReadAloudService::class.java) intent.action = Action.prevParagraph context.startService(intent) } @@ -90,7 +82,7 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener, fun nextParagraph(context: Context) { if (isRun) { - val intent = Intent(context, ReadAloudService::class.java) + val intent = Intent(context, TTSReadAloudService::class.java) intent.action = Action.nextParagraph context.startService(intent) } @@ -98,7 +90,7 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener, fun upTtsSpeechRate(context: Context) { if (isRun) { - val intent = Intent(context, ReadAloudService::class.java) + val intent = Intent(context, TTSReadAloudService::class.java) intent.action = Action.upTtsSpeechRate context.startService(intent) } @@ -106,57 +98,46 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener, fun setTimer(context: Context, minute: Int) { if (isRun) { - val intent = Intent(context, ReadAloudService::class.java) + val intent = Intent(context, TTSReadAloudService::class.java) intent.action = Action.setTimer intent.putExtra("minute", minute) context.startService(intent) } } - - fun clearTTS() { - textToSpeech?.stop() - textToSpeech?.shutdown() - textToSpeech = null - } } private val handler = Handler() - private var ttsIsSuccess: Boolean = false private lateinit var audioManager: AudioManager private lateinit var mFocusRequest: AudioFocusRequest private var broadcastReceiver: BroadcastReceiver? = null - private var nowSpeak: Int = 0 - private val contentList = arrayListOf() - 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 + private var mediaSessionCompat: MediaSessionCompat? = null + private var pause = false var title: String = "" - var subtitle: String = "" + private var subtitle: String = "" + val contentList = arrayListOf() + var nowSpeak: Int = 0 + var readAloudNumber: Int = 0 + var textChapter: TextChapter? = null + var pageIndex = 0 + private val dsRunnable: Runnable? = Runnable { doDs() } override fun onCreate() { super.onCreate() isRun = true - textToSpeech = TextToSpeech(this, this) audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { mFocusRequest = MediaHelp.getFocusRequest(this) } initMediaSession() initBroadcastReceiver() - upSpeechRate() - ReadAloudNotification.upNotification(this) + upNotification() } override fun onDestroy() { super.onDestroy() - upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED) - clearTTS() + isRun = false unregisterReceiver(broadcastReceiver) postEvent(Bus.ALOUD_STATE, Status.STOP) - isRun = false } 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) } - 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@ReadAloudService) - stopSelf() - } else { - textToSpeech?.setOnUtteranceProgressListener(TTSUtteranceListener()) - ttsIsSuccess = true - playTTS() - } - } else { - toast(R.string.tts_init_failed) - } - } - } + abstract fun newReadAloud(dataKey: String?, play: Boolean) - private 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() + @CallSuper + open fun pauseReadAloud(pause: Boolean) { + postEvent(Bus.ALOUD_STATE, Status.PAUSE) + this.pause = pause } - @Suppress("DEPRECATION") - private fun playTTS() { - if (contentList.size < 1 || !ttsIsSuccess) { - 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) - } - } - } + @CallSuper + open fun resumeReadAloud() { + pause = false } - 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)) - ) - } - } + abstract fun upSpeechRate(reset: Boolean = false) + + abstract fun prevP() + + abstract fun nextP() private fun setTimer(minute: Int) { timeMinute = minute @@ -262,7 +190,7 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener, handler.removeCallbacks(dsRunnable) handler.postDelayed(dsRunnable, 60000) } - ReadAloudNotification.upNotification(this) + upNotification() } private fun addTimer() { @@ -276,7 +204,7 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener, handler.postDelayed(dsRunnable, 60000) } postEvent(Bus.TTS_DS, timeMinute) - ReadAloudNotification.upNotification(this) + upNotification() } /** @@ -292,47 +220,57 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener, } } postEvent(Bus.TTS_DS, timeMinute) - ReadAloudNotification.upNotification(this) + upNotification() } - /** - * 更新朗读速度 - */ - private fun upSpeechRate(reset: Boolean = false) { - if (this.getPrefBoolean("ttsFollowSys", true)) { - if (reset) { - clearTTS() - textToSpeech = TextToSpeech(this, this) + + 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 -> { + // 短暂丢失焦点,这种情况是被其他应用申请了短暂的焦点希望其他声音能压低音量(或者关闭声音)凸显这个声音(比如短信提示音), } - } else { - textToSpeech?.setSpeechRate((this.getPrefInt("ttsSpeechRate", 5) + 5) / 10f) } } /** - * 上一段 + * @return 音频焦点 */ - private fun prevP() { - if (nowSpeak > 0) { - textToSpeech?.stop() - nowSpeak-- - readAloudNumber -= contentList[nowSpeak].length.minus(1) - playTTS() + fun requestFocus(): Boolean { + MediaHelp.playSilentSound(this) + val request: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + audioManager.requestAudioFocus(mFocusRequest) + } else { + @Suppress("DEPRECATION") + audioManager.requestAudioFocus( + this, + AudioManager.STREAM_MUSIC, + AudioManager.AUDIOFOCUS_GAIN + ) } + return request == AudioManager.AUDIOFOCUS_REQUEST_GRANTED } - /** - * 下一段 - */ - private fun nextP() { - if (nowSpeak < contentList.size - 1) { - textToSpeech?.stop() - readAloudNumber += contentList[nowSpeak].length.plus(1) - nowSpeak++ - playTTS() - } + fun upMediaSessionPlaybackState(state: Int) { + mediaSessionCompat?.setPlaybackState( + PlaybackStateCompat.Builder() + .setActions(MediaHelp.MEDIA_SESSION_ACTIONS) + .setState(state, nowSpeak.toLong(), 1f) + .build() + ) } + /** * 初始化MediaSession, 注册多媒体按钮 */ @@ -347,10 +285,10 @@ class ReadAloudService : BaseService(), TextToSpeech.OnInitListener, PendingIntent.FLAG_CANCEL_CURRENT ) - mediaSessionCompat = MediaSessionCompat(this, tag) + mediaSessionCompat = MediaSessionCompat(this, TTSReadAloudService.tag) mediaSessionCompat?.setCallback(object : MediaSessionCompat.Callback() { override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { - return MediaButtonReceiver.handleIntent(this@ReadAloudService, mediaButtonEvent) + return MediaButtonReceiver.handleIntent(this@BaseReadAloudService, mediaButtonEvent) } }) 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 { - MediaHelp.playSilentSound(this) - val request: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - audioManager.requestAudioFocus(mFocusRequest) + fun upNotification() { + var nTitle: String = when { + pause -> getString(R.string.read_aloud_pause) + 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 { - @Suppress("DEPRECATION") - audioManager.requestAudioFocus( - this, - AudioManager.STREAM_MUSIC, - AudioManager.AUDIOFOCUS_GAIN + builder.addAction( + R.drawable.ic_pause_24dp, + getString(R.string.pause), + PendingIntentHelp.aloudServicePendingIntent(this, Action.pause) ) } - return request == AudioManager.AUDIOFOCUS_REQUEST_GRANTED - } - - private fun upMediaSessionPlaybackState(state: Int) { - mediaSessionCompat?.setPlaybackState( - PlaybackStateCompat.Builder() - .setActions(MediaHelp.MEDIA_SESSION_ACTIONS) - .setState(state, nowSpeak.toLong(), 1f) - .build() + builder.addAction( + R.drawable.ic_stop_black_24dp, + getString(R.string.stop), + PendingIntentHelp.aloudServicePendingIntent(this, Action.stop) ) + 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) - } - } - - } - } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt b/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt new file mode 100644 index 000000000..195fe4fdf --- /dev/null +++ b/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt @@ -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) + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/notification/ReadAloudNotification.kt b/app/src/main/java/io/legado/app/service/notification/ReadAloudNotification.kt deleted file mode 100644 index d7e0c51ca..000000000 --- a/app/src/main/java/io/legado/app/service/notification/ReadAloudNotification.kt +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/readbook/ReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/readbook/ReadBookActivity.kt index da9028d0e..a6da939f5 100644 --- a/app/src/main/java/io/legado/app/ui/readbook/ReadBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/readbook/ReadBookActivity.kt @@ -25,7 +25,7 @@ import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.okButton 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.chapterlist.ChapterListActivity import io.legado.app.ui.readbook.config.* @@ -190,7 +190,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_rea when (keyCode) { KeyEvent.KEYCODE_BACK -> { if (readAloudStatus == Status.PLAY) { - ReadAloudService.pause(this) + BaseReadAloudService.pause(this) toast(R.string.read_aloud_pause) return true } @@ -444,14 +444,14 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_rea * 朗读按钮 */ private fun onClickReadAloud() { - if (!ReadAloudService.isRun) { + if (!BaseReadAloudService.isRun) { readAloudStatus = Status.STOP SystemUtils.ignoreBatteryOptimization(this) } when (readAloudStatus) { Status.STOP -> readAloud() - Status.PLAY -> ReadAloudService.pause(this) - Status.PAUSE -> ReadAloudService.resume(this) + Status.PLAY -> BaseReadAloudService.pause(this) + Status.PAUSE -> BaseReadAloudService.resume(this) } } @@ -464,7 +464,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_rea if (book != null && textChapter != null) { val key = System.currentTimeMillis().toString() IntentDataHelp.putData(key, textChapter) - ReadAloudService.play( + BaseReadAloudService.play( this, book.name, textChapter.title, viewModel.durPageIndex, key, play ) @@ -569,7 +569,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_rea page_view.upContent() viewModel.saveRead() } - 2 -> if (!moveToNextChapter()) ReadAloudService.stop(this) + 2 -> if (!moveToNextChapter()) BaseReadAloudService.stop(this) -1 -> { if (viewModel.durPageIndex > 0) { viewModel.durPageIndex = viewModel.durPageIndex - 1 diff --git a/app/src/main/java/io/legado/app/ui/readbook/ReadBookViewModel.kt b/app/src/main/java/io/legado/app/ui/readbook/ReadBookViewModel.kt index fe1a00201..14ecccba4 100644 --- a/app/src/main/java/io/legado/app/ui/readbook/ReadBookViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/readbook/ReadBookViewModel.kt @@ -11,7 +11,7 @@ import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.help.BookHelp 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 kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main @@ -332,7 +332,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { override fun onCleared() { super.onCleared() - ReadAloudService.stop(context) + BaseReadAloudService.stop(context) } interface CallBack { diff --git a/app/src/main/java/io/legado/app/ui/readbook/config/ReadAloudDialog.kt b/app/src/main/java/io/legado/app/ui/readbook/config/ReadAloudDialog.kt index 686c0e7b5..846d6ff15 100644 --- a/app/src/main/java/io/legado/app/ui/readbook/config/ReadAloudDialog.kt +++ b/app/src/main/java/io/legado/app/ui/readbook/config/ReadAloudDialog.kt @@ -11,7 +11,7 @@ import androidx.fragment.app.DialogFragment import io.legado.app.R import io.legado.app.constant.Bus 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.utils.* import kotlinx.android.synthetic.main.dialog_read_aloud.* @@ -61,8 +61,9 @@ class ReadAloudDialog : DialogFragment() { callBack?.readAloudStatus?.let { upPlayState(it) } - seek_timer.progress = ReadAloudService.timeMinute - tv_timer.text = requireContext().getString(R.string.timer_m, ReadAloudService.timeMinute) + seek_timer.progress = BaseReadAloudService.timeMinute + tv_timer.text = + requireContext().getString(R.string.timer_m, BaseReadAloudService.timeMinute) cb_by_page.isChecked = requireContext().getPrefBoolean("readAloudByPage") cb_tts_follow_sys.isChecked = requireContext().getPrefBoolean("ttsFollowSys", true) seek_tts_SpeechRate.isEnabled = !cb_tts_follow_sys.isChecked @@ -103,7 +104,7 @@ class ReadAloudDialog : DialogFragment() { override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit 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() { iv_menu.onClick { callBack?.showMenu(); dismiss() } 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_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_next.onClick { ReadAloudService.nextParagraph(requireContext()) } + iv_play_next.onClick { BaseReadAloudService.nextParagraph(requireContext()) } iv_play_next.onLongClick { postEvent(Bus.TTS_TURN_PAGE, 2); true } } @@ -128,10 +129,10 @@ class ReadAloudDialog : DialogFragment() { } private fun upTtsSpeechRate() { - ReadAloudService.upTtsSpeechRate(requireContext()) + BaseReadAloudService.upTtsSpeechRate(requireContext()) if (callBack?.readAloudStatus == Status.PLAY) { - ReadAloudService.pause(requireContext()) - ReadAloudService.resume(requireContext()) + BaseReadAloudService.pause(requireContext()) + BaseReadAloudService.resume(requireContext()) } }