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 --- taler-kotlin-android/.gitignore | 1 + taler-kotlin-android/.gitlab-ci.yml | 12 ++ taler-kotlin-android/build.gradle | 78 +++++++ taler-kotlin-android/consumer-rules.pro | 0 taler-kotlin-android/proguard-rules.pro | 21 ++ taler-kotlin-android/src/main/AndroidManifest.xml | 24 +++ .../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 +++++ .../main/res/drawable/selectable_background.xml | 21 ++ .../src/main/res/values-night/colors.xml | 5 + .../src/main/res/values/colors.xml | 24 +++ .../src/main/res/values/strings.xml | 21 ++ .../java/net/taler/common/ContractTermsTest.kt | 74 +++++++ 21 files changed, 1074 insertions(+) create mode 100644 taler-kotlin-android/.gitignore create mode 100644 taler-kotlin-android/.gitlab-ci.yml create mode 100644 taler-kotlin-android/build.gradle create mode 100644 taler-kotlin-android/consumer-rules.pro create mode 100644 taler-kotlin-android/proguard-rules.pro create mode 100644 taler-kotlin-android/src/main/AndroidManifest.xml 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 create mode 100644 taler-kotlin-android/src/main/res/drawable/selectable_background.xml create mode 100644 taler-kotlin-android/src/main/res/values-night/colors.xml create mode 100644 taler-kotlin-android/src/main/res/values/colors.xml create mode 100644 taler-kotlin-android/src/main/res/values/strings.xml create mode 100644 taler-kotlin-android/src/test/java/net/taler/common/ContractTermsTest.kt (limited to 'taler-kotlin-android') diff --git a/taler-kotlin-android/.gitignore b/taler-kotlin-android/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/taler-kotlin-android/.gitignore @@ -0,0 +1 @@ +/build diff --git a/taler-kotlin-android/.gitlab-ci.yml b/taler-kotlin-android/.gitlab-ci.yml new file mode 100644 index 0000000..bb5af21 --- /dev/null +++ b/taler-kotlin-android/.gitlab-ci.yml @@ -0,0 +1,12 @@ +taler_kotlin_android_test: + stage: test + only: + changes: + - taler-kotlin-android/**/* + - taler-kotlin-common/**/* + - build.gradle + script: ./gradlew :taler-kotlin-android:check + artifacts: + paths: + - taler-kotlin-android/build/reports/lint-results.html + expire_in: 1 week diff --git a/taler-kotlin-android/build.gradle b/taler-kotlin-android/build.gradle new file mode 100644 index 0000000..d6d6003 --- /dev/null +++ b/taler-kotlin-android/build.gradle @@ -0,0 +1,78 @@ +/* + * 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 + */ + +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'kotlin-android-extensions' + id 'kotlinx-serialization' +} + +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' + } + } + + packagingOptions { + exclude("META-INF/*.kotlin_module") + } + +} + +dependencies { + api project(":taler-kotlin-common") + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.core:core-ktx:1.3.0' + + // Navigation + implementation "androidx.navigation:navigation-ui-ktx:$nav_version" + implementation "androidx.navigation:navigation-fragment-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' // needs minSdkVersion 24+ + + // JSON parsing and serialization + api "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0" + implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.10.2" + + lintChecks 'com.github.thirdegg:lint-rules:0.0.4-alpha' + + testImplementation 'junit:junit:4.13' + testImplementation 'org.json:json:20190722' +} diff --git a/taler-kotlin-android/consumer-rules.pro b/taler-kotlin-android/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/taler-kotlin-android/proguard-rules.pro b/taler-kotlin-android/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/taler-kotlin-android/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-android/src/main/AndroidManifest.xml b/taler-kotlin-android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..902ddc1 --- /dev/null +++ b/taler-kotlin-android/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + 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 +} diff --git a/taler-kotlin-android/src/main/res/drawable/selectable_background.xml b/taler-kotlin-android/src/main/res/drawable/selectable_background.xml new file mode 100644 index 0000000..3c383a8 --- /dev/null +++ b/taler-kotlin-android/src/main/res/drawable/selectable_background.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/taler-kotlin-android/src/main/res/values-night/colors.xml b/taler-kotlin-android/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..10bdbb9 --- /dev/null +++ b/taler-kotlin-android/src/main/res/values-night/colors.xml @@ -0,0 +1,5 @@ + + + #2E2E2E + #363636 + diff --git a/taler-kotlin-android/src/main/res/values/colors.xml b/taler-kotlin-android/src/main/res/values/colors.xml new file mode 100644 index 0000000..c916442 --- /dev/null +++ b/taler-kotlin-android/src/main/res/values/colors.xml @@ -0,0 +1,24 @@ + + + + + #E4E4E4 + #DADADA + + #388E3C + #C62828 + diff --git a/taler-kotlin-android/src/main/res/values/strings.xml b/taler-kotlin-android/src/main/res/values/strings.xml new file mode 100644 index 0000000..a5b1df1 --- /dev/null +++ b/taler-kotlin-android/src/main/res/values/strings.xml @@ -0,0 +1,21 @@ + + + + Invalid version. Please try again later! + App too old. Please upgrade! + Service outdated. Please wait until it gets upgraded. + diff --git a/taler-kotlin-android/src/test/java/net/taler/common/ContractTermsTest.kt b/taler-kotlin-android/src/test/java/net/taler/common/ContractTermsTest.kt new file mode 100644 index 0000000..79a7598 --- /dev/null +++ b/taler-kotlin-android/src/test/java/net/taler/common/ContractTermsTest.kt @@ -0,0 +1,74 @@ +/* + * 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.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.fasterxml.jackson.module.kotlin.readValue +import org.junit.Assert.assertEquals +import org.junit.Test + +class ContractTermsTest { + + private val mapper = ObjectMapper() + .registerModule(KotlinModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .addMixIn(Amount::class.java, AmountMixin::class.java) + + @Test + fun test() { + val json = """ + { + "amount":"TESTKUDOS:0.5", + "extra":{ + "article_name":"1._The_Free_Software_Definition" + }, + "fulfillment_url":"https://shop.test.taler.net/essay/1._The_Free_Software_Definition", + "summary":"Essay: 1. The Free Software Definition", + "refund_deadline":{"t_ms":1596128414000}, + "wire_transfer_deadline":{"t_ms":1596128564000}, + "products":[], + "h_wire":"KV40K023N8EC1F5100TYNS23C4XN68Y1Z3PTJSWFGTMCNYD54KT4S791V2VQ91SZANN86VDAA369M4VEZ0KR6DN71EVRRZA71K681M0", + "wire_method":"x-taler-bank", + "order_id":"2020.212-01M9VKEAPF76C", + "timestamp":{"t_ms":1596128114000}, + "pay_deadline":{"t_ms":"never"}, + "max_wire_fee":"TESTKUDOS:1", + "max_fee":"TESTKUDOS:1", + "wire_fee_amortization":3, + "merchant_base_url":"https://backend.test.taler.net/instances/blog/", + "merchant":{"name":"Blog","instance":"blog"}, + "exchanges":[ + { + "url":"https://exchange.test.taler.net/", + "master_pub":"DY95EXAHQ2BKM2WK9YHZHYG1R7PPMMJPY14FNGP662DAKE35AKQG" + }, + { + "url":"https://exchange.test.taler.net/", + "master_pub":"DY95EXAHQ2BKM2WK9YHZHYG1R7PPMMJPY14FNGP662DAKE35AKQG"} + ], + "auditors":[], + "merchant_pub":"8DR9NKSZY1CXFRE47NEYXM0K85C4ZGAYH7Y7VZ22GPNF0BRFNYNG", + "nonce":"FK8ZKJRV6VX6YFAG4CDSC6W0DWD084Q09DP81ANF30GRFQYM2KPG" + } + """.trimIndent() + val contractTerms: ContractTerms = mapper.readValue(json) + assertEquals("Essay: 1. The Free Software Definition", contractTerms.summary) + } + +} -- cgit v1.2.3