diff --git a/app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt b/app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt index 3ace54565..1aca74938 100644 --- a/app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt +++ b/app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt @@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding import io.legado.app.help.coroutine.Coroutine import io.legado.app.utils.buildMainHandler +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.withTimeoutOrNull import splitties.views.onLongClick import java.util.Collections @@ -156,6 +157,7 @@ abstract class RecyclerAdapter(protected val context: Co } else { DiffUtil.calculateDiff(callback) } + ensureActive() handler.post { if (diffResult == null) { setItems(items) diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt index debd07279..b2dc8667c 100644 --- a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt @@ -4,6 +4,7 @@ import android.app.Application import android.os.Bundle import androidx.annotation.CallSuper import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope import io.legado.app.base.BaseViewModel import io.legado.app.constant.AppConst import io.legado.app.constant.AppLog @@ -12,27 +13,34 @@ import io.legado.app.data.appDb import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.BookSourcePart import io.legado.app.data.entities.SearchBook import io.legado.app.exception.NoStackTraceException import io.legado.app.help.book.BookHelp import io.legado.app.help.book.ContentProcessor import io.legado.app.help.config.AppConfig import io.legado.app.help.config.SourceConfig -import io.legado.app.help.coroutine.CompositeCoroutine import io.legado.app.help.coroutine.Coroutine import io.legado.app.model.webBook.WebBook +import io.legado.app.utils.mapParallelSafe import io.legado.app.utils.toastOnUi import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.ExecutorCoroutineDispatcher +import kotlinx.coroutines.Job import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout import java.util.Collections import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors @@ -48,9 +56,8 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a var author: String = "" private var fromReadBookActivity = false private var oldBook: Book? = null - private var tasks = CompositeCoroutine() private var screenKey: String = "" - private var bookSourceList = arrayListOf() + private var bookSourceParts = arrayListOf() private var searchBookList = arrayListOf() private val searchBooks = Collections.synchronizedList(arrayListOf()) private val tocMap = ConcurrentHashMap>() @@ -58,7 +65,6 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a ContentProcessor.get(oldBook!!) } private var searchCallback: SourceCallback? = null - private val emptyBookSource = BookSource() private val chapterNumRegex = "^\\[(\\d+)]".toRegex() private val comparatorBase by lazy { compareByDescending { getBookScore(it) } @@ -73,6 +79,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a .thenByDescending { it.chapterWordCount } .thenBy { it.originOrder } } + private var task: Job? = null val bookMap = ConcurrentHashMap() val searchDataFlow = callbackFlow { @@ -120,9 +127,6 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a }.getOrDefault(searchBooks) }.flowOn(IO) - @Volatile - private var searchIndex = -1 - override fun onCleared() { super.onCleared() searchPool?.close() @@ -145,7 +149,6 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a private fun initSearchPool() { searchPool = Executors .newFixedThreadPool(min(threadCount, AppConst.MAX_THREAD)).asCoroutineDispatcher() - searchIndex = -1 } fun refresh(): Boolean { @@ -160,85 +163,83 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a /** * 搜索书籍 */ - fun startSearch(origin: String? = null) { + fun startSearch() { execute { stopSearch() - origin?.let { - bookSourceList.clear() - bookSourceList.add(appDb.bookSourceDao.getBookSource(origin)!!) - searchStateData.postValue(true) - searchBooks.removeIf { it.origin == origin } - initSearchPool() - search() - return@execute - } appDb.searchBookDao.clear(name, author) searchBooks.clear() searchCallback?.upAdapter() - bookSourceList.clear() + bookSourceParts.clear() val searchGroup = AppConfig.searchGroup if (searchGroup.isBlank()) { - bookSourceList.addAll(appDb.bookSourceDao.allEnabled) + bookSourceParts.addAll(appDb.bookSourceDao.allEnabledPart) } else { - val sources = appDb.bookSourceDao.getEnabledByGroup(searchGroup) + val sources = appDb.bookSourceDao.getEnabledPartByGroup(searchGroup) if (sources.isEmpty()) { AppConfig.searchGroup = "" - bookSourceList.addAll(appDb.bookSourceDao.allEnabled) + bookSourceParts.addAll(appDb.bookSourceDao.allEnabledPart) } else { - bookSourceList.addAll(sources) + bookSourceParts.addAll(sources) } } - searchStateData.postValue(true) initSearchPool() - for (i in 0 until threadCount) { - search() - } + search() + } + } + + fun startSearch(origin: String) { + execute { + stopSearch() + bookSourceParts.clear() + bookSourceParts.add(appDb.bookSourceDao.getBookSourcePart(origin)!!) + searchBooks.removeIf { it.origin == origin } + initSearchPool() + search() } } private fun search() { - val searchIndex = synchronized(this) { - if (searchIndex >= bookSourceList.lastIndex) { - return - } - ++searchIndex + task = viewModelScope.launch(searchPool!!) { + flow { + for (bs in bookSourceParts) { + bs.getBookSource()?.let { + emit(it) + } + } + }.onStart { + searchStateData.postValue(true) + }.mapParallelSafe(threadCount) { + withTimeout(60000L) { + search(it) + } + }.onCompletion { + searchStateData.postValue(false) + searchFinishCallback?.invoke(searchBooks.isEmpty()) + }.catch { + AppLog.put("换源搜索出错\n${it.localizedMessage}", it) + }.collect() } - val source = bookSourceList.getOrNull(searchIndex) ?: return - bookSourceList[searchIndex] = emptyBookSource - val task = execute( - context = searchPool!!, - start = CoroutineStart.LAZY, - executeContext = searchPool!! - ) { - val resultBooks = WebBook.searchBookAwait(source, name) - resultBooks.forEach { searchBook -> - if (searchBook.name != name) { - return@forEach - } - if (AppConfig.changeSourceCheckAuthor && !searchBook.author.contains(author)) { - return@forEach - } - when { - AppConfig.changeSourceLoadInfo || AppConfig.changeSourceLoadToc || AppConfig.changeSourceLoadWordCount -> { - loadBookInfo(source, searchBook.toBook()) - } + } - else -> { - searchCallback?.searchSuccess(searchBook) - } + private suspend fun search(source: BookSource) { + val resultBooks = WebBook.searchBookAwait(source, name) + resultBooks.forEach { searchBook -> + if (searchBook.name != name) { + return@forEach + } + if (AppConfig.changeSourceCheckAuthor && !searchBook.author.contains(author)) { + return@forEach + } + when { + AppConfig.changeSourceLoadInfo || AppConfig.changeSourceLoadToc || AppConfig.changeSourceLoadWordCount -> { + loadBookInfo(source, searchBook.toBook()) + } + + else -> { + searchCallback?.searchSuccess(searchBook) } } - }.timeout(60000L) - .onError { - ensureActive() - nextSearch() - } - .onSuccess { - ensureActive() - nextSearch() - } - task.start() - tasks.add(task) + } } private suspend fun loadBookInfo(source: BookSource, book: Book) { @@ -298,24 +299,6 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a searchCallback?.searchSuccess(searchBook) } - @Synchronized - private fun nextSearch() { - kotlin.runCatching { - if (searchIndex < bookSourceList.lastIndex) { - search() - } else { - searchIndex++ - } - if (searchIndex >= bookSourceList.lastIndex + bookSourceList.size - || searchIndex >= bookSourceList.lastIndex + threadCount - ) { - searchStateData.postValue(false) - tasks.clear() - searchFinishCallback?.invoke(searchBooks.isEmpty()) - } - } - } - fun onLoadWordCountChecked(isChecked: Boolean) { if (isChecked) { startRefreshList(true) @@ -330,54 +313,38 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a stopSearch() searchBookList.clear() if (onlyRefreshNoWordCountBook) { - val noWordCountBook = searchBooks.filter { it.chapterWordCountText == null } - searchBookList.addAll(noWordCountBook) + searchBooks.filterTo(searchBookList) { + it.chapterWordCountText == null + } searchBooks.removeIf { it.chapterWordCountText == null } } else { searchBookList.addAll(searchBooks) searchBooks.clear() } searchCallback?.upAdapter() - searchStateData.postValue(true) initSearchPool() - for (i in 0 until threadCount) { - refreshList() - } + refreshList() } } private fun refreshList() { - synchronized(this) { - if (searchIndex >= searchBookList.lastIndex) { - return - } - searchIndex++ - } - val searchBook = searchBookList[searchIndex] - val task = execute(context = searchPool!!, executeContext = searchPool!!) { - val source = appDb.bookSourceDao.getBookSource(searchBook.origin) ?: return@execute - loadBookInfo(source, searchBook.toBook()) - }.timeout(60000L) - .onError { - nextRefreshList() - } - .onSuccess { - nextRefreshList() - } - tasks.add(task) - } - - private fun nextRefreshList() { - synchronized(this) { - if (searchIndex < searchBookList.lastIndex) { - refreshList() - } else { - searchIndex++ - } - if (searchIndex >= searchBookList.lastIndex + min(searchBookList.size, threadCount)) { + task = viewModelScope.launch(searchPool!!) { + flow { + for (searchBook in searchBookList) { + emit(searchBook) + } + }.onStart { + searchStateData.postValue(true) + }.mapParallelSafe(threadCount) { + val source = appDb.bookSourceDao.getBookSource(it.origin)!! + withTimeout(60000L) { + loadBookInfo(source, it.toBook()) + } + }.onCompletion { searchStateData.postValue(false) - tasks.clear() - } + }.catch { + AppLog.put("换源刷新列表出错\n${it.localizedMessage}", it) + }.collect() } } @@ -420,7 +387,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a } fun startOrStopSearch() { - if (tasks.isEmpty) { + if (task == null || !task!!.isActive) { startSearch() } else { stopSearch() @@ -428,7 +395,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a } fun stopSearch() { - tasks.clear() + task?.cancel() searchPool?.close() searchStateData.postValue(false) }