summaryrefslogtreecommitdiff
path: root/cashier/src/main/java/net/taler/cashier/withdraw
diff options
context:
space:
mode:
authorTorsten Grote <t@grobox.de>2020-03-18 14:24:41 -0300
committerTorsten Grote <t@grobox.de>2020-03-18 14:24:41 -0300
commita4796ec47d89a851b260b6fc195494547208a025 (patch)
treed2941b68ff2ce22c523e7aa634965033b1100560 /cashier/src/main/java/net/taler/cashier/withdraw
downloadtaler-android-a4796ec47d89a851b260b6fc195494547208a025.tar.gz
taler-android-a4796ec47d89a851b260b6fc195494547208a025.tar.bz2
taler-android-a4796ec47d89a851b260b6fc195494547208a025.zip
Merge all three apps into one repository
Diffstat (limited to 'cashier/src/main/java/net/taler/cashier/withdraw')
-rw-r--r--cashier/src/main/java/net/taler/cashier/withdraw/ErrorFragment.kt55
-rw-r--r--cashier/src/main/java/net/taler/cashier/withdraw/NfcManager.kt234
-rw-r--r--cashier/src/main/java/net/taler/cashier/withdraw/QrCodeManager.kt42
-rw-r--r--cashier/src/main/java/net/taler/cashier/withdraw/TransactionFragment.kt174
-rw-r--r--cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt232
5 files changed, 737 insertions, 0 deletions
diff --git a/cashier/src/main/java/net/taler/cashier/withdraw/ErrorFragment.kt b/cashier/src/main/java/net/taler/cashier/withdraw/ErrorFragment.kt
new file mode 100644
index 0000000..ceffcec
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/withdraw/ErrorFragment.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import kotlinx.android.synthetic.main.fragment_error.*
+import net.taler.cashier.MainViewModel
+import net.taler.cashier.R
+
+class ErrorFragment : Fragment() {
+
+ private val viewModel: MainViewModel by activityViewModels()
+ private val withdrawManager by lazy { viewModel.withdrawManager }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_error, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ withdrawManager.withdrawStatus.observe(viewLifecycleOwner, Observer { status ->
+ if (status is WithdrawStatus.Aborted) {
+ textView.setText(R.string.transaction_aborted)
+ }
+ })
+ withdrawManager.completeTransaction()
+ backButton.setOnClickListener {
+ findNavController().popBackStack()
+ }
+ }
+
+}
diff --git a/cashier/src/main/java/net/taler/cashier/withdraw/NfcManager.kt b/cashier/src/main/java/net/taler/cashier/withdraw/NfcManager.kt
new file mode 100644
index 0000000..a487b5f
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/withdraw/NfcManager.kt
@@ -0,0 +1,234 @@
+/*
+ * 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.Activity
+import android.content.Context
+import android.nfc.NfcAdapter
+import android.nfc.NfcAdapter.FLAG_READER_NFC_A
+import android.nfc.NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK
+import android.nfc.NfcAdapter.getDefaultAdapter
+import android.nfc.Tag
+import android.nfc.tech.IsoDep
+import android.util.Log
+import net.taler.cashier.Utils.hexStringToByteArray
+import org.json.JSONObject
+import java.io.ByteArrayOutputStream
+import java.net.URL
+import javax.net.ssl.HttpsURLConnection
+
+@Suppress("unused")
+private const val TALER_AID = "A0000002471001"
+
+class NfcManager : NfcAdapter.ReaderCallback {
+
+ companion object {
+ const val TAG = "taler-merchant"
+
+ /**
+ * Returns true if NFC is supported and false otherwise.
+ */
+ fun hasNfc(context: Context): Boolean {
+ return getNfcAdapter(context) != null
+ }
+
+ /**
+ * Enables NFC reader mode. Don't forget to call [stop] afterwards.
+ */
+ fun start(activity: Activity, nfcManager: NfcManager) {
+ getNfcAdapter(activity)?.enableReaderMode(activity, nfcManager, nfcManager.flags, null)
+ }
+
+ /**
+ * Disables NFC reader mode. Call after [start].
+ */
+ fun stop(activity: Activity) {
+ getNfcAdapter(activity)?.disableReaderMode(activity)
+ }
+
+ private fun getNfcAdapter(context: Context): NfcAdapter? {
+ return getDefaultAdapter(context)
+ }
+ }
+
+ private val flags = FLAG_READER_NFC_A or FLAG_READER_SKIP_NDEF_CHECK
+
+ private var tagString: String? = null
+ private var currentTag: IsoDep? = null
+
+ fun setTagString(tagString: String) {
+ this.tagString = tagString
+ }
+
+ override fun onTagDiscovered(tag: Tag?) {
+
+ Log.v(TAG, "tag discovered")
+
+ val isoDep = IsoDep.get(tag)
+ isoDep.connect()
+
+ currentTag = isoDep
+
+ isoDep.transceive(apduSelectFile())
+
+ val tagString: String? = tagString
+ if (tagString != null) {
+ isoDep.transceive(apduPutTalerData(1, tagString.toByteArray()))
+ }
+
+ // FIXME: use better pattern for sleeps in between requests
+ // -> start with fast polling, poll more slowly if no requests are coming
+
+ while (true) {
+ try {
+ val reqFrame = isoDep.transceive(apduGetData())
+ if (reqFrame.size < 2) {
+ Log.v(TAG, "request frame too small")
+ break
+ }
+ val req = ByteArray(reqFrame.size - 2)
+ if (req.isEmpty()) {
+ continue
+ }
+ reqFrame.copyInto(req, 0, 0, reqFrame.size - 2)
+ val jsonReq = JSONObject(req.toString(Charsets.UTF_8))
+ val reqId = jsonReq.getInt("id")
+ Log.v(TAG, "got request $jsonReq")
+ val jsonInnerReq = jsonReq.getJSONObject("request")
+ val method = jsonInnerReq.getString("method")
+ val urlStr = jsonInnerReq.getString("url")
+ Log.v(TAG, "url '$urlStr'")
+ Log.v(TAG, "method '$method'")
+ val url = URL(urlStr)
+ val conn: HttpsURLConnection = url.openConnection() as HttpsURLConnection
+ conn.setRequestProperty("Accept", "application/json")
+ conn.connectTimeout = 5000
+ conn.doInput = true
+ when (method) {
+ "get" -> {
+ conn.requestMethod = "GET"
+ }
+ "postJson" -> {
+ conn.requestMethod = "POST"
+ conn.doOutput = true
+ conn.setRequestProperty("Content-Type", "application/json; utf-8")
+ val body = jsonInnerReq.getString("body")
+ conn.outputStream.write(body.toByteArray(Charsets.UTF_8))
+ }
+ else -> {
+ throw Exception("method not supported")
+ }
+ }
+ Log.v(TAG, "connecting")
+ conn.connect()
+ Log.v(TAG, "connected")
+
+ val statusCode = conn.responseCode
+ val tunnelResp = JSONObject()
+ tunnelResp.put("id", reqId)
+ tunnelResp.put("status", conn.responseCode)
+
+ if (statusCode == 200) {
+ val stream = conn.inputStream
+ val httpResp = stream.buffered().readBytes()
+ tunnelResp.put("responseJson", JSONObject(httpResp.toString(Charsets.UTF_8)))
+ }
+
+ Log.v(TAG, "sending: $tunnelResp")
+
+ isoDep.transceive(apduPutTalerData(2, tunnelResp.toString().toByteArray()))
+ } catch (e: Exception) {
+ Log.v(TAG, "exception during NFC loop: $e")
+ break
+ }
+ }
+
+ isoDep.close()
+ }
+
+ private fun writeApduLength(stream: ByteArrayOutputStream, size: Int) {
+ when {
+ size == 0 -> {
+ // No size field needed!
+ }
+ size <= 255 -> // One byte size field
+ stream.write(size)
+ size <= 65535 -> {
+ stream.write(0)
+ // FIXME: is this supposed to be little or big endian?
+ stream.write(size and 0xFF)
+ stream.write((size ushr 8) and 0xFF)
+ }
+ else -> throw Error("payload too big")
+ }
+ }
+
+ private fun apduSelectFile(): ByteArray {
+ return hexStringToByteArray("00A4040007A0000002471001")
+ }
+
+ private fun apduPutData(payload: ByteArray): ByteArray {
+ val stream = ByteArrayOutputStream()
+
+ // Class
+ stream.write(0x00)
+
+ // Instruction 0xDA = put data
+ stream.write(0xDA)
+
+ // Instruction parameters
+ // (proprietary encoding)
+ stream.write(0x01)
+ stream.write(0x00)
+
+ writeApduLength(stream, payload.size)
+
+ stream.write(payload)
+
+ return stream.toByteArray()
+ }
+
+ private fun apduPutTalerData(talerInst: Int, payload: ByteArray): ByteArray {
+ val realPayload = ByteArrayOutputStream()
+ realPayload.write(talerInst)
+ realPayload.write(payload)
+ return apduPutData(realPayload.toByteArray())
+ }
+
+ private fun apduGetData(): ByteArray {
+ val stream = ByteArrayOutputStream()
+
+ // Class
+ stream.write(0x00)
+
+ // Instruction 0xCA = get data
+ stream.write(0xCA)
+
+ // Instruction parameters
+ // (proprietary encoding)
+ stream.write(0x01)
+ stream.write(0x00)
+
+ // Max expected response size, two
+ // zero bytes denotes 65536
+ stream.write(0x0)
+ stream.write(0x0)
+
+ return stream.toByteArray()
+ }
+
+}
diff --git a/cashier/src/main/java/net/taler/cashier/withdraw/QrCodeManager.kt b/cashier/src/main/java/net/taler/cashier/withdraw/QrCodeManager.kt
new file mode 100644
index 0000000..e3ffa92
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/withdraw/QrCodeManager.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.graphics.Bitmap
+import android.graphics.Bitmap.Config.RGB_565
+import android.graphics.Color.BLACK
+import android.graphics.Color.WHITE
+import com.google.zxing.BarcodeFormat.QR_CODE
+import com.google.zxing.qrcode.QRCodeWriter
+
+object QrCodeManager {
+
+ fun makeQrCode(text: String, size: Int = 256): Bitmap {
+ val qrCodeWriter = QRCodeWriter()
+ val bitMatrix = qrCodeWriter.encode(text, QR_CODE, size, size)
+ val height = bitMatrix.height
+ val width = bitMatrix.width
+ val bmp = Bitmap.createBitmap(width, height, RGB_565)
+ for (x in 0 until width) {
+ for (y in 0 until height) {
+ bmp.setPixel(x, y, if (bitMatrix.get(x, y)) BLACK else WHITE)
+ }
+ }
+ return bmp
+ }
+
+}
diff --git a/cashier/src/main/java/net/taler/cashier/withdraw/TransactionFragment.kt b/cashier/src/main/java/net/taler/cashier/withdraw/TransactionFragment.kt
new file mode 100644
index 0000000..8b782b0
--- /dev/null
+++ b/cashier/src/main/java/net/taler/cashier/withdraw/TransactionFragment.kt
@@ -0,0 +1,174 @@
+/*
+ * 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.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat.getColor
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import kotlinx.android.synthetic.main.fragment_transaction.*
+import net.taler.cashier.MainViewModel
+import net.taler.cashier.R
+import net.taler.cashier.fadeIn
+import net.taler.cashier.fadeOut
+import net.taler.cashier.withdraw.TransactionFragmentDirections.Companion.actionTransactionFragmentToBalanceFragment
+import net.taler.cashier.withdraw.TransactionFragmentDirections.Companion.actionTransactionFragmentToErrorFragment
+import net.taler.cashier.withdraw.WithdrawResult.Error
+import net.taler.cashier.withdraw.WithdrawResult.InsufficientBalance
+import net.taler.cashier.withdraw.WithdrawResult.Success
+
+class TransactionFragment : Fragment() {
+
+ private val viewModel: MainViewModel by activityViewModels()
+ private val withdrawManager by lazy { viewModel.withdrawManager }
+ private val nfcManager = NfcManager()
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_transaction, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ withdrawManager.withdrawAmount.observe(viewLifecycleOwner, Observer { amount ->
+ amountView.text = amount
+ })
+ withdrawManager.withdrawResult.observe(viewLifecycleOwner, Observer { result ->
+ onWithdrawResultReceived(result)
+ })
+ withdrawManager.withdrawStatus.observe(viewLifecycleOwner, Observer { status ->
+ onWithdrawStatusChanged(status)
+ })
+
+ // change intro text depending on whether NFC is available or not
+ val hasNfc = NfcManager.hasNfc(requireContext())
+ val intro = if (hasNfc) R.string.transaction_intro_nfc else R.string.transaction_intro
+ introView.setText(intro)
+
+ cancelButton.setOnClickListener {
+ findNavController().popBackStack()
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ if (withdrawManager.withdrawResult.value is Success) {
+ NfcManager.start(requireActivity(), nfcManager)
+ }
+ }
+
+ override fun onStop() {
+ super.onStop()
+ NfcManager.stop(requireActivity())
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ if (!requireActivity().isChangingConfigurations) {
+ withdrawManager.abort()
+ }
+ }
+
+ private fun onWithdrawResultReceived(result: WithdrawResult?) {
+ if (result != null) {
+ progressBar.animate()
+ .alpha(0f)
+ .withEndAction { progressBar?.visibility = INVISIBLE }
+ .setDuration(750)
+ .start()
+ }
+ when (result) {
+ is InsufficientBalance -> {
+ val c = getColor(requireContext(), R.color.design_default_color_error)
+ introView.setTextColor(c)
+ introView.text = getString(R.string.withdraw_error_insufficient_balance)
+ }
+ is Error -> {
+ val c = getColor(requireContext(), R.color.design_default_color_error)
+ introView.setTextColor(c)
+ introView.text = result.msg
+ }
+ is Success -> {
+ // start NFC
+ nfcManager.setTagString(result.talerUri)
+ NfcManager.start(
+ requireActivity(),
+ nfcManager
+ )
+ // show QR code
+ qrCodeView.alpha = 0f
+ qrCodeView.animate()
+ .alpha(1f)
+ .withStartAction {
+ qrCodeView.visibility = VISIBLE
+ qrCodeView.setImageBitmap(result.qrCode)
+ }
+ .setDuration(750)
+ .start()
+ }
+ }
+ }
+
+ private fun onWithdrawStatusChanged(status: WithdrawStatus?): Any = when (status) {
+ is WithdrawStatus.SelectionDone -> {
+ qrCodeView.fadeOut {
+ qrCodeView?.setImageResource(R.drawable.ic_arrow)
+ qrCodeView?.fadeIn()
+ }
+ introView.fadeOut {
+ introView?.text = getString(R.string.transaction_intro_scanned)
+ introView?.fadeIn {
+ confirmButton?.isEnabled = true
+ confirmButton?.setOnClickListener {
+ withdrawManager.confirm(status.withdrawalId)
+ }
+ }
+ }
+ }
+ is WithdrawStatus.Confirming -> {
+ confirmButton.isEnabled = false
+ qrCodeView.fadeOut()
+ progressBar.fadeIn()
+ }
+ is WithdrawStatus.Success -> {
+ withdrawManager.completeTransaction()
+ actionTransactionFragmentToBalanceFragment().let {
+ findNavController().navigate(it)
+ }
+ }
+ is WithdrawStatus.Aborted -> onError()
+ is WithdrawStatus.Error -> onError()
+ null -> {
+ // no-op
+ }
+ }
+
+ private fun onError() {
+ actionTransactionFragmentToErrorFragment().let {
+ findNavController().navigate(it)
+ }
+ }
+
+}
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
+)