SettingsManager.kt
/*
* This file is part of GNU Taler
* (C) 2022 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.settings
import android.content.Context
import android.net.Uri
import android.util.Log
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import net.taler.wallet.R
import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.backend.WALLET_DB
import net.taler.wallet.main.ViewMode
import net.taler.wallet.backend.WalletBackendApi
import net.taler.wallet.backend.WalletResponse.Error
import net.taler.wallet.backend.WalletResponse.Success
import net.taler.wallet.balances.BalanceManager
import net.taler.wallet.main.TAG
import org.json.JSONObject
import java.io.File
class SettingsManager(
private val context: Context,
private val api: WalletBackendApi,
private val scope: CoroutineScope,
private val balanceManager: BalanceManager,
) {
private val mPerformanceTable = MutableStateFlow<PerformanceTable?>(null)
val performanceTable: StateFlow<PerformanceTable?> = mPerformanceTable
fun getViewMode() = context.userPreferencesDataStore.data.map { prefs ->
if (prefs.hasViewMode2()) {
ViewMode.fromPrefs(prefs.viewMode2)
} else {
null
}
}
fun saveViewMode(viewMode: ViewMode?) = scope.launch {
context.userPreferencesDataStore.updateData { current ->
val builder = current.toBuilder()
// Drop the deprecated pre-viewMode2 field if it lingers.
.clearViewMode()
if (viewMode != null) {
builder
.setViewMode2(viewMode.toPrefs())
.build()
} else {
builder
.clearViewMode2()
.build()
}
}
}
fun getActionButtonUsed(c: Context) = c.userPreferencesDataStore.data.map { prefs ->
if (prefs.hasActionButtonUsed()) {
prefs.actionButtonUsed
} else {
false
}
}
fun saveActionButtonUsed(c: Context) = scope.launch {
c.userPreferencesDataStore.updateData { current ->
current.toBuilder()
.setActionButtonUsed(true)
.build()
}
}
fun getDevModeEnabled(c: Context) = c.userPreferencesDataStore.data.map { prefs ->
if (prefs.hasDevModeEnabled()) {
prefs.devModeEnabled
} else {
false
}
}
fun setDevModeEnabled(c: Context, enabled: Boolean) = scope.launch {
c.userPreferencesDataStore.updateData { current ->
current.toBuilder()
.setDevModeEnabled(enabled)
.build()
}
}
fun getNewMainScreenEnabled(c: Context) = c.userPreferencesDataStore.data.map { prefs ->
if (prefs.hasNewMainScreenEnabled()) {
prefs.newMainScreenEnabled
} else {
false
}
}
fun setNewMainScreenEnabled(c: Context, enabled: Boolean) = scope.launch {
c.userPreferencesDataStore.updateData { current ->
current.toBuilder()
.setNewMainScreenEnabled(enabled)
.build()
}
}
fun getBiometricLockEnabled(c: Context) = c.userPreferencesDataStore.data.map { prefs ->
if (prefs.hasBiometricLockEnabled()) {
prefs.biometricLockEnabled
} else {
false
}
}
fun setBiometricLockEnabled(c: Context, enabled: Boolean) = scope.launch {
c.userPreferencesDataStore.updateData { current ->
current.toBuilder()
.setBiometricLockEnabled(enabled)
.build()
}
}
fun exportLogcat(uri: Uri?) {
if (uri == null) {
onLogExportError()
return
}
scope.launch(Dispatchers.IO) {
try {
context.contentResolver.openOutputStream(uri, "wt")?.use { outputStream ->
val command = arrayOf("logcat", "-d", "*:V")
val proc = Runtime.getRuntime().exec(command)
proc.inputStream.copyTo(outputStream)
} ?: onLogExportError()
} catch (e: Exception) {
Log.e(SettingsManager::class.simpleName, "Error exporting log: ", e)
onLogExportError()
return@launch
}
withContext(Dispatchers.Main) {
Toast.makeText(context, R.string.settings_logcat_success, LENGTH_LONG).show()
}
}
}
private fun onLogExportError() {
Toast.makeText(context, R.string.settings_logcat_error, LENGTH_LONG).show()
}
fun exportDb(uri: Uri?) {
if (uri == null) {
onDbExportError()
return
}
scope.launch(Dispatchers.IO) {
when (val response = api.rawRequest("exportDb")) {
is Success -> {
try {
context.contentResolver.openOutputStream(uri, "wt")?.use { outputStream ->
val data = Json.encodeToString(response.result)
val writer = outputStream.bufferedWriter()
writer.write(data)
writer.close()
}
} catch(e: Exception) {
Log.e(SettingsManager::class.simpleName, "Error exporting db: ", e)
withContext(Dispatchers.Main) {
onDbExportError()
}
return@launch
}
withContext(Dispatchers.Main) {
Toast.makeText(context, R.string.settings_db_export_success, LENGTH_LONG).show()
}
}
is Error -> {
Log.e(SettingsManager::class.simpleName, "Error exporting db: ${response.error}")
withContext(Dispatchers.Main) {
onDbExportError()
}
return@launch
}
}
}
}
fun exportRawDb(uri: Uri?) {
if (uri == null) {
onDbExportError()
return
}
scope.launch(Dispatchers.IO) {
try {
context.contentResolver.openOutputStream(uri, "wt")?.use { outputStream ->
val dbFile = File(context.filesDir, WALLET_DB)
dbFile.inputStream().use { it.copyTo(outputStream) }
} ?: onDbExportError()
} catch (e: Exception) {
Log.e(SettingsManager::class.simpleName, "Error exporting raw db: ", e)
withContext(Dispatchers.Main) {
onDbExportError()
}
return@launch
}
withContext(Dispatchers.Main) {
Toast.makeText(context, R.string.settings_db_export_success, LENGTH_LONG).show()
}
}
}
fun importDb(uri: Uri?) {
if (uri == null) {
onDbImportError()
return
}
scope.launch(Dispatchers.IO) {
context.contentResolver.openInputStream(uri)?.use { inputStream ->
try {
val reader = inputStream.bufferedReader()
val strData = reader.readText()
reader.close()
val jsonData = JSONObject(strData)
when (val response = api.rawRequest("importDb") {
put("dump", jsonData)
}) {
is Success -> {
withContext(Dispatchers.Main) {
Toast.makeText(context, R.string.settings_db_import_success, LENGTH_LONG).show()
balanceManager.loadAssets(true)
}
}
is Error -> {
Log.e(SettingsManager::class.simpleName, "Error importing db: ${response.error}")
withContext(Dispatchers.Main) {
onDbImportError()
}
return@launch
}
}
} catch (e: Exception) {
Log.e(SettingsManager::class.simpleName, "Error importing db: ", e)
withContext(Dispatchers.Main) {
onDbImportError()
}
return@launch
}
}
}
}
fun clearDb(onSuccess: () -> Unit) {
scope.launch {
when (val response = api.rawRequest("clearDb")) {
is Success -> {
onSuccess()
balanceManager.resetBalances()
}
is Error -> {
Log.e(SettingsManager::class.simpleName, "Error cleaning db: ${response.error}")
onDbClearError()
}
}
}
}
private fun onDbExportError() {
Toast.makeText(context, R.string.settings_db_export_error, LENGTH_LONG).show()
}
private fun onDbImportError() {
Toast.makeText(context, R.string.settings_db_import_error, LENGTH_LONG).show()
}
private fun onDbClearError() {
Toast.makeText(context, R.string.settings_db_clear_error, LENGTH_LONG).show()
}
fun runIntegrationTest(onError: (error: TalerErrorInfo) -> Unit) {
scope.launch {
api.request<Unit>("runIntegrationTestV2") {
put("amountToWithdraw", "KUDOS:42")
put("amountToSpend", "KUDOS:23")
put("corebankApiBaseUrl", "https://bank.demo.taler.net/")
put("exchangeBaseUrl", "https://exchange.demo.taler.net/")
put("merchantBaseUrl", "https://backend.demo.taler.net/instances/sandbox/")
put("merchantAuthToken", "secret-token:sandbox")
}.onError(onError)
}
}
fun loadPerformanceStats(limit: Int? = 10) {
scope.launch {
api.request(
"testingGetPerformanceStats",
TestingGetPerformanceStatsResponse.serializer(),
) {
limit?.let { put("limit", limit) }
this
}.onError { error ->
Log.e(TAG, "got testingGetPerformanceStats error result $error")
}.onSuccess { res ->
mPerformanceTable.value = res.stats
}
}
}
}