aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cashier/build.gradle16
-rw-r--r--cashier/src/main/java/net/taler/cashier/BalanceFragment.kt11
-rw-r--r--cashier/src/main/java/net/taler/cashier/HttpHelper.kt53
-rw-r--r--cashier/src/main/java/net/taler/cashier/MainActivity.kt7
-rw-r--r--cashier/src/main/java/net/taler/cashier/MainViewModel.kt132
-rw-r--r--cashier/src/main/java/net/taler/cashier/Response.kt86
-rw-r--r--cashier/src/main/java/net/taler/cashier/config/Config.kt41
-rw-r--r--cashier/src/main/java/net/taler/cashier/config/ConfigFragment.kt (renamed from cashier/src/main/java/net/taler/cashier/ConfigFragment.kt)19
-rw-r--r--cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt141
-rw-r--r--cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt56
-rw-r--r--cashier/src/main/res/navigation/nav_graph.xml2
-rw-r--r--merchant-lib/build.gradle2
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
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'
}