gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-taler-android] 01/02: [cashier] don't crash on unexpected network


From: gnunet
Subject: [taler-taler-android] 01/02: [cashier] don't crash on unexpected network input
Date: Thu, 27 Aug 2020 22:02:35 +0200

This is an automated email from the git hooks/post-receive script.

torsten-grote pushed a commit to branch master
in repository taler-android.

commit 53d99e46e6b34d4437f46266cb797a65c0736803
Author: Torsten Grote <t@grobox.de>
AuthorDate: Thu Aug 27 16:42:03 2020 -0300

    [cashier] don't crash on unexpected network input
---
 cashier/build.gradle                               |  16 ++-
 .../main/java/net/taler/cashier/BalanceFragment.kt |  11 +-
 .../src/main/java/net/taler/cashier/HttpHelper.kt  |  53 ++++----
 .../main/java/net/taler/cashier/MainActivity.kt    |   7 +-
 .../main/java/net/taler/cashier/MainViewModel.kt   | 132 ++++---------------
 .../src/main/java/net/taler/cashier/Response.kt    |  86 +++++++++++++
 .../main/java/net/taler/cashier/config/Config.kt   |  41 ++++++
 .../taler/cashier/{ => config}/ConfigFragment.kt   |  19 +--
 .../java/net/taler/cashier/config/ConfigManager.kt | 141 +++++++++++++++++++++
 .../net/taler/cashier/withdraw/WithdrawManager.kt  |  56 ++++----
 cashier/src/main/res/navigation/nav_graph.xml      |   2 +-
 merchant-lib/build.gradle                          |   2 +-
 12 files changed, 386 insertions(+), 180 deletions(-)

diff --git a/cashier/build.gradle b/cashier/build.gradle
index 916758b..4defd7a 100644
--- a/cashier/build.gradle
+++ b/cashier/build.gradle
@@ -14,10 +14,13 @@
  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-android-extensions'
-apply plugin: 'androidx.navigation.safeargs.kotlin'
+plugins {
+    id "com.android.application"
+    id "kotlin-android"
+    id "kotlin-android-extensions"
+    id "kotlinx-serialization"
+    id "androidx.navigation.safeargs.kotlin"
+}
 
 android {
     compileSdkVersion 29
@@ -66,8 +69,9 @@ dependencies {
     implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
     implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
 
-    // https://github.com/square/okhttp/releases
-    implementation "com.squareup.okhttp3:okhttp:3.12.12"
+    implementation "io.ktor:ktor-client:$ktor_version"
+    implementation "io.ktor:ktor-client-okhttp:$ktor_version"
+    implementation "io.ktor:ktor-client-serialization-jvm:$ktor_version"
 
     testImplementation 'junit:junit:4.13'
 
diff --git a/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt 
b/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt
index d899e7d..cdfa142 100644
--- a/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt
+++ b/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt
@@ -47,6 +47,7 @@ sealed class BalanceResult {
 class BalanceFragment : Fragment() {
 
     private val viewModel: MainViewModel by activityViewModels()
+    private val configManager by lazy { viewModel.configManager}
     private val withdrawManager by lazy { viewModel.withdrawManager }
 
     override fun onCreateView(
@@ -78,7 +79,7 @@ class BalanceFragment : Fragment() {
                 true
             } else false
         }
-        viewModel.currency.observe(viewLifecycleOwner, Observer { currency ->
+        configManager.currency.observe(viewLifecycleOwner, Observer { currency 
->
             currencyView.text = currency
         })
         confirmWithdrawalButton.setOnClickListener { 
onAmountConfirmed(getAmountFromView()) }
@@ -87,7 +88,7 @@ class BalanceFragment : Fragment() {
     override fun onStart() {
         super.onStart()
         // update balance if there's a config
-        if (viewModel.hasConfig()) {
+        if (configManager.hasConfig()) {
             viewModel.getBalance()
         }
     }
@@ -107,12 +108,12 @@ class BalanceFragment : Fragment() {
 
     override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
         R.id.action_reconfigure -> {
-            findNavController().navigate(viewModel.configDestination)
+            findNavController().navigate(configManager.configDestination)
             true
         }
         R.id.action_lock -> {
             viewModel.lock()
-            findNavController().navigate(viewModel.configDestination)
+            findNavController().navigate(configManager.configDestination)
             true
         }
         else -> super.onOptionsItemSelected(item)
@@ -148,7 +149,7 @@ class BalanceFragment : Fragment() {
 
     private fun getAmountFromView(): Amount {
         val str = amountView.editText!!.text.toString()
-        val currency = viewModel.currency.value!!
+        val currency = configManager.currency.value!!
         if (str.isBlank()) return Amount.zero(currency)
         return Amount.fromString(currency, str)
     }
diff --git a/cashier/src/main/java/net/taler/cashier/HttpHelper.kt 
b/cashier/src/main/java/net/taler/cashier/HttpHelper.kt
index 003c2f6..fd48b2d 100644
--- a/cashier/src/main/java/net/taler/cashier/HttpHelper.kt
+++ b/cashier/src/main/java/net/taler/cashier/HttpHelper.kt
@@ -18,12 +18,15 @@ package net.taler.cashier
 
 import android.util.Log
 import androidx.annotation.WorkerThread
+import net.taler.cashier.config.Config
+import okhttp3.Authenticator
 import okhttp3.Credentials
-import okhttp3.MediaType
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
 import okhttp3.OkHttpClient
 import okhttp3.Request
-import okhttp3.RequestBody
+import okhttp3.RequestBody.Companion.toRequestBody
 import okhttp3.Response
+import okhttp3.Route
 import org.json.JSONException
 import org.json.JSONObject
 
@@ -47,23 +50,23 @@ object HttpHelper {
             Log.e(TAG, "Error retrieving $url", e)
             return HttpJsonResult.Error(0)
         }
-        return if (response.code() == 200 && response.body() != null) {
-            val jsonObject = JSONObject(response.body()!!.string())
+        return if (response.code == 200 && response.body != null) {
+            val jsonObject = JSONObject(response.body!!.string())
             HttpJsonResult.Success(jsonObject)
         } else {
-            Log.e(TAG, "Received status ${response.code()} from $url expected 
200")
-            HttpJsonResult.Error(response.code(), getErrorBody(response))
+            Log.e(TAG, "Received status ${response.code} from $url expected 
200")
+            HttpJsonResult.Error(response.code, getErrorBody(response))
         }
     }
 
-    private val MEDIA_TYPE_JSON = MediaType.parse("$MIME_TYPE_JSON; 
charset=utf-8")
+    private val MEDIA_TYPE_JSON = "$MIME_TYPE_JSON; 
charset=utf-8".toMediaTypeOrNull()
 
     @WorkerThread
     fun makeJsonPostRequest(url: String, body: JSONObject, config: Config): 
HttpJsonResult {
         val request = Request.Builder()
             .addHeader("Accept", MIME_TYPE_JSON)
             .url(url)
-            .post(RequestBody.create(MEDIA_TYPE_JSON, body.toString()))
+            .post(body.toString().toRequestBody(MEDIA_TYPE_JSON))
             .build()
         val response = try {
             getHttpClient(config.username, config.password)
@@ -73,31 +76,33 @@ object HttpHelper {
             Log.e(TAG, "Error retrieving $url", e)
             return HttpJsonResult.Error(0)
         }
-        return if (response.code() == 200 && response.body() != null) {
-            val jsonObject = JSONObject(response.body()!!.string())
+        return if (response.code == 200 && response.body != null) {
+            val jsonObject = JSONObject(response.body!!.string())
             HttpJsonResult.Success(jsonObject)
         } else {
-            Log.e(TAG, "Received status ${response.code()} from $url expected 
200")
-            HttpJsonResult.Error(response.code(), getErrorBody(response))
+            Log.e(TAG, "Received status ${response.code} from $url expected 
200")
+            HttpJsonResult.Error(response.code, getErrorBody(response))
         }
     }
 
     private fun getHttpClient(username: String, password: String) =
-        OkHttpClient.Builder().authenticator { _, response ->
-            val credential = Credentials.basic(username, password)
-            if (credential == response.request().header("Authorization")) {
-                // If we already failed with these credentials, don't retry
-                return@authenticator null
+        OkHttpClient.Builder().authenticator(object : Authenticator {
+            override fun authenticate(route: Route?, response: Response): 
Request? {
+                val credential = Credentials.basic(username, password)
+                if (credential == response.request.header("Authorization")) {
+                    // If we already failed with these credentials, don't retry
+                    return null
+                }
+                return response
+                    .request
+                    .newBuilder()
+                    .header("Authorization", credential)
+                    .build()
             }
-            response
-                .request()
-                .newBuilder()
-                .header("Authorization", credential)
-                .build()
-        }.build()
+        }).build()
 
     private fun getErrorBody(response: Response): String? {
-        val body = response.body()?.string() ?: return null
+        val body = response.body?.string() ?: return null
         Log.e(TAG, "Response body: $body")
         return try {
             val json = JSONObject(body)
diff --git a/cashier/src/main/java/net/taler/cashier/MainActivity.kt 
b/cashier/src/main/java/net/taler/cashier/MainActivity.kt
index 0559b38..ae31be5 100644
--- a/cashier/src/main/java/net/taler/cashier/MainActivity.kt
+++ b/cashier/src/main/java/net/taler/cashier/MainActivity.kt
@@ -30,6 +30,7 @@ import kotlinx.android.synthetic.main.activity_main.*
 class MainActivity : AppCompatActivity() {
 
     private val viewModel: MainViewModel by viewModels()
+    private val configManager by lazy { viewModel.configManager}
     private lateinit var nav: NavController
 
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -43,13 +44,13 @@ class MainActivity : AppCompatActivity() {
 
     override fun onStart() {
         super.onStart()
-        if (!viewModel.hasConfig()) {
-            nav.navigate(viewModel.configDestination)
+        if (!configManager.hasConfig()) {
+            nav.navigate(configManager.configDestination)
         }
     }
 
     override fun onBackPressed() {
-        if (!viewModel.hasConfig() && nav.currentDestination?.id == 
R.id.configFragment) {
+        if (!configManager.hasConfig() && nav.currentDestination?.id == 
R.id.configFragment) {
             // we are in the configuration screen and need a config to continue
             val intent = Intent(ACTION_MAIN).apply {
                 addCategory(CATEGORY_HOME)
diff --git a/cashier/src/main/java/net/taler/cashier/MainViewModel.kt 
b/cashier/src/main/java/net/taler/cashier/MainViewModel.kt
index a25467b..95d94d7 100644
--- a/cashier/src/main/java/net/taler/cashier/MainViewModel.kt
+++ b/cashier/src/main/java/net/taler/cashier/MainViewModel.kt
@@ -16,126 +16,54 @@
 
 package net.taler.cashier
 
-import android.annotation.SuppressLint
 import android.app.Application
 import android.util.Log
-import androidx.annotation.UiThread
-import androidx.annotation.WorkerThread
 import androidx.lifecycle.AndroidViewModel
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.viewModelScope
-import androidx.security.crypto.EncryptedSharedPreferences
-import 
androidx.security.crypto.EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV
-import 
androidx.security.crypto.EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
-import androidx.security.crypto.MasterKeys
-import androidx.security.crypto.MasterKeys.AES256_GCM_SPEC
+import io.ktor.client.HttpClient
+import io.ktor.client.engine.okhttp.OkHttp
+import io.ktor.client.features.json.JsonFeature
+import io.ktor.client.features.json.serializer.KotlinxSerializer
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
+import kotlinx.serialization.json.Json
 import net.taler.cashier.HttpHelper.makeJsonGetRequest
+import net.taler.cashier.config.ConfigManager
 import net.taler.cashier.withdraw.WithdrawManager
-import net.taler.common.getIncompatibleStringOrNull
 import net.taler.common.isOnline
 import net.taler.lib.common.Amount
 import net.taler.lib.common.AmountParserException
-import net.taler.lib.common.Version
 
 private val TAG = MainViewModel::class.java.simpleName
 
-private val VERSION_BANK = Version(0, 0, 0)
-private const val PREF_NAME = "net.taler.cashier.prefs"
-private const val PREF_KEY_BANK_URL = "bankUrl"
-private const val PREF_KEY_USERNAME = "username"
-private const val PREF_KEY_PASSWORD = "password"
-private const val PREF_KEY_CURRENCY = "currency"
-
 class MainViewModel(private val app: Application) : AndroidViewModel(app) {
 
-    val configDestination = 
ConfigFragmentDirections.actionGlobalConfigFragment()
-
-    private val masterKeyAlias = MasterKeys.getOrCreate(AES256_GCM_SPEC)
-    private val prefs = EncryptedSharedPreferences.create(
-        PREF_NAME, masterKeyAlias, app, AES256_SIV, AES256_GCM
-    )
-
-    internal var config = Config(
-        bankUrl = prefs.getString(PREF_KEY_BANK_URL, "")!!,
-        username = prefs.getString(PREF_KEY_USERNAME, "")!!,
-        password = prefs.getString(PREF_KEY_PASSWORD, "")!!
-    )
-
-    private val mCurrency = MutableLiveData<String>(
-        prefs.getString(PREF_KEY_CURRENCY, null)
-    )
-    internal val currency: LiveData<String> = mCurrency
-
-    private val mConfigResult = MutableLiveData<ConfigResult>()
-    val configResult: LiveData<ConfigResult> = mConfigResult
+    private val httpClient = HttpClient(OkHttp) {
+        engine {
+            config {
+                retryOnConnectionFailure(true)
+            }
+        }
+        install(JsonFeature) {
+            serializer = KotlinxSerializer(
+                Json {
+                    ignoreUnknownKeys = true
+                }
+            )
+        }
+    }
+    val configManager = ConfigManager(app, viewModelScope, httpClient)
 
     private val mBalance = MutableLiveData<BalanceResult>()
     val balance: LiveData<BalanceResult> = mBalance
 
     internal val withdrawManager = WithdrawManager(app, this)
 
-    fun hasConfig() = config.bankUrl.isNotEmpty()
-            && config.username.isNotEmpty()
-            && config.password.isNotEmpty()
-
-    /**
-     * Start observing [configResult] after calling this to get the result 
async.
-     * Warning: Ignore null results that are used to reset old results.
-     */
-    @UiThread
-    fun checkAndSaveConfig(config: Config) {
-        mConfigResult.value = null
-        viewModelScope.launch(Dispatchers.IO) {
-            val url = "${config.bankUrl}/config"
-            Log.d(TAG, "Checking config: $url")
-            val result = when (val response = makeJsonGetRequest(url, config)) 
{
-                is HttpJsonResult.Success -> {
-                    // check if bank's version is compatible with app
-                    val version = response.json.getString("version")
-                    val versionIncompatible = 
VERSION_BANK.getIncompatibleStringOrNull(app, version)
-                    if (versionIncompatible != null) {
-                        ConfigResult.Error(false, versionIncompatible)
-                    } else {
-                        val currency = response.json.getString("currency")
-                        try {
-                            mCurrency.postValue(currency)
-                            prefs.edit().putString(PREF_KEY_CURRENCY, 
currency).apply()
-                            // save config
-                            saveConfig(config)
-                            ConfigResult.Success
-                        } catch (e: Exception) {
-                            ConfigResult.Error(false, "Invalid Config: 
${response.json}")
-                        }
-                    }
-                }
-                is HttpJsonResult.Error -> {
-                    if (response.statusCode > 0 && app.isOnline()) {
-                        ConfigResult.Error(response.statusCode == 401, 
response.msg)
-                    } else {
-                        ConfigResult.Offline
-                    }
-                }
-            }
-            mConfigResult.postValue(result)
-        }
-    }
-
-    @WorkerThread
-    @SuppressLint("ApplySharedPref")
-    private fun saveConfig(config: Config) {
-        this.config = config
-        prefs.edit()
-            .putString(PREF_KEY_BANK_URL, config.bankUrl)
-            .putString(PREF_KEY_USERNAME, config.username)
-            .putString(PREF_KEY_PASSWORD, config.password)
-            .commit()
-    }
-
     fun getBalance() = viewModelScope.launch(Dispatchers.IO) {
-        check(hasConfig()) { "No config to get balance" }
+        check(configManager.hasConfig()) { "No config to get balance" }
+        val config = configManager.config
         val url = "${config.bankUrl}/accounts/${config.username}/balance"
         Log.d(TAG, "Checking balance at $url")
         val result = when (val response = makeJsonGetRequest(url, config)) {
@@ -163,19 +91,7 @@ class MainViewModel(private val app: Application) : 
AndroidViewModel(app) {
     }
 
     fun lock() {
-        saveConfig(config.copy(password = ""))
+        configManager.lock()
     }
 
 }
-
-data class Config(
-    val bankUrl: String,
-    val username: String,
-    val password: String
-)
-
-sealed class ConfigResult {
-    class Error(val authError: Boolean, val msg: String) : ConfigResult()
-    object Offline : ConfigResult()
-    object Success : ConfigResult()
-}
diff --git a/cashier/src/main/java/net/taler/cashier/Response.kt 
b/cashier/src/main/java/net/taler/cashier/Response.kt
new file mode 100644
index 0000000..0ad39d0
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/Response.kt
@@ -0,0 +1,86 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 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.cashier
+
+import android.content.Context
+import android.util.Log
+import io.ktor.client.call.receive
+import io.ktor.client.features.ResponseException
+import io.ktor.http.HttpStatusCode
+import kotlinx.serialization.Serializable
+import net.taler.common.isOnline
+import java.net.UnknownHostException
+
+class Response<out T> private constructor(
+    private val value: Any?
+) {
+    companion object {
+        suspend fun <T> response(request: suspend () -> T): Response<T> {
+            return try {
+                Response(request())
+            } catch (e: Throwable) {
+                Log.e("HttpClient", "Error getting request", e)
+                Response(getFailure(e))
+            }
+        }
+
+        private suspend fun getFailure(e: Throwable): Failure = when (e) {
+            is ResponseException -> Failure(e, getExceptionString(e), 
e.response?.status)
+            else -> Failure(e, e.toString())
+        }
+
+        private suspend fun getExceptionString(e: ResponseException): String {
+            val response = e.response ?: return e.toString()
+            return try {
+                Log.e("TEST", "TRY RECEIVE $response")
+                val error: Error = response.receive()
+                "Error ${error.code}: ${error.hint}"
+            } catch (ex: Exception) {
+                "Status code: ${response.status.value}"
+            }
+        }
+    }
+
+    private val isFailure: Boolean get() = value is Failure
+
+    suspend fun onSuccess(block: suspend (result: T) -> Unit): Response<T> {
+        @Suppress("UNCHECKED_CAST")
+        if (!isFailure) block(value as T)
+        return this
+    }
+
+    suspend fun onError(block: suspend (failure: Failure) -> Unit): 
Response<T> {
+        if (value is Failure) block(value)
+        return this
+    }
+
+    data class Failure(
+        val exception: Throwable,
+        val msg: String,
+        val statusCode: HttpStatusCode? = null
+    ) {
+        fun isOffline(context: Context): Boolean {
+            return exception is UnknownHostException && !context.isOnline()
+        }
+    }
+
+    @Serializable
+    private class Error(
+        val code: Int?,
+        val hint: String?
+    )
+}
diff --git a/cashier/src/main/java/net/taler/cashier/config/Config.kt 
b/cashier/src/main/java/net/taler/cashier/config/Config.kt
new file mode 100644
index 0000000..b50cf92
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/config/Config.kt
@@ -0,0 +1,41 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 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.cashier.config
+
+import kotlinx.serialization.Serializable
+import net.taler.lib.common.Version
+import okhttp3.Credentials
+
+data class Config(
+    val bankUrl: String,
+    val username: String,
+    val password: String
+) {
+    val basicAuth: String get() = Credentials.basic(username, password)
+}
+
+@Serializable
+data class ConfigResponse(
+    val version: String,
+    val currency: String
+)
+
+sealed class ConfigResult {
+    class Error(val authError: Boolean, val msg: String) : ConfigResult()
+    object Offline : ConfigResult()
+    object Success : ConfigResult()
+}
diff --git a/cashier/src/main/java/net/taler/cashier/ConfigFragment.kt 
b/cashier/src/main/java/net/taler/cashier/config/ConfigFragment.kt
similarity index 89%
rename from cashier/src/main/java/net/taler/cashier/ConfigFragment.kt
rename to cashier/src/main/java/net/taler/cashier/config/ConfigFragment.kt
index 71495f3..a7aaf2f 100644
--- a/cashier/src/main/java/net/taler/cashier/ConfigFragment.kt
+++ b/cashier/src/main/java/net/taler/cashier/config/ConfigFragment.kt
@@ -14,7 +14,7 @@
  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-package net.taler.cashier
+package net.taler.cashier.config
 
 import android.os.Bundle
 import android.text.method.LinkMovementMethod
@@ -34,6 +34,8 @@ import androidx.navigation.fragment.findNavController
 import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG
 import com.google.android.material.snackbar.Snackbar
 import kotlinx.android.synthetic.main.fragment_config.*
+import net.taler.cashier.MainViewModel
+import net.taler.cashier.R
 import net.taler.common.exhaustive
 
 private const val URL_BANK_TEST = "https://bank.test.taler.net";
@@ -42,6 +44,7 @@ private const val URL_BANK_TEST_REGISTER = 
"$URL_BANK_TEST/accounts/register"
 class ConfigFragment : Fragment() {
 
     private val viewModel: MainViewModel by activityViewModels()
+    private val configManager by lazy { viewModel.configManager}
 
     override fun onCreateView(
         inflater: LayoutInflater,
@@ -53,13 +56,13 @@ class ConfigFragment : Fragment() {
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         if (savedInstanceState == null) {
-            if (viewModel.config.bankUrl.isBlank()) {
+            if (configManager.config.bankUrl.isBlank()) {
                 urlView.editText!!.setText(URL_BANK_TEST)
             } else {
-                urlView.editText!!.setText(viewModel.config.bankUrl)
+                urlView.editText!!.setText(configManager.config.bankUrl)
             }
-            usernameView.editText!!.setText(viewModel.config.username)
-            passwordView.editText!!.setText(viewModel.config.password)
+            usernameView.editText!!.setText(configManager.config.username)
+            passwordView.editText!!.setText(configManager.config.password)
         } else {
             
urlView.editText!!.setText(savedInstanceState.getCharSequence("urlView"))
             
usernameView.editText!!.setText(savedInstanceState.getCharSequence("usernameView"))
@@ -76,8 +79,8 @@ class ConfigFragment : Fragment() {
                 saveButton.visibility = INVISIBLE
                 progressBar.visibility = VISIBLE
                 // kick off check and observe result
-                viewModel.checkAndSaveConfig(config)
-                viewModel.configResult.observe(viewLifecycleOwner, 
onConfigResult)
+                configManager.checkAndSaveConfig(config)
+                configManager.configResult.observe(viewLifecycleOwner, 
onConfigResult)
                 // hide keyboard
                 val inputMethodManager =
                     getSystemService(requireContext(), 
InputMethodManager::class.java)!!
@@ -145,7 +148,7 @@ class ConfigFragment : Fragment() {
         }.exhaustive
         saveButton.visibility = VISIBLE
         progressBar.visibility = INVISIBLE
-        viewModel.configResult.removeObservers(viewLifecycleOwner)
+        configManager.configResult.removeObservers(viewLifecycleOwner)
     }
 
 }
diff --git a/cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt 
b/cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt
new file mode 100644
index 0000000..a18073d
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt
@@ -0,0 +1,141 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 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.cashier.config
+
+import android.annotation.SuppressLint
+import android.app.Application
+import android.util.Log
+import androidx.annotation.UiThread
+import androidx.annotation.WorkerThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.security.crypto.EncryptedSharedPreferences
+import androidx.security.crypto.MasterKeys
+import io.ktor.client.HttpClient
+import io.ktor.client.request.get
+import io.ktor.client.request.header
+import io.ktor.http.HttpHeaders.Authorization
+import io.ktor.http.HttpStatusCode.Companion.Unauthorized
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import net.taler.cashier.Response
+import net.taler.cashier.Response.Companion.response
+import net.taler.common.getIncompatibleStringOrNull
+import net.taler.lib.common.Version
+
+private val VERSION_BANK = Version(0, 0, 0)
+private const val PREF_NAME = "net.taler.cashier.prefs"
+private const val PREF_KEY_BANK_URL = "bankUrl"
+private const val PREF_KEY_USERNAME = "username"
+private const val PREF_KEY_PASSWORD = "password"
+private const val PREF_KEY_CURRENCY = "currency"
+
+private val TAG = ConfigManager::class.java.simpleName
+
+class ConfigManager(
+    private val app: Application,
+    private val scope: CoroutineScope,
+    private val httpClient: HttpClient
+) {
+
+    val configDestination = 
ConfigFragmentDirections.actionGlobalConfigFragment()
+
+    private val masterKeyAlias = 
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
+    private val prefs = EncryptedSharedPreferences.create(
+        PREF_NAME, masterKeyAlias, app,
+        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
+    )
+
+    internal var config = Config(
+        bankUrl = prefs.getString(PREF_KEY_BANK_URL, "")!!,
+        username = prefs.getString(PREF_KEY_USERNAME, "")!!,
+        password = prefs.getString(PREF_KEY_PASSWORD, "")!!
+    )
+
+    private val mCurrency = MutableLiveData<String>(
+        prefs.getString(PREF_KEY_CURRENCY, null)
+    )
+    internal val currency: LiveData<String> = mCurrency
+
+    private val mConfigResult = MutableLiveData<ConfigResult>()
+    val configResult: LiveData<ConfigResult> = mConfigResult
+
+    fun hasConfig() = config.bankUrl.isNotEmpty()
+            && config.username.isNotEmpty()
+            && config.password.isNotEmpty()
+
+    /**
+     * Start observing [configResult] after calling this to get the result 
async.
+     * Warning: Ignore null results that are used to reset old results.
+     */
+    @UiThread
+    fun checkAndSaveConfig(config: Config) = scope.launch {
+        mConfigResult.value = null
+        checkConfig(config).onError { failure ->
+            val result = if (failure.isOffline(app)) {
+                ConfigResult.Offline
+            } else {
+                ConfigResult.Error(failure.statusCode == Unauthorized, 
failure.msg)
+            }
+            mConfigResult.postValue(result)
+        }.onSuccess { response ->
+            val versionIncompatible =
+                VERSION_BANK.getIncompatibleStringOrNull(app, response.version)
+            val result = if (versionIncompatible != null) {
+                ConfigResult.Error(false, versionIncompatible)
+            } else {
+                mCurrency.postValue(response.currency)
+                prefs.edit().putString(PREF_KEY_CURRENCY, 
response.currency).apply()
+                // save config
+                saveConfig(config)
+                ConfigResult.Success
+            }
+            mConfigResult.postValue(result)
+        }
+    }
+
+    private suspend fun checkConfig(config: Config): Response<ConfigResponse> =
+        withContext(Dispatchers.IO) {
+            val url = "${config.bankUrl}/config"
+            Log.d(TAG, "Checking config: $url")
+            response {
+                httpClient.get(url) {
+                    // TODO why does that not fail already?
+                    header(Authorization, config.basicAuth)
+                } as ConfigResponse
+            }
+        }
+
+    @WorkerThread
+    @SuppressLint("ApplySharedPref")
+    internal fun saveConfig(config: Config) {
+        this.config = config
+        prefs.edit()
+            .putString(PREF_KEY_BANK_URL, config.bankUrl)
+            .putString(PREF_KEY_USERNAME, config.username)
+            .putString(PREF_KEY_PASSWORD, config.password)
+            .commit()
+    }
+
+    fun lock() {
+        saveConfig(config.copy(password = ""))
+    }
+
+}
diff --git 
a/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt 
b/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
index 9f3cf54..30ff3d8 100644
--- a/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
+++ b/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
@@ -54,10 +54,10 @@ class WithdrawManager(
         get() = viewModel.viewModelScope
 
     private val config
-        get() = viewModel.config
+        get() = viewModel.configManager.config
 
     private val currency: String?
-        get() = viewModel.currency.value
+        get() = viewModel.configManager.currency.value
 
     private var withdrawStatusCheck: Job? = null
 
@@ -93,13 +93,17 @@ class WithdrawManager(
             val body = JSONObject(map)
             val result = when (val response = makeJsonPostRequest(url, body, 
config)) {
                 is Success -> {
-                    val talerUri = 
response.json.getString("taler_withdraw_uri")
-                    val withdrawResult = WithdrawResult.Success(
-                        id = response.json.getString("withdrawal_id"),
-                        talerUri = talerUri,
-                        qrCode = makeQrCode(talerUri)
-                    )
-                    withdrawResult
+                    try {
+                        val talerUri = 
response.json.getString("taler_withdraw_uri")
+                        val withdrawResult = WithdrawResult.Success(
+                            id = response.json.getString("withdrawal_id"),
+                            talerUri = talerUri,
+                            qrCode = makeQrCode(talerUri)
+                        )
+                        withdrawResult
+                    } catch (e: Exception) {
+                        WithdrawResult.Error(e.toString())
+                    }
                 }
                 is Error -> {
                     if (response.statusCode > 0 && app.isOnline()) {
@@ -147,25 +151,29 @@ class WithdrawManager(
         val response = makeJsonGetRequest(url, config)
         if (response !is Success) return@launch  // ignore errors and continue 
trying
         val oldStatus = mWithdrawStatus.value
-        when {
-            response.json.getBoolean("aborted") -> {
-                cancelWithdrawStatusCheck()
-                mWithdrawStatus.postValue(WithdrawStatus.Aborted)
-            }
-            response.json.getBoolean("confirmation_done") -> {
-                if (oldStatus !is WithdrawStatus.Success) {
+        try {
+            when {
+                response.json.getBoolean("aborted") -> {
                     cancelWithdrawStatusCheck()
-                    mWithdrawStatus.postValue(WithdrawStatus.Success)
-                    viewModel.getBalance()
+                    mWithdrawStatus.postValue(WithdrawStatus.Aborted)
                 }
-            }
-            response.json.getBoolean("selection_done") -> {
-                // only update status, if there's none, yet
-                // so we don't re-notify or overwrite newer status info
-                if (oldStatus == null) {
-                    
mWithdrawStatus.postValue(WithdrawStatus.SelectionDone(withdrawalId))
+                response.json.getBoolean("confirmation_done") -> {
+                    if (oldStatus !is WithdrawStatus.Success) {
+                        cancelWithdrawStatusCheck()
+                        mWithdrawStatus.postValue(WithdrawStatus.Success)
+                        viewModel.getBalance()
+                    }
+                }
+                response.json.getBoolean("selection_done") -> {
+                    // only update status, if there's none, yet
+                    // so we don't re-notify or overwrite newer status info
+                    if (oldStatus == null) {
+                        
mWithdrawStatus.postValue(WithdrawStatus.SelectionDone(withdrawalId))
+                    }
                 }
             }
+        } catch (e: Exception) {
+            mWithdrawStatus.postValue(WithdrawStatus.Error(e.toString()))
         }
     }
 
diff --git a/cashier/src/main/res/navigation/nav_graph.xml 
b/cashier/src/main/res/navigation/nav_graph.xml
index 49f8881..9cce316 100644
--- a/cashier/src/main/res/navigation/nav_graph.xml
+++ b/cashier/src/main/res/navigation/nav_graph.xml
@@ -23,7 +23,7 @@
 
     <fragment
         android:id="@+id/configFragment"
-        android:name="net.taler.cashier.ConfigFragment"
+        android:name="net.taler.cashier.config.ConfigFragment"
         android:label="ConfigFragment"
         tools:layout="@layout/fragment_config">
         <action
diff --git a/merchant-lib/build.gradle b/merchant-lib/build.gradle
index 9b349ea..d76f867 100644
--- a/merchant-lib/build.gradle
+++ b/merchant-lib/build.gradle
@@ -54,5 +54,5 @@ dependencies {
     testImplementation 'junit:junit:4.13'
     testImplementation "io.ktor:ktor-client-mock-jvm:$ktor_version"
     testImplementation "io.ktor:ktor-client-logging-jvm:$ktor_version"
-    testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.8'
+    testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.9'
 }

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]