aboutsummaryrefslogtreecommitdiff
path: root/taler-kotlin-common/src
diff options
context:
space:
mode:
authorTorsten Grote <t@grobox.de>2020-03-18 15:54:36 -0300
committerTorsten Grote <t@grobox.de>2020-03-18 15:54:36 -0300
commit070fd9b7d3793082e447d7713d1dda932f237c99 (patch)
tree5d1c3ec9006eeb8adb23bac34edbced2642dba86 /taler-kotlin-common/src
parentd20a5a81af4fa88eb26ea9678ecc13a0dd3d64a5 (diff)
downloadtaler-android-070fd9b7d3793082e447d7713d1dda932f237c99.tar.gz
taler-android-070fd9b7d3793082e447d7713d1dda932f237c99.tar.bz2
taler-android-070fd9b7d3793082e447d7713d1dda932f237c99.zip
Factor out some cashier code into common library
Diffstat (limited to 'taler-kotlin-common/src')
-rw-r--r--taler-kotlin-common/src/main/AndroidManifest.xml24
-rw-r--r--taler-kotlin-common/src/main/java/net/taler/common/Amount.kt45
-rw-r--r--taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt53
-rw-r--r--taler-kotlin-common/src/main/java/net/taler/common/ByteArrayUtils.kt53
-rw-r--r--taler-kotlin-common/src/main/java/net/taler/common/NfcManager.kt234
-rw-r--r--taler-kotlin-common/src/main/java/net/taler/common/QrCodeManager.kt42
6 files changed, 451 insertions, 0 deletions
diff --git a/taler-kotlin-common/src/main/AndroidManifest.xml b/taler-kotlin-common/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..902ddc1
--- /dev/null
+++ b/taler-kotlin-common/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ This file is part of GNU Taler
+ ~ (C) 2020 Taler Systems S.A.
+ ~
+ ~ GNU Taler is free software; you can redistribute it and/or modify it under the
+ ~ terms of the GNU General Public License as published by the Free Software
+ ~ Foundation; either version 3, or (at your option) any later version.
+ ~
+ ~ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ ~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ ~ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU General Public License along with
+ ~ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="net.taler.common">
+
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.NFC" />
+
+</manifest>
diff --git a/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt b/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt
new file mode 100644
index 0000000..428ddef
--- /dev/null
+++ b/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt
@@ -0,0 +1,45 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.common
+
+data class Amount(val currency: String, val amount: String) {
+
+ companion object {
+
+ private val SIGNED_REGEX = Regex("""([+\-])(\w+):([0-9.]+)""")
+
+ @Suppress("unused")
+ fun fromString(strAmount: String): Amount {
+ val components = strAmount.split(":")
+ return Amount(components[0], components[1])
+ }
+
+ fun fromStringSigned(strAmount: String): Amount? {
+ val groups = SIGNED_REGEX.matchEntire(strAmount)?.groupValues ?: emptyList()
+ if (groups.size < 4) return null
+ var amount = groups[3].toDoubleOrNull() ?: return null
+ if (groups[1] == "-") amount *= -1
+ val currency = groups[2]
+ val amountStr = amount.toString()
+ // only display as many digits as required to precisely render the balance
+ return Amount(currency, amountStr.removeSuffix(".0"))
+ }
+ }
+
+ override fun toString() = "$amount $currency"
+
+}
diff --git a/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt b/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt
new file mode 100644
index 0000000..2fafdf2
--- /dev/null
+++ b/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt
@@ -0,0 +1,53 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.common
+
+import android.content.Context
+import android.content.Context.CONNECTIVITY_SERVICE
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities
+import android.os.Build
+import android.view.View
+
+fun View.fadeIn(endAction: () -> Unit = {}) {
+ if (visibility == View.VISIBLE) return
+ alpha = 0f
+ visibility = View.VISIBLE
+ animate().alpha(1f).withEndAction {
+ endAction.invoke()
+ }.start()
+}
+
+fun View.fadeOut(endAction: () -> Unit = {}) {
+ if (visibility == View.INVISIBLE) return
+ animate().alpha(0f).withEndAction {
+ visibility = View.INVISIBLE
+ alpha = 1f
+ endAction.invoke()
+ }.start()
+}
+
+fun Context.isOnline(): Boolean {
+ val cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
+ return if (Build.VERSION.SDK_INT < 29) {
+ @Suppress("DEPRECATION")
+ cm.activeNetworkInfo?.isConnected == true
+ } else {
+ val capabilities = cm.getNetworkCapabilities(cm.activeNetwork) ?: return false
+ capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+ }
+}
diff --git a/taler-kotlin-common/src/main/java/net/taler/common/ByteArrayUtils.kt b/taler-kotlin-common/src/main/java/net/taler/common/ByteArrayUtils.kt
new file mode 100644
index 0000000..fba0d07
--- /dev/null
+++ b/taler-kotlin-common/src/main/java/net/taler/common/ByteArrayUtils.kt
@@ -0,0 +1,53 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.common
+
+object ByteArrayUtils {
+
+ 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 = HEX_CHARS.toCharArray()
+
+ @Suppress("unused")
+ 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()
+ }
+
+}
diff --git a/taler-kotlin-common/src/main/java/net/taler/common/NfcManager.kt b/taler-kotlin-common/src/main/java/net/taler/common/NfcManager.kt
new file mode 100644
index 0000000..11e1e1e
--- /dev/null
+++ b/taler-kotlin-common/src/main/java/net/taler/common/NfcManager.kt
@@ -0,0 +1,234 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.common
+
+import android.app.Activity
+import android.content.Context
+import android.nfc.NfcAdapter
+import android.nfc.NfcAdapter.FLAG_READER_NFC_A
+import android.nfc.NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK
+import android.nfc.NfcAdapter.getDefaultAdapter
+import android.nfc.Tag
+import android.nfc.tech.IsoDep
+import android.util.Log
+import net.taler.common.ByteArrayUtils.hexStringToByteArray
+import org.json.JSONObject
+import java.io.ByteArrayOutputStream
+import java.net.URL
+import javax.net.ssl.HttpsURLConnection
+
+@Suppress("unused")
+private const val TALER_AID = "A0000002471001"
+
+class NfcManager : NfcAdapter.ReaderCallback {
+
+ companion object {
+ const val TAG = "taler-merchant"
+
+ /**
+ * Returns true if NFC is supported and false otherwise.
+ */
+ fun hasNfc(context: Context): Boolean {
+ return getNfcAdapter(context) != null
+ }
+
+ /**
+ * Enables NFC reader mode. Don't forget to call [stop] afterwards.
+ */
+ fun start(activity: Activity, nfcManager: NfcManager) {
+ getNfcAdapter(activity)?.enableReaderMode(activity, nfcManager, nfcManager.flags, null)
+ }
+
+ /**
+ * Disables NFC reader mode. Call after [start].
+ */
+ fun stop(activity: Activity) {
+ getNfcAdapter(activity)?.disableReaderMode(activity)
+ }
+
+ private fun getNfcAdapter(context: Context): NfcAdapter? {
+ return getDefaultAdapter(context)
+ }
+ }
+
+ private val flags = FLAG_READER_NFC_A or FLAG_READER_SKIP_NDEF_CHECK
+
+ private var tagString: String? = null
+ private var currentTag: IsoDep? = null
+
+ fun setTagString(tagString: String) {
+ this.tagString = tagString
+ }
+
+ override fun onTagDiscovered(tag: Tag?) {
+
+ Log.v(TAG, "tag discovered")
+
+ val isoDep = IsoDep.get(tag)
+ isoDep.connect()
+
+ currentTag = isoDep
+
+ isoDep.transceive(apduSelectFile())
+
+ val tagString: String? = tagString
+ if (tagString != null) {
+ isoDep.transceive(apduPutTalerData(1, tagString.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()
+ }
+
+}
diff --git a/taler-kotlin-common/src/main/java/net/taler/common/QrCodeManager.kt b/taler-kotlin-common/src/main/java/net/taler/common/QrCodeManager.kt
new file mode 100644
index 0000000..e2a9a55
--- /dev/null
+++ b/taler-kotlin-common/src/main/java/net/taler/common/QrCodeManager.kt
@@ -0,0 +1,42 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.common
+
+import android.graphics.Bitmap
+import android.graphics.Bitmap.Config.RGB_565
+import android.graphics.Color.BLACK
+import android.graphics.Color.WHITE
+import com.google.zxing.BarcodeFormat.QR_CODE
+import com.google.zxing.qrcode.QRCodeWriter
+
+object QrCodeManager {
+
+ fun makeQrCode(text: String, size: Int = 256): Bitmap {
+ val qrCodeWriter = QRCodeWriter()
+ val bitMatrix = qrCodeWriter.encode(text, QR_CODE, size, size)
+ val height = bitMatrix.height
+ val width = bitMatrix.width
+ val bmp = Bitmap.createBitmap(width, height, RGB_565)
+ for (x in 0 until width) {
+ for (y in 0 until height) {
+ bmp.setPixel(x, y, if (bitMatrix.get(x, y)) BLACK else WHITE)
+ }
+ }
+ return bmp
+ }
+
+}