summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2019-08-22 23:37:22 +0200
committerFlorian Dold <florian.dold@gmail.com>2019-08-22 23:37:22 +0200
commitb9fd051a1bf453e923ddbbf86cf8602d154278e1 (patch)
tree2dfa438cd76fc4fc41079359a359f2325ac129dd
parentdefff18c7c8dbfc87fbc282da005ff25cda1159f (diff)
downloadwallet-android-b9fd051a1bf453e923ddbbf86cf8602d154278e1.tar.gz
wallet-android-b9fd051a1bf453e923ddbbf86cf8602d154278e1.tar.bz2
wallet-android-b9fd051a1bf453e923ddbbf86cf8602d154278e1.zip
UX improvements / prototype support for NFC tunneling
-rw-r--r--.idea/codeStyles/Project.xml109
-rw-r--r--app/src/main/AndroidManifest.xml23
-rw-r--r--app/src/main/java/net/taler/wallet/AlreadyPaid.kt30
-rw-r--r--app/src/main/java/net/taler/wallet/HostCardEmulatorService.kt204
-rw-r--r--app/src/main/java/net/taler/wallet/MainActivity.kt50
-rw-r--r--app/src/main/java/net/taler/wallet/PromptPayment.kt27
-rw-r--r--app/src/main/java/net/taler/wallet/WalletViewModel.kt39
-rw-r--r--app/src/main/res/layout/fragment_already_paid.xml38
-rw-r--r--app/src/main/res/navigation/nav_graph.xml9
-rw-r--r--app/src/main/res/values/strings.xml2
-rw-r--r--app/src/main/res/xml/apduservice.xml9
11 files changed, 528 insertions, 12 deletions
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 1bec35e..ce889bd 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -3,6 +3,115 @@
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
+ <codeStyleSettings language="XML">
+ <arrangement>
+ <rules>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>xmlns:android</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>xmlns:.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:id</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*:name</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>name</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>style</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>^$</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>ANDROID_ATTRIBUTE_ORDER</order>
+ </rule>
+ </section>
+ <section>
+ <rule>
+ <match>
+ <AND>
+ <NAME>.*</NAME>
+ <XML_ATTRIBUTE />
+ <XML_NAMESPACE>.*</XML_NAMESPACE>
+ </AND>
+ </match>
+ <order>BY_NAME</order>
+ </rule>
+ </section>
+ </rules>
+ </arrangement>
+ </codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b675499..3a853c3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,6 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="net.taler.wallet">
+ xmlns:tools="http://schemas.android.com/tools"
+ package="net.taler.wallet">
+
+ <uses-permission android:name="android.permission.NFC" />
+ <uses-feature android:name="android.hardware.nfc.hce"
+ android:required="false" />
<application
android:allowBackup="true"
@@ -8,7 +13,8 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
- android:theme="@style/AppTheme">
+ android:theme="@style/AppTheme"
+ tools:ignore="GoogleAppIndexingWarning">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
@@ -19,6 +25,19 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
+
+ <service
+ android:name=".HostCardEmulatorService"
+ android:exported="true"
+ android:permission="android.permission.BIND_NFC_SERVICE">
+ <intent-filter>
+ <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
+ </intent-filter>
+
+ <meta-data
+ android:name="android.nfc.cardemulation.host_apdu_service"
+ android:resource="@xml/apduservice" />
+ </service>
</application>
</manifest> \ No newline at end of file
diff --git a/app/src/main/java/net/taler/wallet/AlreadyPaid.kt b/app/src/main/java/net/taler/wallet/AlreadyPaid.kt
new file mode 100644
index 0000000..40903f9
--- /dev/null
+++ b/app/src/main/java/net/taler/wallet/AlreadyPaid.kt
@@ -0,0 +1,30 @@
+package net.taler.wallet
+
+
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import androidx.navigation.findNavController
+
+/**
+ * A simple [Fragment] subclass.
+ */
+class AlreadyPaid : Fragment() {
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ // Inflate the layout for this fragment
+ val view = inflater.inflate(R.layout.fragment_already_paid, container, false)
+ view.findViewById<Button>(R.id.button_success_back).setOnClickListener {
+ activity!!.findNavController(R.id.nav_host_fragment).navigateUp()
+ }
+ return view
+ }
+
+
+}
diff --git a/app/src/main/java/net/taler/wallet/HostCardEmulatorService.kt b/app/src/main/java/net/taler/wallet/HostCardEmulatorService.kt
new file mode 100644
index 0000000..cdcf492
--- /dev/null
+++ b/app/src/main/java/net/taler/wallet/HostCardEmulatorService.kt
@@ -0,0 +1,204 @@
+package net.taler.wallet
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.nfc.cardemulation.HostApduService
+import android.os.Bundle
+import android.util.Log
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.util.*
+import java.util.concurrent.ConcurrentLinkedDeque
+import java.util.concurrent.ConcurrentLinkedQueue
+
+class Utils {
+ companion object {
+ private val HEX_CHARS = "0123456789ABCDEF"
+ fun hexStringToByteArray(data: String) : ByteArray {
+
+ val result = ByteArray(data.length / 2)
+
+ for (i in 0 until data.length step 2) {
+ val firstIndex = HEX_CHARS.indexOf(data[i]);
+ val secondIndex = HEX_CHARS.indexOf(data[i + 1]);
+
+ val octet = firstIndex.shl(4).or(secondIndex)
+ result.set(i.shr(1), octet.toByte())
+ }
+
+ return result
+ }
+
+ private val HEX_CHARS_ARRAY = "0123456789ABCDEF".toCharArray()
+ fun toHex(byteArray: ByteArray) : String {
+ val result = StringBuffer()
+
+ byteArray.forEach {
+ val octet = it.toInt()
+ val firstIndex = (octet and 0xF0).ushr(4)
+ val secondIndex = octet and 0x0F
+ result.append(HEX_CHARS_ARRAY[firstIndex])
+ result.append(HEX_CHARS_ARRAY[secondIndex])
+ }
+
+ return result.toString()
+ }
+ }
+}
+
+
+fun makeApduSuccessResponse(payload: ByteArray): ByteArray {
+ val stream = ByteArrayOutputStream()
+ stream.write(payload)
+ stream.write(0x90)
+ stream.write(0x00)
+ return stream.toByteArray()
+}
+
+
+fun makeApduFailureResponse(): ByteArray {
+ val stream = ByteArrayOutputStream()
+ stream.write(0x6F)
+ stream.write(0x00)
+ return stream.toByteArray()
+}
+
+
+fun readApduBodySize(stream: ByteArrayInputStream): Int {
+ val b0 = stream.read()
+ if (b0 == -1) {
+ return 0;
+ }
+ if (b0 != 0) {
+ return b0
+ }
+ val b1 = stream.read()
+ val b2 = stream.read()
+
+ return (b1 shl 8) and b2
+}
+
+
+class HostCardEmulatorService: HostApduService() {
+
+ val queuedRequests: ConcurrentLinkedDeque<String> = ConcurrentLinkedDeque()
+
+ override fun onCreate() {
+ IntentFilter(HTTP_TUNNEL_REQUEST).also { filter ->
+ registerReceiver(object : BroadcastReceiver() {
+ override fun onReceive(p0: Context?, p1: Intent?) {
+ queuedRequests.addLast(p1!!.getStringExtra("tunnelMessage"))
+ }
+ }, filter)
+ }
+ }
+
+ override fun onDeactivated(reason: Int) {
+ Log.d(TAG, "Deactivated: " + reason)
+ Intent().also { intent ->
+ intent.action = MERCHANT_NFC_DISCONNECTED
+ sendBroadcast(intent)
+ }
+ }
+
+ override fun processCommandApdu(commandApdu: ByteArray?,
+ extras: Bundle?): ByteArray {
+
+ //Log.d(TAG, "Processing command APDU")
+
+ if (commandApdu == null) {
+ Log.d(TAG, "APDU is null")
+ return makeApduFailureResponse()
+ }
+
+ val stream = ByteArrayInputStream(commandApdu)
+
+ val command = stream.read()
+
+ if (command != 0) {
+ Log.d(TAG, "APDU has invalid command")
+ return makeApduFailureResponse()
+ }
+
+ val instruction = stream.read()
+
+ val instructionStr = "%02x".format(instruction)
+
+ //Log.v(TAG, "Processing instruction $instructionStr")
+
+ val p1 = stream.read()
+ val p2 = stream.read()
+
+ //Log.v(TAG, "instruction paramaters $p1 $p2")
+
+ if (instruction == SELECT_INS) {
+ // FIXME: validate body!
+ return makeApduSuccessResponse(ByteArray(0))
+ }
+
+ if (instruction == GET_INS) {
+ val req = queuedRequests.poll()
+ return if (req != null) {
+ Log.v(TAG,"sending tunnel request")
+ makeApduSuccessResponse(req.toByteArray(Charsets.UTF_8))
+ } else {
+ makeApduSuccessResponse(ByteArray(0))
+ }
+ }
+
+ if (instruction == PUT_INS) {
+ val bodySize = readApduBodySize(stream)
+
+
+ val talerInstr = stream.read()
+ val bodyBytes = stream.readBytes()
+
+
+ when (talerInstr.toInt()) {
+ 1 -> {
+ val url = String(bodyBytes, Charsets.UTF_8)
+
+ Intent().also { intent ->
+ intent.action = TRIGGER_PAYMENT_ACTION
+ intent.putExtra("contractUrl", url)
+ sendBroadcast(intent)
+ }
+ }
+ 2 -> {
+ Log.v(TAG, "got http response: ${bodyBytes.toString(Charsets.UTF_8)}")
+
+ Intent().also { intent ->
+ intent.action = HTTP_TUNNEL_RESPONSE
+ intent.putExtra("response", bodyBytes.toString(Charsets.UTF_8))
+ sendBroadcast(intent)
+ }
+ }
+ else -> {
+ Log.v(TAG, "taler instruction $talerInstr unknown")
+ }
+ }
+
+ return makeApduSuccessResponse(ByteArray(0))
+ }
+
+ return makeApduFailureResponse()
+ }
+
+ companion object {
+ val TAG = "taler-wallet-hce"
+ val AID = "A0000002471001"
+ val SELECT_INS = 0xA4
+ val PUT_INS = 0xDA
+ val GET_INS = 0xCA
+
+ val TRIGGER_PAYMENT_ACTION = "net.taler.TRIGGER_PAYMENT_ACTION"
+
+ val MERCHANT_NFC_CONNECTED = "net.taler.MERCHANT_NFC_CONNECTED"
+ val MERCHANT_NFC_DISCONNECTED = "net.taler.MERCHANT_NFC_DISCONNECTED"
+
+ val HTTP_TUNNEL_RESPONSE = "net.taler.HTTP_TUNNEL_RESPONSE"
+ val HTTP_TUNNEL_REQUEST = "net.taler.HTTP_TUNNEL_REQUEST"
+ }
+} \ 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 e539cfd..44cd8a6 100644
--- a/app/src/main/java/net/taler/wallet/MainActivity.kt
+++ b/app/src/main/java/net/taler/wallet/MainActivity.kt
@@ -1,7 +1,10 @@
package net.taler.wallet
+import android.content.BroadcastReceiver
+import android.content.Context
import android.content.Intent
+import android.content.IntentFilter
import android.os.Bundle
import android.util.Log
import android.view.Menu
@@ -11,7 +14,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
-import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
@@ -22,6 +24,8 @@ import com.google.zxing.integration.android.IntentIntegrator
import com.google.zxing.integration.android.IntentResult
import me.zhanghai.android.materialprogressbar.MaterialProgressBar
+
+
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
lateinit var model: WalletViewModel
@@ -54,6 +58,50 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
model.init()
model.getBalances()
+
+ val triggerPaymentFilter = IntentFilter(HostCardEmulatorService.TRIGGER_PAYMENT_ACTION)
+ registerReceiver(object : BroadcastReceiver() {
+ override fun onReceive(p0: Context?, p1: Intent?) {
+
+ if (model.payStatus.value !is PayStatus.None) {
+ return
+ }
+
+ val url = p1!!.extras!!.get("contractUrl") as String
+
+ findNavController(R.id.nav_host_fragment).navigate(R.id.action_showBalance_to_promptPayment)
+ model.preparePay(url)
+
+ }
+ }, triggerPaymentFilter)
+
+ val nfcConnectedFilter = IntentFilter(HostCardEmulatorService.MERCHANT_NFC_CONNECTED)
+ registerReceiver(object : BroadcastReceiver() {
+ override fun onReceive(p0: Context?, p1: Intent?) {
+ Log.v(TAG, "got MERCHANT_NFC_CONNECTED")
+ //model.startTunnel()
+ }
+ }, nfcConnectedFilter)
+
+ val nfcDisconnectedFilter = IntentFilter(HostCardEmulatorService.MERCHANT_NFC_DISCONNECTED)
+ registerReceiver(object : BroadcastReceiver() {
+ override fun onReceive(p0: Context?, p1: Intent?) {
+ Log.v(TAG, "got MERCHANT_NFC_DISCONNECTED")
+ //model.stopTunnel()
+ }
+ }, nfcDisconnectedFilter)
+
+
+ IntentFilter(HostCardEmulatorService.HTTP_TUNNEL_RESPONSE).also { filter ->
+ registerReceiver(object : BroadcastReceiver() {
+ override fun onReceive(p0: Context?, p1: Intent?) {
+ Log.v("taler-tunnel", "got HTTP_TUNNEL_RESPONSE")
+ model.tunnelResponse(p1!!.getStringExtra("response"))
+ }
+ }, filter)
+ }
+
+ //model.startTunnel()
}
override fun onBackPressed() {
diff --git a/app/src/main/java/net/taler/wallet/PromptPayment.kt b/app/src/main/java/net/taler/wallet/PromptPayment.kt
index 9486814..07d3dd2 100644
--- a/app/src/main/java/net/taler/wallet/PromptPayment.kt
+++ b/app/src/main/java/net/taler/wallet/PromptPayment.kt
@@ -32,7 +32,8 @@ class PromptPayment : Fragment() {
var fragmentView: View? = null
- fun triggerLoading(loading: Boolean) {
+ fun triggerLoading() {
+ val loading = model.payStatus.value == null || (model.payStatus.value is PayStatus.Loading)
val myActivity = activity!!
val progressBar = myActivity.findViewById<MaterialProgressBar>(R.id.progress_bar)
if (loading) {
@@ -49,13 +50,13 @@ class PromptPayment : Fragment() {
ViewModelProviders.of(this)[WalletViewModel::class.java]
} ?: throw Exception("Invalid Activity")
- triggerLoading(true)
+ triggerLoading()
}
override fun onResume() {
super.onResume()
Log.v("taler-wallet", "called onResume on PromptPayment")
- triggerLoading(model.payStatus.value == null || model.payStatus.value is PayStatus.Loading)
+ triggerLoading()
}
fun fillOrderInfo(view: View, contractTerms: ContractTerms, totalFees: Amount?) {
@@ -90,24 +91,31 @@ class PromptPayment : Fragment() {
confirmPaymentButton.setOnClickListener {
model.confirmPay(payStatus.proposalId)
- triggerLoading(true)
confirmPaymentButton.isEnabled = false
}
- triggerLoading(false)
}
is PayStatus.InsufficientBalance -> {
fillOrderInfo(view, payStatus.contractTerms, null)
promptPaymentDetails.visibility = View.VISIBLE
balanceInsufficientWarning.visibility = View.VISIBLE
confirmPaymentButton.isEnabled = false
- triggerLoading(false)
}
is PayStatus.Success -> {
- triggerLoading(false)
+ model.payStatus.value = PayStatus.None()
activity!!.findNavController(R.id.nav_host_fragment).navigate(R.id.action_promptPayment_to_paymentSuccessful)
}
+ is PayStatus.AlreadyPaid -> {
+ activity!!.findNavController(R.id.nav_host_fragment).navigate(R.id.action_promptPayment_to_alreadyPaid)
+ model.payStatus.value = PayStatus.None()
+ }
+ is PayStatus.None -> {
+ // No payment active.
+ }
+ is PayStatus.Loading -> {
+ // Wait until loaded ...
+ }
else -> {
- val bar = Snackbar.make(view , "Unexpected result", Snackbar.LENGTH_SHORT)
+ val bar = Snackbar.make(view , "Bug: Unexpected result", Snackbar.LENGTH_SHORT)
bar.show()
}
}
@@ -133,9 +141,10 @@ class PromptPayment : Fragment() {
activity!!.findNavController(R.id.nav_host_fragment).navigateUp()
}
- triggerLoading(true)
+ triggerLoading()
model.payStatus.observe(this, Observer {
+ triggerLoading()
showPayStatus(view, it)
})
return view
diff --git a/app/src/main/java/net/taler/wallet/WalletViewModel.kt b/app/src/main/java/net/taler/wallet/WalletViewModel.kt
index 7c82f81..f644c8d 100644
--- a/app/src/main/java/net/taler/wallet/WalletViewModel.kt
+++ b/app/src/main/java/net/taler/wallet/WalletViewModel.kt
@@ -3,6 +3,7 @@ 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
@@ -139,6 +140,7 @@ data class WalletBalances(val initialized: Boolean, val byCurrency: List<Amount>
data class ContractTerms(val summary: String, val amount: Amount)
open class PayStatus {
+ class None : PayStatus()
class Loading : PayStatus()
data class Prepared(val contractTerms: ContractTerms, val proposalId: Int, val totalFees: Amount) : PayStatus()
data class InsufficientBalance(val contractTerms: ContractTerms) : PayStatus()
@@ -165,6 +167,7 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) {
init {
isBalanceLoading.value = false
balances.value = WalletBalances(false, listOf())
+ payStatus.value = PayStatus.None()
}
fun init() {
@@ -185,6 +188,14 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) {
"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")
@@ -251,6 +262,7 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) {
sendInitMessage()
+
this.initialized = true
}
@@ -301,6 +313,8 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) {
msg.put("args", args)
args.put("url", url)
+ this.payStatus.value = PayStatus.Loading()
+
myAkono.sendMessage(msg.toString())
}
@@ -328,4 +342,29 @@ class WalletViewModel(val app: Application) : AndroidViewModel(app) {
getBalances()
}
+
+ fun startTunnel() {
+ val msg = JSONObject()
+ msg.put("operation", "startTunnel")
+
+ myAkono.sendMessage(msg.toString())
+ }
+
+ fun stopTunnel() {
+ val msg = JSONObject()
+ msg.put("operation", "stopTunnel")
+
+ myAkono.sendMessage(msg.toString())
+ }
+
+ fun tunnelResponse(resp: String) {
+ val respJson = JSONObject(resp)
+
+ val msg = JSONObject()
+ msg.put("operation", "tunnelResponse")
+ msg.put("args", respJson)
+
+ myAkono.sendMessage(msg.toString())
+
+ }
} \ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_already_paid.xml b/app/src/main/res/layout/fragment_already_paid.xml
new file mode 100644
index 0000000..69c949e
--- /dev/null
+++ b/app/src/main/res/layout/fragment_already_paid.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="15dp"
+ tools:context=".PaymentSuccessful">
+
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <Space android:layout_width="match_parent" android:layout_height="0dp"
+ android:layout_weight="1"/>
+
+ <TextView
+ android:layout_gravity="center"
+ android:layout_width="match_parent"
+ android:textAlignment="center"
+ android:layout_height="50dp"
+ android:text="You've already paid for this order."
+ android:autoSizeTextType="uniform"
+ android:textColor="@android:color/holo_green_dark"/>
+
+
+ <Space android:layout_width="match_parent" android:layout_height="0dp"
+ android:layout_weight="1"/>
+ <Button
+ android:text="Go Back"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/button_success_back"/>
+
+ </LinearLayout>
+
+</FrameLayout> \ No newline at end of file
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
index 174c907..80a94ec 100644
--- a/app/src/main/res/navigation/nav_graph.xml
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -12,6 +12,10 @@
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"/>
</fragment>
<fragment android:id="@+id/paymentSuccessful" android:name="net.taler.wallet.PaymentSuccessful"
android:label="Payment Successful" tools:layout="@layout/fragment_payment_successful"/>
@@ -19,4 +23,9 @@
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"
+ android:label="Already Paid"
+ tools:layout="@layout/fragment_already_paid" />
</navigation> \ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f059311..658dc62 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -16,4 +16,6 @@
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
+ <string name="servicedesc">my service</string>
+ <string name="aiddescription">my aid</string>
</resources>
diff --git a/app/src/main/res/xml/apduservice.xml b/app/src/main/res/xml/apduservice.xml
new file mode 100644
index 0000000..b862ccb
--- /dev/null
+++ b/app/src/main/res/xml/apduservice.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
+ android:description="@string/servicedesc"
+ android:requireDeviceUnlock="false">
+ <aid-group android:description="@string/aiddescription"
+ android:category="other">
+ <aid-filter android:name="A0000002471001"/>
+ </aid-group>
+</host-apdu-service> \ No newline at end of file