[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-merchant-terminal-android] 09/19: Factor out NFC code from MainAc
From: |
gnunet |
Subject: |
[taler-merchant-terminal-android] 09/19: Factor out NFC code from MainActivity |
Date: |
Fri, 21 Feb 2020 19:00:02 +0100 |
This is an automated email from the git hooks/post-receive script.
torsten-grote pushed a commit to branch master
in repository merchant-terminal-android.
commit f5740d3c47993577796c43fac068fff9141af3a2
Author: Torsten Grote <address@hidden>
AuthorDate: Mon Feb 3 10:23:50 2020 -0300
Factor out NFC code from MainActivity
---
.../java/net/taler/merchantpos/MainActivity.kt | 283 +++------------------
.../main/java/net/taler/merchantpos/NfcManager.kt | 189 ++++++++++++++
app/src/main/java/net/taler/merchantpos/Utils.kt | 36 +++
3 files changed, 262 insertions(+), 246 deletions(-)
diff --git a/app/src/main/java/net/taler/merchantpos/MainActivity.kt
b/app/src/main/java/net/taler/merchantpos/MainActivity.kt
index 57968c3..9841339 100644
--- a/app/src/main/java/net/taler/merchantpos/MainActivity.kt
+++ b/app/src/main/java/net/taler/merchantpos/MainActivity.kt
@@ -1,285 +1,68 @@
package net.taler.merchantpos
import android.nfc.NfcAdapter
-import android.nfc.Tag
-import android.nfc.tech.IsoDep
import android.os.Bundle
-import android.util.Log
import android.view.MenuItem
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
-import androidx.core.view.GravityCompat
+import androidx.core.view.GravityCompat.START
import androidx.drawerlayout.widget.DrawerLayout
+import androidx.lifecycle.Observer
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.navigation.NavigationView
-import net.taler.merchantpos.Utils.Companion.hexStringToByteArray
-import org.json.JSONObject
-import java.io.ByteArrayOutputStream
-import java.net.URL
-import javax.net.ssl.HttpsURLConnection
+import
com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener
-
-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()
- }
- }
-}
-
-val TALER_AID = "A0000002471001"
-
-
-fun writeApduLength(stream: ByteArrayOutputStream, size: Int) {
- when {
- size == 0 -> {
- // No size field needed!
- }
- size <= 255 -> // One byte size field
- stream.write(size)
- size <= 65535 -> {
- stream.write(0)
- // FIXME: is this supposed to be little or big endian?
- stream.write(size and 0xFF)
- stream.write((size ushr 8) and 0xFF)
- }
- else -> throw Error("payload too big")
- }
-}
-
-fun apduSelectFile(): ByteArray {
- return hexStringToByteArray("00A4040007A0000002471001")
-}
-
-
-fun apduPutData(payload: ByteArray): ByteArray {
- val stream = ByteArrayOutputStream()
-
- // Class
- stream.write(0x00)
-
- // Instruction 0xDA = put data
- stream.write(0xDA)
-
- // Instruction parameters
- // (proprietary encoding)
- stream.write(0x01)
- stream.write(0x00)
-
- writeApduLength(stream, payload.size)
-
- stream.write(payload)
-
- return stream.toByteArray()
-}
-
-fun apduPutTalerData(talerInst: Int, payload: ByteArray): ByteArray {
- val realPayload = ByteArrayOutputStream()
- realPayload.write(talerInst)
- realPayload.write(payload)
- return apduPutData(realPayload.toByteArray())
-}
-
-fun apduGetData(): ByteArray {
- val stream = ByteArrayOutputStream()
-
- // Class
- stream.write(0x00)
-
- // Instruction 0xCA = get data
- stream.write(0xCA)
-
- // Instruction parameters
- // (proprietary encoding)
- stream.write(0x01)
- stream.write(0x00)
-
- // Max expected response size, two
- // zero bytes denotes 65536
- stream.write(0x0)
- stream.write(0x0)
-
- return stream.toByteArray()
-}
-
-
-class MainActivity : AppCompatActivity(),
NavigationView.OnNavigationItemSelectedListener,
- NfcAdapter.ReaderCallback {
-
- companion object {
- const val TAG = "taler-merchant"
- }
+class MainActivity : AppCompatActivity(), OnNavigationItemSelectedListener {
private val model: MainViewModel by viewModels()
+ private val nfcManager = NfcManager()
private var nfcAdapter: NfcAdapter? = null
- private var currentTag: IsoDep? = null
-
- override fun onTagDiscovered(tag: Tag?) {
-
- Log.v(TAG, "tag discovered")
-
- val isoDep = IsoDep.get(tag)
- isoDep.connect()
-
- currentTag = isoDep
-
- isoDep.transceive(apduSelectFile())
-
- val contractUri: String? =
model.paymentManager.payment.value?.talerPayUri
-
- if (contractUri != null) {
- isoDep.transceive(apduPutTalerData(1, contractUri.toByteArray()))
- }
-
- // FIXME: use better pattern for sleeps in between requests
- // -> start with fast polling, poll more slowly if no requests are
coming
-
- while (true) {
- try {
- val reqFrame = isoDep.transceive(apduGetData())
- if (reqFrame.size < 2) {
- Log.v(TAG, "request frame too small")
- break
- }
- val req = ByteArray(reqFrame.size - 2)
- if (req.isEmpty()) {
- continue
- }
- reqFrame.copyInto(req, 0, 0, reqFrame.size - 2)
- val jsonReq = JSONObject(req.toString(Charsets.UTF_8))
- val reqId = jsonReq.getInt("id")
- Log.v(TAG, "got request $jsonReq")
- val jsonInnerReq = jsonReq.getJSONObject("request")
- val method = jsonInnerReq.getString("method")
- val urlStr = jsonInnerReq.getString("url")
- Log.v(TAG, "url '$urlStr'")
- Log.v(TAG, "method '$method'")
- val url = URL(urlStr)
- val conn: HttpsURLConnection = url.openConnection() as
HttpsURLConnection
- conn.setRequestProperty("Accept", "application/json")
- conn.connectTimeout = 5000
- conn.doInput = true
- when (method) {
- "get" -> {
- conn.requestMethod = "GET"
- }
- "postJson" -> {
- conn.requestMethod = "POST"
- conn.doOutput = true
- conn.setRequestProperty("Content-Type",
"application/json; utf-8")
- val body = jsonInnerReq.getString("body")
-
conn.outputStream.write(body.toByteArray(Charsets.UTF_8))
- }
- else -> {
- throw Exception("method not supported")
- }
- }
- Log.v(TAG, "connecting")
- conn.connect()
- Log.v(TAG, "connected")
-
- val statusCode = conn.responseCode
- val tunnelResp = JSONObject()
- tunnelResp.put("id", reqId)
- tunnelResp.put("status", conn.responseCode)
-
- if (statusCode == 200) {
- val stream = conn.inputStream
- val httpResp = stream.buffered().readBytes()
- tunnelResp.put("responseJson",
JSONObject(httpResp.toString(Charsets.UTF_8)))
- }
-
- Log.v(TAG, "sending: $tunnelResp")
-
- isoDep.transceive(apduPutTalerData(2,
tunnelResp.toString().toByteArray()))
- } catch (e: Exception) {
- Log.v(TAG, "exception during NFC loop: ${e}")
- break
- }
- }
-
- isoDep.close()
- }
-
- public override fun onResume() {
- super.onResume()
- nfcAdapter?.enableReaderMode(
- this, this,
- NfcAdapter.FLAG_READER_NFC_A or
- NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
- null
- )
- }
-
- public override fun onPause() {
- super.onPause()
- nfcAdapter?.disableReaderMode(this)
- }
+ private lateinit var drawerLayout: DrawerLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
nfcAdapter = NfcAdapter.getDefaultAdapter(this)
+ model.paymentManager.payment.observe(this, Observer { payment ->
+ payment?.talerPayUri?.let {
+ nfcManager.setContractUri(it)
+ }
+ })
- setContentView(R.layout.activity_main)
val toolbar: Toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
- val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
+ drawerLayout = findViewById(R.id.drawer_layout)
val navView: NavigationView = findViewById(R.id.nav_view)
navView.setNavigationItemSelectedListener(this)
val navController = findNavController(R.id.nav_host_fragment)
val appBarConfiguration = AppBarConfiguration(
- setOf(
- R.id.order,
- R.id.merchantSettings,
- R.id.merchantHistory
- ), drawerLayout
- )
+ setOf(
+ R.id.order,
+ R.id.merchantSettings,
+ R.id.merchantHistory
+ ), drawerLayout
+ )
toolbar.setupWithNavController(navController, appBarConfiguration)
}
- override fun onBackPressed() {
- val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
- if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
- drawerLayout.closeDrawer(GravityCompat.START)
- } else {
- super.onBackPressed()
- }
+ public override fun onResume() {
+ super.onResume()
+ // TODO should we only read tags when a payment is to be made?
+ nfcAdapter?.enableReaderMode(this, nfcManager, nfcManager.flags, null)
+ }
+
+ public override fun onPause() {
+ super.onPause()
+ nfcAdapter?.disableReaderMode(this)
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
@@ -291,8 +74,16 @@ class MainActivity : AppCompatActivity(),
NavigationView.OnNavigationItemSelecte
R.id.nav_settings ->
nav.navigate(R.id.action_global_merchantSettings)
}
val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
- drawerLayout.closeDrawer(GravityCompat.START)
+ drawerLayout.closeDrawer(START)
return true
}
+ override fun onBackPressed() {
+ if (drawerLayout.isDrawerOpen(START)) {
+ drawerLayout.closeDrawer(START)
+ } else {
+ super.onBackPressed()
+ }
+ }
+
}
diff --git a/app/src/main/java/net/taler/merchantpos/NfcManager.kt
b/app/src/main/java/net/taler/merchantpos/NfcManager.kt
new file mode 100644
index 0000000..083586f
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/NfcManager.kt
@@ -0,0 +1,189 @@
+package net.taler.merchantpos
+
+import android.nfc.NfcAdapter
+import android.nfc.NfcAdapter.FLAG_READER_NFC_A
+import android.nfc.NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK
+import android.nfc.Tag
+import android.nfc.tech.IsoDep
+import android.util.Log
+import net.taler.merchantpos.Utils.hexStringToByteArray
+import org.json.JSONObject
+import java.io.ByteArrayOutputStream
+import java.net.URL
+import javax.net.ssl.HttpsURLConnection
+
+class NfcManager : NfcAdapter.ReaderCallback {
+
+ companion object {
+ const val TAG = "taler-merchant"
+ }
+
+ val TALER_AID = "A0000002471001"
+ val flags = FLAG_READER_NFC_A or FLAG_READER_SKIP_NDEF_CHECK
+
+ private var contractUri: String? = null
+ private var currentTag: IsoDep? = null
+
+ fun setContractUri(contractUri: String) {
+ this.contractUri = contractUri
+ }
+
+ override fun onTagDiscovered(tag: Tag?) {
+
+ Log.v(TAG, "tag discovered")
+
+ val isoDep = IsoDep.get(tag)
+ isoDep.connect()
+
+ currentTag = isoDep
+
+ isoDep.transceive(apduSelectFile())
+
+ val contractUri: String? = contractUri
+
+ if (contractUri != null) {
+ isoDep.transceive(apduPutTalerData(1, contractUri.toByteArray()))
+ }
+
+ // FIXME: use better pattern for sleeps in between requests
+ // -> start with fast polling, poll more slowly if no requests are
coming
+
+ while (true) {
+ try {
+ val reqFrame = isoDep.transceive(apduGetData())
+ if (reqFrame.size < 2) {
+ Log.v(TAG, "request frame too small")
+ break
+ }
+ val req = ByteArray(reqFrame.size - 2)
+ if (req.isEmpty()) {
+ continue
+ }
+ reqFrame.copyInto(req, 0, 0, reqFrame.size - 2)
+ val jsonReq = JSONObject(req.toString(Charsets.UTF_8))
+ val reqId = jsonReq.getInt("id")
+ Log.v(TAG, "got request $jsonReq")
+ val jsonInnerReq = jsonReq.getJSONObject("request")
+ val method = jsonInnerReq.getString("method")
+ val urlStr = jsonInnerReq.getString("url")
+ Log.v(TAG, "url '$urlStr'")
+ Log.v(TAG, "method '$method'")
+ val url = URL(urlStr)
+ val conn: HttpsURLConnection = url.openConnection() as
HttpsURLConnection
+ conn.setRequestProperty("Accept", "application/json")
+ conn.connectTimeout = 5000
+ conn.doInput = true
+ when (method) {
+ "get" -> {
+ conn.requestMethod = "GET"
+ }
+ "postJson" -> {
+ conn.requestMethod = "POST"
+ conn.doOutput = true
+ conn.setRequestProperty("Content-Type",
"application/json; utf-8")
+ val body = jsonInnerReq.getString("body")
+
conn.outputStream.write(body.toByteArray(Charsets.UTF_8))
+ }
+ else -> {
+ throw Exception("method not supported")
+ }
+ }
+ Log.v(TAG, "connecting")
+ conn.connect()
+ Log.v(TAG, "connected")
+
+ val statusCode = conn.responseCode
+ val tunnelResp = JSONObject()
+ tunnelResp.put("id", reqId)
+ tunnelResp.put("status", conn.responseCode)
+
+ if (statusCode == 200) {
+ val stream = conn.inputStream
+ val httpResp = stream.buffered().readBytes()
+ tunnelResp.put("responseJson",
JSONObject(httpResp.toString(Charsets.UTF_8)))
+ }
+
+ Log.v(TAG, "sending: $tunnelResp")
+
+ isoDep.transceive(apduPutTalerData(2,
tunnelResp.toString().toByteArray()))
+ } catch (e: Exception) {
+ Log.v(TAG, "exception during NFC loop: $e")
+ break
+ }
+ }
+
+ isoDep.close()
+ }
+
+ private fun writeApduLength(stream: ByteArrayOutputStream, size: Int) {
+ when {
+ size == 0 -> {
+ // No size field needed!
+ }
+ size <= 255 -> // One byte size field
+ stream.write(size)
+ size <= 65535 -> {
+ stream.write(0)
+ // FIXME: is this supposed to be little or big endian?
+ stream.write(size and 0xFF)
+ stream.write((size ushr 8) and 0xFF)
+ }
+ else -> throw Error("payload too big")
+ }
+ }
+
+ private fun apduSelectFile(): ByteArray {
+ return hexStringToByteArray("00A4040007A0000002471001")
+ }
+
+ private fun apduPutData(payload: ByteArray): ByteArray {
+ val stream = ByteArrayOutputStream()
+
+ // Class
+ stream.write(0x00)
+
+ // Instruction 0xDA = put data
+ stream.write(0xDA)
+
+ // Instruction parameters
+ // (proprietary encoding)
+ stream.write(0x01)
+ stream.write(0x00)
+
+ writeApduLength(stream, payload.size)
+
+ stream.write(payload)
+
+ return stream.toByteArray()
+ }
+
+ private fun apduPutTalerData(talerInst: Int, payload: ByteArray):
ByteArray {
+ val realPayload = ByteArrayOutputStream()
+ realPayload.write(talerInst)
+ realPayload.write(payload)
+ return apduPutData(realPayload.toByteArray())
+ }
+
+ private fun apduGetData(): ByteArray {
+ val stream = ByteArrayOutputStream()
+
+ // Class
+ stream.write(0x00)
+
+ // Instruction 0xCA = get data
+ stream.write(0xCA)
+
+ // Instruction parameters
+ // (proprietary encoding)
+ stream.write(0x01)
+ stream.write(0x00)
+
+ // Max expected response size, two
+ // zero bytes denotes 65536
+ stream.write(0x0)
+ stream.write(0x0)
+
+ return stream.toByteArray()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/taler/merchantpos/Utils.kt
b/app/src/main/java/net/taler/merchantpos/Utils.kt
new file mode 100644
index 0000000..b43bc25
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/Utils.kt
@@ -0,0 +1,36 @@
+package net.taler.merchantpos
+
+object Utils {
+
+ private const val HEX_CHARS = "0123456789ABCDEF"
+
+ fun hexStringToByteArray(data: String): ByteArray {
+ val result = ByteArray(data.length / 2)
+
+ for (i in data.indices 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[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()
+ }
+
+}
--
To stop receiving notification emails like this one, please contact
address@hidden.
- [taler-merchant-terminal-android] branch master updated (f463d1b -> 39af919), gnunet, 2020/02/21
- [taler-merchant-terminal-android] 03/19: Fix crash when loading history, gnunet, 2020/02/21
- [taler-merchant-terminal-android] 01/19: Upgrade libraries to latest stable versions, gnunet, 2020/02/21
- [taler-merchant-terminal-android] 05/19: Allow user to undo restarting the order, gnunet, 2020/02/21
- [taler-merchant-terminal-android] 07/19: Add ordered products to order's contract terms, gnunet, 2020/02/21
- [taler-merchant-terminal-android] 08/19: Use actual taler icon for the app, gnunet, 2020/02/21
- [taler-merchant-terminal-android] 09/19: Factor out NFC code from MainActivity,
gnunet <=
- [taler-merchant-terminal-android] 06/19: Create payments directly from the order, gnunet, 2020/02/21
- [taler-merchant-terminal-android] 04/19: Fetch merchant config from central configuration JSON, gnunet, 2020/02/21
- [taler-merchant-terminal-android] 02/19: Add screen to process an order, gnunet, 2020/02/21
- [taler-merchant-terminal-android] 18/19: Don't talk about NFC if it is not supported, gnunet, 2020/02/21
- [taler-merchant-terminal-android] 14/19: Use product categories for order summary, gnunet, 2020/02/21
- [taler-merchant-terminal-android] 11/19: Allow user to decide if they want to save password, add FORGET option, gnunet, 2020/02/21
- [taler-merchant-terminal-android] 13/19: Make NFC and QR code re-useable in another app, gnunet, 2020/02/21
- [taler-merchant-terminal-android] 15/19: Introduce different product classes for re-use in other taler apps, gnunet, 2020/02/21
- [taler-merchant-terminal-android] 17/19: Make order sorting deterministic, gnunet, 2020/02/21
- [taler-merchant-terminal-android] 12/19: Fix invalid product configuration, gnunet, 2020/02/21