diff options
author | Florian Dold <florian.dold@gmail.com> | 2019-08-22 23:37:22 +0200 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2019-08-22 23:37:22 +0200 |
commit | b9fd051a1bf453e923ddbbf86cf8602d154278e1 (patch) | |
tree | 2dfa438cd76fc4fc41079359a359f2325ac129dd | |
parent | defff18c7c8dbfc87fbc282da005ff25cda1159f (diff) | |
download | wallet-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.xml | 109 | ||||
-rw-r--r-- | app/src/main/AndroidManifest.xml | 23 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/AlreadyPaid.kt | 30 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/HostCardEmulatorService.kt | 204 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/MainActivity.kt | 50 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/PromptPayment.kt | 27 | ||||
-rw-r--r-- | app/src/main/java/net/taler/wallet/WalletViewModel.kt | 39 | ||||
-rw-r--r-- | app/src/main/res/layout/fragment_already_paid.xml | 38 | ||||
-rw-r--r-- | app/src/main/res/navigation/nav_graph.xml | 9 | ||||
-rw-r--r-- | app/src/main/res/values/strings.xml | 2 | ||||
-rw-r--r-- | app/src/main/res/xml/apduservice.xml | 9 |
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 |