fix: resolve emulator crash - thread safety and icon fallback

- NativeSearch: move onResult/onError callbacks outside withContext(Dispatchers.IO) to prevent CalledFromWrongThreadException
- SearchStrategy: change callback types to suspend to enable proper coroutine chaining
- SearchCoordinator: remove leaked CoroutineScope, rely on suspend callback chaining for fallback flow
- Resources: add mipmap-hdpi/mdpi/xhdpi/xxhdpi icon fallbacks for API < 26 devices
This commit is contained in:
xiaji
2026-05-24 21:09:05 +08:00
commit 98d05aa90a
57 changed files with 2366 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
package com.videoapp.tv.engine
import com.videoapp.tv.data.SearchResult
import com.videoapp.tv.data.SiteConfig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jsoup.Connection
import org.jsoup.Jsoup
class NativeSearch : SearchStrategy {
override fun getName() = "NativeSearch"
override suspend fun search(
keyword: String,
config: SiteConfig,
onResult: suspend (List<SearchResult>) -> Unit,
onError: suspend (String) -> Unit
) {
try {
val results = withContext(Dispatchers.IO) {
val url = config.getFullSearchUrl()
val connection: Connection = Jsoup.connect(url)
.data(config.keywordParam, keyword)
.timeout(15000)
config.extraParams.forEach { (key, value) ->
connection.data(key, value)
}
val method = config.searchMethod.uppercase()
val doc = if (method == "GET") {
connection.get()
} else {
connection.post()
}
val items = doc.select(config.resultSelector)
val resultsList = mutableListOf<SearchResult>()
for (item in items) {
try {
val titleEl = item.selectFirst(config.titleSelector) ?: continue
val linkEl = item.selectFirst(config.linkSelector)
val coverEl = item.selectFirst(config.coverSelector)
val categoryEl = item.selectFirst(config.categorySelector)
val dateEl = item.selectFirst(config.dateSelector)
val title = titleEl.text().trim()
val detailUrl = linkEl?.attr("href")?.trim() ?: ""
val coverUrl = coverEl?.attr("src")?.trim() ?: ""
val category = categoryEl?.text()?.trim() ?: ""
val date = dateEl?.text()?.trim() ?: ""
if (title.isNotEmpty() && detailUrl.isNotEmpty()) {
resultsList.add(
SearchResult(
title = title,
coverUrl = if (coverUrl.startsWith("http")) coverUrl
else config.baseUrl.trimEnd('/') + "/" + coverUrl.trimStart('/'),
detailUrl = if (detailUrl.startsWith("http")) detailUrl
else config.baseUrl.trimEnd('/') + "/" + detailUrl.trimStart('/'),
category = category,
date = date
)
)
}
} catch (e: Exception) {
// skip malformed items
}
}
resultsList
}
if (results.isEmpty()) {
onError("未找到结果")
} else {
onResult(results)
}
} catch (e: Exception) {
onError("NativeSearch 失败: ${e.message}")
}
}
}