diff --git a/app/src/main/java/io/legado/app/base/BaseService.kt b/app/src/main/java/io/legado/app/base/BaseService.kt index 2922c9795..9db011cd7 100644 --- a/app/src/main/java/io/legado/app/base/BaseService.kt +++ b/app/src/main/java/io/legado/app/base/BaseService.kt @@ -18,8 +18,9 @@ abstract class BaseService : LifecycleService(), CoroutineScope by MainScope() { scope: CoroutineScope = this, context: CoroutineContext = Dispatchers.IO, start: CoroutineStart = CoroutineStart.DEFAULT, + executeContext: CoroutineContext = Dispatchers.Main, block: suspend CoroutineScope.() -> T - ) = Coroutine.async(scope, context, start) { block() } + ) = Coroutine.async(scope, context, start, executeContext, block) @CallSuper override fun onCreate() { @@ -57,7 +58,7 @@ abstract class BaseService : LifecycleService(), CoroutineScope by MainScope() { /** * 检测通知权限 */ - private fun checkNotificationPermission() { + private fun checkNotificationPermission() { PermissionsCompat.Builder() .addPermissions(Permissions.POST_NOTIFICATIONS) .rationale(R.string.notification_permission_rationale) diff --git a/app/src/main/java/io/legado/app/base/BaseViewModel.kt b/app/src/main/java/io/legado/app/base/BaseViewModel.kt index 2422a8a15..f62b8c954 100644 --- a/app/src/main/java/io/legado/app/base/BaseViewModel.kt +++ b/app/src/main/java/io/legado/app/base/BaseViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope import io.legado.app.App import io.legado.app.help.coroutine.Coroutine import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlin.coroutines.CoroutineContext @@ -19,9 +20,11 @@ open class BaseViewModel(application: Application) : AndroidViewModel(applicatio fun execute( scope: CoroutineScope = viewModelScope, context: CoroutineContext = Dispatchers.IO, + start: CoroutineStart = CoroutineStart.DEFAULT, + executeContext: CoroutineContext = Dispatchers.Main, block: suspend CoroutineScope.() -> T ): Coroutine { - return Coroutine.async(scope, context) { block() } + return Coroutine.async(scope, context, start, executeContext, block) } fun submit( diff --git a/app/src/main/java/io/legado/app/data/entities/BookSource.kt b/app/src/main/java/io/legado/app/data/entities/BookSource.kt index c7f467335..d5152a3ed 100644 --- a/app/src/main/java/io/legado/app/data/entities/BookSource.kt +++ b/app/src/main/java/io/legado/app/data/entities/BookSource.kt @@ -183,7 +183,7 @@ data class BookSource( fun getInvalidGroupNames(): String { return bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.filter { - "失效" in it + "失效" in it || it == "校验超时" }?.joinToString() ?: "" } diff --git a/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt b/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt index 31e220577..d96322276 100644 --- a/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt +++ b/app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt @@ -25,6 +25,7 @@ class Coroutine( val scope: CoroutineScope, context: CoroutineContext = Dispatchers.IO, val startOption: CoroutineStart = CoroutineStart.DEFAULT, + val executeContext: CoroutineContext = Dispatchers.Main, block: suspend CoroutineScope.() -> T ) { @@ -36,9 +37,10 @@ class Coroutine( scope: CoroutineScope = DEFAULT, context: CoroutineContext = Dispatchers.IO, start: CoroutineStart = CoroutineStart.DEFAULT, + executeContext: CoroutineContext = Dispatchers.Main, block: suspend CoroutineScope.() -> T ): Coroutine { - return Coroutine(scope, context, start, block) + return Coroutine(scope, context, start, executeContext, block) } } @@ -158,7 +160,7 @@ class Coroutine( context: CoroutineContext, block: suspend CoroutineScope.() -> T ): Job { - return (scope.plus(Dispatchers.Main)).launch(start = startOption) { + return (scope.plus(executeContext)).launch(start = startOption) { try { start?.let { dispatchVoidCallback(this, it) } ensureActive() diff --git a/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt b/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt index 90368f44e..dd90b2c16 100644 --- a/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt +++ b/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt @@ -57,9 +57,8 @@ object LocalBook { }.firstOrNull()?.let { getBookInputStream(it) } - } else if (webDavUrl != null) { + } else if (webDavUrl != null && downloadRemoteBook(book)) { // 下载远程链接 - downloadRemoteBook(book) getBookInputStream(book) } else { null @@ -90,12 +89,15 @@ object LocalBook { book.isEpub -> { EpubFile.getChapterList(book) } + book.isUmd -> { UmdFile.getChapterList(book) } + book.isPdf -> { PdfFile.getChapterList(book) } + else -> { TextFile.getChapterList(book) } @@ -117,12 +119,15 @@ object LocalBook { book.isEpub -> { EpubFile.getContent(book, chapter) } + book.isUmd -> { UmdFile.getContent(book, chapter) } + book.isPdf -> { PdfFile.getContent(book, chapter) } + else -> { TextFile.getContent(book, chapter) } @@ -218,7 +223,7 @@ object LocalBook { } } - /* 批量导入 支持自动导入压缩包的支持书籍 */ + /* 批量导入 支持自动导入压缩包的支持书籍 */ fun importFiles(uri: Uri): List { val books = mutableListOf() val fileDoc = FileDoc.fromUri(uri, false) @@ -329,6 +334,7 @@ object LocalBook { Base64.DEFAULT ) ) + else -> throw NoStackTraceException("在线导入书籍支持http/https/DataURL") } return saveBookFile(inputStream, fileName) @@ -383,7 +389,7 @@ object LocalBook { } //下载book对应的远程文件 并更新Book - private fun downloadRemoteBook(localBook: Book) { + private fun downloadRemoteBook(localBook: Book): Boolean { val webDavUrl = localBook.getRemoteUrl() if (webDavUrl.isNullOrBlank()) throw NoStackTraceException("Book file is not webDav File") try { @@ -415,9 +421,11 @@ object LocalBook { localBook.save() } } + return true } catch (e: Exception) { e.printOnDebug() AppLog.put("自动下载webDav书籍失败", e) + return false } } diff --git a/app/src/main/java/io/legado/app/service/CheckSourceService.kt b/app/src/main/java/io/legado/app/service/CheckSourceService.kt index a2afe1683..143496350 100644 --- a/app/src/main/java/io/legado/app/service/CheckSourceService.kt +++ b/app/src/main/java/io/legado/app/service/CheckSourceService.kt @@ -122,7 +122,11 @@ class CheckSourceService : BaseService() { *校验书源 */ private fun check(source: BookSource) { - execute(context = searchCoroutine, start = CoroutineStart.LAZY) { + execute( + context = searchCoroutine, + start = CoroutineStart.LAZY, + executeContext = IO + ) { Debug.startChecking(source) var searchWord = CheckSource.keyword source.ruleSearch?.checkKeyWord?.let { @@ -188,7 +192,6 @@ class CheckSourceService : BaseService() { "" else "\n\n${source.bookSourceComment}" Debug.updateFinalMessage(source.bookSourceUrl, "校验失败:${it.localizedMessage}") }.onSuccess(searchCoroutine) { - source.removeGroup("校验超时") Debug.updateFinalMessage(source.bookSourceUrl, "校验成功") }.onFinally(IO) { source.respondTime = Debug.getRespondTime(source.bookSourceUrl) diff --git a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt index 6f2fde001..b9b0d1cf8 100644 --- a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt @@ -26,7 +26,7 @@ import io.legado.app.utils.printOnDebug import io.legado.app.utils.servicePendingIntent import io.legado.app.utils.toastOnUi import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive @@ -68,32 +68,21 @@ class HttpReadAloudService : BaseReadAloudService(), super.onDestroy() downloadTask?.cancel() exoPlayer.release() + Coroutine.async { + removeCacheFile() + } } override fun play() { pageChanged = false exoPlayer.stop() + if (!requestFocus()) return if (contentList.isEmpty()) { AppLog.putDebug("朗读列表为空") ReadBook.readAloud() } else { super.play() - kotlin.runCatching { - if (nowSpeak == 0) { - downloadAudio() - } else { - val fileName = md5SpeakFileName(contentList[nowSpeak]) - val file = getSpeakFileAsMd5(fileName) - if (file.exists()) { - playAudio(file) - } else if (!downloadTaskActiveLock.isLocked) { - downloadAudio() - } - } - }.onFailure { - toastOnUi("朗读出错:${it.localizedMessage}") - AppLog.put("朗读出错:${it.localizedMessage}", it) - } + downloadAndPlayAudios() } } @@ -105,21 +94,21 @@ class HttpReadAloudService : BaseReadAloudService(), readAloudNumber += contentList[nowSpeak].length + 1 if (nowSpeak < contentList.lastIndex) { nowSpeak++ - play() } else { nextChapter() } } - private fun downloadAudio() { + private fun downloadAndPlayAudios() { + exoPlayer.clearMediaItems() downloadTask?.cancel() downloadTask = execute { downloadTaskActiveLock.withLock { ensureActive() - removeCacheFile() val httpTts = ReadAloud.httpTTS ?: throw NoStackTraceException("tts is null") contentList.forEachIndexed { index, content -> ensureActive() + if (index < nowSpeak) return@forEachIndexed val fileName = md5SpeakFileName(content) val speakText = content.replace(AppPattern.notReadAloudRegex, "") if (speakText.isEmpty()) { @@ -141,13 +130,23 @@ class HttpReadAloudService : BaseReadAloudService(), return@execute } } - if (index == nowSpeak) { - val file = getSpeakFileAsMd5(fileName) - playAudio(file) + val file = getSpeakFileAsMd5(fileName) + val mediaItem = MediaItem.fromUri(Uri.fromFile(file)) + launch(Main) { + if (exoPlayer.playbackState == Player.STATE_ENDED) { + exoPlayer.stop() + exoPlayer.clearMediaItems() + } + exoPlayer.addMediaItem(mediaItem) + if (!exoPlayer.isPlaying) { + exoPlayer.playWhenReady = !pause + exoPlayer.prepare() + } } } } - }.onError(IO) { + }.onError { + toastOnUi("朗读出错:${it.localizedMessage}") AppLog.put("朗读下载出错\n${it.localizedMessage}", it) } } @@ -193,6 +192,7 @@ class HttpReadAloudService : BaseReadAloudService(), e.printOnDebug() throw e } + is SocketTimeoutException, is ConnectException -> { downloadErrorNo++ if (downloadErrorNo > 5) { @@ -202,6 +202,7 @@ class HttpReadAloudService : BaseReadAloudService(), throw e } } + else -> { downloadErrorNo++ val msg = "tts下载错误\n${e.localizedMessage}" @@ -223,22 +224,6 @@ class HttpReadAloudService : BaseReadAloudService(), return null } - @Synchronized - private fun playAudio(file: File) { - if (requestFocus()) { - launch { - kotlin.runCatching { - val mediaItem = MediaItem.fromUri(Uri.fromFile(file)) - exoPlayer.setMediaItem(mediaItem) - exoPlayer.playWhenReady = true - exoPlayer.prepare() - }.onFailure { - it.printOnDebug() - } - } - } - } - private fun md5SpeakFileName(content: String): String { return MD5Utils.md5Encode16(textChapter?.title ?: "") + "_" + MD5Utils.md5Encode16("${ReadAloud.httpTTS?.url}-|-$speechRate-|-$content") @@ -338,7 +323,7 @@ class HttpReadAloudService : BaseReadAloudService(), downloadTask?.cancel() exoPlayer.stop() speechRate = AppConfig.speechRatePlay + 5 - downloadAudio() + downloadAndPlayAudios() } override fun onPlaybackStateChanged(playbackState: Int) { @@ -347,15 +332,18 @@ class HttpReadAloudService : BaseReadAloudService(), Player.STATE_IDLE -> { // 空闲 } + Player.STATE_BUFFERING -> { // 缓冲中 } + Player.STATE_READY -> { // 准备好 if (pause) return exoPlayer.play() upPlayPos() } + Player.STATE_ENDED -> { // 结束 playErrorNo = 0 @@ -364,6 +352,12 @@ class HttpReadAloudService : BaseReadAloudService(), } } + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) return + upPlayPos() + playNext() + } + override fun onPlayerError(error: PlaybackException) { super.onPlayerError(error) AppLog.put("朗读错误\n${contentList[nowSpeak]}", error) diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt index 9a4a1ba47..46c1a8740 100644 --- a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt @@ -44,6 +44,7 @@ import io.legado.app.utils.viewbindingdelegate.viewBinding import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.delay import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch /** @@ -204,7 +205,7 @@ class ChangeBookSourceDialog() : BaseDialogFragment(R.layout.dialog_book_change_ } } launch { - appDb.bookSourceDao.flowEnabledGroups().conflate().collect { + appDb.bookSourceDao.flowEnabledGroups().flowOn(IO).conflate().collect { groups.clear() groups.addAll(it) upGroupMenu() diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt index 93c504856..755110bf1 100644 --- a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt @@ -47,8 +47,10 @@ import io.legado.app.utils.startActivity import io.legado.app.utils.toastOnUi import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.visible +import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.delay import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch @@ -230,7 +232,7 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c } } launch { - appDb.bookSourceDao.flowEnabledGroups().conflate().collect { + appDb.bookSourceDao.flowEnabledGroups().flowOn(IO).conflate().collect { groups.clear() groups.addAll(it) upGroupMenu() diff --git a/app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt index 8f9542b9d..37636fbb0 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt @@ -108,7 +108,8 @@ abstract class BaseReadBookActivity : */ fun upSystemUiVisibility( isInMultiWindow: Boolean, - toolBarHide: Boolean = true + toolBarHide: Boolean = true, + useBgMeanColor: Boolean = false ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.insetsController?.run { @@ -129,7 +130,10 @@ abstract class BaseReadBookActivity : setLightStatusBar(ReadBookConfig.durConfig.curStatusIconDark()) } else { val statusBarColor = - if (AppConfig.readBarStyleFollowPage && ReadBookConfig.durConfig.curBgType() == 0) { + if (AppConfig.readBarStyleFollowPage + && ReadBookConfig.durConfig.curBgType() == 0 + || useBgMeanColor + ) { ReadBookConfig.bgMeanColor } else { ThemeStore.statusBarColor(this, AppConfig.isTransparentStatusBar) @@ -184,12 +188,14 @@ abstract class BaseReadBookActivity : width = MATCH_PARENT gravity = Gravity.BOTTOM } + Gravity.LEFT -> layoutParams = (layoutParams as FrameLayout.LayoutParams).apply { height = MATCH_PARENT width = navigationBarHeight gravity = Gravity.LEFT } + Gravity.RIGHT -> layoutParams = (layoutParams as FrameLayout.LayoutParams).apply { height = MATCH_PARENT diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt index 0afc84ad8..04d2d4bab 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt @@ -1044,7 +1044,7 @@ class ReadBookActivity : BaseReadBookActivity(), * 更新状态栏,导航栏 */ override fun upSystemUiVisibility() { - upSystemUiVisibility(isInMultiWindow, !menuLayoutIsVisible) + upSystemUiVisibility(isInMultiWindow, !menuLayoutIsVisible, bottomDialog > 0) upNavigationBarColor() } diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineDialog.kt index 6f22624a7..fc5de6bc4 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineDialog.kt @@ -93,7 +93,7 @@ class SpeakEngineDialog(val callBack: CallBack) : BaseDialogFragment(R.layout.di labelSys.visible() cbName.text = "系统默认" cbName.tag = "" - cbName.isChecked = ttsEngine == null || ttsEngine!!.toIntOrNull() == null + cbName.isChecked = ttsEngine == null || ttsEngine!!.isJsonObject() && GSON.fromJsonObject>(ttsEngine) .getOrNull()?.value.isNullOrEmpty() cbName.setOnClickListener { diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt index af9fca043..b0381ff05 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt @@ -31,6 +31,7 @@ import io.legado.app.utils.viewbindingdelegate.viewBinding import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Job import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import splitties.init.appCtx @@ -279,7 +280,7 @@ class SearchActivity : VMBaseActivity + }.flowOn(IO).conflate().collect { data -> adapter.setItems(data, adapter.diffItemCallback) itemTouchCallback.isCanDrag = sort == BookSourceSort.Default delay(500) @@ -336,10 +338,11 @@ class BookSourceActivity : VMBaseActivity { updateToc() } + else -> { delay(500) } @@ -119,7 +122,7 @@ class MainViewModel(application: Application) : BaseViewModel(application) { } waitUpTocBooks.remove(bookUrl) upTocAdd(bookUrl) - execute(context = upTocPool) { + execute(context = upTocPool, executeContext = IO) { kotlin.runCatching { val oldBook = book.copy() WebBook.runPreUpdateJs(source, book) diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt index 7622a2d40..16afdc259 100644 --- a/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt @@ -29,9 +29,11 @@ import io.legado.app.utils.setEdgeEffectColor import io.legado.app.utils.startActivity import io.legado.app.utils.viewbindingdelegate.viewBinding import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Job import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch /** @@ -107,7 +109,7 @@ class ExploreFragment : VMBaseFragment(R.layout.fragment_explo private fun initGroupData() { launch { - appDb.bookSourceDao.flowExploreGroups().conflate().collect { + appDb.bookSourceDao.flowExploreGroups().flowOn(IO).conflate().collect { groups.clear() groups.addAll(it) upGroupsMenu() diff --git a/app/src/main/res/layout/item_source_import.xml b/app/src/main/res/layout/item_source_import.xml index 32e04692c..6fb8809fd 100644 --- a/app/src/main/res/layout/item_source_import.xml +++ b/app/src/main/res/layout/item_source_import.xml @@ -11,6 +11,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:maxLines="1" + android:singleLine="true" app:layout_constrainedWidth="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0" @@ -25,12 +26,13 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="8dp" + android:singleLine="true" android:textColor="@color/secondaryText" app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintLeft_toRightOf="@id/cb_source_name" app:layout_constraintRight_toLeftOf="@id/tv_open" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintHorizontal_chainStyle="packed" /> + app:layout_constraintTop_toTopOf="parent" />