summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIván Ávalos <avalos@disroot.org>2024-03-27 12:08:29 -0600
committerIván Ávalos <avalos@disroot.org>2024-03-28 12:21:11 -0600
commit4c78c29776fdd482e13fc445b68ad7fe091b4def (patch)
tree39eec0484c596a61a04d22c87d5f4ee541fb2ee5
parentdc2a7071aa9f056f45890b35373be1e6c4c30d20 (diff)
downloadtaler-android-4c78c29776fdd482e13fc445b68ad7fe091b4def.tar.gz
taler-android-4c78c29776fdd482e13fc445b68ad7fe091b4def.tar.bz2
taler-android-4c78c29776fdd482e13fc445b68ad7fe091b4def.zip
[wallet] WIP: observability events
bug 0008509
-rw-r--r--wallet/src/main/java/net/taler/wallet/MainViewModel.kt14
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt2
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt43
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt8
-rw-r--r--wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt129
-rw-r--r--wallet/src/main/java/net/taler/wallet/events/ObservabilityEvent.kt181
-rw-r--r--wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt6
-rw-r--r--wallet/src/main/res/values/strings.xml21
8 files changed, 403 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
diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml
index 2ec3d40..db2630f 100644
--- a/wallet/src/main/res/values/strings.xml
+++ b/wallet/src/main/res/values/strings.xml
@@ -271,6 +271,27 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="pending_operations_refuse">Refuse Proposal</string>
<string name="pending_operations_no_action">(no action)</string>
+ <!-- Observability events -->
+ <string name="observability_title">Internal event log</string>
+ <string name="event_http_fetch_start">HTTP request started</string>
+ <string name="event_http_fetch_finish_success">HTTP request succeeded</string>
+ <string name="event_http_fetch_finish_error">HTTP request failed</string>
+ <string name="event_db_query_start">Database query started</string>
+ <string name="event_db_query_finish_success">Database query succeeded</string>
+ <string name="event_db_query_finish_error">Database query failed</string>
+ <string name="event_request_start">Request started</string>
+ <string name="event_request_finish_success">Request succeeded</string>
+ <string name="event_request_finish_error">Request failed</string>
+ <string name="event_task_start">Task started</string>
+ <string name="event_task_stop">Task stopped</string>
+ <string name="event_task_reset">Task reset</string>
+ <string name="event_declare_task_dependency">Task dependency declared</string>
+ <string name="event_crypto_start">Crypto operation started</string>
+ <string name="event_crypto_finished_success">Crypto operation succeeded</string>
+ <string name="event_crypto_finished_error">Crypto operation finished</string>
+ <string name="event_shepherd_task_result">Shepherd task result</string>
+ <string name="event_unknown">Unknown event</string>
+
<string name="settings_dev_mode">Developer Mode</string>
<string name="settings_dev_mode_summary">Shows more information intended for debugging</string>
<string name="settings_withdraw_testkudos">Withdraw TESTKUDOS</string>