From ccc6b47f8daf6e07fbfdf91d9ac2b84313172968 Mon Sep 17 00:00:00 2001 From: Iván Ávalos Date: Wed, 27 Mar 2024 13:54:19 -0600 Subject: [wallet] Improve observability UI and make it globally reachable from the toolbar bug 0008509 --- .../src/main/java/net/taler/wallet/MainActivity.kt | 23 ++++++ .../main/java/net/taler/wallet/MainViewModel.kt | 15 ++-- .../net/taler/wallet/events/ObservabilityDialog.kt | 82 +++++++++++++--------- wallet/src/main/res/menu/global_dev.xml | 24 +++++++ wallet/src/main/res/values/strings.xml | 5 +- 5 files changed, 112 insertions(+), 37 deletions(-) create mode 100644 wallet/src/main/res/menu/global_dev.xml (limited to 'wallet') 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 cd1fbac..b4da875 100644 --- a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt +++ b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt @@ -51,6 +51,7 @@ 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", @@ -88,8 +89,8 @@ class MainViewModel( private val mTransactionsEvent = MutableLiveData>() val transactionsEvent: LiveData> = mTransactionsEvent - private val mObservabilityStream = MutableLiveData>() - val observabilityStream: LiveData> = mObservabilityStream + private val mObservabilityLog = MutableLiveData>(emptyList()) + val observabilityLog: LiveData> = mObservabilityLog private val mScanCodeEvent = MutableLiveData>() val scanCodeEvent: LiveData> = mScanCodeEvent @@ -112,8 +113,14 @@ class MainViewModel( balanceManager.loadBalances() } - if (payload.type == "task-observability-event" && payload.event != null) { - mObservabilityStream.postValue(payload.event.toEvent()) + if (payload.type == "task-observability-event" + && payload.event != null + && devMode.value == true) { + val logs = mObservabilityLog.value + ?.takeLast(OBSERVABILITY_LIMIT) + ?.toMutableList() ?: mutableListOf() + logs.add(payload.event) + mObservabilityLog.postValue(logs) } if (payload.type in transactionNotifications) viewModelScope.launch(Dispatchers.Main) { diff --git a/wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt b/wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt index 9164e77..eae5758 100644 --- a/wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt +++ b/wallet/src/main/java/net/taler/wallet/events/ObservabilityDialog.kt @@ -27,14 +27,19 @@ 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.Button import androidx.compose.material3.Icon import androidx.compose.material3.IconButton 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.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext @@ -42,18 +47,16 @@ 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.ExperimentalSerializationApi import kotlinx.serialization.encodeToString -import net.taler.common.EventObserver +import kotlinx.serialization.json.Json import net.taler.wallet.MainViewModel import net.taler.wallet.R -import net.taler.wallet.backend.BackendManager import net.taler.wallet.compose.copyToClipBoard +import net.taler.wallet.events.ObservabilityDialog.Companion.json class ObservabilityDialog: DialogFragment() { private val model: MainViewModel by activityViewModels() - private val eventsFlow: MutableStateFlow> = MutableStateFlow(emptyList()) override fun onCreateView( inflater: LayoutInflater, @@ -61,22 +64,19 @@ class ObservabilityDialog: DialogFragment() { savedInstanceState: Bundle? ): View = ComposeView(requireContext()).apply { setContent { - val events by eventsFlow.collectAsState() - ObservabilityComposable(events = events) { + val events by model.observabilityLog.observeAsState() + ObservabilityComposable(events?.reversed() ?: emptyList()) { 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() - } - }) + companion object { + @OptIn(ExperimentalSerializationApi::class) + val json = Json { + prettyPrint = true + prettyPrintIndent = " " + } } } @@ -85,45 +85,63 @@ fun ObservabilityComposable( events: List, 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) + ObservabilityItem(event, showJson) } } }, onDismissRequest = onDismiss, - confirmButton = {}, 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) { +fun ObservabilityItem( + event: ObservabilityEvent, + showJson: Boolean, +) { val context = LocalContext.current val title = event.getTitle(context) - val body = BackendManager.json.encodeToString(event.body) + val body = json.encodeToString(event.body) ListItem( modifier = Modifier.fillMaxWidth(), headlineContent = { Text(title) }, - supportingContent = { Text(body, fontFamily = FontFamily.Monospace) }, - trailingContent = { - IconButton( - content = { Icon( + supportingContent = if (!showJson) null else { -> + Text( + text = body, + fontFamily = FontFamily.Monospace, + style = MaterialTheme.typography.bodySmall, + ) + }, + trailingContent = if(!showJson) null else { -> + IconButton(onClick = { + copyToClipBoard(context, "Event", body) + }) { + 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/res/menu/global_dev.xml b/wallet/src/main/res/menu/global_dev.xml new file mode 100644 index 0000000..d6f73b9 --- /dev/null +++ b/wallet/src/main/res/menu/global_dev.xml @@ -0,0 +1,24 @@ + + + + + \ 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 31d007d..e597b4e 100644 --- a/wallet/src/main/res/values/strings.xml +++ b/wallet/src/main/res/values/strings.xml @@ -271,8 +271,11 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card Refuse Proposal (no action) - + + Show logs Internal event log + Show JSON + Hide JSON HTTP request started HTTP request succeeded HTTP request failed -- cgit v1.2.3