Horis 2 лет назад
Родитель
Сommit
419fd25024

+ 6 - 5
app/src/main/AndroidManifest.xml

@@ -4,6 +4,7 @@
 
     <uses-permission android:name="android.permission.INTERNET" />
     <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_WIFI_STATE" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@@ -368,12 +369,12 @@
                 <!-- pdf -->
                 <data android:mimeType="application/pdf" />
                 <!-- mobi -->
-                <data android:mimeType="application/mobi"/>
-                <data android:mimeType="application/x-mobipocket-ebook"/>
+                <data android:mimeType="application/mobi" />
+                <data android:mimeType="application/x-mobipocket-ebook" />
                 <!-- azw/azw3 -->
-                <data android:mimeType="application/azw"/>
-                <data android:mimeType="application/azw3"/>
-                <data android:mimeType="application/x-mobi8-ebook"/>
+                <data android:mimeType="application/azw" />
+                <data android:mimeType="application/azw3" />
+                <data android:mimeType="application/x-mobi8-ebook" />
                 <data android:mimeType="application/octet-stream" />
             </intent-filter>
             <!-- Works when an app doesn't know the media type, e.g. Dropbox -->

+ 214 - 83
app/src/main/java/io/legado/app/model/AudioPlay.kt

@@ -1,8 +1,9 @@
 package io.legado.app.model
 
+import android.annotation.SuppressLint
 import android.content.Context
 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.IntentAction
 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.help.book.ContentProcessor
 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.coroutine.Coroutine
+import io.legado.app.model.webBook.WebBook
 import io.legado.app.service.AudioPlayService
 import io.legado.app.utils.postEvent
 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
 
+@SuppressLint("StaticFieldLeak")
 @Suppress("unused")
-object AudioPlay {
-    var titleData = MutableLiveData<String>()
-    var coverData = MutableLiveData<String>()
+object AudioPlay : CoroutineScope by MainScope() {
     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 chapterSize = 0
+    var simulatedChapterSize = 0
+    var durChapterIndex = 0
+    var durChapterPos = 0
     var durChapter: BookChapter? = null
+    var durPlayUrl = ""
+    var durAudioSize = 0
     var inBookshelf = false
     var bookSource: BookSource? = null
     val loadingChapters = arrayListOf<Int>()
-    var durChapterIndex = 0
 
-    fun upData(context: Context, book: Book) {
+    fun upData(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) {
             durChapterIndex = book.durChapterIndex
-            playNew(context)
+            durChapterPos = book.durChapterPos
+            durPlayUrl = ""
+            durAudioSize = 0
         }
+        upDurChapter()
     }
 
-    fun resetData(context: Context, book: Book) {
-        stop(context)
+    fun resetData(book: Book) {
+        stop()
         AudioPlay.book = book
-        titleData.postValue(book.name)
-        coverData.postValue(book.getDisplayCover())
+        chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl)
+        simulatedChapterSize = if (book.readSimulating()) {
+            book.simulatedTotalChapterNum()
+        } else {
+            chapterSize
+        }
         bookSource = book.getBookSource()
         durChapterIndex = book.durChapterIndex
-        upDurChapter(book)
+        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
+        }
+    }
+
+    private fun removeLoading(index: Int) {
+        synchronized(this) {
+            loadingChapters.remove(index)
+        }
+    }
+
+    fun loadOrUpPlayUrl() {
+        if (durPlayUrl.isEmpty()) {
+            loadPlayUrl()
+        } else {
+            upPlayUrl()
+        }
     }
 
     /**
-     * 播放当前章节
+     * 加载播放URL
      */
-    fun play(context: Context) {
-        book?.let {
-            if (durChapter == null) {
-                upDurChapter(it)
-            }
-            durChapter?.let {
-                context.startService<AudioPlayService> {
-                    action = IntentAction.play
+    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.startService<AudioPlayService> {
+            action = IntentAction.play
+        }
+    }
+
     /**
      * 从头播放新章节
      */
-    fun playNew(context: Context) {
-        book?.let {
-            if (durChapter == null) {
-                upDurChapter(it)
-            }
-            durChapter?.let {
-                context.startService<AudioPlayService> {
-                    action = IntentAction.playNew
-                }
-            }
+    private fun playNew() {
+        context.startService<AudioPlayService> {
+            action = IntentAction.playNew
         }
     }
 
     /**
      * 更新当前章节
      */
-    fun upDurChapter(book: Book) {
-        durChapter = appDb.bookChapterDao.getChapter(book.bookUrl, book.durChapterIndex)
+    fun upDurChapter() {
+        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_SIZE, durChapter?.end?.toInt() ?: 0)
-        postEvent(EventBus.AUDIO_PROGRESS, book.durChapterPos)
+        postEvent(EventBus.AUDIO_SIZE, durAudioSize)
+        postEvent(EventBus.AUDIO_PROGRESS, durChapterPos)
     }
 
     fun pause(context: Context) {
@@ -108,7 +201,7 @@ object AudioPlay {
         }
     }
 
-    fun stop(context: Context) {
+    fun stop() {
         if (AudioPlayService.isRun) {
             context.startService<AudioPlayService> {
                 action = IntentAction.stop
@@ -116,7 +209,7 @@ object AudioPlay {
         }
     }
 
-    fun adjustSpeed(context: Context, adjust: Float) {
+    fun adjustSpeed(adjust: Float) {
         if (AudioPlayService.isRun) {
             context.startService<AudioPlayService> {
                 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) {
             context.startService<AudioPlayService> {
                 action = IntentAction.adjustProgress
@@ -134,57 +229,47 @@ object AudioPlay {
         }
     }
 
-    fun skipTo(context: Context, index: Int) {
+    fun skipTo(index: Int) {
         Coroutine.async {
-            book?.let { book ->
-                book.durChapterIndex = index
-                book.durChapterPos = 0
-                durChapterIndex = book.durChapterIndex
-                durChapter = null
-                saveRead()
-                playNew(context)
-            }
+            stop()
+            durChapterIndex = index
+            durChapterPos = 0
+            durPlayUrl = ""
+            saveRead()
+            loadPlayUrl()
         }
     }
 
-    fun prev(context: Context) {
+    fun prev() {
         Coroutine.async {
-            book?.let { book ->
-                if (book.durChapterIndex > 0) {
-                    book.durChapterIndex -= 1
-                    book.durChapterPos = 0
-                    durChapterIndex = book.durChapterIndex
-                    durChapter = null
-                    saveRead()
-                    play(context)
-                } else {
-                    stop(context)
-                }
+            stop()
+            if (durChapterIndex > 0) {
+                durChapterIndex -= 1
+                durChapterPos = 0
+                durPlayUrl = ""
+                saveRead()
+                loadPlayUrl()
             }
         }
     }
 
-    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 next() {
+        stop()
+        if (durChapterIndex + 1 < simulatedChapterSize) {
+            durChapterIndex += 1
+            durChapterPos = 0
+            durPlayUrl = ""
+            saveRead()
+            loadPlayUrl()
         }
     }
 
     fun setTimer(minute: Int) {
         if (AudioPlayService.isRun) {
-            val intent = Intent(appCtx, AudioPlayService::class.java)
+            val intent = Intent(context, AudioPlayService::class.java)
             intent.action = IntentAction.setTimer
             intent.putExtra("minute", minute)
-            appCtx.startService(intent)
+            context.startService(intent)
         } else {
             AudioPlayService.timeMinute = minute
             postEvent(EventBus.AUDIO_DS, minute)
@@ -192,24 +277,28 @@ object AudioPlay {
     }
 
     fun addTimer() {
-        val intent = Intent(appCtx, AudioPlayService::class.java)
+        val intent = Intent(context, AudioPlayService::class.java)
         intent.action = IntentAction.addTimer
-        appCtx.startService(intent)
+        context.startService(intent)
     }
 
     fun saveRead() {
-        book?.let { book ->
+        val book = book ?: return
+        Coroutine.async {
             book.lastCheckCount = 0
             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 {
                     book.durChapterTitle = it.getDisplayTitle(
                         ContentProcessor.get(book.name, book.origin).getTitleReplaceRules(),
                         book.getUseReplaceRule()
                     )
                 }
-                book.update()
             }
+            book.update()
         }
     }
 
@@ -217,11 +306,53 @@ object AudioPlay {
      * 保存章节长度
      */
     fun saveDurChapter(audioSize: Long) {
+        val chapter = durChapter ?: return
         Coroutine.async {
-            durChapter?.let {
-                it.end = audioSize
-                appDb.bookChapterDao.update(it)
-            }
+            durAudioSize = audioSize.toInt()
+            chapter.end = audioSize
+            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)
+
+    }
+
 }

+ 19 - 85
app/src/main/java/io/legado/app/service/AudioPlayService.kt

@@ -29,16 +29,12 @@ import io.legado.app.constant.EventBus
 import io.legado.app.constant.IntentAction
 import io.legado.app.constant.NotificationId
 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.config.AppConfig
 import io.legado.app.help.exoplayer.ExoPlayerHelper
 import io.legado.app.help.glide.ImageLoader
 import io.legado.app.model.AudioPlay
 import io.legado.app.model.analyzeRule.AnalyzeUrl
-import io.legado.app.model.webBook.WebBook
 import io.legado.app.receiver.MediaButtonReceiver
 import io.legado.app.ui.book.audio.AudioPlayActivity
 import io.legado.app.utils.activityPendingIntent
@@ -122,6 +118,7 @@ class AudioPlayService : BaseService(),
         super.onCreate()
         isRun = true
         exoPlayer.addListener(this)
+        AudioPlay.registerService(this)
         initMediaSession()
         initBroadcastReceiver()
         upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
@@ -147,7 +144,8 @@ class AudioPlayService : BaseService(),
                     upPlayProgressJob?.cancel()
                     pause = false
                     position = AudioPlay.book?.durChapterPos ?: 0
-                    loadContent()
+                    url = AudioPlay.durPlayUrl
+                    play()
                 }
 
                 IntentAction.playNew -> {
@@ -155,13 +153,14 @@ class AudioPlayService : BaseService(),
                     upPlayProgressJob?.cancel()
                     pause = false
                     position = 0
-                    loadContent()
+                    url = AudioPlay.durPlayUrl
+                    play()
                 }
 
                 IntentAction.pause -> pause()
                 IntentAction.resume -> resume()
-                IntentAction.prev -> AudioPlay.prev(this)
-                IntentAction.next -> AudioPlay.next(this)
+                IntentAction.prev -> AudioPlay.prev()
+                IntentAction.next -> AudioPlay.next()
                 IntentAction.adjustSpeed -> upSpeed(intent.getFloatExtra("adjust", 1f))
                 IntentAction.addTimer -> addTimer()
                 IntentAction.setTimer -> setTimer(intent.getIntExtra("minute", 0))
@@ -189,6 +188,7 @@ class AudioPlayService : BaseService(),
         upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED)
         AudioPlay.status = Status.STOP
         postEvent(EventBus.AUDIO_STATE, Status.STOP)
+        AudioPlay.unregisterService()
     }
 
     /**
@@ -216,6 +216,7 @@ class AudioPlayService : BaseService(),
             )
             exoPlayer.setMediaItem(analyzeUrl.getMediaItem())
             exoPlayer.playWhenReady = true
+            exoPlayer.seekTo(position.toLong())
             exoPlayer.prepare()
         }.onError {
             AppLog.put("播放出错\n${it.localizedMessage}", it)
@@ -261,7 +262,7 @@ class AudioPlayService : BaseService(),
         try {
             pause = false
             if (url.isEmpty()) {
-                loadContent()
+                AudioPlay.loadOrUpPlayUrl()
                 return
             }
             if (!exoPlayer.isPlaying) {
@@ -317,9 +318,7 @@ class AudioPlayService : BaseService(),
 
             Player.STATE_READY -> {
                 // 准备好
-                if (exoPlayer.currentPosition != position.toLong()) {
-                    exoPlayer.seekTo(position.toLong())
-                }
+                AudioPlay.upLoading(false)
                 if (exoPlayer.playWhenReady) {
                     AudioPlay.status = Status.PLAY
                     postEvent(EventBus.AUDIO_STATE, Status.PLAY)
@@ -336,7 +335,8 @@ class AudioPlayService : BaseService(),
             Player.STATE_ENDED -> {
                 // 结束
                 upPlayProgressJob?.cancel()
-                AudioPlay.next(this)
+                AudioPlay.playPositionChanged(exoPlayer.duration.toInt())
+                AudioPlay.next()
             }
         }
         upAudioPlayNotification()
@@ -395,7 +395,7 @@ class AudioPlayService : BaseService(),
                         timeMinute--
                     }
                     if (timeMinute == 0) {
-                        AudioPlay.stop(this@AudioPlayService)
+                        AudioPlay.stop()
                     }
                 }
                 postEvent(EventBus.AUDIO_DS, timeMinute)
@@ -411,81 +411,16 @@ class AudioPlayService : BaseService(),
         upPlayProgressJob?.cancel()
         upPlayProgressJob = lifecycleScope.launch {
             while (isActive) {
-                AudioPlay.book?.let {
-                    //更新buffer位置
-                    postEvent(EventBus.AUDIO_BUFFER_PROGRESS, exoPlayer.bufferedPosition.toInt())
-                    it.durChapterPos = exoPlayer.currentPosition.toInt()
-                    postEvent(EventBus.AUDIO_PROGRESS, it.durChapterPos)
-                    upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
-                    saveProgress(it)
-                }
+                //更新buffer位置
+                AudioPlay.playPositionChanged(exoPlayer.currentPosition.toInt())
+                postEvent(EventBus.AUDIO_BUFFER_PROGRESS, exoPlayer.bufferedPosition.toInt())
+                postEvent(EventBus.AUDIO_PROGRESS, AudioPlay.durChapterPos)
+                upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
                 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 -> {
                 AppLog.put("音频焦点暂时丢失并会很快再次获得,暂停播放")
-                needResumeOnAudioFocusGain = true
                 if (!pause) {
                     needResumeOnAudioFocusGain = true
                     pause(false)

+ 25 - 19
app/src/main/java/io/legado/app/ui/book/audio/AudioPlayActivity.kt

@@ -24,7 +24,6 @@ import io.legado.app.data.entities.BookSource
 import io.legado.app.databinding.ActivityAudioPlayBinding
 import io.legado.app.help.book.isAudio
 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.lib.dialogs.alert
 import io.legado.app.model.AudioPlay
@@ -59,7 +58,8 @@ import java.util.Locale
 @SuppressLint("ObsoleteSdkInt")
 class AudioPlayActivity :
     VMBaseActivity<ActivityAudioPlayBinding, AudioPlayViewModel>(toolBarTheme = Theme.Dark),
-    ChangeBookSourceDialog.CallBack {
+    ChangeBookSourceDialog.CallBack,
+    AudioPlay.CallBack {
 
     override val binding by viewBinding(ActivityAudioPlayBinding::inflate)
     override val viewModel by viewModels<AudioPlayViewModel>()
@@ -78,7 +78,7 @@ class AudioPlayActivity :
             if (it.first != AudioPlay.book?.durChapterIndex
                 || it.second == 0
             ) {
-                AudioPlay.skipTo(this, it.first)
+                AudioPlay.skipTo(it.first)
             }
         }
     }
@@ -91,10 +91,11 @@ class AudioPlayActivity :
 
     override fun onActivityCreated(savedInstanceState: Bundle?) {
         binding.titleBar.setBackgroundResource(R.color.transparent)
-        AudioPlay.titleData.observe(this) {
+        AudioPlay.register(this)
+        viewModel.titleData.observe(this) {
             binding.titleBar.title = it
         }
-        AudioPlay.coverData.observe(this) {
+        viewModel.coverData.observe(this) {
             upCover(it)
         }
         viewModel.initData(intent)
@@ -143,13 +144,13 @@ class AudioPlayActivity :
             playButton()
         }
         binding.fabPlayStop.onLongClick {
-            AudioPlay.stop(this@AudioPlayActivity)
+            AudioPlay.stop()
         }
         binding.ivSkipNext.setOnClickListener {
-            AudioPlay.next(this@AudioPlayActivity)
+            AudioPlay.next()
         }
         binding.ivSkipPrevious.setOnClickListener {
-            AudioPlay.prev(this@AudioPlayActivity)
+            AudioPlay.prev()
         }
         binding.playerProgress.setOnSeekBarChangeListener(object : SeekBarChangeListener {
             override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
@@ -162,7 +163,7 @@ class AudioPlayActivity :
 
             override fun onStopTrackingTouch(seekBar: SeekBar) {
                 adjustProgress = false
-                AudioPlay.adjustProgress(this@AudioPlayActivity, seekBar.progress)
+                AudioPlay.adjustProgress(seekBar.progress)
             }
         })
         binding.ivChapter.setOnClickListener {
@@ -175,10 +176,10 @@ class AudioPlayActivity :
             binding.ivFastForward.invisible()
         }
         binding.ivFastForward.setOnClickListener {
-            AudioPlay.adjustSpeed(this@AudioPlayActivity, 0.1f)
+            AudioPlay.adjustSpeed(0.1f)
         }
         binding.ivFastRewind.setOnClickListener {
-            AudioPlay.adjustSpeed(this@AudioPlayActivity, -0.1f)
+            AudioPlay.adjustSpeed(-0.1f)
         }
         binding.ivTimer.setOnClickListener {
             timerSliderPopup.showAsDropDown(it, 0, (-100).dpToPx(), Gravity.TOP)
@@ -196,7 +197,7 @@ class AudioPlayActivity :
         when (AudioPlay.status) {
             Status.PLAY -> AudioPlay.pause(this)
             Status.PAUSE -> AudioPlay.resume(this)
-            else -> AudioPlay.play(this)
+            else -> AudioPlay.loadOrUpPlayUrl()
         }
     }
 
@@ -207,7 +208,7 @@ class AudioPlayActivity :
         if (book.isAudio) {
             viewModel.changeTo(source, book, toc)
         } else {
-            AudioPlay.stop(this)
+            AudioPlay.stop()
             lifecycleScope.launch {
                 withContext(IO) {
                     AudioPlay.book?.migrateTo(book, toc)
@@ -247,8 +248,9 @@ class AudioPlayActivity :
     override fun onDestroy() {
         super.onDestroy()
         if (AudioPlay.status != Status.PLAY) {
-            AudioPlay.stop(this)
+            AudioPlay.stop()
         }
+        AudioPlay.unregister(this)
     }
 
     @SuppressLint("SetTextI18n")
@@ -268,11 +270,9 @@ class AudioPlayActivity :
         }
         observeEventSticky<String>(EventBus.AUDIO_SUB_TITLE) {
             binding.tvSubTitle.text = it
-            AudioPlay.book?.let { book ->
-                binding.ivSkipPrevious.isEnabled = book.durChapterIndex > 0
-                binding.ivSkipNext.isEnabled =
-                    book.durChapterIndex < book.simulatedTotalChapterNum() - 1
-            }
+            binding.ivSkipPrevious.isEnabled = AudioPlay.durChapterIndex > 0
+            binding.ivSkipNext.isEnabled =
+                AudioPlay.durChapterIndex < AudioPlay.simulatedChapterSize - 1
         }
         observeEventSticky<Int>(EventBus.AUDIO_SIZE) {
             binding.playerProgress.max = it
@@ -296,4 +296,10 @@ class AudioPlayActivity :
         }
     }
 
+    override fun upLoading(loading: Boolean) {
+        runOnUiThread {
+            binding.progressLoading.visible(loading)
+        }
+    }
+
 }

+ 41 - 23
app/src/main/java/io/legado/app/ui/book/audio/AudioPlayViewModel.kt

@@ -2,9 +2,10 @@ package io.legado.app.ui.book.audio
 
 import android.app.Application
 import android.content.Intent
-import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.MutableLiveData
 import io.legado.app.R
 import io.legado.app.base.BaseViewModel
+import io.legado.app.constant.AppLog
 import io.legado.app.constant.BookType
 import io.legado.app.constant.EventBus
 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.help.book.getBookSource
 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.durChapter
 import io.legado.app.model.webBook.WebBook
 import io.legado.app.utils.postEvent
 import io.legado.app.utils.toastOnUi
-import kotlinx.coroutines.Dispatchers.IO
 
 class AudioPlayViewModel(application: Application) : BaseViewModel(application) {
+    val titleData = MutableLiveData<String>()
+    val coverData = MutableLiveData<String>()
 
     fun initData(intent: Intent) = AudioPlay.apply {
         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
         if (isSameBook) {
-            AudioPlay.upData(context, book)
+            AudioPlay.upData(book)
         } else {
-            AudioPlay.resetData(context, book)
+            AudioPlay.resetData(book)
         }
-        if (durChapter == null) {
-            if (book.tocUrl.isEmpty()) {
-                loadBookInfo(book)
-            } else {
-                loadChapterList(book)
-            }
+        titleData.postValue(book.name)
+        coverData.postValue(book.getDisplayCover())
+        if (book.tocUrl.isEmpty() && !loadBookInfo(book)) {
+            return
+        }
+        if (AudioPlay.chapterSize == 0 && !loadChapterList(book)) {
+            return
         }
     }
 
-    private fun loadBookInfo(book: Book) {
-        val bookSource = AudioPlay.bookSource ?: return
-        WebBook.getBookInfo(viewModelScope, bookSource, book).onSuccess(IO) {
-            loadChapterList(book)
+    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 fun loadChapterList(book: Book) {
-        val bookSource = AudioPlay.bookSource ?: return
-        WebBook.getChapterList(viewModelScope, bookSource, book).onSuccess(IO) { cList ->
-            book.save()
+    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 {
+                appDb.bookDao.insert(book)
+            }
+            appDb.bookChapterDao.delByBook(book.bookUrl)
             appDb.bookChapterDao.insert(*cList.toTypedArray())
-            AudioPlay.upDurChapter(book)
-        }.onError {
+            AudioPlay.chapterSize = cList.size
+            AudioPlay.simulatedChapterSize = book.simulatedTotalChapterNum()
+            AudioPlay.upDurChapter()
+            return true
+        } catch (e: Exception) {
             context.toastOnUi(R.string.error_load_toc)
+            return false
         }
     }
 
@@ -83,7 +101,7 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
             AudioPlay.book = book
             AudioPlay.bookSource = source
             appDb.bookChapterDao.insert(*toc.toTypedArray())
-            AudioPlay.upDurChapter(book)
+            AudioPlay.upDurChapter()
         }.onFinally {
             postEvent(EventBus.SOURCE_CHANGED, book.bookUrl)
         }

+ 40 - 18
app/src/main/res/layout/activity_audio_play.xml

@@ -130,9 +130,9 @@
             android:id="@+id/iv_timer"
             android:layout_width="46dp"
             android:layout_height="46dp"
-            android:padding="5dp"
             android:background="@drawable/selector_circle_btn_bg"
             android:contentDescription="@string/set_timer"
+            android:padding="5dp"
             android:src="@drawable/ic_timer_black_24dp"
             app:tint="@color/md_white_1000"
             tools:ignore="ImageContrastCheck" />
@@ -146,9 +146,9 @@
             android:id="@+id/iv_fast_rewind"
             android:layout_width="46dp"
             android:layout_height="46dp"
-            android:padding="5dp"
             android:background="@drawable/selector_circle_btn_bg"
             android:contentDescription="@string/skip_previous"
+            android:padding="5dp"
             android:src="@drawable/ic_fast_rewind"
             app:tint="@color/md_white_1000"
             tools:ignore="ImageContrastCheck" />
@@ -162,34 +162,56 @@
             android:id="@+id/iv_skip_previous"
             android:layout_width="46dp"
             android:layout_height="46dp"
-            android:padding="5dp"
             android:background="@drawable/selector_circle_btn_bg"
             android:contentDescription="@string/skip_previous"
+            android:padding="5dp"
             android:src="@drawable/ic_skip_previous"
             app:tint="@color/md_white_1000"
             tools:ignore="ImageContrastCheck" />
 
-        <com.google.android.material.floatingactionbutton.FloatingActionButton
-            android:id="@+id/fab_play_stop"
+        <androidx.constraintlayout.widget.ConstraintLayout
             android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_margin="10dp"
-            android:contentDescription="@string/audio_play"
-            android:src="@drawable/ic_play_24dp"
-            android:tint="@color/md_black_1000"
-            app:backgroundTint="@color/md_white_1000"
-            app:elevation="2dp"
-            app:fabSize="normal"
-            app:pressedTranslationZ="2dp"
-            tools:ignore="ImageContrastCheck" />
+            android:layout_height="wrap_content">
+
+            <com.google.android.material.floatingactionbutton.FloatingActionButton
+                android:id="@+id/fab_play_stop"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_margin="10dp"
+                android:contentDescription="@string/audio_play"
+                android:src="@drawable/ic_play_24dp"
+                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
             android:id="@+id/iv_skip_next"
             android:layout_width="46dp"
             android:layout_height="46dp"
-            android:padding="5dp"
             android:background="@drawable/selector_circle_btn_bg"
             android:contentDescription="@string/skip_next"
+            android:padding="5dp"
             android:src="@drawable/ic_skip_next"
             app:tint="@color/md_white_1000"
             tools:ignore="ImageContrastCheck" />
@@ -203,9 +225,9 @@
             android:id="@+id/iv_fast_forward"
             android:layout_width="46dp"
             android:layout_height="46dp"
-            android:padding="5dp"
             android:background="@drawable/selector_circle_btn_bg"
             android:contentDescription="@string/skip_next"
+            android:padding="5dp"
             android:src="@drawable/ic_fast_forward"
             app:tint="@color/md_white_1000"
             tools:ignore="ImageContrastCheck" />
@@ -219,9 +241,9 @@
             android:id="@+id/iv_chapter"
             android:layout_width="46dp"
             android:layout_height="46dp"
-            android:padding="5dp"
             android:background="@drawable/selector_circle_btn_bg"
             android:contentDescription="@string/chapter_list"
+            android:padding="5dp"
             android:src="@drawable/ic_chapter_list"
             app:tint="@color/md_white_1000"
             tools:ignore="ImageContrastCheck" />

+ 1 - 1
app/src/main/res/layout/dialog_wait.xml

@@ -7,7 +7,7 @@
     android:padding="20dp"
     android:orientation="horizontal">
 
-    <ProgressBar
+    <io.legado.app.lib.theme.view.ThemeProgressBar
         android:id="@+id/pb"
         android:layout_width="30dp"
         android:layout_height="30dp"