[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-taler-android] branch master updated (dc2a707 -> f885557)
From: |
gnunet |
Subject: |
[taler-taler-android] branch master updated (dc2a707 -> f885557) |
Date: |
Mon, 01 Apr 2024 14:34:25 +0200 |
This is an automated email from the git hooks/post-receive script.
torsten-grote pushed a change to branch master
in repository taler-android.
from dc2a707 [wallet] Bump qtart to 0.10.1
new 4c78c29 [wallet] WIP: observability events
new c3c7cd0 [wallet] Deserialize observability event to JSON object
new ccc6b47 [wallet] Improve observability UI and make it globally
reachable from the toolbar
new e33bc72 [wallet] Make observability log atomic and improve dialog UI
new f885557 [wallet] Store logs in non-dev mode and remove event type
translations
The 5 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../src/main/java/net/taler/wallet/MainActivity.kt | 23 +++
.../main/java/net/taler/wallet/MainViewModel.kt | 28 +++-
.../java/net/taler/wallet/backend/ApiResponse.kt | 2 +
.../java/net/taler/wallet/backend/InitResponse.kt | 43 ++++++
.../net/taler/wallet/backend/WalletBackendApi.kt | 8 +
.../net/taler/wallet/events/ObservabilityDialog.kt | 163 +++++++++++++++++++++
.../net/taler/wallet/events/ObservabilityEvent.kt | 66 +++++++++
.../java/net/taler/wallet/exchanges/Exchanges.kt | 6 +
.../res/menu/{exchange_list.xml => global_dev.xml} | 11 +-
wallet/src/main/res/values/strings.xml | 6 +
10 files changed, 349 insertions(+), 7 deletions(-)
create mode 100644
wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt
create mode 100644
wallet/src/main/java/net/taler/wallet/events/ObservabilityEvent.kt
copy wallet/src/main/res/menu/{exchange_list.xml => global_dev.xml} (83%)
diff --git a/wallet/src/main/java/net/taler/wallet/MainActivity.kt
b/wallet/src/main/java/net/taler/wallet/MainActivity.kt
index 5dfd920..80b26a5 100644
--- a/wallet/src/main/java/net/taler/wallet/MainActivity.kt
+++ b/wallet/src/main/java/net/taler/wallet/MainActivity.kt
@@ -25,6 +25,7 @@ import android.content.IntentFilter
import android.net.Uri
import android.os.Bundle
import android.util.Log
+import android.view.Menu
import android.view.MenuItem
import android.view.View.GONE
import android.view.View.INVISIBLE
@@ -66,6 +67,7 @@ import
net.taler.wallet.HostCardEmulatorService.Companion.MERCHANT_NFC_CONNECTED
import
net.taler.wallet.HostCardEmulatorService.Companion.MERCHANT_NFC_DISCONNECTED
import
net.taler.wallet.HostCardEmulatorService.Companion.TRIGGER_PAYMENT_ACTION
import net.taler.wallet.databinding.ActivityMainBinding
+import net.taler.wallet.events.ObservabilityDialog
import net.taler.wallet.refund.RefundStatus
import java.io.IOException
import java.net.HttpURLConnection
@@ -144,6 +146,10 @@ class MainActivity : AppCompatActivity(),
OnNavigationItemSelectedListener,
model.networkManager.networkStatus.observe(this) { online ->
ui.content.offlineBanner.visibility = if (online) GONE else VISIBLE
}
+
+ model.devMode.observe(this) {
+ invalidateMenu()
+ }
}
@Deprecated("Deprecated in Java")
@@ -159,6 +165,14 @@ class MainActivity : AppCompatActivity(),
OnNavigationItemSelectedListener,
}
}
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ if (model.devMode.value == true) {
+ menuInflater.inflate(R.menu.global_dev, menu)
+ }
+
+ return super.onCreateOptionsMenu(menu)
+ }
+
override fun onNavigationItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.nav_home -> nav.navigate(R.id.nav_main)
@@ -168,6 +182,15 @@ class MainActivity : AppCompatActivity(),
OnNavigationItemSelectedListener,
return true
}
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.action_show_logs -> {
+ ObservabilityDialog().show(supportFragmentManager,
"OBSERVABILITY")
+ }
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
override fun onDestroy() {
unregisterReceiver(triggerPaymentReceiver)
unregisterReceiver(nfcConnectedReceiver)
diff --git a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
index 5903446..0f4b94a 100644
--- a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
+++ b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
@@ -24,12 +24,17 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.getAndUpdate
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 +43,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
@@ -48,11 +54,17 @@ import net.taler.wallet.withdraw.WithdrawManager
import org.json.JSONObject
const val TAG = "taler-wallet"
+const val OBSERVABILITY_LIMIT = 100
private val transactionNotifications = listOf(
"transaction-state-transition",
)
+private val observabilityNotifications = listOf(
+ "task-observability-event",
+ "request-observability-event",
+)
+
class MainViewModel(
app: Application,
) : AndroidViewModel(app), VersionReceiver, NotificationReceiver {
@@ -85,6 +97,9 @@ class MainViewModel(
private val mTransactionsEvent = MutableLiveData<Event<ScopeInfo>>()
val transactionsEvent: LiveData<Event<ScopeInfo>> = mTransactionsEvent
+ private val mObservabilityLog =
MutableStateFlow<List<ObservabilityEvent>>(emptyList())
+ val observabilityLog: StateFlow<List<ObservabilityEvent>> =
mObservabilityLog
+
private val mScanCodeEvent = MutableLiveData<Event<Boolean>>()
val scanCodeEvent: LiveData<Event<Boolean>> = mScanCodeEvent
@@ -97,13 +112,24 @@ 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 in observabilityNotifications && payload.event !=
null) {
+ mObservabilityLog.getAndUpdate { logs ->
+ logs.takeLast(OBSERVABILITY_LIMIT)
+ .toMutableList().apply {
+ add(payload.event)
+ }
+ }
+ }
+
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..0ce5c01
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.MaterialTheme
+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.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.unit.dp
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import net.taler.wallet.MainViewModel
+import net.taler.wallet.R
+import net.taler.wallet.compose.CopyToClipboardButton
+import net.taler.wallet.events.ObservabilityDialog.Companion.json
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+
+class ObservabilityDialog: DialogFragment() {
+ private val model: MainViewModel by activityViewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View = ComposeView(requireContext()).apply {
+ setContent {
+ val events by model.observabilityLog.collectAsState()
+ ObservabilityComposable(events.reversed()) {
+ dismiss()
+ }
+ }
+ }
+
+ companion object {
+ @OptIn(ExperimentalSerializationApi::class)
+ val json = Json {
+ prettyPrint = true
+ prettyPrintIndent = " "
+ }
+ }
+}
+
+@Composable
+fun ObservabilityComposable(
+ events: List<ObservabilityEvent>,
+ onDismiss: () -> Unit,
+) {
+ var showJson by remember { mutableStateOf(false) }
+
+ AlertDialog(
+ title = { Text(stringResource(R.string.observability_title)) },
+ text = {
+ LazyColumn(modifier = Modifier.fillMaxSize()) {
+ items(events) { event ->
+ ObservabilityItem(event, showJson)
+ }
+ }
+ },
+ onDismissRequest = onDismiss,
+ dismissButton = {
+ Button(onClick = { showJson = !showJson }) {
+ Text(if (showJson) {
+ stringResource(R.string.observability_hide_json)
+ } else {
+ stringResource(R.string.observability_show_json)
+ })
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = onDismiss) {
+ Text(stringResource(R.string.close))
+ }
+ },
+ )
+}
+
+@Composable
+fun ObservabilityItem(
+ event: ObservabilityEvent,
+ showJson: Boolean,
+) {
+ val body = json.encodeToString(event.body)
+ val timestamp = DateTimeFormatter
+ .ofLocalizedDateTime(FormatStyle.MEDIUM)
+ .format(event.timestamp)
+
+ ListItem(
+ modifier = Modifier.fillMaxWidth(),
+ headlineContent = { Text(event.type) },
+ overlineContent = { Text(timestamp) },
+ supportingContent = if (!showJson) null else { ->
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Box(
+ modifier = Modifier.background(
+ MaterialTheme.colorScheme.secondaryContainer,
+ shape = MaterialTheme.shapes.small,
+ )
+ ) {
+ Text(
+ modifier = Modifier
+ .padding(10.dp)
+ .fillMaxWidth(),
+ text = body,
+ fontFamily = FontFamily.Monospace,
+ style = MaterialTheme.typography.bodySmall,
+ )
+ }
+
+ CopyToClipboardButton(
+ label = "Event",
+ content = body,
+ colors = ButtonDefaults.textButtonColors(),
+ )
+ }
+ },
+ )
+}
\ 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..a50cde2
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/events/ObservabilityEvent.kt
@@ -0,0 +1,66 @@
+/*
+ * 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 kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import java.time.LocalDateTime
+
+
+@Serializable(with = ObservabilityEventSerializer::class)
+class ObservabilityEvent(
+ val body: JsonObject,
+ val timestamp: LocalDateTime,
+ val type: String,
+)
+
+class ObservabilityEventSerializer: KSerializer<ObservabilityEvent> {
+ private val jsonElementSerializer = JsonElement.serializer()
+
+ override val descriptor: SerialDescriptor
+ get() = jsonElementSerializer.descriptor
+
+ override fun deserialize(decoder: Decoder): ObservabilityEvent {
+ require(decoder is JsonDecoder)
+ val jsonObject = decoder
+ .decodeJsonElement()
+ .jsonObject
+
+ val type = jsonObject["type"]
+ ?.jsonPrimitive
+ ?.content
+ ?: "unknown"
+
+ return ObservabilityEvent(
+ body = jsonObject,
+ timestamp = LocalDateTime.now(),
+ type = type,
+ )
+ }
+
+ override fun serialize(encoder: Encoder, value: ObservabilityEvent) {
+ encoder.encodeSerializableValue(JsonObject.serializer(), value.body)
+ }
+}
\ 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
@@ -20,6 +20,12 @@ import kotlinx.serialization.Serializable
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,
diff --git a/wallet/src/main/res/menu/exchange_list.xml
b/wallet/src/main/res/menu/global_dev.xml
similarity index 83%
copy from wallet/src/main/res/menu/exchange_list.xml
copy to wallet/src/main/res/menu/global_dev.xml
index 21c028d..d6f73b9 100644
--- a/wallet/src/main/res/menu/exchange_list.xml
+++ b/wallet/src/main/res/menu/global_dev.xml
@@ -16,10 +16,9 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
-
<item
- android:id="@+id/action_add_dev_exchanges"
- android:title="@string/exchange_list_add_dev"
- app:showAsAction="never" />
-
-</menu>
+ android:id="@+id/action_show_logs"
+ android:title="@string/show_logs"
+ android:icon="@drawable/ic_bug_report"
+ app:showAsAction="ifRoom" />
+</menu>
\ No newline at end of file
diff --git a/wallet/src/main/res/values/strings.xml
b/wallet/src/main/res/values/strings.xml
index 2ec3d40..9e3c3a6 100644
--- a/wallet/src/main/res/values/strings.xml
+++ b/wallet/src/main/res/values/strings.xml
@@ -271,6 +271,12 @@ 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 -->
+ <string name="show_logs">Show logs</string>
+ <string name="observability_title">Internal event log</string>
+ <string name="observability_show_json">Show JSON</string>
+ <string name="observability_hide_json">Hide JSON</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>
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
- [taler-taler-android] branch master updated (dc2a707 -> f885557),
gnunet <=
- [taler-taler-android] 04/05: [wallet] Make observability log atomic and improve dialog UI, gnunet, 2024/04/01
- [taler-taler-android] 02/05: [wallet] Deserialize observability event to JSON object, gnunet, 2024/04/01
- [taler-taler-android] 01/05: [wallet] WIP: observability events, gnunet, 2024/04/01
- [taler-taler-android] 05/05: [wallet] Store logs in non-dev mode and remove event type translations, gnunet, 2024/04/01
- [taler-taler-android] 03/05: [wallet] Improve observability UI and make it globally reachable from the toolbar, gnunet, 2024/04/01