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:
237
app/src/main/java/com/videoapp/tv/ui/SearchFragment.kt
Normal file
237
app/src/main/java/com/videoapp/tv/ui/SearchFragment.kt
Normal file
@@ -0,0 +1,237 @@
|
||||
package com.videoapp.tv.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.videoapp.tv.R
|
||||
import com.videoapp.tv.PlayerActivity
|
||||
import com.videoapp.tv.SettingsActivity
|
||||
import com.videoapp.tv.data.AppDatabase
|
||||
import com.videoapp.tv.data.SearchHistory
|
||||
import com.videoapp.tv.data.SearchResult
|
||||
import com.videoapp.tv.engine.SearchCoordinator
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SearchFragment : Fragment() {
|
||||
|
||||
private lateinit var searchInput: EditText
|
||||
private lateinit var btnSearch: Button
|
||||
private lateinit var btnSettings: Button
|
||||
private lateinit var historyContainer: ViewGroup
|
||||
private lateinit var historyList: ViewGroup
|
||||
private lateinit var btnClearHistory: Button
|
||||
private lateinit var resultsGrid: RecyclerView
|
||||
private lateinit var loadingProgress: View
|
||||
private lateinit var statusText: TextView
|
||||
private lateinit var fallbackWebView: WebView
|
||||
|
||||
private val searchCoordinator by lazy { SearchCoordinator(requireContext()) }
|
||||
private val historyDao by lazy { AppDatabase.getInstance(requireContext()).searchHistoryDao() }
|
||||
private val adapter by lazy {
|
||||
SearchResultAdapter(
|
||||
onItemClick = { result -> openPlayer(result) },
|
||||
onItemFocus = { view -> view.animate().scaleX(1.05f).scaleY(1.05f).setDuration(150).start() }
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_search, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
searchInput = view.findViewById(R.id.search_input)
|
||||
btnSearch = view.findViewById(R.id.btn_search)
|
||||
btnSettings = view.findViewById(R.id.btn_settings)
|
||||
historyContainer = view.findViewById(R.id.history_container)
|
||||
historyList = view.findViewById(R.id.history_list)
|
||||
btnClearHistory = view.findViewById(R.id.btn_clear_history)
|
||||
resultsGrid = view.findViewById(R.id.results_grid)
|
||||
loadingProgress = view.findViewById(R.id.loading_progress)
|
||||
statusText = view.findViewById(R.id.status_text)
|
||||
fallbackWebView = view.findViewById(R.id.fallback_webview)
|
||||
|
||||
setupResultsGrid()
|
||||
setupListeners()
|
||||
loadHistory()
|
||||
}
|
||||
|
||||
private fun setupResultsGrid() {
|
||||
resultsGrid.layoutManager = GridLayoutManager(context, 4)
|
||||
resultsGrid.adapter = adapter
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
searchInput.setOnEditorActionListener { _, actionId, event ->
|
||||
if (actionId == EditorInfo.IME_ACTION_SEARCH ||
|
||||
(event?.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)
|
||||
) {
|
||||
performSearch(searchInput.text.toString().trim())
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
btnSearch.setOnClickListener {
|
||||
performSearch(searchInput.text.toString().trim())
|
||||
}
|
||||
|
||||
btnSettings.setOnClickListener {
|
||||
startActivity(Intent(requireContext(), SettingsActivity::class.java))
|
||||
}
|
||||
|
||||
btnClearHistory.setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
historyDao.clearAll()
|
||||
showHistory(emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
searchInput.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
historyContainer.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun performSearch(keyword: String) {
|
||||
if (keyword.isEmpty()) return
|
||||
|
||||
hideFallbackWebView()
|
||||
showLoading(true)
|
||||
statusText.visibility = View.GONE
|
||||
historyContainer.visibility = View.GONE
|
||||
|
||||
lifecycleScope.launch {
|
||||
// Save to history
|
||||
val existing = historyDao.findByKeyword(keyword)
|
||||
if (existing != null) {
|
||||
historyDao.updateTime(existing.id, System.currentTimeMillis())
|
||||
} else {
|
||||
historyDao.insert(SearchHistory(keyword = keyword))
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
searchCoordinator.search(
|
||||
keyword = keyword,
|
||||
onResult = { results ->
|
||||
showLoading(false)
|
||||
adapter.setItems(results)
|
||||
if (results.isEmpty()) {
|
||||
showStatus("未找到结果")
|
||||
}
|
||||
},
|
||||
onError = { error ->
|
||||
showLoading(false)
|
||||
if (error == "FALLBACK_WEBVIEW") {
|
||||
showFallbackWebView(keyword)
|
||||
} else {
|
||||
showStatus("搜索失败,请重试")
|
||||
Toast.makeText(requireContext(), error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
},
|
||||
onFallbackToWebView = {
|
||||
showFallbackWebView(keyword)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFallbackWebView(keyword: String) {
|
||||
val config = searchCoordinator.getConfig()
|
||||
val base = config.baseUrl.trimEnd('/')
|
||||
val path = config.searchPath.trimStart('/')
|
||||
val params = "${config.keywordParam}=${java.net.URLEncoder.encode(keyword, "UTF-8")}"
|
||||
val extraParams = config.extraParams.entries.joinToString("&") { "${it.key}=${it.value}" }
|
||||
val url = "$base/$path?$params&$extraParams"
|
||||
|
||||
fallbackWebView.visibility = View.VISIBLE
|
||||
resultsGrid.visibility = View.GONE
|
||||
fallbackWebView.settings.javaScriptEnabled = true
|
||||
fallbackWebView.webViewClient = WebViewClient()
|
||||
fallbackWebView.loadUrl(url)
|
||||
}
|
||||
|
||||
private fun hideFallbackWebView() {
|
||||
fallbackWebView.visibility = View.GONE
|
||||
fallbackWebView.stopLoading()
|
||||
resultsGrid.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun showLoading(show: Boolean) {
|
||||
loadingProgress.visibility = if (show) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun showStatus(msg: String) {
|
||||
statusText.text = msg
|
||||
statusText.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun openPlayer(result: SearchResult) {
|
||||
val intent = Intent(requireContext(), PlayerActivity::class.java).apply {
|
||||
putExtra("detail_url", result.detailUrl)
|
||||
putExtra("title", result.title)
|
||||
putExtra("category", result.category)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun loadHistory() {
|
||||
lifecycleScope.launch {
|
||||
historyDao.getRecentHistory().collect { list ->
|
||||
showHistory(list)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showHistory(list: List<SearchHistory>) {
|
||||
historyList.removeAllViews()
|
||||
if (list.isEmpty()) {
|
||||
historyContainer.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
|
||||
for (item in list) {
|
||||
val chip = Button(requireContext()).apply {
|
||||
text = item.keyword
|
||||
setTextColor(resources.getColor(R.color.text_primary, null))
|
||||
setBackgroundResource(R.drawable.history_chip_selector)
|
||||
textSize = 14f
|
||||
setPadding(24, 8, 24, 8)
|
||||
isFocusable = true
|
||||
isFocusableInTouchMode = true
|
||||
setOnClickListener {
|
||||
searchInput.setText(item.keyword)
|
||||
performSearch(item.keyword)
|
||||
}
|
||||
}
|
||||
historyList.addView(chip)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
fallbackWebView.destroy()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user