From e850b7319601d9c9f575853f51208669432b2baa Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 16 Apr 2026 14:16:15 +0800 Subject: [PATCH 1/2] Fix SearchMessageFragment view lifecycle crash --- .../ui/search/SearchMessageFragment.kt | 64 +++++++++++++------ .../mixin/android/ui/wallet/InputFragment.kt | 34 ++++++---- 2 files changed, 67 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/search/SearchMessageFragment.kt b/app/src/main/java/one/mixin/android/ui/search/SearchMessageFragment.kt index 52d06bbebb..d8169ca0c1 100644 --- a/app/src/main/java/one/mixin/android/ui/search/SearchMessageFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/search/SearchMessageFragment.kt @@ -15,6 +15,7 @@ import com.jakewharton.rxbinding3.widget.textChanges import com.uber.autodispose.autoDispose import dagger.hilt.android.AndroidEntryPoint import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable import kotlinx.coroutines.Job import kotlinx.coroutines.launch import one.mixin.android.R @@ -23,6 +24,7 @@ import one.mixin.android.extension.getParcelableCompat import one.mixin.android.extension.hideKeyboard import one.mixin.android.extension.inTransaction import one.mixin.android.extension.showKeyboard +import one.mixin.android.extension.viewDestroyed import one.mixin.android.extension.withArgs import one.mixin.android.ui.common.BaseFragment import one.mixin.android.ui.conversation.ConversationActivity @@ -67,8 +69,19 @@ class SearchMessageFragment : BaseFragment(R.layout.fragment_search_message) { private val binding by viewBinding(FragmentSearchMessageBinding::bind) private var searchJob: Job? = null + private var textChangesDisposable: Disposable? = null private var cancellationSignal: CancellationSignal? = null + private val initialSearchRunnable = + Runnable { + if (viewDestroyed()) return@Runnable + searchJob = onTextChanged(query) + } + private val showKeyboardRunnable = + Runnable { + if (viewDestroyed()) return@Runnable + binding.searchEt.showKeyboard() + } override fun onViewCreated( view: View, @@ -97,10 +110,13 @@ class SearchMessageFragment : BaseFragment(R.layout.fragment_search_message) { adapter.callback = object : SearchMessageAdapter.SearchMessageCallback { override fun onItemClick(item: SearchMessageDetailItem) { + val keyword = binding.searchEt.text.toString() searchViewModel.findConversationById(searchMessageItem.conversationId) .autoDispose(stopScope) .subscribe { - binding.searchEt.hideKeyboard() + if (!viewDestroyed()) { + binding.searchEt.hideKeyboard() + } val activity = requireActivity() val conversationFragment = activity.supportFragmentManager.findFragmentByTag(ConversationFragment.TAG) as? ConversationFragment if (activity is ConversationActivity && conversationFragment != null) { @@ -111,14 +127,14 @@ class SearchMessageFragment : BaseFragment(R.layout.fragment_search_message) { hide(this@SearchMessageFragment) addToBackStack(null) } - conversationFragment.updateConversationInfo(item.messageId, binding.searchEt.text.toString()) + conversationFragment.updateConversationInfo(item.messageId, keyword) } } else { ConversationActivity.show( requireContext(), conversationId = searchMessageItem.conversationId, messageId = item.messageId, - keyword = binding.searchEt.text.toString(), + keyword = keyword, ) if (isConversationSearch()) { parentFragmentManager.popBackStack() @@ -131,33 +147,38 @@ class SearchMessageFragment : BaseFragment(R.layout.fragment_search_message) { binding.clearIb.setOnClickListener { binding.searchEt.setText("") } binding.searchEt.setText(query) - binding.searchEt.textChanges().debounce(SEARCH_DEBOUNCE, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(destroyScope) - .subscribe( + textChangesDisposable = + binding.searchEt.textChanges().debounce(SEARCH_DEBOUNCE, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(destroyScope) + .subscribe( { + if (viewDestroyed()) return@subscribe binding.clearIb.isVisible = it.isNotEmpty() searchJob?.cancel() searchJob = onTextChanged(it.toString()) }, {}, ) - binding.searchEt.postDelayed( - { - searchJob = onTextChanged(query) - }, - 50, - ) + binding.searchEt.postDelayed(initialSearchRunnable, 50) if (isConversationSearch()) { - binding.searchEt.postDelayed( - { - binding.searchEt.showKeyboard() - }, - 500, - ) + binding.searchEt.postDelayed(showKeyboardRunnable, 500) } } + override fun onDestroyView() { + binding.searchEt.removeCallbacks(initialSearchRunnable) + binding.searchEt.removeCallbacks(showKeyboardRunnable) + textChangesDisposable?.dispose() + textChangesDisposable = null + searchJob?.cancel() + searchJob = null + removeObserverAndCancel() + observer = null + curLiveData = null + super.onDestroyView() + } + override fun onDestroy() { super.onDestroy() cancellationSignal?.cancel() @@ -179,7 +200,8 @@ class SearchMessageFragment : BaseFragment(R.layout.fragment_search_message) { private fun isConversationSearch() = searchMessageItem.messageCount == 0 private fun onTextChanged(s: String) = - lifecycleScope.launch { + viewLifecycleOwner.lifecycleScope.launch { + if (viewDestroyed()) return@launch if (s == adapter.query) { return@launch } @@ -202,6 +224,7 @@ class SearchMessageFragment : BaseFragment(R.layout.fragment_search_message) { } private fun bindAndSearch(s: String) { + if (viewDestroyed()) return binding.progress.isVisible = true removeObserverAndCancel() @@ -209,6 +232,7 @@ class SearchMessageFragment : BaseFragment(R.layout.fragment_search_message) { curLiveData = searchViewModel.observeFuzzySearchMessageDetail(s, searchMessageItem.conversationId, cancellationSignal!!) observer = Observer { + if (viewDestroyed()) return@Observer if (s != binding.searchEt.text.toString()) return@Observer binding.progress.isVisible = false diff --git a/app/src/main/java/one/mixin/android/ui/wallet/InputFragment.kt b/app/src/main/java/one/mixin/android/ui/wallet/InputFragment.kt index 5611a04e2e..6343b61dcc 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/InputFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/InputFragment.kt @@ -217,6 +217,18 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC binding.root.hideKeyboard() } + override fun onDestroyView() { + btcFeeRecalculateJob?.cancel() + btcFeeRecalculateJob = null + if (dialog.isShowing) { + dialog.dismiss() + } + if (alertDialog.isShowing) { + alertDialog.dismiss() + } + super.onDestroyView() + } + @SuppressLint("SetTextI18n") override fun onViewCreated( view: View, @@ -224,7 +236,7 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC ) { super.onViewCreated(view, savedInstanceState) jobManager.addJobInBackground(SyncOutputJob()) - lifecycleScope.launch { + viewLifecycleOwner.lifecycleScope.launch { binding.apply { if (requireActivity() !is WalletActivity){ root.fitsSystemWindows = false @@ -518,7 +530,7 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC } else { v } - lifecycleScope.launch( + viewLifecycleOwner.lifecycleScope.launch( CoroutineExceptionHandler { _, error -> ErrorHandler.handleError(error) alertDialog.dismiss() @@ -586,7 +598,7 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC val fromAddress = requireNotNull(fromAddress) val toAddress = requireNotNull(toAddress) val amount = currentInputAmount() - lifecycleScope.launch( + viewLifecycleOwner.lifecycleScope.launch( CoroutineExceptionHandler { _, error -> Timber.e("Error: ${error.message}") ErrorHandler.handleError(error) @@ -1030,7 +1042,7 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC private var isFeeWaived = false private fun renderTitle(toAddress: String, tag: String? = null) { - lifecycleScope.launch { + viewLifecycleOwner.lifecycleScope.launch { val (label, index, _) = web3ViewModel.checkAddressAndGetDisplayName(requireNotNull(toAddress), tag, requireNotNull(token?.chainId ?: web3Token?.chainId)) ?: Triple(null, 0, null) isFeeWaived = index == 1 || index == 2 || index == 4 // Privacy(1), Safe(2), Fee-free(4) binding.titleView.setLabel( @@ -1218,7 +1230,7 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC if (lastBtcFeeAmount == amount) return lastBtcFeeAmount = amount btcFeeRecalculateJob?.cancel() - btcFeeRecalculateJob = lifecycleScope.launch { + btcFeeRecalculateJob = viewLifecycleOwner.lifecycleScope.launch { delay(300L) val currentAmount: String = lastBtcFeeAmount ?: return@launch refreshBtcFeeForAmount(currentAmount) @@ -1432,7 +1444,7 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC web3Token != null && web3Token?.assetId == chainToken?.assetId -> { if (gas == null) { if (!dialog.isShowing) { - lifecycleScope.launch { + viewLifecycleOwner.lifecycleScope.launch { dialog.show() refreshFee() } @@ -1529,13 +1541,13 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC private var gaslessFeeToken: Web3TokenItem? = null private var hasManuallySelectedWeb3Fee = false - private fun refreshFeeTokenExtra(tokenId: String?) = lifecycleScope.launch { + private fun refreshFeeTokenExtra(tokenId: String?) = viewLifecycleOwner.lifecycleScope.launch { feeTokensExtra = if (tokenId == null) null else web3ViewModel.findTokensExtra(tokenId) updateUI() } - private fun refreshGaslessFeeToken(tokenId: String?) = lifecycleScope.launch { + private fun refreshGaslessFeeToken(tokenId: String?) = viewLifecycleOwner.lifecycleScope.launch { gaslessFeeToken = if (tokenId == null || web3Token == null) { null } else { @@ -1613,7 +1625,7 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC } private fun prepareCheck(item: BiometricItem) { - lifecycleScope.launch { + viewLifecycleOwner.lifecycleScope.launch { val amount = item.amount val rawTransaction = web3ViewModel.firstUnspentTransaction() if (rawTransaction != null) { @@ -1629,7 +1641,7 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC private fun checkUtxo(amount: String, callback: () -> Unit) { val token = token ?: return - lifecycleScope.launch { + viewLifecycleOwner.lifecycleScope.launch { val consolidationAmount = web3ViewModel.checkUtxoSufficiency(token.assetId, amount) if (consolidationAmount != null) { UtxoConsolidationBottomSheetDialogFragment.newInstance(buildTransferBiometricItem(Session.getAccount()!!.toUser(), token, consolidationAmount, UUID.randomUUID().toString(), null, null)) @@ -1641,7 +1653,7 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC } private fun prepareTransferBottom(amount: String, item: BiometricItem) = - lifecycleScope.launch { + viewLifecycleOwner.lifecycleScope.launch { val t = item if (t !is TransferBiometricItem && t !is AddressTransferBiometricItem && t !is WithdrawBiometricItem) { return@launch From 1b0d4ec9181ef7e4d6170cbc2a48bb51f946951c Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 16 Apr 2026 15:00:12 +0800 Subject: [PATCH 2/2] Refine wallet connect loading transition --- app/src/main/AndroidManifest.xml | 2 +- .../ui/search/SearchMessageFragment.kt | 14 ++- .../ui/tip/wc/WalletConnectActivity.kt | 7 ++ .../WalletConnectBottomSheetDialogFragment.kt | 4 + .../ui/tip/wc/WalletConnectFragment.kt | 103 ------------------ .../mixin/android/ui/wallet/InputFragment.kt | 29 ++++- 6 files changed, 45 insertions(+), 114 deletions(-) delete mode 100644 app/src/main/java/one/mixin/android/ui/tip/wc/WalletConnectFragment.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9cda6cddb0..cc387e59e5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -375,7 +375,7 @@ android:name=".ui.tip.wc.WalletConnectActivity" android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" android:launchMode="singleTop" - android:theme="@style/AppTheme.Music" /> + android:theme="@style/AppTheme.Transparent" /> - navBackStackEntry.arguments?.getString("connectionId")?.toIntOrNull().let { connectionId -> - ConnectionDetailsPage(connectionId) { - navigateUp(navController) - } - } - } - } - } - } - } - } - - private fun navigateUp(navController: NavHostController) { - if (!navController.safeNavigateUp()) { - activity?.onBackPressedDispatcher?.onBackPressed() - } - } -} diff --git a/app/src/main/java/one/mixin/android/ui/wallet/InputFragment.kt b/app/src/main/java/one/mixin/android/ui/wallet/InputFragment.kt index 6343b61dcc..36d7f5e62a 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/InputFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/InputFragment.kt @@ -214,7 +214,7 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC override fun onResume() { super.onResume() - binding.root.hideKeyboard() + bindingOrNull()?.root?.hideKeyboard() } override fun onDestroyView() { @@ -731,6 +731,7 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC } private fun applyFeeUi() { + val binding = bindingOrNull() ?: return val hasFeeText: Boolean = binding.contentTextView.text.toString().isNotEmpty() val showFee: Boolean = isFeeWaived && hasFeeText binding.contentTextView.paintFlags = @@ -946,6 +947,7 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC } private fun updateWeb3AvailableBalance() { + val binding = bindingOrNull() ?: return val transferToken = web3Token ?: return val displayBalance = when { @@ -971,6 +973,7 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC } private fun updateWeb3FeeDisplay() { + val binding = bindingOrNull() ?: return val token = web3Token ?: return if (binding.loadingProgressBar.isVisible) return val feeOptions = web3FeeOptions() @@ -1287,6 +1290,7 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC } private fun updateAvailableBalanceForBtcFee() { + val binding = bindingOrNull() ?: return val token: Web3TokenItem = web3Token ?: return if (token.chainId != Constants.ChainId.BITCOIN_CHAIN_ID) return val reservedFee: BigDecimal = gas ?: return @@ -1320,6 +1324,7 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC } private fun updateAddText() { + val binding = bindingOrNull() ?: return if (transferType == TransferType.WEB3 && shouldUseGaslessFlow()) { if (!isGaslessFeeEnough(currentInputAmount())) { binding.addTv.text = "${getString(R.string.Add)} ${currentGaslessFee?.token?.symbol ?: ""}" @@ -1513,22 +1518,25 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC get() = field set(value) { field = value + val binding = bindingOrNull() if (value != null) { - if (value.token.assetId == token?.assetId || value.token.assetId == web3Token?.assetId) { + if (binding != null && (value.token.assetId == token?.assetId || value.token.assetId == web3Token?.assetId)) { val balance = runCatching { tokenBalance.toBigDecimalOrNull()?.subtract(value.fee.toBigDecimalOrNull() ?: BigDecimal.ZERO)?.max(BigDecimal.ZERO)?.let { if (web3Token == null) { it.numberFormat8() } else { it.numberFormat12() } } }.getOrDefault("0") binding.balanceTv.text = getString(R.string.available_balance, "$balance $tokenSymbol") - } else { + } else if (binding != null) { binding.balanceTv.text = getString(R.string.available_balance, "${tokenBalance.let { if (web3Token == null) { it.numberFormat8() } else { it.numberFormat12() } } } $tokenSymbol") } - binding.insufficientFeeBalance.text = getString(R.string.insufficient_gas, value.token.symbol) + binding?.insufficientFeeBalance?.text = getString(R.string.insufficient_gas, value.token.symbol) + } + if (binding != null) { + refreshFeeTokenExtra(value?.token?.assetId) } - refreshFeeTokenExtra(value?.token?.assetId) } private var feeTokensExtra: TokensExtra? = null @@ -1566,11 +1574,13 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC private var isAdjustingBtcAmount: Boolean = false private fun setFeeLoading(isLoading: Boolean) { + val binding = bindingOrNull() ?: return binding.loadingProgressBar.isVisible = isLoading binding.contentTextView.isVisible = !isLoading } private suspend fun refreshFee(t: TokenItem) { + val binding = bindingOrNull() ?: return val toAddress = toAddress?: return setFeeLoading(true) val feeResponse = runCatching { web3ViewModel.getFees(t.assetId, toAddress) }.getOrNull() @@ -1756,6 +1766,7 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC } private suspend fun refreshGas(t: Web3TokenItem) { + val binding = bindingOrNull() ?: return val toAddress = toAddress?: return val fromAddress = fromAddress ?: return if (t.chainId == Constants.ChainId.BITCOIN_CHAIN_ID) { @@ -1831,6 +1842,14 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC } } + private fun bindingOrNull(): FragmentInputBinding? { + return if (view == null) { + null + } else { + binding + } + } + private suspend fun refreshGaslessFees(t: Web3TokenItem) { val fromAddress = fromAddress ?: return val toAddress = toAddress ?: return