Generate subscription QR codes off the UI thread (#6194)

Move subscription QR bitmap generation from the synchronous share callback into the existing ViewModel, running CPU-bound encoding on Dispatchers.Default and exposing the completed bitmap through StateFlow.

Cancel pending result delivery when the dialog is dismissed, a newer share request replaces it, or the ViewModel is cleared. Keep dialog rendering lifecycle-aware and reuse the existing localized failure message when generation returns no bitmap.

Extract only QR generation and dialog-state wiring from local/talkback-all-10. Leave subscription row accessibility, custom actions, update handling, and clipboard sharing unchanged.
This commit is contained in:
eliotcougar authored and nerpa committed 2026-09-14 16:53:36 +03:00
1 parent 6400d33b87
commit 7d9fa970dc
2 files changed
+30 -9

No files matched your search

@@ -1,7 +1,6 @@
package com.v2ray.ang.ui.subscription
import android.content.Intent
import android.graphics.Bitmap
import android.os.Bundle
import androidx.activity.viewModels
import androidx.annotation.StringRes
@@ -53,7 +52,6 @@ import com.v2ray.ang.ui.compose.ReorderableListItem
import com.v2ray.ang.ui.compose.SelectListDialog
import com.v2ray.ang.ui.compose.SettingsSwitchItem
import com.v2ray.ang.ui.compose.verticalScrollbar
import com.v2ray.ang.util.QRCodeDecoder
import com.v2ray.ang.util.Utils
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
@@ -83,7 +81,7 @@ class SubSettingActivity : BaseComponentActivity() {
startActivity(Intent(this, SubEditActivity::class.java).putExtra("subId", subId))
},
onRemoveSub = { subId -> removeSub(subId) },
onShareQRCode = { url -> QRCodeDecoder.createQRCode(url) },
onShareQRCode = viewModel::shareQRCode,
onShareClipboard = { url ->
Utils.setClipboard(this, url)
toast(getString(R.string.toast_success))
@@ -110,7 +108,7 @@ fun SubSettingScreen(
onSubUpdate: () -> Unit,
onEditSub: (String) -> Unit,
onRemoveSub: (String) -> Unit,
onShareQRCode: (String) -> Bitmap?,
onShareQRCode: (String) -> Unit,
onShareClipboard: (String) -> Unit
) {
val subscriptions by viewModel.subsFlow.collectAsStateWithLifecycle()
@@ -119,7 +117,7 @@ fun SubSettingScreen(
val confirmRemove = MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE, false)
var shareTarget by remember { mutableStateOf<Pair<String, String>?>(null) }
var showQRCodeBitmap by remember { mutableStateOf<Bitmap?>(null) }
val qrCodeBitmap by viewModel.qrCode.collectAsStateWithLifecycle()
val lazyListState = rememberLazyListState()
val reorderableState = rememberReorderableLazyListState(lazyListState) { from, to ->
@@ -254,7 +252,7 @@ fun SubSettingScreen(
onSelected = { action ->
shareTarget = null
when (action) {
SubscriptionShareAction.QRCode -> showQRCodeBitmap = onShareQRCode(url)
SubscriptionShareAction.QRCode -> onShareQRCode(url)
SubscriptionShareAction.Clipboard -> onShareClipboard(url)
}
},
@@ -263,10 +261,10 @@ fun SubSettingScreen(
}
// QR Code Dialog
if (showQRCodeBitmap != null) {
if (qrCodeBitmap != null) {
QRCodeDialog(
bitmap = showQRCodeBitmap,
onDismiss = { showQRCodeBitmap = null }
bitmap = qrCodeBitmap,
onDismiss = viewModel::dismissQRCode
)
}
@@ -1,6 +1,8 @@
package com.v2ray.ang.ui.subscription
import android.app.Application
import android.graphics.Bitmap
import androidx.lifecycle.viewModelScope
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.dto.SubscriptionUpdateMessage
@@ -14,14 +16,20 @@ import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.helper.MessageHelper
import com.v2ray.ang.ui.base.BaseViewModel
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.QRCodeDecoder
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SubscriptionsViewModel(application: Application) : BaseViewModel(application) {
private var qrCodeJob: Job? = null
private val _qrCode = MutableStateFlow<Bitmap?>(null)
internal val qrCode = _qrCode.asStateFlow()
private val subscriptions: MutableList<SubscriptionCache> =
MmkvManager.decodeSubscriptions().toMutableList()
@@ -55,6 +63,21 @@ class SubscriptionsViewModel(application: Application) : BaseViewModel(applicati
_subsFlow.value = subscriptions.toList()
}
internal fun shareQRCode(url: String) {
dismissQRCode()
qrCodeJob = viewModelScope.launch {
val bitmap = withContext(Dispatchers.Default) { QRCodeDecoder.createQRCode(url) }
if (bitmap == null) toastError(R.string.toast_failure)
_qrCode.value = bitmap
}
}
internal fun dismissQRCode() {
qrCodeJob?.cancel()
qrCodeJob = null
_qrCode.value = null
}
fun move(fromPosition: Int, toPosition: Int) {
if (subscriptions.moveItem(fromPosition, toPosition)) {
MmkvManager.encodeSubsList(subscriptions.mapTo(mutableListOf()) { it.guid })