From 070fd9b7d3793082e447d7713d1dda932f237c99 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 18 Mar 2020 15:54:36 -0300 Subject: Factor out some cashier code into common library --- .idea/gradle.xml | 1 + build.gradle | 1 + cashier/build.gradle | 16 +- cashier/src/main/AndroidManifest.xml | 4 - cashier/src/main/java/net/taler/cashier/Amount.kt | 45 ---- .../main/java/net/taler/cashier/BalanceFragment.kt | 3 + .../main/java/net/taler/cashier/MainViewModel.kt | 3 +- cashier/src/main/java/net/taler/cashier/Utils.kt | 91 -------- .../java/net/taler/cashier/withdraw/NfcManager.kt | 234 --------------------- .../net/taler/cashier/withdraw/QrCodeManager.kt | 42 ---- .../taler/cashier/withdraw/TransactionFragment.kt | 5 +- .../net/taler/cashier/withdraw/WithdrawManager.kt | 3 +- gradle.properties | 1 + settings.gradle | 1 + taler-kotlin-common/.gitignore | 1 + taler-kotlin-common/build.gradle | 56 +++++ taler-kotlin-common/consumer-rules.pro | 0 taler-kotlin-common/proguard-rules.pro | 21 ++ taler-kotlin-common/src/main/AndroidManifest.xml | 24 +++ .../src/main/java/net/taler/common/Amount.kt | 45 ++++ .../src/main/java/net/taler/common/AndroidUtils.kt | 53 +++++ .../main/java/net/taler/common/ByteArrayUtils.kt | 53 +++++ .../src/main/java/net/taler/common/NfcManager.kt | 234 +++++++++++++++++++++ .../main/java/net/taler/common/QrCodeManager.kt | 42 ++++ 24 files changed, 547 insertions(+), 432 deletions(-) delete mode 100644 cashier/src/main/java/net/taler/cashier/Amount.kt delete mode 100644 cashier/src/main/java/net/taler/cashier/Utils.kt delete mode 100644 cashier/src/main/java/net/taler/cashier/withdraw/NfcManager.kt delete mode 100644 cashier/src/main/java/net/taler/cashier/withdraw/QrCodeManager.kt create mode 100644 taler-kotlin-common/.gitignore create mode 100644 taler-kotlin-common/build.gradle create mode 100644 taler-kotlin-common/consumer-rules.pro create mode 100644 taler-kotlin-common/proguard-rules.pro create mode 100644 taler-kotlin-common/src/main/AndroidManifest.xml create mode 100644 taler-kotlin-common/src/main/java/net/taler/common/Amount.kt create mode 100644 taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt create mode 100644 taler-kotlin-common/src/main/java/net/taler/common/ByteArrayUtils.kt create mode 100644 taler-kotlin-common/src/main/java/net/taler/common/NfcManager.kt create mode 100644 taler-kotlin-common/src/main/java/net/taler/common/QrCodeManager.kt diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 65dee6e..603ebc0 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -13,6 +13,7 @@ diff --git a/build.gradle b/build.gradle index f286dfe..4c2476d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ buildscript { ext.kotlin_version = '1.3.70' ext.nav_version = "2.2.1" + ext.build_tools_version = "29.0.2" repositories { google() jcenter() diff --git a/cashier/build.gradle b/cashier/build.gradle index b77dc30..5fe7e16 100644 --- a/cashier/build.gradle +++ b/cashier/build.gradle @@ -21,11 +21,12 @@ apply plugin: 'androidx.navigation.safeargs.kotlin' android { compileSdkVersion 29 - buildToolsVersion "29.0.3" + //noinspection GradleDependency + buildToolsVersion "$build_tools_version" defaultConfig { applicationId "net.taler.cashier" - minSdkVersion 23 + minSdkVersion 24 targetSdkVersion 29 versionCode 1 versionName "0.1" @@ -46,9 +47,7 @@ android { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.core:core-ktx:1.2.0' + implementation project(":taler-kotlin-common") implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.security:security-crypto:1.0.0-beta01' implementation 'com.google.android.material:material:1.1.0' @@ -56,13 +55,6 @@ dependencies { implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" - // ViewModel and LiveData - def lifecycle_version = "2.2.0" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" - - // QR codes - implementation 'com.google.zxing:core:3.4.0' - implementation "com.squareup.okhttp3:okhttp:3.12.6" testImplementation 'junit:junit:4.13' diff --git a/cashier/src/main/AndroidManifest.xml b/cashier/src/main/AndroidManifest.xml index 345c9a1..bdc5ec0 100644 --- a/cashier/src/main/AndroidManifest.xml +++ b/cashier/src/main/AndroidManifest.xml @@ -3,10 +3,6 @@ xmlns:tools="http://schemas.android.com/tools" package="net.taler.cashier"> - - - - - */ - -package net.taler.cashier - -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/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt b/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt index b3a0221..2178a78 100644 --- a/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt +++ b/cashier/src/main/java/net/taler/cashier/BalanceFragment.kt @@ -34,6 +34,9 @@ import kotlinx.android.synthetic.main.fragment_balance.* import net.taler.cashier.BalanceFragmentDirections.Companion.actionBalanceFragmentToTransactionFragment import net.taler.cashier.withdraw.LastTransaction import net.taler.cashier.withdraw.WithdrawStatus +import net.taler.common.Amount +import net.taler.common.fadeIn +import net.taler.common.fadeOut sealed class BalanceResult { object Error : BalanceResult() diff --git a/cashier/src/main/java/net/taler/cashier/MainViewModel.kt b/cashier/src/main/java/net/taler/cashier/MainViewModel.kt index 3874038..6cd12ff 100644 --- a/cashier/src/main/java/net/taler/cashier/MainViewModel.kt +++ b/cashier/src/main/java/net/taler/cashier/MainViewModel.kt @@ -32,9 +32,10 @@ import androidx.security.crypto.MasterKeys import androidx.security.crypto.MasterKeys.AES256_GCM_SPEC import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import net.taler.cashier.Amount.Companion.fromStringSigned import net.taler.cashier.HttpHelper.makeJsonGetRequest import net.taler.cashier.withdraw.WithdrawManager +import net.taler.common.Amount.Companion.fromStringSigned +import net.taler.common.isOnline private val TAG = MainViewModel::class.java.simpleName diff --git a/cashier/src/main/java/net/taler/cashier/Utils.kt b/cashier/src/main/java/net/taler/cashier/Utils.kt deleted file mode 100644 index 62f7a77..0000000 --- a/cashier/src/main/java/net/taler/cashier/Utils.kt +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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 - */ - -package net.taler.cashier - -import android.content.Context -import android.content.Context.CONNECTIVITY_SERVICE -import android.net.ConnectivityManager -import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET -import android.os.Build.VERSION.SDK_INT -import android.view.View -import android.view.View.INVISIBLE -import android.view.View.VISIBLE - -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 = 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() - } - -} - -fun View.fadeIn(endAction: () -> Unit = {}) { - if (visibility == VISIBLE) return - alpha = 0f - visibility = VISIBLE - animate().alpha(1f).withEndAction { - endAction.invoke() - }.start() -} - -fun View.fadeOut(endAction: () -> Unit = {}) { - if (visibility == INVISIBLE) return - animate().alpha(0f).withEndAction { - visibility = INVISIBLE - alpha = 1f - endAction.invoke() - }.start() -} - -fun Context.isOnline(): Boolean { - val cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager - return if (SDK_INT < 29) { - @Suppress("DEPRECATION") - cm.activeNetworkInfo?.isConnected == true - } else { - val capabilities = cm.getNetworkCapabilities(cm.activeNetwork) ?: return false - capabilities.hasCapability(NET_CAPABILITY_INTERNET) - } -} diff --git a/cashier/src/main/java/net/taler/cashier/withdraw/NfcManager.kt b/cashier/src/main/java/net/taler/cashier/withdraw/NfcManager.kt deleted file mode 100644 index a487b5f..0000000 --- a/cashier/src/main/java/net/taler/cashier/withdraw/NfcManager.kt +++ /dev/null @@ -1,234 +0,0 @@ -/* - * 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 - */ - -package net.taler.cashier.withdraw - -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.cashier.Utils.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/cashier/src/main/java/net/taler/cashier/withdraw/QrCodeManager.kt b/cashier/src/main/java/net/taler/cashier/withdraw/QrCodeManager.kt deleted file mode 100644 index e3ffa92..0000000 --- a/cashier/src/main/java/net/taler/cashier/withdraw/QrCodeManager.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 - */ - -package net.taler.cashier.withdraw - -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 - } - -} diff --git a/cashier/src/main/java/net/taler/cashier/withdraw/TransactionFragment.kt b/cashier/src/main/java/net/taler/cashier/withdraw/TransactionFragment.kt index 8b782b0..8857bfa 100644 --- a/cashier/src/main/java/net/taler/cashier/withdraw/TransactionFragment.kt +++ b/cashier/src/main/java/net/taler/cashier/withdraw/TransactionFragment.kt @@ -30,13 +30,14 @@ import androidx.navigation.fragment.findNavController import kotlinx.android.synthetic.main.fragment_transaction.* import net.taler.cashier.MainViewModel import net.taler.cashier.R -import net.taler.cashier.fadeIn -import net.taler.cashier.fadeOut import net.taler.cashier.withdraw.TransactionFragmentDirections.Companion.actionTransactionFragmentToBalanceFragment import net.taler.cashier.withdraw.TransactionFragmentDirections.Companion.actionTransactionFragmentToErrorFragment import net.taler.cashier.withdraw.WithdrawResult.Error import net.taler.cashier.withdraw.WithdrawResult.InsufficientBalance import net.taler.cashier.withdraw.WithdrawResult.Success +import net.taler.common.NfcManager +import net.taler.common.fadeIn +import net.taler.common.fadeOut class TransactionFragment : Fragment() { diff --git a/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt b/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt index 4c618ac..bfc82ce 100644 --- a/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt +++ b/cashier/src/main/java/net/taler/cashier/withdraw/WithdrawManager.kt @@ -34,6 +34,7 @@ import net.taler.cashier.HttpJsonResult.Error import net.taler.cashier.HttpJsonResult.Success import net.taler.cashier.MainViewModel import net.taler.cashier.R +import net.taler.common.QrCodeManager.makeQrCode import org.json.JSONObject import java.util.concurrent.TimeUnit.MINUTES import java.util.concurrent.TimeUnit.SECONDS @@ -95,7 +96,7 @@ class WithdrawManager( val withdrawResult = WithdrawResult.Success( id = result.json.getString("withdrawal_id"), talerUri = talerUri, - qrCode = QrCodeManager.makeQrCode(talerUri) + qrCode = makeQrCode(talerUri) ) mWithdrawResult.postValue(withdrawResult) timer.start() diff --git a/gradle.properties b/gradle.properties index 00f6d64..337c38f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,7 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx1536m +org.gradle.configureondemand=true # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects diff --git a/settings.gradle b/settings.gradle index a1882de..f254aa2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,3 +15,4 @@ */ include ':akono', ':cashier', ':merchant-terminal', ':wallet' +include ':taler-kotlin-common' diff --git a/taler-kotlin-common/.gitignore b/taler-kotlin-common/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/taler-kotlin-common/.gitignore @@ -0,0 +1 @@ +/build diff --git a/taler-kotlin-common/build.gradle b/taler-kotlin-common/build.gradle new file mode 100644 index 0000000..d7c9362 --- /dev/null +++ b/taler-kotlin-common/build.gradle @@ -0,0 +1,56 @@ +/* + * 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 + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 29 + //noinspection GradleDependency + buildToolsVersion "$build_tools_version" + + defaultConfig { + minSdkVersion 24 + targetSdkVersion 29 + versionCode 1 + versionName "0.1" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'consumer-rules.pro' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.core:core-ktx:1.2.0' + + // ViewModel and LiveData + def lifecycle_version = "2.2.0" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" + + // QR codes + implementation 'com.google.zxing:core:3.4.0' // needs minSdkVersion 24+ +} diff --git a/taler-kotlin-common/consumer-rules.pro b/taler-kotlin-common/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/taler-kotlin-common/proguard-rules.pro b/taler-kotlin-common/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/taler-kotlin-common/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile 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 @@ + + + + + + + + + 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 + */ + +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 + */ + +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 + */ + +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 + */ + +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 + */ + +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 + } + +} -- cgit v1.2.3