This commit is contained in:
Horis
2024-08-24 15:05:53 +08:00
parent 7a7cb28813
commit 84bcd42644
7 changed files with 357 additions and 245 deletions
+6 -5
View File
@@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@@ -368,12 +369,12 @@
<!-- pdf --> <!-- pdf -->
<data android:mimeType="application/pdf" /> <data android:mimeType="application/pdf" />
<!-- mobi --> <!-- mobi -->
<data android:mimeType="application/mobi"/> <data android:mimeType="application/mobi" />
<data android:mimeType="application/x-mobipocket-ebook"/> <data android:mimeType="application/x-mobipocket-ebook" />
<!-- azw/azw3 --> <!-- azw/azw3 -->
<data android:mimeType="application/azw"/> <data android:mimeType="application/azw" />
<data android:mimeType="application/azw3"/> <data android:mimeType="application/azw3" />
<data android:mimeType="application/x-mobi8-ebook"/> <data android:mimeType="application/x-mobi8-ebook" />
<data android:mimeType="application/octet-stream" /> <data android:mimeType="application/octet-stream" />
</intent-filter> </intent-filter>
<!-- Works when an app doesn't know the media type, e.g. Dropbox --> <!-- Works when an app doesn't know the media type, e.g. Dropbox -->
@@ -1,8 +1,9 @@
package io.legado.app.model package io.legado.app.model
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.lifecycle.MutableLiveData import io.legado.app.constant.AppLog
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.constant.Status import io.legado.app.constant.Status
@@ -12,84 +13,176 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.book.ContentProcessor import io.legado.app.help.book.ContentProcessor
import io.legado.app.help.book.getBookSource import io.legado.app.help.book.getBookSource
import io.legado.app.help.book.readSimulating
import io.legado.app.help.book.simulatedTotalChapterNum import io.legado.app.help.book.simulatedTotalChapterNum
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.webBook.WebBook
import io.legado.app.service.AudioPlayService import io.legado.app.service.AudioPlayService
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.startService import io.legado.app.utils.startService
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancelChildren
import splitties.init.appCtx import splitties.init.appCtx
@SuppressLint("StaticFieldLeak")
@Suppress("unused") @Suppress("unused")
object AudioPlay { object AudioPlay : CoroutineScope by MainScope() {
var titleData = MutableLiveData<String>()
var coverData = MutableLiveData<String>()
var status = Status.STOP var status = Status.STOP
private var activityContext: Context? = null
private var serviceContext: Context? = null
private val context: Context get() = activityContext ?: serviceContext ?: appCtx
var callback: CallBack? = null
var book: Book? = null var book: Book? = null
var chapterSize = 0
var simulatedChapterSize = 0
var durChapterIndex = 0
var durChapterPos = 0
var durChapter: BookChapter? = null var durChapter: BookChapter? = null
var durPlayUrl = ""
var durAudioSize = 0
var inBookshelf = false var inBookshelf = false
var bookSource: BookSource? = null var bookSource: BookSource? = null
val loadingChapters = arrayListOf<Int>() val loadingChapters = arrayListOf<Int>()
var durChapterIndex = 0
fun upData(context: Context, book: Book) { fun upData(book: Book) {
AudioPlay.book = book AudioPlay.book = book
upDurChapter(book) chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl)
simulatedChapterSize = if (book.readSimulating()) {
book.simulatedTotalChapterNum()
} else {
chapterSize
}
if (durChapterIndex != book.durChapterIndex) { if (durChapterIndex != book.durChapterIndex) {
durChapterIndex = book.durChapterIndex durChapterIndex = book.durChapterIndex
playNew(context) durChapterPos = book.durChapterPos
durPlayUrl = ""
durAudioSize = 0
}
upDurChapter()
}
fun resetData(book: Book) {
stop()
AudioPlay.book = book
chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl)
simulatedChapterSize = if (book.readSimulating()) {
book.simulatedTotalChapterNum()
} else {
chapterSize
}
bookSource = book.getBookSource()
durChapterIndex = book.durChapterIndex
durChapterPos = book.durChapterPos
durPlayUrl = ""
durAudioSize = 0
upDurChapter()
}
private fun addLoading(index: Int): Boolean {
synchronized(this) {
if (loadingChapters.contains(index)) return false
loadingChapters.add(index)
return true
} }
} }
fun resetData(context: Context, book: Book) { private fun removeLoading(index: Int) {
stop(context) synchronized(this) {
AudioPlay.book = book loadingChapters.remove(index)
titleData.postValue(book.name) }
coverData.postValue(book.getDisplayCover()) }
bookSource = book.getBookSource()
durChapterIndex = book.durChapterIndex fun loadOrUpPlayUrl() {
upDurChapter(book) if (durPlayUrl.isEmpty()) {
loadPlayUrl()
} else {
upPlayUrl()
}
}
/**
* 加载播放URL
*/
private fun loadPlayUrl() {
val index = durChapterIndex
if (addLoading(index)) {
val book = book
val bookSource = bookSource
if (book != null && bookSource != null) {
upDurChapter()
val chapter = durChapter
if (chapter == null) {
removeLoading(index)
return
}
upLoading(true)
WebBook.getContent(this, bookSource, book, chapter)
.onSuccess { content ->
if (content.isEmpty()) {
appCtx.toastOnUi("未获取到资源链接")
} else {
contentLoadFinish(chapter, content)
}
}.onError {
AppLog.put("获取资源链接出错\n$it", it, true)
}.onFinally {
removeLoading(index)
}
} else {
removeLoading(index)
appCtx.toastOnUi("book or source is null")
}
}
}
/**
* 加载完成
*/
private fun contentLoadFinish(chapter: BookChapter, content: String) {
if (chapter.index == book?.durChapterIndex) {
durPlayUrl = content
upPlayUrl()
}
}
private fun upPlayUrl() {
if (isPlayToEnd()) {
playNew()
} else {
play()
}
} }
/** /**
* 播放当前章节 * 播放当前章节
*/ */
fun play(context: Context) { fun play() {
book?.let { context.startService<AudioPlayService> {
if (durChapter == null) { action = IntentAction.play
upDurChapter(it)
}
durChapter?.let {
context.startService<AudioPlayService> {
action = IntentAction.play
}
}
} }
} }
/** /**
* 从头播放新章节 * 从头播放新章节
*/ */
fun playNew(context: Context) { private fun playNew() {
book?.let { context.startService<AudioPlayService> {
if (durChapter == null) { action = IntentAction.playNew
upDurChapter(it)
}
durChapter?.let {
context.startService<AudioPlayService> {
action = IntentAction.playNew
}
}
} }
} }
/** /**
* 更新当前章节 * 更新当前章节
*/ */
fun upDurChapter(book: Book) { fun upDurChapter() {
durChapter = appDb.bookChapterDao.getChapter(book.bookUrl, book.durChapterIndex) val book = book ?: return
durChapter = appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex)
durAudioSize = durChapter?.end?.toInt() ?: 0
postEvent(EventBus.AUDIO_SUB_TITLE, durChapter?.title ?: "") postEvent(EventBus.AUDIO_SUB_TITLE, durChapter?.title ?: "")
postEvent(EventBus.AUDIO_SIZE, durChapter?.end?.toInt() ?: 0) postEvent(EventBus.AUDIO_SIZE, durAudioSize)
postEvent(EventBus.AUDIO_PROGRESS, book.durChapterPos) postEvent(EventBus.AUDIO_PROGRESS, durChapterPos)
} }
fun pause(context: Context) { fun pause(context: Context) {
@@ -108,7 +201,7 @@ object AudioPlay {
} }
} }
fun stop(context: Context) { fun stop() {
if (AudioPlayService.isRun) { if (AudioPlayService.isRun) {
context.startService<AudioPlayService> { context.startService<AudioPlayService> {
action = IntentAction.stop action = IntentAction.stop
@@ -116,7 +209,7 @@ object AudioPlay {
} }
} }
fun adjustSpeed(context: Context, adjust: Float) { fun adjustSpeed(adjust: Float) {
if (AudioPlayService.isRun) { if (AudioPlayService.isRun) {
context.startService<AudioPlayService> { context.startService<AudioPlayService> {
action = IntentAction.adjustSpeed action = IntentAction.adjustSpeed
@@ -125,7 +218,9 @@ object AudioPlay {
} }
} }
fun adjustProgress(context: Context, position: Int) { fun adjustProgress(position: Int) {
durChapterPos = position
saveRead()
if (AudioPlayService.isRun) { if (AudioPlayService.isRun) {
context.startService<AudioPlayService> { context.startService<AudioPlayService> {
action = IntentAction.adjustProgress action = IntentAction.adjustProgress
@@ -134,57 +229,47 @@ object AudioPlay {
} }
} }
fun skipTo(context: Context, index: Int) { fun skipTo(index: Int) {
Coroutine.async { Coroutine.async {
book?.let { book -> stop()
book.durChapterIndex = index durChapterIndex = index
book.durChapterPos = 0 durChapterPos = 0
durChapterIndex = book.durChapterIndex durPlayUrl = ""
durChapter = null saveRead()
loadPlayUrl()
}
}
fun prev() {
Coroutine.async {
stop()
if (durChapterIndex > 0) {
durChapterIndex -= 1
durChapterPos = 0
durPlayUrl = ""
saveRead() saveRead()
playNew(context) loadPlayUrl()
} }
} }
} }
fun prev(context: Context) { fun next() {
Coroutine.async { stop()
book?.let { book -> if (durChapterIndex + 1 < simulatedChapterSize) {
if (book.durChapterIndex > 0) { durChapterIndex += 1
book.durChapterIndex -= 1 durChapterPos = 0
book.durChapterPos = 0 durPlayUrl = ""
durChapterIndex = book.durChapterIndex saveRead()
durChapter = null loadPlayUrl()
saveRead()
play(context)
} else {
stop(context)
}
}
}
}
fun next(context: Context) {
book?.let { book ->
if (book.durChapterIndex + 1 < book.simulatedTotalChapterNum()) {
book.durChapterIndex += 1
book.durChapterPos = 0
durChapterIndex = book.durChapterIndex
durChapter = null
saveRead()
play(context)
} else {
stop(context)
}
} }
} }
fun setTimer(minute: Int) { fun setTimer(minute: Int) {
if (AudioPlayService.isRun) { if (AudioPlayService.isRun) {
val intent = Intent(appCtx, AudioPlayService::class.java) val intent = Intent(context, AudioPlayService::class.java)
intent.action = IntentAction.setTimer intent.action = IntentAction.setTimer
intent.putExtra("minute", minute) intent.putExtra("minute", minute)
appCtx.startService(intent) context.startService(intent)
} else { } else {
AudioPlayService.timeMinute = minute AudioPlayService.timeMinute = minute
postEvent(EventBus.AUDIO_DS, minute) postEvent(EventBus.AUDIO_DS, minute)
@@ -192,24 +277,28 @@ object AudioPlay {
} }
fun addTimer() { fun addTimer() {
val intent = Intent(appCtx, AudioPlayService::class.java) val intent = Intent(context, AudioPlayService::class.java)
intent.action = IntentAction.addTimer intent.action = IntentAction.addTimer
appCtx.startService(intent) context.startService(intent)
} }
fun saveRead() { fun saveRead() {
book?.let { book -> val book = book ?: return
Coroutine.async {
book.lastCheckCount = 0 book.lastCheckCount = 0
book.durChapterTime = System.currentTimeMillis() book.durChapterTime = System.currentTimeMillis()
Coroutine.async { val chapterChanged = book.durChapterIndex != durChapterIndex
book.durChapterIndex = durChapterIndex
book.durChapterPos = durChapterPos
if (chapterChanged) {
appDb.bookChapterDao.getChapter(book.bookUrl, book.durChapterIndex)?.let { appDb.bookChapterDao.getChapter(book.bookUrl, book.durChapterIndex)?.let {
book.durChapterTitle = it.getDisplayTitle( book.durChapterTitle = it.getDisplayTitle(
ContentProcessor.get(book.name, book.origin).getTitleReplaceRules(), ContentProcessor.get(book.name, book.origin).getTitleReplaceRules(),
book.getUseReplaceRule() book.getUseReplaceRule()
) )
} }
book.update()
} }
book.update()
} }
} }
@@ -217,11 +306,53 @@ object AudioPlay {
* 保存章节长度 * 保存章节长度
*/ */
fun saveDurChapter(audioSize: Long) { fun saveDurChapter(audioSize: Long) {
val chapter = durChapter ?: return
Coroutine.async { Coroutine.async {
durChapter?.let { durAudioSize = audioSize.toInt()
it.end = audioSize chapter.end = audioSize
appDb.bookChapterDao.update(it) appDb.bookChapterDao.update(chapter)
}
} }
} }
fun playPositionChanged(position: Int) {
durChapterPos = position
saveRead()
}
fun upLoading(loading: Boolean) {
callback?.upLoading(loading)
}
private fun isPlayToEnd(): Boolean {
return durChapterIndex + 1 == simulatedChapterSize
&& durChapterPos == durAudioSize
}
fun register(context: Context) {
activityContext = context
callback = context as CallBack
}
fun unregister(context: Context) {
if (activityContext === context) {
activityContext = null
callback = null
}
coroutineContext.cancelChildren()
}
fun registerService(context: Context) {
serviceContext = context
}
fun unregisterService() {
serviceContext = null
}
interface CallBack {
fun upLoading(loading: Boolean)
}
} }
@@ -29,16 +29,12 @@ import io.legado.app.constant.EventBus
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.constant.NotificationId import io.legado.app.constant.NotificationId
import io.legado.app.constant.Status import io.legado.app.constant.Status
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.MediaHelp import io.legado.app.help.MediaHelp
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.exoplayer.ExoPlayerHelper import io.legado.app.help.exoplayer.ExoPlayerHelper
import io.legado.app.help.glide.ImageLoader import io.legado.app.help.glide.ImageLoader
import io.legado.app.model.AudioPlay import io.legado.app.model.AudioPlay
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.webBook.WebBook
import io.legado.app.receiver.MediaButtonReceiver import io.legado.app.receiver.MediaButtonReceiver
import io.legado.app.ui.book.audio.AudioPlayActivity import io.legado.app.ui.book.audio.AudioPlayActivity
import io.legado.app.utils.activityPendingIntent import io.legado.app.utils.activityPendingIntent
@@ -122,6 +118,7 @@ class AudioPlayService : BaseService(),
super.onCreate() super.onCreate()
isRun = true isRun = true
exoPlayer.addListener(this) exoPlayer.addListener(this)
AudioPlay.registerService(this)
initMediaSession() initMediaSession()
initBroadcastReceiver() initBroadcastReceiver()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING) upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
@@ -147,7 +144,8 @@ class AudioPlayService : BaseService(),
upPlayProgressJob?.cancel() upPlayProgressJob?.cancel()
pause = false pause = false
position = AudioPlay.book?.durChapterPos ?: 0 position = AudioPlay.book?.durChapterPos ?: 0
loadContent() url = AudioPlay.durPlayUrl
play()
} }
IntentAction.playNew -> { IntentAction.playNew -> {
@@ -155,13 +153,14 @@ class AudioPlayService : BaseService(),
upPlayProgressJob?.cancel() upPlayProgressJob?.cancel()
pause = false pause = false
position = 0 position = 0
loadContent() url = AudioPlay.durPlayUrl
play()
} }
IntentAction.pause -> pause() IntentAction.pause -> pause()
IntentAction.resume -> resume() IntentAction.resume -> resume()
IntentAction.prev -> AudioPlay.prev(this) IntentAction.prev -> AudioPlay.prev()
IntentAction.next -> AudioPlay.next(this) IntentAction.next -> AudioPlay.next()
IntentAction.adjustSpeed -> upSpeed(intent.getFloatExtra("adjust", 1f)) IntentAction.adjustSpeed -> upSpeed(intent.getFloatExtra("adjust", 1f))
IntentAction.addTimer -> addTimer() IntentAction.addTimer -> addTimer()
IntentAction.setTimer -> setTimer(intent.getIntExtra("minute", 0)) IntentAction.setTimer -> setTimer(intent.getIntExtra("minute", 0))
@@ -189,6 +188,7 @@ class AudioPlayService : BaseService(),
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED) upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED)
AudioPlay.status = Status.STOP AudioPlay.status = Status.STOP
postEvent(EventBus.AUDIO_STATE, Status.STOP) postEvent(EventBus.AUDIO_STATE, Status.STOP)
AudioPlay.unregisterService()
} }
/** /**
@@ -216,6 +216,7 @@ class AudioPlayService : BaseService(),
) )
exoPlayer.setMediaItem(analyzeUrl.getMediaItem()) exoPlayer.setMediaItem(analyzeUrl.getMediaItem())
exoPlayer.playWhenReady = true exoPlayer.playWhenReady = true
exoPlayer.seekTo(position.toLong())
exoPlayer.prepare() exoPlayer.prepare()
}.onError { }.onError {
AppLog.put("播放出错\n${it.localizedMessage}", it) AppLog.put("播放出错\n${it.localizedMessage}", it)
@@ -261,7 +262,7 @@ class AudioPlayService : BaseService(),
try { try {
pause = false pause = false
if (url.isEmpty()) { if (url.isEmpty()) {
loadContent() AudioPlay.loadOrUpPlayUrl()
return return
} }
if (!exoPlayer.isPlaying) { if (!exoPlayer.isPlaying) {
@@ -317,9 +318,7 @@ class AudioPlayService : BaseService(),
Player.STATE_READY -> { Player.STATE_READY -> {
// 准备好 // 准备好
if (exoPlayer.currentPosition != position.toLong()) { AudioPlay.upLoading(false)
exoPlayer.seekTo(position.toLong())
}
if (exoPlayer.playWhenReady) { if (exoPlayer.playWhenReady) {
AudioPlay.status = Status.PLAY AudioPlay.status = Status.PLAY
postEvent(EventBus.AUDIO_STATE, Status.PLAY) postEvent(EventBus.AUDIO_STATE, Status.PLAY)
@@ -336,7 +335,8 @@ class AudioPlayService : BaseService(),
Player.STATE_ENDED -> { Player.STATE_ENDED -> {
// 结束 // 结束
upPlayProgressJob?.cancel() upPlayProgressJob?.cancel()
AudioPlay.next(this) AudioPlay.playPositionChanged(exoPlayer.duration.toInt())
AudioPlay.next()
} }
} }
upAudioPlayNotification() upAudioPlayNotification()
@@ -395,7 +395,7 @@ class AudioPlayService : BaseService(),
timeMinute-- timeMinute--
} }
if (timeMinute == 0) { if (timeMinute == 0) {
AudioPlay.stop(this@AudioPlayService) AudioPlay.stop()
} }
} }
postEvent(EventBus.AUDIO_DS, timeMinute) postEvent(EventBus.AUDIO_DS, timeMinute)
@@ -411,81 +411,16 @@ class AudioPlayService : BaseService(),
upPlayProgressJob?.cancel() upPlayProgressJob?.cancel()
upPlayProgressJob = lifecycleScope.launch { upPlayProgressJob = lifecycleScope.launch {
while (isActive) { while (isActive) {
AudioPlay.book?.let { //更新buffer位置
//更新buffer位置 AudioPlay.playPositionChanged(exoPlayer.currentPosition.toInt())
postEvent(EventBus.AUDIO_BUFFER_PROGRESS, exoPlayer.bufferedPosition.toInt()) postEvent(EventBus.AUDIO_BUFFER_PROGRESS, exoPlayer.bufferedPosition.toInt())
it.durChapterPos = exoPlayer.currentPosition.toInt() postEvent(EventBus.AUDIO_PROGRESS, AudioPlay.durChapterPos)
postEvent(EventBus.AUDIO_PROGRESS, it.durChapterPos) upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
saveProgress(it)
}
delay(1000) delay(1000)
} }
} }
} }
/**
* 加载播放URL
*/
private fun loadContent() = with(AudioPlay) {
durChapter?.let { chapter ->
if (addLoading(chapter.index)) {
val book = AudioPlay.book
val bookSource = AudioPlay.bookSource
if (book != null && bookSource != null) {
WebBook.getContent(lifecycleScope, bookSource, book, chapter)
.onSuccess { content ->
if (content.isEmpty()) {
toastOnUi("未获取到资源链接")
} else {
contentLoadFinish(chapter, content)
}
}.onError {
contentLoadFinish(chapter, it.localizedMessage ?: toString())
}.onFinally {
removeLoading(chapter.index)
}
} else {
removeLoading(chapter.index)
toastOnUi("book or source is null")
}
}
}
}
private fun addLoading(index: Int): Boolean {
synchronized(this) {
if (AudioPlay.loadingChapters.contains(index)) return false
AudioPlay.loadingChapters.add(index)
return true
}
}
private fun removeLoading(index: Int) {
synchronized(this) {
AudioPlay.loadingChapters.remove(index)
}
}
/**
* 加载完成
*/
private fun contentLoadFinish(chapter: BookChapter, content: String) {
if (chapter.index == AudioPlay.book?.durChapterIndex) {
url = content
play()
}
}
/**
* 保存播放进度
*/
private fun saveProgress(book: Book) {
execute {
appDb.bookDao.upProgress(book.bookUrl, book.durChapterPos)
}
}
/** /**
* 更新媒体状态 * 更新媒体状态
*/ */
@@ -584,7 +519,6 @@ class AudioPlayService : BaseService(),
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
AppLog.put("音频焦点暂时丢失并会很快再次获得,暂停播放") AppLog.put("音频焦点暂时丢失并会很快再次获得,暂停播放")
needResumeOnAudioFocusGain = true
if (!pause) { if (!pause) {
needResumeOnAudioFocusGain = true needResumeOnAudioFocusGain = true
pause(false) pause(false)
@@ -24,7 +24,6 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.databinding.ActivityAudioPlayBinding import io.legado.app.databinding.ActivityAudioPlayBinding
import io.legado.app.help.book.isAudio import io.legado.app.help.book.isAudio
import io.legado.app.help.book.removeType import io.legado.app.help.book.removeType
import io.legado.app.help.book.simulatedTotalChapterNum
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.model.AudioPlay import io.legado.app.model.AudioPlay
@@ -59,7 +58,8 @@ import java.util.Locale
@SuppressLint("ObsoleteSdkInt") @SuppressLint("ObsoleteSdkInt")
class AudioPlayActivity : class AudioPlayActivity :
VMBaseActivity<ActivityAudioPlayBinding, AudioPlayViewModel>(toolBarTheme = Theme.Dark), VMBaseActivity<ActivityAudioPlayBinding, AudioPlayViewModel>(toolBarTheme = Theme.Dark),
ChangeBookSourceDialog.CallBack { ChangeBookSourceDialog.CallBack,
AudioPlay.CallBack {
override val binding by viewBinding(ActivityAudioPlayBinding::inflate) override val binding by viewBinding(ActivityAudioPlayBinding::inflate)
override val viewModel by viewModels<AudioPlayViewModel>() override val viewModel by viewModels<AudioPlayViewModel>()
@@ -78,7 +78,7 @@ class AudioPlayActivity :
if (it.first != AudioPlay.book?.durChapterIndex if (it.first != AudioPlay.book?.durChapterIndex
|| it.second == 0 || it.second == 0
) { ) {
AudioPlay.skipTo(this, it.first) AudioPlay.skipTo(it.first)
} }
} }
} }
@@ -91,10 +91,11 @@ class AudioPlayActivity :
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
binding.titleBar.setBackgroundResource(R.color.transparent) binding.titleBar.setBackgroundResource(R.color.transparent)
AudioPlay.titleData.observe(this) { AudioPlay.register(this)
viewModel.titleData.observe(this) {
binding.titleBar.title = it binding.titleBar.title = it
} }
AudioPlay.coverData.observe(this) { viewModel.coverData.observe(this) {
upCover(it) upCover(it)
} }
viewModel.initData(intent) viewModel.initData(intent)
@@ -143,13 +144,13 @@ class AudioPlayActivity :
playButton() playButton()
} }
binding.fabPlayStop.onLongClick { binding.fabPlayStop.onLongClick {
AudioPlay.stop(this@AudioPlayActivity) AudioPlay.stop()
} }
binding.ivSkipNext.setOnClickListener { binding.ivSkipNext.setOnClickListener {
AudioPlay.next(this@AudioPlayActivity) AudioPlay.next()
} }
binding.ivSkipPrevious.setOnClickListener { binding.ivSkipPrevious.setOnClickListener {
AudioPlay.prev(this@AudioPlayActivity) AudioPlay.prev()
} }
binding.playerProgress.setOnSeekBarChangeListener(object : SeekBarChangeListener { binding.playerProgress.setOnSeekBarChangeListener(object : SeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
@@ -162,7 +163,7 @@ class AudioPlayActivity :
override fun onStopTrackingTouch(seekBar: SeekBar) { override fun onStopTrackingTouch(seekBar: SeekBar) {
adjustProgress = false adjustProgress = false
AudioPlay.adjustProgress(this@AudioPlayActivity, seekBar.progress) AudioPlay.adjustProgress(seekBar.progress)
} }
}) })
binding.ivChapter.setOnClickListener { binding.ivChapter.setOnClickListener {
@@ -175,10 +176,10 @@ class AudioPlayActivity :
binding.ivFastForward.invisible() binding.ivFastForward.invisible()
} }
binding.ivFastForward.setOnClickListener { binding.ivFastForward.setOnClickListener {
AudioPlay.adjustSpeed(this@AudioPlayActivity, 0.1f) AudioPlay.adjustSpeed(0.1f)
} }
binding.ivFastRewind.setOnClickListener { binding.ivFastRewind.setOnClickListener {
AudioPlay.adjustSpeed(this@AudioPlayActivity, -0.1f) AudioPlay.adjustSpeed(-0.1f)
} }
binding.ivTimer.setOnClickListener { binding.ivTimer.setOnClickListener {
timerSliderPopup.showAsDropDown(it, 0, (-100).dpToPx(), Gravity.TOP) timerSliderPopup.showAsDropDown(it, 0, (-100).dpToPx(), Gravity.TOP)
@@ -196,7 +197,7 @@ class AudioPlayActivity :
when (AudioPlay.status) { when (AudioPlay.status) {
Status.PLAY -> AudioPlay.pause(this) Status.PLAY -> AudioPlay.pause(this)
Status.PAUSE -> AudioPlay.resume(this) Status.PAUSE -> AudioPlay.resume(this)
else -> AudioPlay.play(this) else -> AudioPlay.loadOrUpPlayUrl()
} }
} }
@@ -207,7 +208,7 @@ class AudioPlayActivity :
if (book.isAudio) { if (book.isAudio) {
viewModel.changeTo(source, book, toc) viewModel.changeTo(source, book, toc)
} else { } else {
AudioPlay.stop(this) AudioPlay.stop()
lifecycleScope.launch { lifecycleScope.launch {
withContext(IO) { withContext(IO) {
AudioPlay.book?.migrateTo(book, toc) AudioPlay.book?.migrateTo(book, toc)
@@ -247,8 +248,9 @@ class AudioPlayActivity :
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
if (AudioPlay.status != Status.PLAY) { if (AudioPlay.status != Status.PLAY) {
AudioPlay.stop(this) AudioPlay.stop()
} }
AudioPlay.unregister(this)
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@@ -268,11 +270,9 @@ class AudioPlayActivity :
} }
observeEventSticky<String>(EventBus.AUDIO_SUB_TITLE) { observeEventSticky<String>(EventBus.AUDIO_SUB_TITLE) {
binding.tvSubTitle.text = it binding.tvSubTitle.text = it
AudioPlay.book?.let { book -> binding.ivSkipPrevious.isEnabled = AudioPlay.durChapterIndex > 0
binding.ivSkipPrevious.isEnabled = book.durChapterIndex > 0 binding.ivSkipNext.isEnabled =
binding.ivSkipNext.isEnabled = AudioPlay.durChapterIndex < AudioPlay.simulatedChapterSize - 1
book.durChapterIndex < book.simulatedTotalChapterNum() - 1
}
} }
observeEventSticky<Int>(EventBus.AUDIO_SIZE) { observeEventSticky<Int>(EventBus.AUDIO_SIZE) {
binding.playerProgress.max = it binding.playerProgress.max = it
@@ -296,4 +296,10 @@ class AudioPlayActivity :
} }
} }
override fun upLoading(loading: Boolean) {
runOnUiThread {
binding.progressLoading.visible(loading)
}
}
} }
@@ -2,9 +2,10 @@ package io.legado.app.ui.book.audio
import android.app.Application import android.app.Application
import android.content.Intent import android.content.Intent
import androidx.lifecycle.viewModelScope import androidx.lifecycle.MutableLiveData
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppLog
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.data.appDb import io.legado.app.data.appDb
@@ -13,14 +14,15 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.book.getBookSource import io.legado.app.help.book.getBookSource
import io.legado.app.help.book.removeType import io.legado.app.help.book.removeType
import io.legado.app.help.book.simulatedTotalChapterNum
import io.legado.app.model.AudioPlay import io.legado.app.model.AudioPlay
import io.legado.app.model.AudioPlay.durChapter
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers.IO
class AudioPlayViewModel(application: Application) : BaseViewModel(application) { class AudioPlayViewModel(application: Application) : BaseViewModel(application) {
val titleData = MutableLiveData<String>()
val coverData = MutableLiveData<String>()
fun initData(intent: Intent) = AudioPlay.apply { fun initData(intent: Intent) = AudioPlay.apply {
execute { execute {
@@ -33,37 +35,53 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
} }
} }
private fun initBook(book: Book) { private suspend fun initBook(book: Book) {
val isSameBook = AudioPlay.book?.bookUrl == book.bookUrl val isSameBook = AudioPlay.book?.bookUrl == book.bookUrl
if (isSameBook) { if (isSameBook) {
AudioPlay.upData(context, book) AudioPlay.upData(book)
} else { } else {
AudioPlay.resetData(context, book) AudioPlay.resetData(book)
} }
if (durChapter == null) { titleData.postValue(book.name)
if (book.tocUrl.isEmpty()) { coverData.postValue(book.getDisplayCover())
loadBookInfo(book) if (book.tocUrl.isEmpty() && !loadBookInfo(book)) {
return
}
if (AudioPlay.chapterSize == 0 && !loadChapterList(book)) {
return
}
}
private suspend fun loadBookInfo(book: Book): Boolean {
val bookSource = AudioPlay.bookSource ?: return true
try {
WebBook.getBookInfoAwait(bookSource, book)
return true
} catch (e: Exception) {
AppLog.put("详情页出错: ${e.localizedMessage}", e, true)
return false
}
}
private suspend fun loadChapterList(book: Book): Boolean {
val bookSource = AudioPlay.bookSource ?: return true
try {
val oldBook = book.copy()
val cList = WebBook.getChapterListAwait(bookSource, book).getOrThrow()
if (oldBook.bookUrl == book.bookUrl) {
appDb.bookDao.update(book)
} else { } else {
loadChapterList(book) appDb.bookDao.insert(book)
} }
} appDb.bookChapterDao.delByBook(book.bookUrl)
}
private fun loadBookInfo(book: Book) {
val bookSource = AudioPlay.bookSource ?: return
WebBook.getBookInfo(viewModelScope, bookSource, book).onSuccess(IO) {
loadChapterList(book)
}
}
private fun loadChapterList(book: Book) {
val bookSource = AudioPlay.bookSource ?: return
WebBook.getChapterList(viewModelScope, bookSource, book).onSuccess(IO) { cList ->
book.save()
appDb.bookChapterDao.insert(*cList.toTypedArray()) appDb.bookChapterDao.insert(*cList.toTypedArray())
AudioPlay.upDurChapter(book) AudioPlay.chapterSize = cList.size
}.onError { AudioPlay.simulatedChapterSize = book.simulatedTotalChapterNum()
AudioPlay.upDurChapter()
return true
} catch (e: Exception) {
context.toastOnUi(R.string.error_load_toc) context.toastOnUi(R.string.error_load_toc)
return false
} }
} }
@@ -83,7 +101,7 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
AudioPlay.book = book AudioPlay.book = book
AudioPlay.bookSource = source AudioPlay.bookSource = source
appDb.bookChapterDao.insert(*toc.toTypedArray()) appDb.bookChapterDao.insert(*toc.toTypedArray())
AudioPlay.upDurChapter(book) AudioPlay.upDurChapter()
}.onFinally { }.onFinally {
postEvent(EventBus.SOURCE_CHANGED, book.bookUrl) postEvent(EventBus.SOURCE_CHANGED, book.bookUrl)
} }
+40 -18
View File
@@ -130,9 +130,9 @@
android:id="@+id/iv_timer" android:id="@+id/iv_timer"
android:layout_width="46dp" android:layout_width="46dp"
android:layout_height="46dp" android:layout_height="46dp"
android:padding="5dp"
android:background="@drawable/selector_circle_btn_bg" android:background="@drawable/selector_circle_btn_bg"
android:contentDescription="@string/set_timer" android:contentDescription="@string/set_timer"
android:padding="5dp"
android:src="@drawable/ic_timer_black_24dp" android:src="@drawable/ic_timer_black_24dp"
app:tint="@color/md_white_1000" app:tint="@color/md_white_1000"
tools:ignore="ImageContrastCheck" /> tools:ignore="ImageContrastCheck" />
@@ -146,9 +146,9 @@
android:id="@+id/iv_fast_rewind" android:id="@+id/iv_fast_rewind"
android:layout_width="46dp" android:layout_width="46dp"
android:layout_height="46dp" android:layout_height="46dp"
android:padding="5dp"
android:background="@drawable/selector_circle_btn_bg" android:background="@drawable/selector_circle_btn_bg"
android:contentDescription="@string/skip_previous" android:contentDescription="@string/skip_previous"
android:padding="5dp"
android:src="@drawable/ic_fast_rewind" android:src="@drawable/ic_fast_rewind"
app:tint="@color/md_white_1000" app:tint="@color/md_white_1000"
tools:ignore="ImageContrastCheck" /> tools:ignore="ImageContrastCheck" />
@@ -162,34 +162,56 @@
android:id="@+id/iv_skip_previous" android:id="@+id/iv_skip_previous"
android:layout_width="46dp" android:layout_width="46dp"
android:layout_height="46dp" android:layout_height="46dp"
android:padding="5dp"
android:background="@drawable/selector_circle_btn_bg" android:background="@drawable/selector_circle_btn_bg"
android:contentDescription="@string/skip_previous" android:contentDescription="@string/skip_previous"
android:padding="5dp"
android:src="@drawable/ic_skip_previous" android:src="@drawable/ic_skip_previous"
app:tint="@color/md_white_1000" app:tint="@color/md_white_1000"
tools:ignore="ImageContrastCheck" /> tools:ignore="ImageContrastCheck" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/fab_play_stop"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:layout_margin="10dp"
android:contentDescription="@string/audio_play" <com.google.android.material.floatingactionbutton.FloatingActionButton
android:src="@drawable/ic_play_24dp" android:id="@+id/fab_play_stop"
android:tint="@color/md_black_1000" android:layout_width="wrap_content"
app:backgroundTint="@color/md_white_1000" android:layout_height="wrap_content"
app:elevation="2dp" android:layout_margin="10dp"
app:fabSize="normal" android:contentDescription="@string/audio_play"
app:pressedTranslationZ="2dp" android:src="@drawable/ic_play_24dp"
tools:ignore="ImageContrastCheck" /> android:tint="@color/md_black_1000"
app:backgroundTint="@color/md_white_1000"
app:elevation="2dp"
app:fabSize="normal"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:pressedTranslationZ="2dp"
tools:ignore="ImageContrastCheck" />
<io.legado.app.lib.theme.view.ThemeProgressBar
android:id="@+id/progress_loading"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="invisible"
app:layout_constraintWidth="parent"
app:layout_constraintHeight="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<io.legado.app.ui.widget.image.ImageButton <io.legado.app.ui.widget.image.ImageButton
android:id="@+id/iv_skip_next" android:id="@+id/iv_skip_next"
android:layout_width="46dp" android:layout_width="46dp"
android:layout_height="46dp" android:layout_height="46dp"
android:padding="5dp"
android:background="@drawable/selector_circle_btn_bg" android:background="@drawable/selector_circle_btn_bg"
android:contentDescription="@string/skip_next" android:contentDescription="@string/skip_next"
android:padding="5dp"
android:src="@drawable/ic_skip_next" android:src="@drawable/ic_skip_next"
app:tint="@color/md_white_1000" app:tint="@color/md_white_1000"
tools:ignore="ImageContrastCheck" /> tools:ignore="ImageContrastCheck" />
@@ -203,9 +225,9 @@
android:id="@+id/iv_fast_forward" android:id="@+id/iv_fast_forward"
android:layout_width="46dp" android:layout_width="46dp"
android:layout_height="46dp" android:layout_height="46dp"
android:padding="5dp"
android:background="@drawable/selector_circle_btn_bg" android:background="@drawable/selector_circle_btn_bg"
android:contentDescription="@string/skip_next" android:contentDescription="@string/skip_next"
android:padding="5dp"
android:src="@drawable/ic_fast_forward" android:src="@drawable/ic_fast_forward"
app:tint="@color/md_white_1000" app:tint="@color/md_white_1000"
tools:ignore="ImageContrastCheck" /> tools:ignore="ImageContrastCheck" />
@@ -219,9 +241,9 @@
android:id="@+id/iv_chapter" android:id="@+id/iv_chapter"
android:layout_width="46dp" android:layout_width="46dp"
android:layout_height="46dp" android:layout_height="46dp"
android:padding="5dp"
android:background="@drawable/selector_circle_btn_bg" android:background="@drawable/selector_circle_btn_bg"
android:contentDescription="@string/chapter_list" android:contentDescription="@string/chapter_list"
android:padding="5dp"
android:src="@drawable/ic_chapter_list" android:src="@drawable/ic_chapter_list"
app:tint="@color/md_white_1000" app:tint="@color/md_white_1000"
tools:ignore="ImageContrastCheck" /> tools:ignore="ImageContrastCheck" />
+1 -1
View File
@@ -7,7 +7,7 @@
android:padding="20dp" android:padding="20dp"
android:orientation="horizontal"> android:orientation="horizontal">
<ProgressBar <io.legado.app.lib.theme.view.ThemeProgressBar
android:id="@+id/pb" android:id="@+id/pb"
android:layout_width="30dp" android:layout_width="30dp"
android:layout_height="30dp" android:layout_height="30dp"