/* * 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 } }