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