summaryrefslogtreecommitdiff
path: root/app/src/main/java/net/taler/wallet/backend
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2019-11-14 17:41:18 +0100
committerFlorian Dold <florian.dold@gmail.com>2019-11-14 17:41:18 +0100
commit87c1b63c4cf2b81963735feb0bce8b8f0b004dba (patch)
treeb0c27dd9c2667adb72861df05d9d404eb4777307 /app/src/main/java/net/taler/wallet/backend
parentc146f9f42e2582307deeab90e2d4acbfe01673e0 (diff)
downloadwallet-android-87c1b63c4cf2b81963735feb0bce8b8f0b004dba.tar.gz
wallet-android-87c1b63c4cf2b81963735feb0bce8b8f0b004dba.tar.bz2
wallet-android-87c1b63c4cf2b81963735feb0bce8b8f0b004dba.zip
put wallet node backend into its own service process
Diffstat (limited to 'app/src/main/java/net/taler/wallet/backend')
-rw-r--r--app/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt116
-rw-r--r--app/src/main/java/net/taler/wallet/backend/WalletBackendService.kt315
2 files changed, 431 insertions, 0 deletions
diff --git a/app/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt b/app/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
new file mode 100644
index 0000000..45c719d
--- /dev/null
+++ b/app/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt
@@ -0,0 +1,116 @@
+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.*
+import android.util.Log
+import android.util.SparseArray
+import org.json.JSONObject
+import java.lang.ref.WeakReference
+import java.util.*
+
+class WalletBackendApi(private val app: Application) {
+
+ private var walletBackendMessenger: Messenger? = null
+ private val queuedMessages = LinkedList<Message>()
+ private val handlers = SparseArray<(message: JSONObject) -> Unit>()
+ private var nextRequestID = 1
+ var notificationHandler: (() -> Unit)? = null
+
+ private val walletBackendConn = object : ServiceConnection {
+ override fun onServiceDisconnected(p0: ComponentName?) {
+ Log.w(TAG, "wallet backend service disconnected (crash?)")
+ }
+
+ 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, WalletBackendService.MSG_SUBSCRIBE_NOTIFY)
+ msg.replyTo = incomingMessenger
+ bm.send(msg)
+ }
+ }
+
+ private class IncomingHandler(strongApi: WalletBackendApi) : Handler() {
+ private val weakApi = WeakReference<WalletBackendApi>(strongApi)
+ override fun handleMessage(msg: Message) {
+ val api = weakApi.get() ?: return
+ when (msg.what) {
+ WalletBackendService.MSG_REPLY -> {
+ val requestID = msg.data.getInt("requestID", 0)
+ val h = api.handlers.get(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 json = JSONObject(response)
+ h(json)
+ }
+ WalletBackendService.MSG_NOTIFY -> {
+ val nh = api.notificationHandler
+ if (nh != null) {
+ nh()
+ }
+ }
+ }
+ }
+ }
+
+ private val incomingMessenger = Messenger(IncomingHandler(this))
+
+ init {
+ Intent(app, WalletBackendService::class.java).also { intent ->
+ app.bindService(intent, walletBackendConn, Context.BIND_AUTO_CREATE)
+ }
+ }
+
+ private fun pumpQueue(bm: Messenger) {
+ while (true) {
+ val msg = queuedMessages.pollFirst() ?: return
+ bm.send(msg)
+ }
+ }
+
+
+ fun sendRequest(
+ operation: String,
+ args: JSONObject?,
+ onResponse: (message: JSONObject) -> Unit = { }
+ ) {
+ Log.i(TAG, "sending request for operation $operation")
+ val requestID = nextRequestID++
+ val msg = Message.obtain(null, WalletBackendService.MSG_COMMAND)
+ handlers.put(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)
+ }
+ }
+
+ fun destroy() {
+ // FIXME: implement this!
+ }
+
+ companion object {
+ const val TAG = "WalletBackendApi"
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/net/taler/wallet/backend/WalletBackendService.kt b/app/src/main/java/net/taler/wallet/backend/WalletBackendService.kt
new file mode 100644
index 0000000..916dfdb
--- /dev/null
+++ b/app/src/main/java/net/taler/wallet/backend/WalletBackendService.kt
@@ -0,0 +1,315 @@
+package net.taler.wallet.backend
+
+import akono.AkonoJni
+import akono.ModuleResult
+import android.app.Service
+import android.content.Intent
+import android.content.res.AssetManager
+import android.os.*
+import android.util.Log
+import android.util.SparseArray
+import android.widget.Toast
+import androidx.core.util.set
+import net.taler.wallet.HostCardEmulatorService
+import org.json.JSONObject
+import java.io.File
+import java.io.InputStream
+import java.lang.ref.WeakReference
+import java.util.*
+import java.util.concurrent.ConcurrentHashMap
+
+val TAG = "taler-wallet-backend"
+
+/**
+ * Module loader to handle module loading requests from the wallet-core running on node/v8.
+ */
+private class AssetModuleLoader(
+ private val assetManager: AssetManager,
+ private val rootPath: String = "node_modules"
+) : AkonoJni.LoadModuleHandler {
+
+ private fun makeResult(localPath: String, stream: InputStream): ModuleResult {
+ val moduleString = stream.bufferedReader().use {
+ it.readText()
+ }
+ return ModuleResult("/vmodroot/$localPath", moduleString)
+ }
+
+ private fun tryPath(rawAssetPath: String): ModuleResult? {
+ //val assetPath = Paths.get(rawAssetPath).normalize().toString()
+ val assetPath = File(rawAssetPath).normalize().path
+ try {
+ val moduleStream = assetManager.open(assetPath)
+ return makeResult(assetPath, moduleStream)
+ } catch (e: Exception) {
+ }
+ try {
+ val jsPath = "$assetPath.js"
+ val moduleStream = assetManager.open(jsPath)
+ return makeResult(jsPath, moduleStream)
+ } catch (e: Exception) {
+ // ignore
+ }
+ val packageJsonPath = "$assetPath/package.json"
+ try {
+ val packageStream = assetManager.open(packageJsonPath)
+ val packageString = packageStream.bufferedReader().use {
+ it.readText()
+ }
+ val packageJson = JSONObject(packageString)
+ val mainFile = try {
+ packageJson.getString("main")
+ } catch (e: Exception) {
+ Log.w(TAG, "package.json does not have a 'main' filed")
+ throw e
+ }
+ try {
+ //val modPath = Paths.get("$assetPath/$mainFile").normalize().toString()
+ val modPath = File("$assetPath/$mainFile").normalize().path
+ return makeResult(modPath, assetManager.open(modPath))
+ } catch (e: Exception) {
+ // ignore
+ }
+ try {
+ //val modPath = Paths.get("$assetPath/$mainFile.js").normalize().toString()
+ val modPath = File("$assetPath/$mainFile.js").normalize().path
+ return makeResult(modPath, assetManager.open(modPath))
+ } catch (e: Exception) {
+ }
+ } catch (e: Exception) {
+ }
+ try {
+ val jsPath = "$assetPath/index.js"
+ val moduleStream = assetManager.open(jsPath)
+ return makeResult(jsPath, moduleStream)
+ } catch (e: Exception) {
+ }
+ return null
+ }
+
+ override fun loadModule(name: String, paths: Array<String>): ModuleResult? {
+ for (path in paths) {
+ val prefix = "/vmodroot"
+ if (!path.startsWith(prefix)) {
+ continue
+ }
+ if (path == prefix) {
+ val res = tryPath("$rootPath/$name")
+ if (res != null)
+ return res
+ } else {
+ val res = tryPath(path.drop(prefix.length + 1) + "/$name")
+ if (res != null)
+ return res
+ }
+ }
+ return null
+ }
+}
+
+
+private class AssetDataHandler(private val assetManager: AssetManager) : AkonoJni.GetDataHandler {
+ override fun handleGetData(what: String): ByteArray? {
+ if (what == "taler-emscripten-lib.wasm") {
+ Log.i(TAG, "loading emscripten binary from taler-wallet")
+ val stream =
+ assetManager.open("node_modules/taler-wallet/emscripten/taler-emscripten-lib.wasm")
+ val bytes: ByteArray = stream.readBytes()
+ Log.i(TAG, "size of emscripten binary: ${bytes.size}")
+ return bytes
+ } else {
+ Log.w(TAG, "data '$what' requested by akono not found")
+ return null
+ }
+ }
+}
+
+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<Int, RequestData>()
+
+ private val subscribers = LinkedList<Messenger>()
+
+ override fun onCreate() {
+ akono = AkonoJni()
+ akono.setLoadModuleHandler(AssetModuleLoader(application.assets))
+ akono.setGetDataHandler(AssetDataHandler(application.assets))
+ akono.setMessageHandler(object : AkonoJni.MessageHandler {
+ override fun handleMessage(message: String) {
+ this@WalletBackendService.handleAkonoMessage(message)
+ }
+ })
+ akono.evalNodeCode("console.log('hello world from taler wallet-android')")
+ akono.evalNodeCode("require('source-map-support').install();")
+ akono.evalNodeCode("tw = require('taler-wallet');")
+ akono.evalNodeCode("tw.installAndroidWalletListener();")
+ sendInitMessage()
+ initialized = true
+ super.onCreate()
+ }
+
+ fun sendInitMessage() {
+ val msg = JSONObject()
+ msg.put("operation", "init")
+ val args = JSONObject()
+ msg.put("args", args)
+ args.put("persistentStoragePath", "${application.filesDir}/talerwalletdb.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.getData()
+ 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.put(
+ 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 handleAkonoMessage(messageStr: String) {
+ Log.v(TAG, "got back message: ${messageStr}")
+ val message = JSONObject(messageStr)
+ val type = message.getString("type")
+ when (type) {
+ "notification" -> {
+ var rm: LinkedList<Messenger>? = null
+ for (s in subscribers) {
+ val m = Message.obtain(null, MSG_NOTIFY)
+ try {
+ s.send(m)
+ } catch (e: RemoteException) {
+ if (rm == null) {
+ rm = LinkedList<Messenger>()
+ }
+ rm.add(s)
+ subscribers.remove(s)
+ }
+ }
+ if (rm != null) {
+ for (s in rm) {
+ subscribers.remove(s)
+ }
+ }
+ }
+ "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" -> {
+ val operation = message.getString("operation")
+ when (operation) {
+ "init" -> {
+ Log.v(TAG, "got response for init operation")
+ }
+ else -> {
+ val id = message.getInt("id")
+ Log.v(TAG, "got response for operation $operation")
+ val rd = requests.get(id)
+ if (rd == null) {
+ Log.e(TAG, "wallet returned unknown request ID ($id)")
+ return
+ }
+ val m = Message.obtain(null, MSG_REPLY)
+ val b = m.data
+ if (message.has("result")) {
+ val respJson = message.getJSONObject("result")
+ b.putString("response", respJson.toString(2))
+ } else {
+ b.putString("response", "{}")
+ }
+ b.putInt("requestID", rd.clientRequestID)
+ rd.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
+ }
+}