diff options
Diffstat (limited to 'wallet/src/main/java/net/taler')
7 files changed, 382 insertions, 1 deletions
diff --git a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt index 5903446..cd1fbac 100644 --- a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt +++ b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt @@ -25,11 +25,13 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.serialization.encodeToString import net.taler.common.Amount import net.taler.common.AmountParserException import net.taler.common.Event import net.taler.common.toEvent import net.taler.wallet.accounts.AccountManager +import net.taler.wallet.backend.BackendManager import net.taler.wallet.backend.NotificationPayload import net.taler.wallet.backend.NotificationReceiver import net.taler.wallet.backend.VersionReceiver @@ -38,6 +40,7 @@ import net.taler.wallet.backend.WalletCoreVersion import net.taler.wallet.balances.BalanceManager import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.deposit.DepositManager +import net.taler.wallet.events.ObservabilityEvent import net.taler.wallet.exchanges.ExchangeManager import net.taler.wallet.payment.PaymentManager import net.taler.wallet.peer.PeerManager @@ -85,6 +88,9 @@ class MainViewModel( private val mTransactionsEvent = MutableLiveData<Event<ScopeInfo>>() val transactionsEvent: LiveData<Event<ScopeInfo>> = mTransactionsEvent + private val mObservabilityStream = MutableLiveData<Event<ObservabilityEvent>>() + val observabilityStream: LiveData<Event<ObservabilityEvent>> = mObservabilityStream + private val mScanCodeEvent = MutableLiveData<Event<Boolean>>() val scanCodeEvent: LiveData<Event<Boolean>> = mScanCodeEvent @@ -97,13 +103,19 @@ class MainViewModel( override fun onNotificationReceived(payload: NotificationPayload) { if (payload.type == "waiting-for-retry") return // ignore ping) - Log.i(TAG, "Received notification from wallet-core: $payload") + + val str = BackendManager.json.encodeToString(payload) + Log.i(TAG, "Received notification from wallet-core: $str") // Only update balances when we're told they changed if (payload.type == "balance-change") viewModelScope.launch(Dispatchers.Main) { balanceManager.loadBalances() } + if (payload.type == "task-observability-event" && payload.event != null) { + mObservabilityStream.postValue(payload.event.toEvent()) + } + if (payload.type in transactionNotifications) viewModelScope.launch(Dispatchers.Main) { // TODO notification API should give us a currency to update // update currently selected transaction list diff --git a/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt b/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt index 46eb2f0..def4668 100644 --- a/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt +++ b/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt @@ -19,6 +19,7 @@ package net.taler.wallet.backend import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonObject +import net.taler.wallet.events.ObservabilityEvent @Serializable sealed class ApiMessage { @@ -35,6 +36,7 @@ sealed class ApiMessage { data class NotificationPayload( val type: String, val id: String? = null, + val event: ObservabilityEvent? = null, ) @Serializable diff --git a/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt b/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt index e9f7fcd..7fe1a6b 100644 --- a/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt +++ b/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt @@ -17,12 +17,55 @@ package net.taler.wallet.backend import kotlinx.serialization.Serializable +import net.taler.wallet.exchanges.BuiltinExchange @Serializable data class InitResponse( val versionInfo: WalletCoreVersion, ) +@Serializable +data class WalletRunConfig( + val builtin: Builtin? = Builtin(), + val testing: Testing? = Testing(), + val features: Features? = Features(), +) { + /** + * Initialization values useful for a complete startup. + * + * These are values may be overridden by different wallets + */ + @Serializable + data class Builtin( + val exchanges: List<BuiltinExchange> = emptyList(), + ) + + /** + * Unsafe options which it should only be used to create + * testing environment. + */ + @Serializable + data class Testing( + /** + * Allow withdrawal of denominations even though they are about to expire. + */ + val denomselAllowLate: Boolean = false, + val devModeActive: Boolean = false, + val insecureTrustExchange: Boolean = false, + val preventThrottling: Boolean = false, + val skipDefaults: Boolean = false, + val emitObservabilityEvents: Boolean? = false, + ) + + /** + * Configurations values that may be safe to show to the user + */ + @Serializable + data class Features( + val allowHttp: Boolean = false, + ) +} + fun interface VersionReceiver { fun onVersionReceived(versionInfo: WalletCoreVersion) } diff --git a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt index 4e179bb..0619a4e 100644 --- a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt +++ b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt @@ -23,11 +23,13 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.KSerializer +import kotlinx.serialization.encodeToString import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.decodeFromJsonElement import net.taler.wallet.backend.TalerErrorCode.NONE import org.json.JSONObject import java.io.File +import net.taler.wallet.backend.WalletRunConfig.* private const val WALLET_DB = "talerwalletdb.sqlite3" @@ -54,9 +56,15 @@ class WalletBackendApi( } else { "${app.filesDir}/${WALLET_DB}" } + + val config = WalletRunConfig(testing = Testing( + emitObservabilityEvents = true, + )) + request("init", InitResponse.serializer()) { put("persistentStoragePath", db) put("logLevel", "INFO") + put("config", JSONObject(BackendManager.json.encodeToString(config))) }.onSuccess { response -> versionReceiver.onVersionReceived(response.versionInfo) }.onError { error -> diff --git a/wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt b/wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt new file mode 100644 index 0000000..600f143 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt @@ -0,0 +1,129 @@ +/* + * This file is part of GNU Taler + * (C) 2024 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.wallet.events + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.getAndUpdate +import kotlinx.serialization.encodeToString +import net.taler.common.EventObserver +import net.taler.wallet.MainViewModel +import net.taler.wallet.R +import net.taler.wallet.backend.BackendManager +import net.taler.wallet.compose.copyToClipBoard + +class ObservabilityDialog: DialogFragment() { + private val model: MainViewModel by activityViewModels() + private val eventsFlow: MutableStateFlow<List<ObservabilityEvent>> = MutableStateFlow(emptyList()) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = ComposeView(requireContext()).apply { + setContent { + val events by eventsFlow.collectAsState() + ObservabilityComposable(events = events) { + dismiss() + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + model.observabilityStream.observe(viewLifecycleOwner, EventObserver { event -> + eventsFlow.getAndUpdate { + it.toMutableList().apply { + add(0, event) + }.toList() + } + }) + } +} + +@Composable +fun ObservabilityComposable( + events: List<ObservabilityEvent>, + onDismiss: () -> Unit, +) { + AlertDialog( + title = { Text(stringResource(R.string.observability_title)) }, + text = { + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(events) { event -> + ObservabilityItem(event) + } + } + }, + onDismissRequest = onDismiss, + confirmButton = {}, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.close)) + } + } + ) +} + +@Composable +fun ObservabilityItem(event: ObservabilityEvent) { + val title = stringResource(event.titleRes) + val body = BackendManager.json.encodeToString(event) + val context = LocalContext.current + + ListItem( + modifier = Modifier.fillMaxWidth(), + headlineContent = { Text(title) }, + supportingContent = { Text(body, fontFamily = FontFamily.Monospace) }, + trailingContent = { + IconButton( + content = { Icon( + Icons.Default.ContentCopy, + contentDescription = stringResource(R.string.copy), + ) }, + onClick = { + copyToClipBoard(context, "Event", body) + } + ) + } + ) +}
\ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/events/ObservabilityEvent.kt b/wallet/src/main/java/net/taler/wallet/events/ObservabilityEvent.kt new file mode 100644 index 0000000..51e4f7a --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/events/ObservabilityEvent.kt @@ -0,0 +1,181 @@ +/* + * This file is part of GNU Taler + * (C) 2024 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.wallet.events + +import androidx.annotation.StringRes +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import net.taler.common.Timestamp +import net.taler.wallet.R +import net.taler.wallet.backend.TalerErrorInfo + +@Serializable +sealed class ObservabilityEvent { + @get:StringRes + abstract val titleRes: Int + + @Serializable + @SerialName("http-fetch-start") + data class HttpFetchStart( + val id: String, + @SerialName("when") + val timestamp: Timestamp, + val url: String, + + override val titleRes: Int = R.string.event_http_fetch_start, + ): ObservabilityEvent() + + @Serializable + @SerialName("http-fetch-finish-success") + data class HttpFetchFinishSuccess( + val id: String, + @SerialName("when") + val timestamp: Timestamp, + val url: String, + val status: Int, + + override val titleRes: Int = R.string.event_http_fetch_finish_success, + ): ObservabilityEvent() + + @Serializable + @SerialName("http-fetch-finish-error") + data class HttpFetchFinishError( + val id: String, + @SerialName("when") + val timestamp: Timestamp, + val url: String, + val error: TalerErrorInfo, + + override val titleRes: Int = R.string.event_http_fetch_finish_error, + ): ObservabilityEvent() + + @Serializable + @SerialName("db-query-start") + data class DbQueryStart( + val name: String, + val location: String, + + override val titleRes: Int = R.string.event_db_query_start, + ): ObservabilityEvent() + + @Serializable + @SerialName("db-query-finish-success") + data class DbQueryFinishSuccess( + val name: String, + val location: String, + + override val titleRes: Int = R.string.event_db_query_finish_success, + ): ObservabilityEvent() + + @Serializable + @SerialName("db-query-finish-error") + data class DbQueryFinishError( + val name: String, + val location: String, + + override val titleRes: Int = R.string.event_db_query_finish_error, + ): ObservabilityEvent() + + @Serializable + @SerialName("request-start") + data class RequestStart( + override val titleRes: Int = R.string.event_request_start, + ): ObservabilityEvent() + + @Serializable + @SerialName("request-finish-success") + data class RequestFinishSuccess( + override val titleRes: Int = R.string.event_request_finish_success, + ): ObservabilityEvent() + + @Serializable + @SerialName("request-finish-error") + data class RequestFinishError( + override val titleRes: Int = R.string.event_request_finish_error, + ): ObservabilityEvent() + + @Serializable + @SerialName("task-start") + data class TaskStart( + val taskId: String, + + override val titleRes: Int = R.string.event_task_start, + ): ObservabilityEvent() + + @Serializable + @SerialName("task-stop") + data class TaskStop( + val taskId: String, + + override val titleRes: Int = R.string.event_task_stop, + ): ObservabilityEvent() + + @Serializable + @SerialName("task-reset") + data class TaskReset( + val taskId: String, + + override val titleRes: Int = R.string.event_task_reset, + ): ObservabilityEvent() + + @Serializable + @SerialName("declare-task-dependency") + data class DeclareTaskDependency( + val taskId: String, + + override val titleRes: Int = R.string.event_declare_task_dependency, + ): ObservabilityEvent() + + @Serializable + @SerialName("crypto-start") + data class CryptoStart( + val operation: String, + + override val titleRes: Int = R.string.event_crypto_start, + ): ObservabilityEvent() + + @Serializable + @SerialName("crypto-finish-success") + data class CryptoFinishSuccess( + val operation: String, + + override val titleRes: Int = R.string.event_crypto_finished_success, + ): ObservabilityEvent() + + @Serializable + @SerialName("crypto-finish-error") + data class CryptoFinishError( + val operation: String, + + override val titleRes: Int = R.string.event_crypto_finished_error, + ): ObservabilityEvent() + + @Serializable + @SerialName("sheperd-task-result") + data class ShepherdTaskResult( + val resultType: String, + + override val titleRes: Int = R.string.event_shepherd_task_result, + ): ObservabilityEvent() + + @Serializable + @SerialName("unknown") + data class Unknown( + override val titleRes: Int = R.string.event_unknown, + ): ObservabilityEvent() +}
\ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt b/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt index ce0bd82..0015e1c 100644 --- a/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt +++ b/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt @@ -21,6 +21,12 @@ import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.cleanExchange @Serializable +data class BuiltinExchange( + val exchangeBaseUrl: String, + val currencyHint: String? = null, +) + +@Serializable data class ExchangeItem( val exchangeBaseUrl: String, // can be null before exchange info in wallet-core was fully loaded |