summaryrefslogtreecommitdiff
path: root/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
diff options
context:
space:
mode:
Diffstat (limited to 'cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt')
-rw-r--r--cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt232
1 files changed, 232 insertions, 0 deletions
diff --git a/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt b/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
new file mode 100644
index 0000000..4c618ac
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt
@@ -0,0 +1,232 @@
+/*
+ * 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.withdraw
+
+import android.app.Application
+import android.graphics.Bitmap
+import android.os.CountDownTimer
+import android.util.Log
+import androidx.annotation.UiThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import net.taler.cashier.BalanceResult
+import net.taler.cashier.HttpHelper.makeJsonGetRequest
+import net.taler.cashier.HttpHelper.makeJsonPostRequest
+import net.taler.cashier.HttpJsonResult.Error
+import net.taler.cashier.HttpJsonResult.Success
+import net.taler.cashier.MainViewModel
+import net.taler.cashier.R
+import org.json.JSONObject
+import java.util.concurrent.TimeUnit.MINUTES
+import java.util.concurrent.TimeUnit.SECONDS
+
+private val TAG = WithdrawManager::class.java.simpleName
+
+private val INTERVAL = SECONDS.toMillis(1)
+private val TIMEOUT = MINUTES.toMillis(2)
+
+class WithdrawManager(
+ private val app: Application,
+ private val viewModel: MainViewModel
+) {
+ private val scope
+ get() = viewModel.viewModelScope
+
+ private val config
+ get() = viewModel.config
+
+ private val currency: String?
+ get() = viewModel.currency.value
+
+ private var withdrawStatusCheck: Job? = null
+
+ private val mWithdrawAmount = MutableLiveData<String>()
+ val withdrawAmount: LiveData<String> = mWithdrawAmount
+
+ private val mWithdrawResult = MutableLiveData<WithdrawResult>()
+ val withdrawResult: LiveData<WithdrawResult> = mWithdrawResult
+
+ private val mWithdrawStatus = MutableLiveData<WithdrawStatus>()
+ val withdrawStatus: LiveData<WithdrawStatus> = mWithdrawStatus
+
+ private val mLastTransaction = MutableLiveData<LastTransaction>()
+ val lastTransaction: LiveData<LastTransaction> = mLastTransaction
+
+ @UiThread
+ fun hasSufficientBalance(amount: Int): Boolean {
+ val balanceResult = viewModel.balance.value
+ if (balanceResult !is BalanceResult.Success) return false
+ val balanceStr = balanceResult.amount.amount
+ val balanceDouble = balanceStr.toDouble()
+ return amount <= balanceDouble
+ }
+
+ @UiThread
+ fun withdraw(amount: Int) {
+ check(amount > 0) { "Withdraw amount was <= 0" }
+ check(currency != null) { "Currency is null" }
+ mWithdrawResult.value = null
+ mWithdrawAmount.value = "$amount $currency"
+ scope.launch(Dispatchers.IO) {
+ val url = "${config.bankUrl}/accounts/${config.username}/withdrawals"
+ Log.d(TAG, "Starting withdrawal at $url")
+ val body = JSONObject(mapOf("amount" to "${currency}:${amount}")).toString()
+ when (val result = makeJsonPostRequest(url, body, config)) {
+ is Success -> {
+ val talerUri = result.json.getString("taler_withdraw_uri")
+ val withdrawResult = WithdrawResult.Success(
+ id = result.json.getString("withdrawal_id"),
+ talerUri = talerUri,
+ qrCode = QrCodeManager.makeQrCode(talerUri)
+ )
+ mWithdrawResult.postValue(withdrawResult)
+ timer.start()
+ }
+ is Error -> {
+ val errorStr = app.getString(R.string.withdraw_error_fetch)
+ mWithdrawResult.postValue(WithdrawResult.Error(errorStr))
+ }
+ }
+ }
+ }
+
+ private val timer: CountDownTimer = object : CountDownTimer(TIMEOUT, INTERVAL) {
+ override fun onTick(millisUntilFinished: Long) {
+ val result = withdrawResult.value
+ if (result is WithdrawResult.Success) {
+ // check for active jobs and only do one at a time
+ val hasActiveCheck = withdrawStatusCheck?.isActive ?: false
+ if (!hasActiveCheck) {
+ withdrawStatusCheck = checkWithdrawStatus(result.id)
+ }
+ } else {
+ cancel()
+ }
+ }
+
+ override fun onFinish() {
+ abort()
+ mWithdrawStatus.postValue(WithdrawStatus.Error)
+ cancel()
+ }
+ }
+
+ private fun checkWithdrawStatus(withdrawalId: String) = scope.launch(Dispatchers.IO) {
+ val url = "${config.bankUrl}/accounts/${config.username}/withdrawals/${withdrawalId}"
+ Log.d(TAG, "Checking withdraw status at $url")
+ 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) {
+ 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))
+ }
+ }
+ }
+ }
+
+ private fun cancelWithdrawStatusCheck() {
+ timer.cancel()
+ withdrawStatusCheck?.cancel()
+ }
+
+ /**
+ * Aborts the last [withdrawResult], if it exists und there is no [withdrawStatus].
+ * Otherwise this is a no-op.
+ */
+ @UiThread
+ fun abort() {
+ val result = withdrawResult.value
+ val status = withdrawStatus.value
+ if (result is WithdrawResult.Success && status == null) {
+ cancelWithdrawStatusCheck()
+ abort(result.id)
+ }
+ }
+
+ private fun abort(withdrawalId: String) = scope.launch(Dispatchers.IO) {
+ val url = "${config.bankUrl}/accounts/${config.username}/withdrawals/${withdrawalId}/abort"
+ Log.d(TAG, "Aborting withdrawal at $url")
+ makeJsonPostRequest(url, "", config)
+ }
+
+ @UiThread
+ fun confirm(withdrawalId: String) {
+ mWithdrawStatus.value = WithdrawStatus.Confirming
+ scope.launch(Dispatchers.IO) {
+ val url =
+ "${config.bankUrl}/accounts/${config.username}/withdrawals/${withdrawalId}/confirm"
+ Log.d(TAG, "Confirming withdrawal at $url")
+ when (val result = makeJsonPostRequest(url, "", config)) {
+ is Success -> {
+ // no-op still waiting for [timer] to confirm our confirmation
+ }
+ is Error -> {
+ Log.e(TAG, "Error confirming withdrawal. Status code: ${result.statusCode}")
+ mWithdrawStatus.postValue(WithdrawStatus.Error)
+ }
+ }
+ }
+ }
+
+ @UiThread
+ fun completeTransaction() {
+ mLastTransaction.value = LastTransaction(withdrawAmount.value!!, withdrawStatus.value!!)
+ withdrawStatusCheck = null
+ mWithdrawAmount.value = null
+ mWithdrawResult.value = null
+ mWithdrawStatus.value = null
+ }
+
+}
+
+sealed class WithdrawResult {
+ object InsufficientBalance : WithdrawResult()
+ class Error(val msg: String) : WithdrawResult()
+ class Success(val id: String, val talerUri: String, val qrCode: Bitmap) : WithdrawResult()
+}
+
+sealed class WithdrawStatus {
+ object Error : WithdrawStatus()
+ object Aborted : WithdrawStatus()
+ class SelectionDone(val withdrawalId: String) : WithdrawStatus()
+ object Confirming : WithdrawStatus()
+ object Success : WithdrawStatus()
+}
+
+data class LastTransaction(
+ val withdrawAmount: String,
+ val withdrawStatus: WithdrawStatus
+)