From ffe67691fe7ad995113eacb0ee5785f51e0051de Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 27 Sep 2022 17:56:05 -0300 Subject: [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. --- wallet/src/main/AndroidManifest.xml | 4 - .../src/main/java/net/taler/wallet/MainActivity.kt | 1 - .../main/java/net/taler/wallet/MainViewModel.kt | 74 +++--- wallet/src/main/java/net/taler/wallet/WalletApp.kt | 2 +- .../java/net/taler/wallet/backend/ApiResponse.kt | 61 +++++ .../net/taler/wallet/backend/BackendManager.kt | 89 +++++++ .../java/net/taler/wallet/backend/InitResponse.kt | 38 +++ .../net/taler/wallet/backend/RequestManager.kt | 44 ++++ .../net/taler/wallet/backend/TalerErrorCode.kt | 5 +- .../net/taler/wallet/backend/WalletBackendApi.kt | 175 ++++---------- .../taler/wallet/backend/WalletBackendService.kt | 266 --------------------- .../wallet/pending/PendingOperationsManager.kt | 45 ++-- 12 files changed, 346 insertions(+), 458 deletions(-) create mode 100644 wallet/src/main/java/net/taler/wallet/backend/ApiResponse.kt create mode 100644 wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt create mode 100644 wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt create mode 100644 wallet/src/main/java/net/taler/wallet/backend/RequestManager.kt delete mode 100644 wallet/src/main/java/net/taler/wallet/backend/WalletBackendService.kt 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" /> - - 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>() val balances: LiveData> = 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 = 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 + */ + +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 + */ + +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(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 + */ + +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 + */ + +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>() + + @Volatile + @GuardedBy("this") + private var currentId = 0 + + @Synchronized + fun addRequest(cont: Continuation, block: (Int) -> Unit) { + val id = currentId++ + contMap[id] = cont + block(id) + } + + @Synchronized + fun getAndRemoveContinuation(id: Int): Continuation? { + 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 { +object TalerErrorCodeSerializer : KSerializer { - 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() - private val handlers = ConcurrentHashMap 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 request( @@ -148,36 +66,23 @@ class WalletBackendApi( serializer: KSerializer? = null, noinline args: (JSONObject.() -> JSONObject)? = null, ): WalletResponse = 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 - */ - - -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() - - private val subscribers = LinkedList() - - 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? = 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>() 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() + 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() - 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") + } } } -- cgit v1.2.3