From 8815105bf2462787885214a12af927d484226f21 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 30 Jul 2020 16:40:23 -0300 Subject: Split out common code into multiplatform Kotlin library --- .../src/main/java/net/taler/common/AmountMixin.kt | 51 +++++ .../src/main/java/net/taler/common/AndroidUtils.kt | 123 +++++++++++ .../main/java/net/taler/common/ByteArrayUtils.kt | 53 +++++ .../main/java/net/taler/common/CombinedLiveData.kt | 51 +++++ .../main/java/net/taler/common/ContractTerms.kt | 91 ++++++++ .../src/main/java/net/taler/common/Event.kt | 51 +++++ .../src/main/java/net/taler/common/NfcManager.kt | 234 +++++++++++++++++++++ .../main/java/net/taler/common/QrCodeManager.kt | 42 ++++ .../src/main/java/net/taler/common/SignedAmount.kt | 40 ++++ .../src/main/java/net/taler/common/TalerUtils.kt | 57 +++++ 10 files changed, 793 insertions(+) create mode 100644 taler-kotlin-android/src/main/java/net/taler/common/AmountMixin.kt create mode 100644 taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt create mode 100644 taler-kotlin-android/src/main/java/net/taler/common/ByteArrayUtils.kt create mode 100644 taler-kotlin-android/src/main/java/net/taler/common/CombinedLiveData.kt create mode 100644 taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt create mode 100644 taler-kotlin-android/src/main/java/net/taler/common/Event.kt create mode 100644 taler-kotlin-android/src/main/java/net/taler/common/NfcManager.kt create mode 100644 taler-kotlin-android/src/main/java/net/taler/common/QrCodeManager.kt create mode 100644 taler-kotlin-android/src/main/java/net/taler/common/SignedAmount.kt create mode 100644 taler-kotlin-android/src/main/java/net/taler/common/TalerUtils.kt (limited to 'taler-kotlin-android/src/main/java/net') diff --git a/taler-kotlin-android/src/main/java/net/taler/common/AmountMixin.kt b/taler-kotlin-android/src/main/java/net/taler/common/AmountMixin.kt new file mode 100644 index 0000000..f9b1330 --- /dev/null +++ b/taler-kotlin-android/src/main/java/net/taler/common/AmountMixin.kt @@ -0,0 +1,51 @@ +/* + * 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 com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonMappingException +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.databind.ser.std.StdSerializer + +/** + * Used to support Jackson serialization along with KotlinX. + */ +@JsonSerialize(using = AmountSerializer::class) +@JsonDeserialize(using = AmountDeserializer::class) +abstract class AmountMixin + +class AmountSerializer : StdSerializer(Amount::class.java) { + override fun serialize(value: Amount, gen: JsonGenerator, provider: SerializerProvider) { + gen.writeString(value.toJSONString()) + } +} + +class AmountDeserializer : StdDeserializer(Amount::class.java) { + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Amount { + val node = p.codec.readValue(p, String::class.java) + try { + return Amount.fromJSONString(node) + } catch (e: AmountParserException) { + throw JsonMappingException(p, "Error parsing Amount", e) + } + } +} diff --git a/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt b/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt new file mode 100644 index 0000000..b46f306 --- /dev/null +++ b/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt @@ -0,0 +1,123 @@ +/* + * 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.content.Intent +import android.content.pm.PackageManager.MATCH_DEFAULT_ONLY +import android.net.ConnectivityManager +import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET +import android.os.Build.VERSION.SDK_INT +import android.os.Looper +import android.text.format.DateUtils.DAY_IN_MILLIS +import android.text.format.DateUtils.FORMAT_ABBREV_ALL +import android.text.format.DateUtils.FORMAT_ABBREV_MONTH +import android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE +import android.text.format.DateUtils.FORMAT_NO_YEAR +import android.text.format.DateUtils.FORMAT_SHOW_DATE +import android.text.format.DateUtils.FORMAT_SHOW_TIME +import android.text.format.DateUtils.FORMAT_SHOW_YEAR +import android.text.format.DateUtils.MINUTE_IN_MILLIS +import android.text.format.DateUtils.formatDateTime +import android.text.format.DateUtils.getRelativeTimeSpanString +import android.view.View +import android.view.View.INVISIBLE +import android.view.View.VISIBLE +import android.view.inputmethod.InputMethodManager +import androidx.core.content.ContextCompat.getSystemService +import androidx.fragment.app.Fragment +import androidx.navigation.NavDirections +import androidx.navigation.fragment.findNavController + +fun View.fadeIn(endAction: () -> Unit = {}) { + if (visibility == VISIBLE && alpha == 1f) return + alpha = 0f + visibility = VISIBLE + animate().alpha(1f).withEndAction { + if (context != null) endAction.invoke() + }.start() +} + +fun View.fadeOut(endAction: () -> Unit = {}) { + if (visibility == INVISIBLE) return + animate().alpha(0f).withEndAction { + if (context == null) return@withEndAction + visibility = INVISIBLE + alpha = 1f + endAction.invoke() + }.start() +} + +fun View.hideKeyboard() { + getSystemService(context, InputMethodManager::class.java) + ?.hideSoftInputFromWindow(windowToken, 0) +} + +fun assertUiThread() { + check(Looper.getMainLooper().thread == Thread.currentThread()) +} + +/** + * Use this with 'when' expressions when you need it to handle all possibilities/branches. + */ +val T.exhaustive: T + get() = this + +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) + } +} + +fun Intent.isSafe(context: Context): Boolean { + return context.packageManager.queryIntentActivities(this, MATCH_DEFAULT_ONLY).isNotEmpty() +} + +fun Fragment.navigate(directions: NavDirections) = findNavController().navigate(directions) + +fun Long.toRelativeTime(context: Context): CharSequence { + val now = System.currentTimeMillis() + return if (now - this > DAY_IN_MILLIS * 2) { + val flags = FORMAT_SHOW_TIME or FORMAT_SHOW_DATE or FORMAT_ABBREV_MONTH or FORMAT_NO_YEAR + formatDateTime(context, this, flags) + } else getRelativeTimeSpanString(this, now, MINUTE_IN_MILLIS, FORMAT_ABBREV_RELATIVE) +} + +fun Long.toAbsoluteTime(context: Context): CharSequence { + val flags = FORMAT_SHOW_TIME or FORMAT_SHOW_DATE or FORMAT_SHOW_YEAR + return formatDateTime(context, this, flags) +} + +fun Long.toShortDate(context: Context): CharSequence { + val flags = FORMAT_SHOW_DATE or FORMAT_SHOW_YEAR or FORMAT_ABBREV_ALL + return formatDateTime(context, this, flags) +} + +fun Version.getIncompatibleStringOrNull(context: Context, otherVersion: String): String? { + val other = Version.parse(otherVersion) ?: return context.getString(R.string.version_invalid) + val match = compare(other) ?: return context.getString(R.string.version_invalid) + if (match.compatible) return null + if (match.currentCmp < 0) return context.getString(R.string.version_too_old) + if (match.currentCmp > 0) return context.getString(R.string.version_too_new) + throw AssertionError("$this == $other") +} diff --git a/taler-kotlin-android/src/main/java/net/taler/common/ByteArrayUtils.kt b/taler-kotlin-android/src/main/java/net/taler/common/ByteArrayUtils.kt new file mode 100644 index 0000000..fba0d07 --- /dev/null +++ b/taler-kotlin-android/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-android/src/main/java/net/taler/common/CombinedLiveData.kt b/taler-kotlin-android/src/main/java/net/taler/common/CombinedLiveData.kt new file mode 100644 index 0000000..4e7016b --- /dev/null +++ b/taler-kotlin-android/src/main/java/net/taler/common/CombinedLiveData.kt @@ -0,0 +1,51 @@ +/* + * 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 androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.Observer + +class CombinedLiveData( + source1: LiveData, + source2: LiveData, + private val combine: (data1: T?, data2: K?) -> S +) : MediatorLiveData() { + + private var data1: T? = null + private var data2: K? = null + + init { + super.addSource(source1) { t -> + data1 = t + value = combine(data1, data2) + } + super.addSource(source2) { k -> + data2 = k + value = combine(data1, data2) + } + } + + override fun addSource(source: LiveData, onChanged: Observer) { + throw UnsupportedOperationException() + } + + override fun removeSource(toRemote: LiveData) { + throw UnsupportedOperationException() + } + +} diff --git a/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt b/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt new file mode 100644 index 0000000..0d5fe5b --- /dev/null +++ b/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt @@ -0,0 +1,91 @@ +/* + * 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 androidx.annotation.RequiresApi +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY +import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL +import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.taler.common.TalerUtils.getLocalizedString + +@Serializable +data class ContractTerms( + val summary: String, + @SerialName("summary_i18n") + @get:JsonProperty("summary_i18n") + val summaryI18n: Map? = null, + val amount: Amount, + @SerialName("fulfillment_url") + @get:JsonProperty("fulfillment_url") + val fulfillmentUrl: String, + val products: List +) + +@JsonInclude(NON_NULL) +abstract class Product { + @get:JsonProperty("product_id") + abstract val productId: String? + abstract val description: String + + @get:JsonProperty("description_i18n") + abstract val descriptionI18n: Map? + abstract val price: Amount + + @get:JsonProperty("delivery_location") + abstract val location: String? + abstract val image: String? + + @get:JsonIgnore + val localizedDescription: String + @RequiresApi(26) + get() = getLocalizedString(descriptionI18n, description) +} + +@Serializable +data class ContractProduct( + @SerialName("product_id") + override val productId: String? = null, + override val description: String, + @SerialName("description_i18n") + override val descriptionI18n: Map? = null, + override val price: Amount, + @SerialName("delivery_location") + override val location: String? = null, + override val image: String? = null, + val quantity: Int +) : Product() { + @get:JsonIgnore + val totalPrice: Amount by lazy { + price * quantity + } +} + +data class ContractMerchant( + val name: String +) + +@Serializable +@JsonInclude(NON_EMPTY) +class Timestamp( + @SerialName("t_ms") + @JsonProperty("t_ms") + val ms: Long +) diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Event.kt b/taler-kotlin-android/src/main/java/net/taler/common/Event.kt new file mode 100644 index 0000000..779247f --- /dev/null +++ b/taler-kotlin-android/src/main/java/net/taler/common/Event.kt @@ -0,0 +1,51 @@ +/* + * 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 androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Used as a wrapper for data that is exposed via a [LiveData] that represents an one-time event. + */ +open class Event(private val content: T) { + + private val isConsumed = AtomicBoolean(false) + + /** + * Returns the content and prevents its use again. + */ + fun getIfNotConsumed(): T? { + return if (isConsumed.compareAndSet(false, true)) content else null + } + +} + +fun T.toEvent() = Event(this) + +/** + * An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has + * already been consumed. + * + * [onEvent] is *only* called if the [Event]'s contents has not been consumed. + */ +class EventObserver(private val onEvent: (T) -> Unit) : Observer> { + override fun onChanged(event: Event?) { + event?.getIfNotConsumed()?.let { onEvent(it) } + } +} diff --git a/taler-kotlin-android/src/main/java/net/taler/common/NfcManager.kt b/taler-kotlin-android/src/main/java/net/taler/common/NfcManager.kt new file mode 100644 index 0000000..11e1e1e --- /dev/null +++ b/taler-kotlin-android/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-android/src/main/java/net/taler/common/QrCodeManager.kt b/taler-kotlin-android/src/main/java/net/taler/common/QrCodeManager.kt new file mode 100644 index 0000000..e2a9a55 --- /dev/null +++ b/taler-kotlin-android/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 + } + +} diff --git a/taler-kotlin-android/src/main/java/net/taler/common/SignedAmount.kt b/taler-kotlin-android/src/main/java/net/taler/common/SignedAmount.kt new file mode 100644 index 0000000..03a0d6e --- /dev/null +++ b/taler-kotlin-android/src/main/java/net/taler/common/SignedAmount.kt @@ -0,0 +1,40 @@ +/* + * 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.annotation.SuppressLint + +data class SignedAmount( + val positive: Boolean, + val amount: Amount +) { + + companion object { + @Throws(AmountParserException::class) + @SuppressLint("CheckedExceptions") + fun fromJSONString(str: String): SignedAmount = when (str.substring(0, 1)) { + "-" -> SignedAmount(false, Amount.fromJSONString(str.substring(1))) + "+" -> SignedAmount(true, Amount.fromJSONString(str.substring(1))) + else -> SignedAmount(true, Amount.fromJSONString(str)) + } + } + + override fun toString(): String { + return if (positive) "$amount" else "-$amount" + } + +} \ No newline at end of file diff --git a/taler-kotlin-android/src/main/java/net/taler/common/TalerUtils.kt b/taler-kotlin-android/src/main/java/net/taler/common/TalerUtils.kt new file mode 100644 index 0000000..bb2e78a --- /dev/null +++ b/taler-kotlin-android/src/main/java/net/taler/common/TalerUtils.kt @@ -0,0 +1,57 @@ +/* + * 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 androidx.annotation.RequiresApi +import androidx.core.os.LocaleListCompat +import java.util.Locale + +object TalerUtils { + + @RequiresApi(26) + fun getLocalizedString(map: Map?, default: String): String { + // just return the default, if it is the only element + if (map == null) return default + // create a priority list of language ranges from system locales + val locales = LocaleListCompat.getDefault() + val priorityList = ArrayList(locales.size()) + for (i in 0 until locales.size()) { + priorityList.add(Locale.LanguageRange(locales[i].toLanguageTag())) + } + // create a list of locales available in the given map + val availableLocales = map.keys.mapNotNull { + if (it == "_") return@mapNotNull null + val list = it.split("_") + when (list.size) { + 1 -> Locale(list[0]) + 2 -> Locale(list[0], list[1]) + 3 -> Locale(list[0], list[1], list[2]) + else -> null + } + } + val match = Locale.lookup(priorityList, availableLocales) + return match?.toString()?.let { map[it] } ?: default + } + +} + +/** + * Returns the current time in milliseconds epoch rounded to nearest seconds. + */ +fun now(): Long { + return ((System.currentTimeMillis() + 500) / 1000) * 1000 +} -- cgit v1.2.3