summaryrefslogtreecommitdiff
path: root/app
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
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')
-rw-r--r--app/build.gradle7
-rw-r--r--app/src/main/AndroidManifest.xml11
-rw-r--r--app/src/main/java/net/taler/wallet/MainActivity.kt19
-rw-r--r--app/src/main/java/net/taler/wallet/WalletViewModel.kt409
-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
-rw-r--r--app/src/main/res/layout/fragment_settings.xml60
-rw-r--r--app/src/main/res/navigation/nav_graph.xml61
8 files changed, 660 insertions, 338 deletions
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