MainViewModel.kt (11101B)
1 /* 2 * This file is part of GNU Taler 3 * (C) 2026 Taler Systems S.A. 4 * 5 * GNU Taler is free software; you can redistribute it and/or modify it under the 6 * terms of the GNU General Public License as published by the Free Software 7 * Foundation; either version 3, or (at your option) any later version. 8 * 9 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY 10 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along with 14 * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 package net.taler.wallet.main 18 19 import android.app.Application 20 import android.util.Log 21 import androidx.annotation.UiThread 22 import androidx.lifecycle.AndroidViewModel 23 import androidx.lifecycle.LiveData 24 import androidx.lifecycle.MutableLiveData 25 import androidx.lifecycle.viewModelScope 26 import kotlinx.coroutines.Dispatchers 27 import kotlinx.coroutines.flow.MutableStateFlow 28 import kotlinx.coroutines.flow.StateFlow 29 import kotlinx.coroutines.flow.getAndUpdate 30 import kotlinx.coroutines.launch 31 import net.taler.common.Amount 32 import net.taler.common.AmountParserException 33 import net.taler.common.Event 34 import net.taler.common.toEvent 35 import net.taler.wallet.accounts.AccountManager 36 import net.taler.wallet.backend.BackendManager 37 import net.taler.wallet.backend.NotificationPayload 38 import net.taler.wallet.backend.NotificationReceiver 39 import net.taler.wallet.backend.TalerErrorInfo 40 import net.taler.wallet.backend.VersionReceiver 41 import net.taler.wallet.backend.WalletBackendApi 42 import net.taler.wallet.backend.WalletCoreVersion 43 import net.taler.wallet.backend.WalletRunConfig 44 import net.taler.wallet.backend.WalletRunConfig.Features 45 import net.taler.wallet.backend.WalletRunConfig.Testing 46 import net.taler.wallet.balances.BalanceManager 47 import net.taler.wallet.balances.ScopeInfo 48 import net.taler.wallet.deposit.DepositManager 49 import net.taler.wallet.events.ObservabilityEvent 50 import net.taler.wallet.exchanges.ExchangeManager 51 import net.taler.wallet.payment.PaymentManager 52 import net.taler.wallet.peer.PeerManager 53 import net.taler.wallet.refund.RefundManager 54 import net.taler.wallet.settings.SettingsManager 55 import net.taler.wallet.transactions.TransactionManager 56 import net.taler.wallet.transactions.TransactionStateFilter 57 import net.taler.wallet.withdraw.WithdrawManager 58 import net.taler.wallet.BuildConfig 59 import net.taler.wallet.NetworkManager 60 import net.taler.wallet.donau.DonauManager 61 import net.taler.wallet.tokens.TokenManager 62 63 const val TAG = "taler-wallet" 64 const val OBSERVABILITY_LIMIT = 100 65 66 private val transactionNotifications = listOf( 67 "transaction-state-transition", 68 ) 69 70 private val observabilityNotifications = listOf( 71 "task-observability-event", 72 "request-observability-event", 73 ) 74 75 class MainViewModel( 76 app: Application, 77 ) : AndroidViewModel(app), VersionReceiver, NotificationReceiver { 78 79 private val mDevMode = MutableLiveData(BuildConfig.DEBUG) 80 val devMode: LiveData<Boolean> = mDevMode 81 82 val showProgressBar = MutableLiveData<Boolean>() 83 var walletVersion: String? = null 84 private set 85 var walletVersionHash: String? = null 86 private set 87 var exchangeVersion: String? = null 88 private set 89 var merchantVersion: String? = null 90 private set 91 92 @set:Synchronized 93 private var walletConfig = WalletRunConfig( 94 testing = Testing( 95 emitObservabilityEvents = true, 96 devModeActive = devMode.value == true, 97 ), 98 features = Features( 99 enableV1Contracts = true, 100 ), 101 logLevel = if (devMode.value == true) "TRACE" else "INFO", 102 ) 103 104 private val api = WalletBackendApi(app, walletConfig, this, this) 105 106 val networkManager = NetworkManager(app.applicationContext) 107 val exchangeManager: ExchangeManager = ExchangeManager(api, viewModelScope) 108 val balanceManager = BalanceManager(api, viewModelScope, exchangeManager) 109 val paymentManager = PaymentManager(api, viewModelScope, exchangeManager) 110 val transactionManager: TransactionManager = TransactionManager(api, viewModelScope) 111 val refundManager = RefundManager(api, viewModelScope) 112 val withdrawManager = WithdrawManager(api, viewModelScope, exchangeManager, transactionManager) 113 val peerManager: PeerManager = PeerManager(api, exchangeManager, viewModelScope) 114 val settingsManager: SettingsManager = SettingsManager(app.applicationContext, api, viewModelScope, balanceManager) 115 val accountManager: AccountManager = AccountManager(api, viewModelScope) 116 val depositManager: DepositManager = DepositManager(api, viewModelScope, balanceManager) 117 val tokenManager: TokenManager = TokenManager(api) 118 val donauManager: DonauManager = DonauManager(api, viewModelScope, exchangeManager) 119 120 private val mAuthenticated = MutableStateFlow(false) 121 val authenticated: StateFlow<Boolean> = mAuthenticated 122 123 private val mTransactionsEvent = MutableLiveData<Event<ScopeInfo>>() 124 val transactionsEvent: LiveData<Event<ScopeInfo>> = mTransactionsEvent 125 126 private val mObservabilityLog = MutableStateFlow<List<ObservabilityEvent>>(emptyList()) 127 val observabilityLog: StateFlow<List<ObservabilityEvent>> = mObservabilityLog 128 129 private val mShowObservabilityLog = MutableStateFlow(false) 130 val showObservabilityLog: StateFlow<Boolean> = mShowObservabilityLog 131 132 private val mScanCodeEvent = MutableLiveData<Event<Boolean>>() 133 val scanCodeEvent: LiveData<Event<Boolean>> = mScanCodeEvent 134 135 private val mViewMode = MutableStateFlow<ViewMode>(ViewMode.Assets) 136 val viewMode: StateFlow<ViewMode> = mViewMode 137 138 fun startWallet() { 139 api.startWallet() 140 } 141 142 fun stopWallet() { 143 api.stopWallet() 144 } 145 146 override fun onVersionReceived(versionInfo: WalletCoreVersion) { 147 walletVersion = versionInfo.implementationSemver 148 walletVersionHash = versionInfo.implementationGitHash 149 exchangeVersion = versionInfo.exchange 150 merchantVersion = versionInfo.merchant 151 } 152 153 override fun onNotificationReceived(payload: NotificationPayload) { 154 if (payload.type == "waiting-for-retry") return // ignore ping) 155 156 val str = BackendManager.json.encodeToString(payload) 157 Log.i(TAG, "Received notification from wallet-core: $str") 158 159 // Only update balances when we're told they changed 160 if (payload.type == "balance-change") viewModelScope.launch(Dispatchers.Main) { 161 balanceManager.loadAssets() 162 } 163 164 if (payload.type in observabilityNotifications && payload.event != null) { 165 mObservabilityLog.getAndUpdate { logs -> 166 logs.takeLast(OBSERVABILITY_LIMIT) 167 .toMutableList().apply { 168 add(payload.event) 169 } 170 } 171 } 172 173 if (payload.type in transactionNotifications) viewModelScope.launch(Dispatchers.Main) { 174 payload.transactionId?.let { id -> 175 // update currently selected transaction 176 transactionManager.updateTransactionIfSelected(id) 177 // update currently selected transaction list 178 if (payload.type == "transaction-state-transition") { 179 transactionManager.getTransactionById(id)?.let { tx -> 180 val v = viewMode.value 181 if (v is ViewMode.Transactions && v.selectedScope in tx.scopes) { 182 transactionManager.loadTransactions(v.selectedScope) 183 } 184 } 185 } 186 } 187 } 188 } 189 190 @UiThread 191 fun lockWallet() { 192 mAuthenticated.value = false 193 } 194 195 @UiThread 196 fun unlockWallet() { 197 mAuthenticated.value = true 198 } 199 200 fun setViewMode(v: ViewMode?) = viewModelScope.launch { 201 mViewMode.value = when(v) { 202 null -> ViewMode.Assets 203 is ViewMode.Transactions -> v.copy( 204 // fill-in currency spec from DB 205 selectedSpec = exchangeManager.getCurrencySpecification(v.selectedScope), 206 ) 207 else -> v 208 } 209 } 210 211 fun selectScope(scopeInfo: ScopeInfo?) { 212 if (scopeInfo != null) { 213 setViewMode(ViewMode.Transactions(scopeInfo)) 214 } else { 215 setViewMode(ViewMode.Assets) 216 } 217 } 218 219 fun showAssets() { 220 if (viewMode.value != ViewMode.Assets) { 221 selectScope(null) 222 } 223 } 224 225 /** 226 * Navigates to the given scope info's transaction list, when [MainScreen] is shown. 227 */ 228 @UiThread 229 fun showTransactions(scopeInfo: ScopeInfo, stateFilter: TransactionStateFilter? = null) { 230 mViewMode.value = ViewMode.Transactions(scopeInfo, stateFilter = stateFilter) 231 } 232 233 @UiThread 234 fun createAmount(amountText: String, currency: String, incoming: Boolean = false): AmountResult { 235 val amount = try { 236 Amount.fromString(currency, amountText) 237 } catch (e: AmountParserException) { 238 return AmountResult.InvalidAmount 239 } 240 if (incoming || balanceManager.hasSufficientBalance(amount)) return AmountResult.Success(amount) 241 return AmountResult.InsufficientBalance(amount) 242 } 243 244 @UiThread 245 fun dangerouslyReset() { 246 withdrawManager.resetTestWithdrawal() 247 balanceManager.resetBalances() 248 } 249 250 @UiThread 251 fun scanCode() { 252 mScanCodeEvent.value = true.toEvent() 253 } 254 255 fun setDevMode(enabled: Boolean, onError: (error: TalerErrorInfo) -> Unit) { 256 mDevMode.postValue(enabled) 257 viewModelScope.launch { 258 val config = walletConfig.copy( 259 testing = walletConfig.testing?.copy( 260 devModeActive = enabled, 261 ) ?: Testing( 262 devModeActive = enabled, 263 ), 264 logLevel = if (enabled) "TRACE" else "INFO", 265 ) 266 267 api.setWalletConfig(config) 268 .onSuccess { 269 walletConfig = config 270 }.onError(onError) 271 } 272 } 273 274 fun showObservabilityLog() { 275 mShowObservabilityLog.value = true 276 } 277 278 fun hideObservabilityLog() { 279 mShowObservabilityLog.value = false 280 } 281 282 fun hintNetworkAvailability(isAvailable: Boolean) { 283 viewModelScope.launch { 284 api.request<Unit>("hintNetworkAvailability") { 285 put("isNetworkAvailable", isAvailable) 286 } 287 } 288 } 289 290 fun applyDevExperiment(uri: String, onError: (error: TalerErrorInfo) -> Unit) { 291 viewModelScope.launch { 292 api.request<Unit>("applyDevExperiment") { 293 put("devExperimentUri", uri) 294 }.onError(onError) 295 } 296 } 297 } 298 299 sealed class AmountResult { 300 data class Success(val amount: Amount) : AmountResult() 301 data class InsufficientBalance(val amount: Amount) : AmountResult() 302 data object InvalidAmount : AmountResult() 303 }