gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-android] branch master updated: put wallet node backend in


From: gnunet
Subject: [taler-wallet-android] branch master updated: put wallet node backend into its own service process
Date: Thu, 14 Nov 2019 17:41:34 +0100

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository wallet-android.

The following commit(s) were added to refs/heads/master by this push:
     new 87c1b63  put wallet node backend into its own service process
87c1b63 is described below

commit 87c1b63c4cf2b81963735feb0bce8b8f0b004dba
Author: Florian Dold <address@hidden>
AuthorDate: Thu Nov 14 17:41:18 2019 +0100

    put wallet node backend into its own service process
---
 .idea/codeStyles/Project.xml                       |   3 +
 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 ++-
 build.gradle                                       |   2 +-
 10 files changed, 664 insertions(+), 339 deletions(-)

diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index ce889bd..a88ded0 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -1,5 +1,8 @@
 <component name="ProjectCodeStyleConfiguration">
   <code_scheme name="Project" version="173">
+    <AndroidXmlCodeStyleSettings>
+      <option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
+    </AndroidXmlCodeStyleSettings>
     <JetCodeStyleSettings>
       <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
     </JetCodeStyleSettings>
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">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
-
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="taler" />
+            </intent-filter>
         </activity>
 
         <service
@@ -38,6 +43,10 @@
                     android:name="android.nfc.cardemulation.host_apdu_service"
                     android:resource="@xml/apduservice" />
         </service>
+
+        <service
+                android:name=".backend.WalletBackendService"
+                android:process=":WalletBackendService" />
     </application>
 
 </manifest>
\ 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<String>): 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<Boolean> = 
MutableLiveData<Boolean>().apply {
+    val testWithdrawalInProgress = MutableLiveData<Boolean>().apply {
         value = false
     }
 
-    val balances: MutableLiveData<WalletBalances> = 
MutableLiveData<WalletBalances>().apply {
+    val balances = MutableLiveData<WalletBalances>().apply {
         value = WalletBalances(false, listOf())
     }
 
-    val payStatus: MutableLiveData<PayStatus> = 
MutableLiveData<PayStatus>().apply {
+    val payStatus = MutableLiveData<PayStatus>().apply {
         value = PayStatus.None()
     }
 
-    val withdrawStatus: MutableLiveData<WithdrawStatus> = 
MutableLiveData<WithdrawStatus>().apply {
+    val withdrawStatus = MutableLiveData<WithdrawStatus>().apply {
         value = WithdrawStatus.None()
     }
 
+    private val walletBackendApi = WalletBackendApi(app)
+
     fun init() {
         if (initialized) {
             Log.e(TAG, "WalletViewModel already initialized")
             return
         }
 
-        val app = this.getApplication<Application>()
-        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<BalanceEntry>();
-                                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<BalanceEntry>()
+            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<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
+    }
+}
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">
 
+        <EditText
+                android:id="@+id/editText2"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:ems="10"
+                android:inputType="textPersonName"
+                android:text="Version Information" />
+
+
+        <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal">
+
+            <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:text="Android Wallet" />
+
+
+            <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:text="0.6.0pre3 (Sat 02 Nov 2019)" />
+
+        </LinearLayout>
+
+
+        <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal">
+
+            <TextView
+                    android:id="@+id/textView5"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:text="Wallet Backend" />
+
+
+            <TextView
+                    android:id="@+id/textView4"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:text="0.6.0pre3 (Sat 02 Nov 2019, 70a2322940)" />
+
+        </LinearLayout>
+
+        <EditText
+                android:id="@+id/editText"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:ems="10"
+                android:inputType="textPersonName"
+                android:text="Test Settings" />
+
         <Button
                 android:text="Reset Wallet (Dangerous!)"
                 android:layout_width="wrap_content"
diff --git a/app/src/main/res/navigation/nav_graph.xml 
b/app/src/main/res/navigation/nav_graph.xml
index 2f9787b..e85427a 100644
--- a/app/src/main/res/navigation/nav_graph.xml
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -1,31 +1,51 @@
 <?xml version="1.0" encoding="utf-8"?>
 <navigation xmlns:android="http://schemas.android.com/apk/res/android";
-            xmlns:app="http://schemas.android.com/apk/res-auto";
-            xmlns:tools="http://schemas.android.com/tools"; 
android:id="@+id/nav_graph"
-            app:startDestination="@id/showBalance">
+        xmlns:app="http://schemas.android.com/apk/res-auto";
+        xmlns:tools="http://schemas.android.com/tools";
+        android:id="@+id/nav_graph"
+        app:startDestination="@id/showBalance">
 
-    <fragment android:id="@+id/showBalance" 
android:name="net.taler.wallet.ShowBalance"
-              android:label="Balances" 
tools:layout="@layout/fragment_show_balance">
-        <action android:id="@+id/action_showBalance_to_promptPayment" 
app:destination="@id/promptPayment"/>
+    <fragment
+            android:id="@+id/showBalance"
+            android:name="net.taler.wallet.ShowBalance"
+            android:label="Balances"
+            tools:layout="@layout/fragment_show_balance">
+        <action
+                android:id="@+id/action_showBalance_to_promptPayment"
+                app:destination="@id/promptPayment" />
         <action
                 android:id="@+id/action_showBalance_to_promptWithdraw"
                 app:destination="@id/promptWithdraw" />
     </fragment>
-    <fragment android:id="@+id/promptPayment" 
android:name="net.taler.wallet.PromptPayment"
-              android:label="Review Payment" 
tools:layout="@layout/fragment_prompt_payment">
-        <action android:id="@+id/action_promptPayment_to_paymentSuccessful" 
app:destination="@id/paymentSuccessful"
-                app:popUpTo="@id/showBalance"/>
+    <fragment
+            android:id="@+id/promptPayment"
+            android:name="net.taler.wallet.PromptPayment"
+            android:label="Review Payment"
+            tools:layout="@layout/fragment_prompt_payment">
+        <action
+                android:id="@+id/action_promptPayment_to_paymentSuccessful"
+                app:destination="@id/paymentSuccessful"
+                app:popUpTo="@id/showBalance" />
         <action
                 android:id="@+id/action_promptPayment_to_alreadyPaid"
                 app:destination="@id/alreadyPaid"
-                app:popUpTo="@id/showBalance"/>
+                app:popUpTo="@id/showBalance" />
     </fragment>
-    <fragment android:id="@+id/paymentSuccessful" 
android:name="net.taler.wallet.PaymentSuccessful"
-              android:label="Payment Successful" 
tools:layout="@layout/fragment_payment_successful"/>
-    <fragment android:id="@+id/settings" 
android:name="net.taler.wallet.Settings" android:label="Settings"
-              tools:layout="@layout/fragment_settings"/>
-    <fragment android:id="@+id/walletHistory" 
android:name="net.taler.wallet.WalletHistory"
-              android:label="History" 
tools:layout="@layout/fragment_show_history"/>
+    <fragment
+            android:id="@+id/paymentSuccessful"
+            android:name="net.taler.wallet.PaymentSuccessful"
+            android:label="Payment Successful"
+            tools:layout="@layout/fragment_payment_successful" />
+    <fragment
+            android:id="@+id/settings"
+            android:name="net.taler.wallet.Settings"
+            android:label="Settings"
+            tools:layout="@layout/fragment_settings" />
+    <fragment
+            android:id="@+id/walletHistory"
+            android:name="net.taler.wallet.WalletHistory"
+            android:label="History"
+            tools:layout="@layout/fragment_show_history" />
     <fragment
             android:id="@+id/alreadyPaid"
             android:name="net.taler.wallet.AlreadyPaid"
@@ -35,15 +55,18 @@
             android:id="@+id/promptWithdraw"
             android:name="net.taler.wallet.PromptWithdraw"
             android:label="Withdraw Digital Cash"
-            tools:layout="@layout/fragment_prompt_withdraw" >
+            tools:layout="@layout/fragment_prompt_withdraw">
         <action
                 android:id="@+id/action_promptWithdraw_to_withdrawSuccessful"
                 app:destination="@id/withdrawSuccessful"
-                app:popUpTo="@id/showBalance"/>
+                app:popUpTo="@id/showBalance" />
     </fragment>
     <fragment
             android:id="@+id/withdrawSuccessful"
             android:name="net.taler.wallet.WithdrawSuccessful"
             android:label="Withdrawal Confirmed"
             tools:layout="@layout/fragment_withdraw_successful" />
+    <action
+            android:id="@+id/action_global_promptPayment"
+            app:destination="@id/promptPayment" />
 </navigation>
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index df8fe1b..b8692a6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -8,7 +8,7 @@ buildscript {
         
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.5.1'
+        classpath 'com.android.tools.build:gradle:3.5.2'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files

-- 
To stop receiving notification emails like this one, please contact
address@hidden.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]