/*
* 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 akono.AkonoJni
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.wallet.BuildConfig.WALLET_CORE_VERSION
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"
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 lateinit var akono: AkonoJni
private var initialized = false
private var nextRequestID = 1
private val requests = ConcurrentHashMap()
private val subscribers = LinkedList()
override fun onCreate() {
val talerWalletAndroidCode =
assets.open("taler-wallet-android-$WALLET_CORE_VERSION.js").use {
it.readBytes().toString(Charsets.UTF_8)
}
Log.i(TAG, "onCreate in wallet backend service")
akono = AkonoJni()
akono.putModuleCode("taler-wallet-android", talerWalletAndroidCode)
akono.setMessageHandler(object : AkonoJni.MessageHandler {
override fun handleMessage(message: String) {
this@WalletBackendService.handleAkonoMessage(message)
}
})
//akono.evalNodeCode("require('source-map-support').install();")
akono.evalNodeCode("require('akono');")
akono.evalNodeCode("tw = require('taler-wallet-android');")
akono.evalNodeCode("tw.installAndroidWalletListener();")
sendInitMessage()
initialized = true
super.onCreate()
}
private fun sendInitMessage() {
val msg = JSONObject()
msg.put("operation", "init")
val args = JSONObject()
msg.put("args", args)
args.put("persistentStoragePath", "${application.filesDir}/talerwalletdb-v30.json")
akono.sendMessage(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
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.akono.sendMessage(request.toString(2))
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)
Log.v(TAG, "got back message: ${message.toString(2)}")
when (val type = message.getString("type")) {
"notification" -> {
sendNotify(message.getString("payload"))
}
"tunnelHttp" -> {
Log.v(TAG, "got http tunnel request!")
Intent().also { intent ->
intent.action = HostCardEmulatorService.HTTP_TUNNEL_REQUEST
intent.putExtra("tunnelMessage", messageStr)
application.sendBroadcast(intent)
}
}
"response" -> {
when (message.getString("operation")) {
"init" -> {
Log.v(TAG, "got response for init operation: ${message.toString(2)}")
sendNotify(message.toString(2))
}
"reset" -> {
exitProcess(1)
}
else -> {
val payload = message.getJSONObject("result").toString(2)
handleResponse(false, message, payload)
}
}
}
"error" -> {
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
}
}