summaryrefslogtreecommitdiff
path: root/cashier/src/main/java/net/taler/cashier/config
diff options
context:
space:
mode:
Diffstat (limited to 'cashier/src/main/java/net/taler/cashier/config')
-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.kt154
-rw-r--r--cashier/src/main/java/net/taler/cashier/config/ConfigManager.kt141
3 files changed, 336 insertions, 0 deletions
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/config/ConfigFragment.kt b/cashier/src/main/java/net/taler/cashier/config/ConfigFragment.kt
new file mode 100644
index 0000000..a7aaf2f
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/config/ConfigFragment.kt
@@ -0,0 +1,154 @@
+/*
+ * 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.os.Bundle
+import android.text.method.LinkMovementMethod
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.view.inputmethod.InputMethodManager
+import androidx.core.content.ContextCompat.getSystemService
+import androidx.core.text.HtmlCompat
+import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+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"
+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,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_config, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ if (savedInstanceState == null) {
+ if (configManager.config.bankUrl.isBlank()) {
+ urlView.editText!!.setText(URL_BANK_TEST)
+ } else {
+ urlView.editText!!.setText(configManager.config.bankUrl)
+ }
+ 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"))
+ passwordView.editText!!.setText(savedInstanceState.getCharSequence("passwordView"))
+ }
+ saveButton.setOnClickListener {
+ val config = Config(
+ bankUrl = urlView.editText!!.text.toString(),
+ username = usernameView.editText!!.text.toString(),
+ password = passwordView.editText!!.text.toString()
+ )
+ if (checkConfig(config)) {
+ // show progress
+ saveButton.visibility = INVISIBLE
+ progressBar.visibility = VISIBLE
+ // kick off check and observe result
+ configManager.checkAndSaveConfig(config)
+ configManager.configResult.observe(viewLifecycleOwner, onConfigResult)
+ // hide keyboard
+ val inputMethodManager =
+ getSystemService(requireContext(), InputMethodManager::class.java)!!
+ inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
+ }
+ }
+ demoView.text = HtmlCompat.fromHtml(
+ getString(R.string.config_demo_hint, URL_BANK_TEST_REGISTER), FROM_HTML_MODE_LEGACY
+ )
+ demoView.movementMethod = LinkMovementMethod.getInstance()
+ }
+
+ override fun onStart() {
+ super.onStart()
+ // focus on password if it is the only missing value (like after locking)
+ if (urlView.editText!!.text.isNotBlank()
+ && usernameView.editText!!.text.isNotBlank()
+ && passwordView.editText!!.text.isBlank()
+ ) {
+ passwordView.editText!!.requestFocus()
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ // for some reason automatic restore isn't working at the moment!?
+ outState.putCharSequence("urlView", urlView.editText?.text)
+ outState.putCharSequence("usernameView", usernameView.editText?.text)
+ outState.putCharSequence("passwordView", passwordView.editText?.text)
+ }
+
+ private fun checkConfig(config: Config): Boolean {
+ if (!config.bankUrl.startsWith("https://")) {
+ urlView.error = getString(R.string.config_bank_url_error)
+ urlView.requestFocus()
+ return false
+ }
+ if (config.username.isBlank()) {
+ usernameView.error = getString(R.string.config_username_error)
+ usernameView.requestFocus()
+ return false
+ }
+ urlView.isErrorEnabled = false
+ return true
+ }
+
+ private val onConfigResult = Observer<ConfigResult> { result ->
+ if (result == null) return@Observer
+ when (result) {
+ is ConfigResult.Success -> {
+ val action = ConfigFragmentDirections.actionConfigFragmentToBalanceFragment()
+ findNavController().navigate(action)
+ }
+ ConfigResult.Offline -> {
+ Snackbar.make(requireView(), R.string.config_error_offline, LENGTH_LONG).show()
+ }
+ is ConfigResult.Error -> {
+ if (result.authError) {
+ Snackbar.make(requireView(), R.string.config_error_auth, LENGTH_LONG).show()
+ } else {
+ val str = getString(R.string.config_error, result.msg)
+ Snackbar.make(requireView(), str, LENGTH_LONG).show()
+ }
+ }
+ }.exhaustive
+ saveButton.visibility = VISIBLE
+ progressBar.visibility = INVISIBLE
+ 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 = ""))
+ }
+
+}