From 87c1b63c4cf2b81963735feb0bce8b8f0b004dba Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 14 Nov 2019 17:41:18 +0100 Subject: put wallet node backend into its own service process --- app/build.gradle | 7 +- app/src/main/AndroidManifest.xml | 11 +- app/src/main/java/net/taler/wallet/MainActivity.kt | 19 +- .../main/java/net/taler/wallet/WalletViewModel.kt | 409 +++++---------------- .../net/taler/wallet/backend/WalletBackendApi.kt | 116 ++++++ .../taler/wallet/backend/WalletBackendService.kt | 315 ++++++++++++++++ app/src/main/res/layout/fragment_settings.xml | 60 +++ app/src/main/res/navigation/nav_graph.xml | 61 ++- 8 files changed, 660 insertions(+), 338 deletions(-) create mode 100644 app/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt create mode 100644 app/src/main/java/net/taler/wallet/backend/WalletBackendService.kt (limited to 'app') diff --git a/app/build.gradle b/app/build.gradle index 4f8ea09..e49c3d8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "net.taler.wallet" minSdkVersion 18 targetSdkVersion 29 - versionCode 1 - versionName "1.0" + versionCode 2 + versionName "0.6.0pre3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -61,4 +61,7 @@ dependencies { // Nicer ProgressBar implementation 'me.zhanghai.android.materialprogressbar:library:1.6.1' + + // JSON parsing and serialization + implementation 'com.google.code.gson:gson:2.8.6' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3a853c3..0855b92 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,9 +21,14 @@ android:theme="@style/AppTheme.NoActionBar"> - + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/net/taler/wallet/MainActivity.kt b/app/src/main/java/net/taler/wallet/MainActivity.kt index 3d07821..3f19986 100644 --- a/app/src/main/java/net/taler/wallet/MainActivity.kt +++ b/app/src/main/java/net/taler/wallet/MainActivity.kt @@ -26,7 +26,6 @@ import com.google.zxing.integration.android.IntentResult import me.zhanghai.android.materialprogressbar.MaterialProgressBar - class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener { lateinit var model: WalletViewModel @@ -50,7 +49,6 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte } fab.hide() - navView.setNavigationItemSelectedListener(this) val navController = findNavController(R.id.nav_host_fragment) @@ -78,7 +76,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte val url = p1!!.extras!!.get("contractUrl") as String - findNavController(R.id.nav_host_fragment).navigate(R.id.action_showBalance_to_promptPayment) + findNavController(R.id.nav_host_fragment).navigate(R.id.action_global_promptPayment) model.preparePay(url) } @@ -100,7 +98,6 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte } }, nfcDisconnectedFilter) - IntentFilter(HostCardEmulatorService.HTTP_TUNNEL_RESPONSE).also { filter -> registerReceiver(object : BroadcastReceiver() { override fun onReceive(p0: Context?, p1: Intent?) { @@ -110,6 +107,12 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte }, filter) } + if (intent.action == Intent.ACTION_VIEW) { + val uri = intent.dataString + if (uri != null) + handleTalerUri(uri, "intent") + } + //model.startTunnel() } @@ -175,6 +178,10 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte } val url = scanResult.contents!! + handleTalerUri(url, "QR code") + } + + private fun handleTalerUri(url: String, from: String) { when { url.startsWith("taler://pay") -> { Log.v(TAG, "navigating!") @@ -189,13 +196,11 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte else -> { val bar: Snackbar = Snackbar.make( findViewById(R.id.nav_host_fragment), - "Scanned QR code doesn't contain Taler payment.", + "URL from $from doesn't contain Taler payment.", Snackbar.LENGTH_SHORT ) bar.show() } } } - - } diff --git a/app/src/main/java/net/taler/wallet/WalletViewModel.kt b/app/src/main/java/net/taler/wallet/WalletViewModel.kt index 4981ae7..34e8eed 100644 --- a/app/src/main/java/net/taler/wallet/WalletViewModel.kt +++ b/app/src/main/java/net/taler/wallet/WalletViewModel.kt @@ -1,127 +1,14 @@ package net.taler.wallet -import akono.AkonoJni -import akono.ModuleResult import android.app.Application -import android.content.Intent -import android.content.res.AssetManager import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData +import net.taler.wallet.backend.WalletBackendApi import org.json.JSONObject -import java.io.File -import java.io.InputStream -import java.lang.Exception val TAG = "taler-wallet" -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 - } - Log.i(TAG, "main field is $mainFile") - 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" - Log.i(TAG, "trying to open $jsPath") - val moduleStream = assetManager.open(jsPath) - return makeResult(jsPath, moduleStream) - } catch (e: Exception) { - } - return null - } - - override fun loadModule(name: String, paths: Array): ModuleResult? { - Log.i(TAG, "loading module $name from paths [${paths.fold("", { acc, s -> "$acc,$s" })}]") - for (path in paths) { - Log.i(TAG, "trying from path $path") - val prefix = "/vmodroot" - if (!path.startsWith(prefix)) { - continue - } - if (path == prefix) { - Log.i(TAG, "path is prefix") - val res = tryPath("$rootPath/$name") - if (res != null) - return res - } else { - Log.i(TAG, "path is not prefix") - val res = tryPath(path.drop(prefix.length + 1) + "/$name") - if (res != null) - return res - } - } - return null - } -} - - -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 - } - } -} data class Amount(val currency: String, val amount: String) { fun isZero(): Boolean { @@ -129,7 +16,7 @@ data class Amount(val currency: String, val amount: String) { } companion object { - const val FRACTIONAL_BASE = 1e8; + const val FRACTIONAL_BASE = 1e8 fun fromJson(jsonAmount: JSONObject): Amount { val amountCurrency = jsonAmount.getString("currency") val amountValue = jsonAmount.getString("value") @@ -186,205 +73,106 @@ open class WithdrawStatus { class WalletViewModel(val app: Application) : AndroidViewModel(app) { - private lateinit var myAkono: AkonoJni private var initialized = false - val testWithdrawalInProgress: MutableLiveData = MutableLiveData().apply { + val testWithdrawalInProgress = MutableLiveData().apply { value = false } - val balances: MutableLiveData = MutableLiveData().apply { + val balances = MutableLiveData().apply { value = WalletBalances(false, listOf()) } - val payStatus: MutableLiveData = MutableLiveData().apply { + val payStatus = MutableLiveData().apply { value = PayStatus.None() } - val withdrawStatus: MutableLiveData = MutableLiveData().apply { + val withdrawStatus = MutableLiveData().apply { value = WithdrawStatus.None() } + private val walletBackendApi = WalletBackendApi(app) + fun init() { if (initialized) { Log.e(TAG, "WalletViewModel already initialized") return } - val app = this.getApplication() - myAkono = AkonoJni() - myAkono.setLoadModuleHandler(AssetModuleLoader(app.assets)) - myAkono.setGetDataHandler(AssetDataHandler(app.assets)) - myAkono.setMessageHandler(object : AkonoJni.MessageHandler { - override fun handleMessage(messageStr: String) { - Log.v(TAG, "got back message: ${messageStr}") - val message = JSONObject(messageStr) - val type = message.getString("type") - when (type) { - "notification" -> { - getBalances() - } - "tunnelHttp" -> { - Log.v(TAG, "got http tunnel request!") - Intent().also { intent -> - intent.action = HostCardEmulatorService.HTTP_TUNNEL_REQUEST - intent.putExtra("tunnelMessage", messageStr) - app.sendBroadcast(intent) - } - } - "response" -> { - val operation = message.getString("operation") - Log.v(TAG, "got response for operation $operation") - when (operation) { - "withdrawTestkudos" -> { - testWithdrawalInProgress.postValue(false) - } - "getBalances" -> { - val balanceList = mutableListOf(); - val result = message.getJSONObject("result") - val byCurrency = result.getJSONObject("byCurrency") - val currencyList = byCurrency.keys().asSequence().toList().sorted() - for (currency in currencyList) { - val jsonAmount = byCurrency.getJSONObject(currency) - .getJSONObject("available") - val amount = Amount.fromJson(jsonAmount) - val jsonAmountIncoming = byCurrency.getJSONObject(currency) - .getJSONObject("pendingIncoming") - val amountIncoming = Amount.fromJson(jsonAmountIncoming) - balanceList.add(BalanceEntry(amount, amountIncoming)) - } - balances.postValue(WalletBalances(true, balanceList)) - } - "getWithdrawalInfo" -> { - Log.v(TAG, "got getWithdrawalInfo result") - val status = withdrawStatus.value - if (status !is WithdrawStatus.Loading) { - Log.v(TAG, "ignoring withdrawal info result, not loading.") - return - } - val result = message.getJSONObject("result") - val suggestedExchange = result.getString("suggestedExchange") - val amount = Amount.fromJson(result.getJSONObject("amount")) - withdrawStatus.postValue( - WithdrawStatus.ReceivedDetails( - status.talerWithdrawUri, - amount, - suggestedExchange - ) - ) - } - "acceptWithdrawal" -> { - Log.v(TAG, "got acceptWithdrawal result") - val status = withdrawStatus.value - if (status !is WithdrawStatus.Withdrawing) { - Log.v(TAG, "ignoring acceptWithdrawal result, invalid state") - } - withdrawStatus.postValue(WithdrawStatus.Success()) - } - "preparePay" -> { - Log.v(TAG, "got preparePay result") - val result = message.getJSONObject("result") - val status = result.getString("status") - var contractTerms: ContractTerms? = null - var proposalId: Int? = null - var totalFees: Amount? = null - if (result.has("proposalId")) { - proposalId = result.getInt("proposalId") - } - if (result.has("contractTerms")) { - val ctJson = result.getJSONObject("contractTerms") - val amount = Amount.fromString(ctJson.getString("amount")) - val summary = ctJson.getString("summary") - contractTerms = ContractTerms(summary, amount) - } - if (result.has("totalFees")) { - totalFees = Amount.fromJson(result.getJSONObject("totalFees")) - } - val res = when (status) { - "payment-possible" -> PayStatus.Prepared( - contractTerms!!, - proposalId!!, - totalFees!! - ) - "paid" -> PayStatus.AlreadyPaid(contractTerms!!) - "insufficient-balance" -> PayStatus.InsufficientBalance( - contractTerms!! - ) - "error" -> PayStatus.Error("got some error") - else -> PayStatus.Error("unkown status") - } - payStatus.postValue(res) - } - "confirmPay" -> { - payStatus.postValue(PayStatus.Success()) - } - } - - } - } - } - }) - - myAkono.evalNodeCode("console.log('hello world from taler wallet-android')") - myAkono.evalNodeCode("require('source-map-support').install();") - myAkono.evalNodeCode("tw = require('taler-wallet');") - myAkono.evalNodeCode("tw.installAndroidWalletListener();") - - sendInitMessage() - - this.initialized = true - } - - private fun sendInitMessage() { - val msg = JSONObject() - msg.put("operation", "init") - val args = JSONObject() - msg.put("args", args) - args.put("persistentStoragePath", "${app.filesDir}/talerwalletdb.json") - - Log.v(TAG, "sending message ${msg}") - myAkono.sendMessage(msg.toString()) + walletBackendApi.notificationHandler = { + Log.i(TAG, "got notification from wallet") + getBalances() + } } + fun getBalances() { - if (!initialized) { - Log.e(TAG, "WalletViewModel not initialized") - return + walletBackendApi.sendRequest("getBalances", null) { result -> + val balanceList = mutableListOf() + val byCurrency = result.getJSONObject("byCurrency") + val currencyList = byCurrency.keys().asSequence().toList().sorted() + for (currency in currencyList) { + val jsonAmount = byCurrency.getJSONObject(currency) + .getJSONObject("available") + val amount = Amount.fromJson(jsonAmount) + val jsonAmountIncoming = byCurrency.getJSONObject(currency) + .getJSONObject("pendingIncoming") + val amountIncoming = Amount.fromJson(jsonAmountIncoming) + balanceList.add(BalanceEntry(amount, amountIncoming)) + } + balances.postValue(WalletBalances(true, balanceList)) } - - val msg = JSONObject() - msg.put("operation", "getBalances") - - myAkono.sendMessage(msg.toString()) } fun withdrawTestkudos() { - if (!initialized) { - Log.e(TAG, "WalletViewModel not initialized") - return - } - testWithdrawalInProgress.value = true - val msg = JSONObject() - msg.put("operation", "withdrawTestkudos") - - myAkono.sendMessage(msg.toString()) + walletBackendApi.sendRequest("withdrawTestkudos", null) { + testWithdrawalInProgress.postValue(false) + } } fun preparePay(url: String) { - val msg = JSONObject() - msg.put("operation", "preparePay") - val args = JSONObject() - msg.put("args", args) args.put("url", url) this.payStatus.value = PayStatus.Loading() - myAkono.sendMessage(msg.toString()) + walletBackendApi.sendRequest("preparePay", args) { result -> + Log.v(TAG, "got preparePay result") + val status = result.getString("status") + var contractTerms: ContractTerms? = null + var proposalId: Int? = null + var totalFees: Amount? = null + if (result.has("proposalId")) { + proposalId = result.getInt("proposalId") + } + if (result.has("contractTerms")) { + val ctJson = result.getJSONObject("contractTerms") + val amount = Amount.fromString(ctJson.getString("amount")) + val summary = ctJson.getString("summary") + contractTerms = ContractTerms(summary, amount) + } + if (result.has("totalFees")) { + totalFees = Amount.fromJson(result.getJSONObject("totalFees")) + } + val res = when (status) { + "payment-possible" -> PayStatus.Prepared( + contractTerms!!, + proposalId!!, + totalFees!! + ) + "paid" -> PayStatus.AlreadyPaid(contractTerms!!) + "insufficient-balance" -> PayStatus.InsufficientBalance( + contractTerms!! + ) + "error" -> PayStatus.Error("got some error") + else -> PayStatus.Error("unkown status") + } + payStatus.postValue(res) + } } fun confirmPay(proposalId: Int) { @@ -392,74 +180,77 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) { msg.put("operation", "confirmPay") val args = JSONObject() - msg.put("args", args) args.put("proposalId", proposalId) - myAkono.sendMessage(msg.toString()) + walletBackendApi.sendRequest("confirmPay", args) { + payStatus.postValue(PayStatus.Success()) + } } fun dangerouslyReset() { - val msg = JSONObject() - msg.put("operation", "reset") - - myAkono.sendMessage(msg.toString()) - - sendInitMessage() - + walletBackendApi.sendRequest("reset", null) testWithdrawalInProgress.value = false balances.value = WalletBalances(false, listOf()) - getBalances() } fun startTunnel() { - val msg = JSONObject() - msg.put("operation", "startTunnel") - - myAkono.sendMessage(msg.toString()) + walletBackendApi.sendRequest("startTunnel", null) } fun stopTunnel() { - val msg = JSONObject() - msg.put("operation", "stopTunnel") - - myAkono.sendMessage(msg.toString()) + walletBackendApi.sendRequest("stopTunnel", null) } fun tunnelResponse(resp: String) { val respJson = JSONObject(resp) - - val msg = JSONObject() - msg.put("operation", "tunnelResponse") - msg.put("args", respJson) - - myAkono.sendMessage(msg.toString()) + walletBackendApi.sendRequest("tunnelResponse", respJson) } fun getWithdrawalInfo(talerWithdrawUri: String) { - val msg = JSONObject() - msg.put("operation", "getWithdrawalInfo") - val args = JSONObject() - msg.put("args", args) args.put("talerWithdrawUri", talerWithdrawUri) withdrawStatus.value = WithdrawStatus.Loading(talerWithdrawUri) - myAkono.sendMessage(msg.toString()) + walletBackendApi.sendRequest("getWithdrawalInfo", args) { result -> + Log.v(TAG, "got getWithdrawalInfo result") + val status = withdrawStatus.value + if (status !is WithdrawStatus.Loading) { + Log.v(TAG, "ignoring withdrawal info result, not loading.") + return@sendRequest + } + val suggestedExchange = result.getString("suggestedExchange") + val amount = Amount.fromJson(result.getJSONObject("amount")) + withdrawStatus.postValue( + WithdrawStatus.ReceivedDetails( + status.talerWithdrawUri, + amount, + suggestedExchange + ) + ) + } } fun acceptWithdrawal(talerWithdrawUri: String, selectedExchange: String) { - val msg = JSONObject() - msg.put("operation", "acceptWithdrawal") - val args = JSONObject() - msg.put("args", args) args.put("talerWithdrawUri", talerWithdrawUri) args.put("selectedExchange", selectedExchange) withdrawStatus.value = WithdrawStatus.Withdrawing(talerWithdrawUri) - myAkono.sendMessage(msg.toString()) + walletBackendApi.sendRequest("acceptWithdrawal", args) { + Log.v(TAG, "got acceptWithdrawal result") + val status = withdrawStatus.value + if (status !is WithdrawStatus.Withdrawing) { + Log.v(TAG, "ignoring acceptWithdrawal result, invalid state") + } + withdrawStatus.postValue(WithdrawStatus.Success()) + } + } + + override fun onCleared() { + walletBackendApi.destroy() + super.onCleared() } } 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() + 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(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): 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() + + private val subscribers = LinkedList() + + 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? = null + for (s in subscribers) { + val m = Message.obtain(null, MSG_NOTIFY) + 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) + } + } + } + "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 + } +} diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index d675d98..5d9ac0a 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -11,6 +11,66 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + + + + + + + + + + + + + + + + + + + + + + +