summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTorsten Grote <t@grobox.de>2022-09-27 17:56:05 -0300
committerTorsten Grote <t@grobox.de>2023-03-28 13:25:30 -0300
commitffe67691fe7ad995113eacb0ee5785f51e0051de (patch)
tree78e687a1a665a73547f34ea699787a99dc342ae2
parent8c098d96fc2c0e2d4fa96561f44980e0df5dad87 (diff)
downloadtaler-android-ffe67691fe7ad995113eacb0ee5785f51e0051de.tar.gz
taler-android-ffe67691fe7ad995113eacb0ee5785f51e0051de.tar.bz2
taler-android-ffe67691fe7ad995113eacb0ee5785f51e0051de.zip
[wallet] Don't run qtart in a separate process
This required IPC and the mechanism chosen was limiting us to transferring 1MB (or less!) to/from wallet-core. Now we simply run it in an IO thread. The should be no functional difference (except new bugs introduced when swapping in a new mechanism). The second process with qtart running in WalletBackendService also got killed when the OS killed our main process.
-rw-r--r--wallet/src/main/AndroidManifest.xml4
-rw-r--r--wallet/src/main/java/net/taler/wallet/MainActivity.kt1
-rw-r--r--wallet/src/main/java/net/taler/wallet/MainViewModel.kt74
-rw-r--r--wallet/src/main/java/net/taler/wallet/WalletApp.kt2
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt61
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt89
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt38
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/RequestManager.kt44
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/TalerErrorCode.kt5
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt175
-rw-r--r--wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt266
-rw-r--r--wallet/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt45
12 files changed, 346 insertions, 458 deletions
diff --git a/wallet/src/main/AndroidManifest.xml b/wallet/src/main/AndroidManifest.xml
index 0b0a5b6..96c2958 100644
--- a/wallet/src/main/AndroidManifest.xml
+++ b/wallet/src/main/AndroidManifest.xml
@@ -89,10 +89,6 @@
android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/apduservice" />
</service>
-
- <service
- android:name=".backend.WalletBackendService"
- android:process=":WalletBackendService" />
</application>
<queries>
diff --git a/wallet/src/main/java/net/taler/wallet/MainActivity.kt b/wallet/src/main/java/net/taler/wallet/MainActivity.kt
index 7a40b4b..e91b983 100644
--- a/wallet/src/main/java/net/taler/wallet/MainActivity.kt
+++ b/wallet/src/main/java/net/taler/wallet/MainActivity.kt
@@ -70,7 +70,6 @@ import java.net.URL
import java.util.Locale.ROOT
import javax.net.ssl.HttpsURLConnection
-
class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener,
OnPreferenceStartFragmentCallback {
diff --git a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
index ed12533..bbd3ca3 100644
--- a/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
+++ b/wallet/src/main/java/net/taler/wallet/MainViewModel.kt
@@ -24,15 +24,19 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import net.taler.common.Amount
import net.taler.common.AmountParserException
import net.taler.common.Event
-import net.taler.common.assertUiThread
import net.taler.common.toEvent
import net.taler.wallet.accounts.AccountManager
+import net.taler.wallet.backend.NotificationPayload
+import net.taler.wallet.backend.NotificationReceiver
+import net.taler.wallet.backend.VersionReceiver
import net.taler.wallet.backend.WalletBackendApi
+import net.taler.wallet.backend.WalletCoreVersion
import net.taler.wallet.balances.BalanceItem
import net.taler.wallet.balances.BalanceResponse
import net.taler.wallet.deposit.DepositManager
@@ -58,7 +62,9 @@ private val transactionNotifications = listOf(
"withdraw-group-finished"
)
-class MainViewModel(val app: Application) : AndroidViewModel(app) {
+class MainViewModel(
+ app: Application,
+) : AndroidViewModel(app), VersionReceiver, NotificationReceiver {
private val mBalances = MutableLiveData<List<BalanceItem>>()
val balances: LiveData<List<BalanceItem>> = mBalances.distinctUntilChanged()
@@ -70,32 +76,13 @@ class MainViewModel(val app: Application) : AndroidViewModel(app) {
var merchantVersion: String? = null
private set
- private val api = WalletBackendApi(app) { payload ->
- if (payload.optString("operation") == "init") {
- val result = payload.getJSONObject("result")
- val versions = result.getJSONObject("versionInfo")
- exchangeVersion = versions.getString("exchange")
- merchantVersion = versions.getString("merchant")
- } else if (payload.getString("type") != "waiting-for-retry") { // ignore ping
- Log.i(TAG, "Received notification from wallet-core: ${payload.toString(2)}")
- loadBalances()
- if (payload.optString("type") in transactionNotifications) {
- assertUiThread()
- // TODO notification API should give us a currency to update
- // update currently selected transaction list
- transactionManager.loadTransactions()
- }
- // refresh pending ops and history with each notification
- if (devMode.value == true) {
- pendingOperationsManager.getPending()
- }
- }
- }
+ private val api = WalletBackendApi(app, this, this)
val withdrawManager = WithdrawManager(api, viewModelScope)
val tipManager = TipManager(api, viewModelScope)
val paymentManager = PaymentManager(api, viewModelScope)
- val pendingOperationsManager: PendingOperationsManager = PendingOperationsManager(api)
+ val pendingOperationsManager: PendingOperationsManager =
+ PendingOperationsManager(api, viewModelScope)
val transactionManager: TransactionManager = TransactionManager(api, viewModelScope)
val refundManager = RefundManager(api, viewModelScope)
val exchangeManager: ExchangeManager = ExchangeManager(api, viewModelScope)
@@ -117,9 +104,25 @@ class MainViewModel(val app: Application) : AndroidViewModel(app) {
)
val lastBackup: LiveData<Long> = mLastBackup
- override fun onCleared() {
- api.destroy()
- super.onCleared()
+ override fun onVersionReceived(versionInfo: WalletCoreVersion) {
+ exchangeVersion = versionInfo.exchange
+ merchantVersion = versionInfo.merchant
+ }
+
+ override fun onNotificationReceived(payload: NotificationPayload) {
+ if (payload.type == "waiting-for-retry") return // ignore ping)
+ Log.i(TAG, "Received notification from wallet-core: $payload")
+
+ loadBalances()
+ if (payload.type in transactionNotifications) viewModelScope.launch(Dispatchers.Main) {
+ // TODO notification API should give us a currency to update
+ // update currently selected transaction list
+ transactionManager.loadTransactions()
+ }
+ // refresh pending ops and history with each notification
+ if (devMode.value == true) {
+ pendingOperationsManager.getPending()
+ }
}
@UiThread
@@ -174,22 +177,29 @@ class MainViewModel(val app: Application) : AndroidViewModel(app) {
@UiThread
fun dangerouslyReset() {
- api.sendRequest("reset")
+ viewModelScope.launch {
+ api.sendRequest("reset")
+ }
withdrawManager.testWithdrawalStatus.value = null
mBalances.value = emptyList()
}
fun startTunnel() {
- api.sendRequest("startTunnel")
+ viewModelScope.launch {
+ api.sendRequest("startTunnel")
+ }
}
fun stopTunnel() {
- api.sendRequest("stopTunnel")
+ viewModelScope.launch {
+ api.sendRequest("stopTunnel")
+ }
}
fun tunnelResponse(resp: String) {
- val respJson = JSONObject(resp)
- api.sendRequest("tunnelResponse", respJson)
+ viewModelScope.launch {
+ api.sendRequest("tunnelResponse", JSONObject(resp))
+ }
}
@UiThread
diff --git a/wallet/src/main/java/net/taler/wallet/WalletApp.kt b/wallet/src/main/java/net/taler/wallet/WalletApp.kt
index 1076364..1384f76 100644
--- a/wallet/src/main/java/net/taler/wallet/WalletApp.kt
+++ b/wallet/src/main/java/net/taler/wallet/WalletApp.kt
@@ -19,7 +19,7 @@ package net.taler.wallet
import android.app.Application
import com.google.android.material.color.DynamicColors
-class WalletApp: Application() {
+class WalletApp : Application() {
override fun onCreate() {
super.onCreate()
DynamicColors.applyToActivitiesIfAvailable(this)
diff --git a/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt b/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt
new file mode 100644
index 0000000..46eb2f0
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt
@@ -0,0 +1,61 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 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.wallet.backend
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonObject
+
+@Serializable
+sealed class ApiMessage {
+
+ @Serializable
+ @SerialName("notification")
+ data class Notification(
+ val payload: NotificationPayload,
+ ) : ApiMessage()
+
+}
+
+@Serializable
+data class NotificationPayload(
+ val type: String,
+ val id: String? = null,
+)
+
+@Serializable
+sealed class ApiResponse : ApiMessage() {
+
+ abstract val id: Int
+ abstract val operation: String
+
+ @Serializable
+ @SerialName("response")
+ data class Response(
+ override val id: Int,
+ override val operation: String,
+ val result: JsonObject,
+ ) : ApiResponse()
+
+ @Serializable
+ @SerialName("error")
+ data class Error(
+ override val id: Int,
+ override val operation: String,
+ val error: JsonObject,
+ ) : ApiResponse()
+}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt b/wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt
new file mode 100644
index 0000000..ae338e8
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt
@@ -0,0 +1,89 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2022 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.wallet.backend
+
+import android.util.Log
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import net.taler.qtart.TalerWalletCore
+import net.taler.wallet.BuildConfig
+import org.json.JSONObject
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+
+fun interface NotificationReceiver {
+ fun onNotificationReceived(payload: NotificationPayload)
+}
+
+class BackendManager(
+ private val notificationReceiver: NotificationReceiver,
+) {
+
+ companion object {
+ private const val TAG = "BackendManager"
+ private const val TAG_CORE = "taler-wallet-embedded"
+ val json = Json {
+ ignoreUnknownKeys = true
+ }
+ }
+
+ private val walletCore = TalerWalletCore()
+ private val requestManager = RequestManager()
+
+ init {
+ walletCore.setMessageHandler { onMessageReceived(it) }
+ if (BuildConfig.DEBUG) walletCore.setStdoutHandler {
+ Log.d(TAG_CORE, it)
+ }
+ }
+
+ fun run() {
+ walletCore.run()
+ }
+
+ suspend fun send(operation: String, args: JSONObject? = null): ApiResponse =
+ suspendCoroutine { cont ->
+ requestManager.addRequest(cont) { id ->
+ val request = JSONObject().apply {
+ put("id", id)
+ put("operation", operation)
+ if (args != null) put("args", args)
+ }
+ Log.d(TAG, "sending message:\n${request.toString(2)}")
+ walletCore.sendRequest(request.toString())
+ }
+ }
+
+ private fun onMessageReceived(msg: String) {
+ Log.d(TAG, "message received: $msg")
+ when (val message = json.decodeFromString<ApiMessage>(msg)) {
+ is ApiMessage.Notification -> {
+ notificationReceiver.onNotificationReceived(message.payload)
+ }
+ is ApiResponse -> {
+ val id = message.id
+ val cont = requestManager.getAndRemoveContinuation(id)
+ if (cont == null) {
+ Log.e(TAG, "wallet returned unknown request ID ($id)")
+ } else {
+ cont.resume(message)
+ }
+ }
+ }
+ }
+}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt b/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt
new file mode 100644
index 0000000..076af87
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt
@@ -0,0 +1,38 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 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.wallet.backend
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class InitResponse(
+ val versionInfo: WalletCoreVersion,
+)
+
+fun interface VersionReceiver {
+ fun onVersionReceived(versionInfo: WalletCoreVersion)
+}
+
+@Serializable
+data class WalletCoreVersion(
+ val hash: String? = null,
+ val version: String,
+ val exchange: String,
+ val merchant: String,
+ val bank: String,
+ val devMode: Boolean,
+)
diff --git a/wallet/src/main/java/net/taler/wallet/backend/RequestManager.kt b/wallet/src/main/java/net/taler/wallet/backend/RequestManager.kt
new file mode 100644
index 0000000..041656e
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/backend/RequestManager.kt
@@ -0,0 +1,44 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2023 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.wallet.backend
+
+import androidx.annotation.GuardedBy
+import java.util.concurrent.ConcurrentHashMap
+import kotlin.coroutines.Continuation
+
+class RequestManager {
+
+ @GuardedBy("this")
+ private val contMap = ConcurrentHashMap<Int, Continuation<ApiResponse>>()
+
+ @Volatile
+ @GuardedBy("this")
+ private var currentId = 0
+
+ @Synchronized
+ fun addRequest(cont: Continuation<ApiResponse>, block: (Int) -> Unit) {
+ val id = currentId++
+ contMap[id] = cont
+ block(id)
+ }
+
+ @Synchronized
+ fun getAndRemoveContinuation(id: Int): Continuation<ApiResponse>? {
+ return contMap.remove(id)
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/TalerErrorCode.kt b/wallet/src/main/java/net/taler/wallet/backend/TalerErrorCode.kt
index edcfd17..2242e33 100644
--- a/wallet/src/main/java/net/taler/wallet/backend/TalerErrorCode.kt
+++ b/wallet/src/main/java/net/taler/wallet/backend/TalerErrorCode.kt
@@ -3879,9 +3879,10 @@ enum class TalerErrorCode(val code: Int) {
@OptIn(ExperimentalSerializationApi::class)
@Serializer(forClass = TalerErrorCode::class)
-object TalerErrorCodeSerializer: KSerializer<TalerErrorCode> {
+object TalerErrorCodeSerializer : KSerializer<TalerErrorCode> {
- override val descriptor = PrimitiveSerialDescriptor("TalerErrorCodeSerializer", PrimitiveKind.INT)
+ override val descriptor =
+ PrimitiveSerialDescriptor("TalerErrorCodeSerializer", PrimitiveKind.INT)
override fun deserialize(decoder: Decoder): TalerErrorCode {
val code = decoder.decodeInt()
diff --git a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
index 8ec5873..06b8cee 100644
--- a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
+++ b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
@@ -17,130 +17,48 @@
package net.taler.wallet.backend
import android.app.Application
-import android.content.ComponentName
-import android.content.Context
-import android.content.Intent
-import android.content.ServiceConnection
-import android.os.Handler
-import android.os.IBinder
-import android.os.Message
-import android.os.Messenger
-import android.util.Log
+import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.KSerializer
-import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.decodeFromJsonElement
import net.taler.wallet.backend.TalerErrorCode.NONE
-import net.taler.wallet.backend.WalletBackendService.Companion.MSG_COMMAND
-import net.taler.wallet.backend.WalletBackendService.Companion.MSG_NOTIFY
-import net.taler.wallet.backend.WalletBackendService.Companion.MSG_REPLY
-import net.taler.wallet.backend.WalletBackendService.Companion.MSG_SUBSCRIBE_NOTIFY
import org.json.JSONObject
-import java.lang.ref.WeakReference
-import java.util.LinkedList
-import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.atomic.AtomicInteger
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
+const val WALLET_DB = "talerwalletdb-v30.json"
+
+@OptIn(DelicateCoroutinesApi::class)
class WalletBackendApi(
- private val app: Application,
- private val notificationHandler: ((payload: JSONObject) -> Unit),
+ app: Application,
+ private val versionReceiver: VersionReceiver,
+ notificationReceiver: NotificationReceiver,
) {
- private var walletBackendMessenger: Messenger? = null
- private val queuedMessages = LinkedList<Message>()
- private val handlers = ConcurrentHashMap<Int, (isError: Boolean, message: JSONObject) -> Unit>()
- private var nextRequestID = AtomicInteger(0)
- private val incomingMessenger = Messenger(IncomingHandler(this))
-
- private val walletBackendConn = object : ServiceConnection {
- override fun onServiceDisconnected(p0: ComponentName?) {
- Log.w(TAG, "wallet backend service disconnected (crash?)")
- walletBackendMessenger = null
- }
- override fun onServiceConnected(componentName: ComponentName?, binder: IBinder?) {
- Log.i(TAG, "connected to wallet backend service")
- val bm = Messenger(binder)
- walletBackendMessenger = bm
- pumpQueue(bm)
- val msg = Message.obtain(null, MSG_SUBSCRIBE_NOTIFY)
- msg.replyTo = incomingMessenger
- bm.send(msg)
- }
- }
+ private val backendManager = BackendManager(notificationReceiver)
+ private val dbPath = "${app.filesDir}/${WALLET_DB}"
init {
- Intent(app, WalletBackendService::class.java).also { intent ->
- app.bindService(intent, walletBackendConn, Context.BIND_AUTO_CREATE)
- }
- }
-
- private class IncomingHandler(strongApi: WalletBackendApi) : Handler() {
- private val weakApi = WeakReference(strongApi)
- override fun handleMessage(msg: Message) {
- val api = weakApi.get() ?: return
- when (msg.what) {
- MSG_REPLY -> {
- val requestID = msg.data.getInt("requestID", 0)
- val operation = msg.data.getString("operation", "??")
- Log.i(TAG, "got reply for operation $operation ($requestID)")
- val h = api.handlers.remove(requestID)
- if (h == null) {
- Log.e(TAG, "request ID not associated with a handler")
- return
- }
- val response = msg.data.getString("response")
- if (response == null) {
- Log.e(TAG, "response did not contain response payload")
- return
- }
- val isError = msg.data.getBoolean("isError")
- val json = JSONObject(response)
- h(isError, json)
- }
- MSG_NOTIFY -> {
- val payloadStr = msg.data.getString("payload")
- if (payloadStr == null) {
- Log.e(TAG, "Notification had no payload: $msg")
- } else {
- val payload = JSONObject(payloadStr)
- api.notificationHandler.invoke(payload)
- }
- }
- }
+ GlobalScope.launch(Dispatchers.IO) {
+ backendManager.run()
+ sendInitMessage()
}
}
- private fun pumpQueue(bm: Messenger) {
- while (true) {
- val msg = queuedMessages.pollFirst() ?: return
- bm.send(msg)
+ private suspend fun sendInitMessage() {
+ request("init", InitResponse.serializer()) {
+ put("persistentStoragePath", dbPath)
+ put("logLevel", "INFO")
+ }.onSuccess { response ->
+ versionReceiver.onVersionReceived(response.versionInfo)
+ }.onError { error ->
+ error("Error on init message: $error")
}
}
- fun sendRequest(
- operation: String,
- args: JSONObject? = null,
- onResponse: (isError: Boolean, message: JSONObject) -> Unit = { _, _ -> },
- ) {
- val requestID = nextRequestID.incrementAndGet()
- Log.i(TAG, "sending request for operation $operation ($requestID)\n${args?.toString(2)}")
- val msg = Message.obtain(null, MSG_COMMAND)
- handlers[requestID] = onResponse
- msg.replyTo = incomingMessenger
- val data = msg.data
- data.putString("operation", operation)
- data.putInt("requestID", requestID)
- if (args != null) {
- data.putString("args", args.toString())
- }
- val bm = walletBackendMessenger
- if (bm != null) {
- bm.send(msg)
- } else {
- queuedMessages.add(msg)
- }
+ suspend fun sendRequest(operation: String, args: JSONObject? = null): ApiResponse {
+ return backendManager.send(operation, args)
}
suspend inline fun <reified T> request(
@@ -148,36 +66,23 @@ class WalletBackendApi(
serializer: KSerializer<T>? = null,
noinline args: (JSONObject.() -> JSONObject)? = null,
): WalletResponse<T> = withContext(Dispatchers.Default) {
- suspendCoroutine { cont ->
- val json = Json {
- ignoreUnknownKeys = true
- }
- sendRequest(operation, args?.invoke(JSONObject())) { isError, message ->
- val response = try {
- if (isError) {
- val error =
- json.decodeFromString(TalerErrorInfo.serializer(), message.toString())
- WalletResponse.Error(error)
- } else {
- val t: T = serializer?.let {
- json.decodeFromString(serializer, message.toString())
- } ?: Unit as T
- WalletResponse.Success(t)
- }
- } catch (e: Exception) {
- val info = TalerErrorInfo(NONE, "", e.toString())
- WalletResponse.Error(info)
+ val json = BackendManager.json
+ try {
+ when (val response = sendRequest(operation, args?.invoke(JSONObject()))) {
+ is ApiResponse.Response -> {
+ val t: T = serializer?.let {
+ json.decodeFromJsonElement(serializer, response.result)
+ } ?: Unit as T
+ WalletResponse.Success(t)
+ }
+ is ApiResponse.Error -> {
+ val error: TalerErrorInfo = json.decodeFromJsonElement(response.error)
+ WalletResponse.Error(error)
}
- cont.resume(response)
}
+ } catch (e: Exception) {
+ val info = TalerErrorInfo(NONE, "", e.toString())
+ WalletResponse.Error(info)
}
}
-
- fun destroy() {
- // FIXME: implement this!
- }
-
- companion object {
- const val TAG = "WalletBackendApi"
- }
}
diff --git a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt
deleted file mode 100644
index 6411b8b..0000000
--- a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt
+++ /dev/null
@@ -1,266 +0,0 @@
-/*
- * 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.wallet.backend
-
-import android.app.Service
-import android.content.Intent
-import android.os.Handler
-import android.os.IBinder
-import android.os.Message
-import android.os.Messenger
-import android.os.RemoteException
-import android.util.Log
-import net.taler.qtart.TalerWalletCore
-import net.taler.wallet.BuildConfig
-import net.taler.wallet.HostCardEmulatorService
-import org.json.JSONObject
-import java.lang.ref.WeakReference
-import java.util.LinkedList
-import java.util.concurrent.ConcurrentHashMap
-import kotlin.system.exitProcess
-
-private const val TAG = "taler-wallet-backend"
-const val WALLET_DB = "talerwalletdb-v30.json"
-
-class RequestData(val clientRequestId: Int, val messenger: Messenger)
-
-
-class WalletBackendService : Service() {
- /**
- * Target we publish for clients to send messages to IncomingHandler.
- */
- private val messenger: Messenger = Messenger(IncomingHandler(this))
-
- private val walletCore = TalerWalletCore()
-
- private var initialized = false
-
- private var nextRequestID = 1
-
- private val requests = ConcurrentHashMap<Int, RequestData>()
-
- private val subscribers = LinkedList<Messenger>()
-
- override fun onCreate() {
- Log.i(TAG, "onCreate in wallet backend service")
-
- walletCore.setMessageHandler {
- this@WalletBackendService.handleAkonoMessage(it)
- }
- if (BuildConfig.DEBUG) walletCore.setStdoutHandler {
- Log.d(TAG, it)
- }
- walletCore.run()
- sendInitMessage()
- // runIntegrationTest()
- super.onCreate()
- }
-
- private fun sendInitMessage() {
- val msg = JSONObject()
- msg.put("operation", "init")
- val args = JSONObject()
- msg.put("args", args)
- args.put("persistentStoragePath", "${application.filesDir}/$WALLET_DB")
- args.put("logLevel", "INFO")
- Log.d(TAG, "init message: ${msg.toString(2)}")
- walletCore.sendRequest(msg.toString())
- }
-
- /**
- * Run the integration tests for wallet-core.
- */
- private fun runIntegrationTest() {
- val msg = JSONObject()
- msg.put("operation", "runIntegrationTest")
- val args = JSONObject()
- msg.put("args", args)
- args.put("amountToWithdraw", "KUDOS:3")
- args.put("amountToSpend", "KUDOS:1")
- args.put("bankBaseUrl", "https://bank.demo.taler.net/demobanks/default/access-api/")
- args.put("exchangeBaseUrl", "https://exchange.demo.taler.net/")
- args.put("merchantBaseUrl", "https://backend.demo.taler.net/")
- args.put("merchantAuthToken", "secret-token:sandbox")
- Log.d(TAG, "integration test message: ${msg.toString(2)}")
- walletCore.sendRequest(msg.toString())
- }
-
- /**
- * Handler of incoming messages from clients.
- */
- class IncomingHandler(
- service: WalletBackendService,
- ) : Handler() {
-
- private val serviceWeakRef = WeakReference(service)
-
- override fun handleMessage(msg: Message) {
- val svc = serviceWeakRef.get() ?: return
- if (!svc.initialized) Log.w(TAG, "Warning: Not yet initialized")
- when (msg.what) {
- MSG_COMMAND -> {
- val data = msg.data
- val serviceRequestID = svc.nextRequestID++
- val clientRequestID = data.getInt("requestID", 0)
- if (clientRequestID == 0) {
- Log.e(TAG, "client requestID missing")
- return
- }
- val args = data.getString("args")
- val argsObj = if (args == null) {
- JSONObject()
- } else {
- JSONObject(args)
- }
- val operation = data.getString("operation", "")
- if (operation == "") {
- Log.e(TAG, "client command missing")
- return
- }
- Log.i(TAG, "got request for operation $operation")
- val request = JSONObject()
- request.put("operation", operation)
- request.put("id", serviceRequestID)
- request.put("args", argsObj)
- svc.walletCore.sendRequest(request.toString())
- Log.i(
- TAG,
- "mapping service request ID $serviceRequestID to client request ID $clientRequestID"
- )
- svc.requests[serviceRequestID] = RequestData(clientRequestID, msg.replyTo)
- }
- MSG_SUBSCRIBE_NOTIFY -> {
- Log.i(TAG, "subscribing client")
- val r = msg.replyTo
- if (r == null) {
- Log.e(
- TAG,
- "subscriber did not specify replyTo object in MSG_SUBSCRIBE_NOTIFY"
- )
- } else {
- svc.subscribers.add(msg.replyTo)
- }
- }
- MSG_UNSUBSCRIBE_NOTIFY -> {
- Log.i(TAG, "unsubscribing client")
- svc.subscribers.remove(msg.replyTo)
- }
- else -> {
- Log.e(TAG, "unknown message from client")
- super.handleMessage(msg)
- }
- }
- }
- }
-
- override fun onBind(p0: Intent?): IBinder? {
- return messenger.binder
- }
-
- private fun sendNotify(payload: String) {
- var rm: LinkedList<Messenger>? = null
- for (s in subscribers) {
- val m = Message.obtain(null, MSG_NOTIFY)
- val b = m.data
- b.putString("payload", payload)
- try {
- s.send(m)
- } catch (e: RemoteException) {
- if (rm == null) {
- rm = LinkedList()
- }
- rm.add(s)
- subscribers.remove(s)
- }
- }
- if (rm != null) {
- for (s in rm) {
- subscribers.remove(s)
- }
- }
- }
-
- private fun handleAkonoMessage(messageStr: String) {
- val message = JSONObject(messageStr)
- when (val type = message.getString("type")) {
- "notification" -> {
- val payload = message.getJSONObject("payload")
- if (payload.optString("type") != "waiting-for-retry") {
- Log.v(TAG, "got back notification: ${message.toString(2)}")
- }
- sendNotify(payload.toString())
- }
- "tunnelHttp" -> {
- Log.v(TAG, "got http tunnel request! ${message.toString(2)}")
- Intent().also { intent ->
- intent.action = HostCardEmulatorService.HTTP_TUNNEL_REQUEST
- intent.putExtra("tunnelMessage", messageStr)
- application.sendBroadcast(intent)
- }
- }
- "response" -> {
- when (message.getString("operation")) {
- "init" -> {
- Log.d(TAG, "got response for init operation: ${message.toString(2)}")
- initialized = true
- sendNotify(message.toString(2))
- }
- "reset" -> {
- Log.v(TAG, "got back message: ${message.toString(2)}")
- exitProcess(1)
- }
- else -> {
- Log.v(TAG, "got back response: ${message.toString(2)}")
- val payload = message.getJSONObject("result").toString(2)
- handleResponse(false, message, payload)
- }
- }
- }
- "error" -> {
- Log.v(TAG, "got back error: ${message.toString(2)}")
- val payload = message.getJSONObject("error").toString(2)
- handleResponse(true, message, payload)
- }
- else -> throw IllegalArgumentException("Unknown message type: $type")
- }
- }
-
- private fun handleResponse(isError: Boolean, message: JSONObject, payload: String) {
- val id = message.getInt("id")
- val rId = requests[id]
- if (rId == null) {
- Log.e(TAG, "wallet returned unknown request ID ($id)")
- return
- }
- val m = Message.obtain(null, MSG_REPLY)
- val b = m.data
- b.putInt("requestID", rId.clientRequestId)
- b.putBoolean("isError", isError)
- b.putString("response", payload)
- b.putString("operation", message.getString("operation"))
- rId.messenger.send(m)
- }
-
- companion object {
- const val MSG_SUBSCRIBE_NOTIFY = 1
- const val MSG_UNSUBSCRIBE_NOTIFY = 2
- const val MSG_COMMAND = 3
- const val MSG_REPLY = 4
- const val MSG_NOTIFY = 5
- }
-}
diff --git a/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt b/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt
index df778ed..f5079f6 100644
--- a/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/pending/PendingOperationsManager.kt
@@ -18,40 +18,51 @@ package net.taler.wallet.pending
import android.util.Log
import androidx.lifecycle.MutableLiveData
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.serialization.json.jsonArray
import net.taler.wallet.TAG
+import net.taler.wallet.backend.ApiResponse
import net.taler.wallet.backend.WalletBackendApi
import org.json.JSONObject
open class PendingOperationInfo(
val type: String,
- val detail: JSONObject
+ val detail: JSONObject,
)
-class PendingOperationsManager(private val walletBackendApi: WalletBackendApi) {
+class PendingOperationsManager(
+ private val walletBackendApi: WalletBackendApi,
+ private val scope: CoroutineScope,
+) {
val pendingOperations = MutableLiveData<List<PendingOperationInfo>>()
internal fun getPending() {
- walletBackendApi.sendRequest("getPendingOperations") { isError, result ->
- if (isError) {
- Log.i(TAG, "got getPending error result: ${result.toString(2)}")
- return@sendRequest
+ scope.launch {
+ val response = walletBackendApi.sendRequest("getPendingOperations")
+ if (response is ApiResponse.Error) {
+ Log.i(TAG, "got getPending error result: ${response.error}")
+ return@launch
+ } else if (response is ApiResponse.Response) {
+ Log.i(TAG, "got getPending result")
+ val pendingList = mutableListOf<PendingOperationInfo>()
+ val pendingJson = response.result["pendingOperations"]?.jsonArray ?: return@launch
+ for (i in 0 until pendingJson.size) {
+ val p = JSONObject(pendingJson[i].toString())
+ val type = p.getString("type")
+ pendingList.add(PendingOperationInfo(type, p))
+ }
+ Log.i(TAG, "Got ${pendingList.size} pending operations")
+ pendingOperations.postValue((pendingList))
}
- Log.i(TAG, "got getPending result")
- val pendingList = mutableListOf<PendingOperationInfo>()
- val pendingJson = result.getJSONArray("pendingOperations")
- for (i in 0 until pendingJson.length()) {
- val p = pendingJson.getJSONObject(i)
- val type = p.getString("type")
- pendingList.add(PendingOperationInfo(type, p))
- }
- Log.i(TAG, "Got ${pendingList.size} pending operations")
- pendingOperations.postValue((pendingList))
}
}
fun retryPendingNow() {
- walletBackendApi.sendRequest("retryPendingNow")
+ scope.launch {
+ walletBackendApi.sendRequest("retryPendingNow")
+ }
}
}