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-common/.gitlab-ci.yml | 6 +- taler-kotlin-common/build.gradle | 130 +++++---- taler-kotlin-common/consumer-rules.pro | 0 taler-kotlin-common/proguard-rules.pro | 21 -- .../commonMain/kotlin/net/taler/common/Amount.kt | 198 +++++++++++++ .../src/commonMain/kotlin/net/taler/common/Time.kt | 81 ++++++ .../commonMain/kotlin/net/taler/common/Version.kt | 70 +++++ .../kotlin/net/taler/common/AmountTest.kt | 234 ++++++++++++++++ .../kotlin/net/taler/common/TestUtils.kt | 26 ++ .../kotlin/net/taler/common/VersionTest.kt | 65 +++++ .../src/jsMain/kotlin/net/taler/common/Time.kt | 23 ++ .../src/jvmMain/kotlin/net/taler/common/Time.kt | 21 ++ taler-kotlin-common/src/main/AndroidManifest.xml | 24 -- .../src/main/java/net/taler/common/Amount.kt | 246 ---------------- .../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 | 58 ---- .../src/main/java/net/taler/common/Version.kt | 70 ----- .../main/res/drawable/selectable_background.xml | 21 -- .../src/main/res/values-night/colors.xml | 5 - taler-kotlin-common/src/main/res/values/colors.xml | 24 -- .../src/main/res/values/strings.xml | 21 -- .../src/nativeMain/kotlin/net/taler/common/Time.kt | 23 ++ .../src/test/java/net/taler/common/AmountTest.kt | 311 --------------------- .../src/test/java/net/taler/common/VersionTest.kt | 65 ----- 31 files changed, 812 insertions(+), 1616 deletions(-) delete mode 100644 taler-kotlin-common/consumer-rules.pro delete mode 100644 taler-kotlin-common/proguard-rules.pro create mode 100644 taler-kotlin-common/src/commonMain/kotlin/net/taler/common/Amount.kt create mode 100644 taler-kotlin-common/src/commonMain/kotlin/net/taler/common/Time.kt create mode 100644 taler-kotlin-common/src/commonMain/kotlin/net/taler/common/Version.kt create mode 100644 taler-kotlin-common/src/commonTest/kotlin/net/taler/common/AmountTest.kt create mode 100644 taler-kotlin-common/src/commonTest/kotlin/net/taler/common/TestUtils.kt create mode 100644 taler-kotlin-common/src/commonTest/kotlin/net/taler/common/VersionTest.kt create mode 100644 taler-kotlin-common/src/jsMain/kotlin/net/taler/common/Time.kt create mode 100644 taler-kotlin-common/src/jvmMain/kotlin/net/taler/common/Time.kt delete mode 100644 taler-kotlin-common/src/main/AndroidManifest.xml delete mode 100644 taler-kotlin-common/src/main/java/net/taler/common/Amount.kt delete mode 100644 taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt delete mode 100644 taler-kotlin-common/src/main/java/net/taler/common/ByteArrayUtils.kt delete mode 100644 taler-kotlin-common/src/main/java/net/taler/common/CombinedLiveData.kt delete mode 100644 taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt delete mode 100644 taler-kotlin-common/src/main/java/net/taler/common/Event.kt delete mode 100644 taler-kotlin-common/src/main/java/net/taler/common/NfcManager.kt delete mode 100644 taler-kotlin-common/src/main/java/net/taler/common/QrCodeManager.kt delete mode 100644 taler-kotlin-common/src/main/java/net/taler/common/SignedAmount.kt delete mode 100644 taler-kotlin-common/src/main/java/net/taler/common/TalerUtils.kt delete mode 100644 taler-kotlin-common/src/main/java/net/taler/common/Version.kt delete mode 100644 taler-kotlin-common/src/main/res/drawable/selectable_background.xml delete mode 100644 taler-kotlin-common/src/main/res/values-night/colors.xml delete mode 100644 taler-kotlin-common/src/main/res/values/colors.xml delete mode 100644 taler-kotlin-common/src/main/res/values/strings.xml create mode 100644 taler-kotlin-common/src/nativeMain/kotlin/net/taler/common/Time.kt delete mode 100644 taler-kotlin-common/src/test/java/net/taler/common/AmountTest.kt delete mode 100644 taler-kotlin-common/src/test/java/net/taler/common/VersionTest.kt (limited to 'taler-kotlin-common') diff --git a/taler-kotlin-common/.gitlab-ci.yml b/taler-kotlin-common/.gitlab-ci.yml index 49d3e98..c241e31 100644 --- a/taler-kotlin-common/.gitlab-ci.yml +++ b/taler-kotlin-common/.gitlab-ci.yml @@ -4,8 +4,4 @@ taler_kotlin_common_test: changes: - taler-kotlin-common/**/* - build.gradle - script: ./gradlew :taler-kotlin-common:check - artifacts: - paths: - - taler-kotlin-common/build/reports/lint-results.html - expire_in: 1 week + script: ./gradlew :taler-kotlin-common:jvmTest diff --git a/taler-kotlin-common/build.gradle b/taler-kotlin-common/build.gradle index dd083b7..129881d 100644 --- a/taler-kotlin-common/build.gradle +++ b/taler-kotlin-common/build.gradle @@ -1,72 +1,82 @@ -/* - * 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 'org.jetbrains.kotlin.multiplatform' id 'kotlinx-serialization' } -android { - compileSdkVersion 29 - //noinspection GradleDependency - buildToolsVersion "$build_tools_version" +group 'net.taler' +version '0.0.1' - defaultConfig { - minSdkVersion 24 - targetSdkVersion 29 - versionCode 1 - versionName "0.1" +apply plugin: 'maven-publish' - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles 'consumer-rules.pro' +kotlin { + jvm() + // This is for iPhone simulator + // Switch here to iosArm64 (or iosArm32) to build library for iPhone device + iosX64("ios") { + binaries { + framework() + } } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + linuxX64("linux") + js { + browser { + } + nodejs { + } + } + sourceSets { + def serialization_version = "0.20.0" + commonMain { + dependencies { + implementation kotlin('stdlib-common') + implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version" + } + } + commonTest { + dependencies { + implementation kotlin('test-common') + implementation kotlin('test-annotations-common') + } + } + jvmMain { + dependencies { + implementation kotlin('stdlib-jdk8') + implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serialization_version" + } + } + jvmTest { + dependencies { + implementation kotlin('test') + implementation kotlin('test-junit') + } + } + jsMain { + dependencies { + implementation kotlin('stdlib-js') + implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serialization_version" + } + } + jsTest { + dependencies { + implementation kotlin('test-js') + } + } + nativeMain { + dependsOn commonMain + dependencies { + implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:$serialization_version" + } + } + nativeTest { + dependsOn commonTest + } + configure([targets.linux, targets.ios]) { + compilations.main.source(sourceSets.nativeMain) + compilations.test.source(sourceSets.nativeTest) } } - } -dependencies { - 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' +configurations { + compileClasspath } diff --git a/taler-kotlin-common/consumer-rules.pro b/taler-kotlin-common/consumer-rules.pro deleted file mode 100644 index e69de29..0000000 diff --git a/taler-kotlin-common/proguard-rules.pro b/taler-kotlin-common/proguard-rules.pro deleted file mode 100644 index f1b4245..0000000 --- a/taler-kotlin-common/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# 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/commonMain/kotlin/net/taler/common/Amount.kt b/taler-kotlin-common/src/commonMain/kotlin/net/taler/common/Amount.kt new file mode 100644 index 0000000..84d10c5 --- /dev/null +++ b/taler-kotlin-common/src/commonMain/kotlin/net/taler/common/Amount.kt @@ -0,0 +1,198 @@ +/* + * 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 kotlinx.serialization.Decoder +import kotlinx.serialization.Encoder +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.Serializer +import kotlin.math.floor +import kotlin.math.pow +import kotlin.math.roundToInt + +class AmountParserException(msg: String? = null, cause: Throwable? = null) : Exception(msg, cause) +class AmountOverflowException(msg: String? = null, cause: Throwable? = null) : Exception(msg, cause) + +@Serializable(with = KotlinXAmountSerializer::class) +data class Amount( + /** + * name of the currency using either a three-character ISO 4217 currency code, + * or a regional currency identifier starting with a "*" followed by at most 10 characters. + * ISO 4217 exponents in the name are not supported, + * although the "fraction" is corresponds to an ISO 4217 exponent of 6. + */ + val currency: String, + + /** + * The integer part may be at most 2^52. + * Note that "1" here would correspond to 1 EUR or 1 USD, depending on currency, not 1 cent. + */ + val value: Long, + + /** + * Unsigned 32 bit fractional value to be added to value representing + * an additional currency fraction, in units of one hundred millionth (1e-8) + * of the base currency value. For example, a fraction + * of 50_000_000 would correspond to 50 cents. + */ + val fraction: Int +) : Comparable { + + companion object { + + private const val FRACTIONAL_BASE: Int = 100000000 // 1e8 + + private val REGEX_CURRENCY = Regex("""^[-_*A-Za-z0-9]{1,12}$""") + val MAX_VALUE = 2.0.pow(52).toLong() + private const val MAX_FRACTION_LENGTH = 8 + const val MAX_FRACTION = 99_999_999 + + fun zero(currency: String): Amount { + return Amount(checkCurrency(currency), 0, 0) + } + + fun fromJSONString(str: String): Amount { + val split = str.split(":") + if (split.size != 2) throw AmountParserException("Invalid Amount Format") + return fromString(split[0], split[1]) + } + + fun fromString(currency: String, str: String): Amount { + // value + val valueSplit = str.split(".") + val value = checkValue(valueSplit[0].toLongOrNull()) + // fraction + val fraction: Int = if (valueSplit.size > 1) { + val fractionStr = valueSplit[1] + if (fractionStr.length > MAX_FRACTION_LENGTH) + throw AmountParserException("Fraction $fractionStr too long") + val fraction = "0.$fractionStr".toDoubleOrNull() + ?.times(FRACTIONAL_BASE) + ?.roundToInt() + checkFraction(fraction) + } else 0 + return Amount(checkCurrency(currency), value, fraction) + } + + fun min(currency: String): Amount = Amount(currency, 0, 1) + fun max(currency: String): Amount = Amount(currency, MAX_VALUE, MAX_FRACTION) + + + internal fun checkCurrency(currency: String): String { + if (!REGEX_CURRENCY.matches(currency)) + throw AmountParserException("Invalid currency: $currency") + return currency + } + + internal fun checkValue(value: Long?): Long { + if (value == null || value > MAX_VALUE) + throw AmountParserException("Value $value greater than $MAX_VALUE") + return value + } + + internal fun checkFraction(fraction: Int?): Int { + if (fraction == null || fraction > MAX_FRACTION) + throw AmountParserException("Fraction $fraction greater than $MAX_FRACTION") + return fraction + } + + } + + val amountStr: String + get() = if (fraction == 0) "$value" else { + var f = fraction + var fractionStr = "" + while (f > 0) { + fractionStr += f / (FRACTIONAL_BASE / 10) + f = (f * 10) % FRACTIONAL_BASE + } + "$value.$fractionStr" + } + + operator fun plus(other: Amount): Amount { + check(currency == other.currency) { "Can only subtract from same currency" } + val resultValue = value + other.value + floor((fraction + other.fraction).toDouble() / FRACTIONAL_BASE).toLong() + if (resultValue > MAX_VALUE) + throw AmountOverflowException() + val resultFraction = (fraction + other.fraction) % FRACTIONAL_BASE + return Amount(currency, resultValue, resultFraction) + } + + operator fun times(factor: Int): Amount { + // TODO consider replacing with a faster implementation + if (factor == 0) return zero(currency) + var result = this + for (i in 1 until factor) result += this + return result + } + + operator fun minus(other: Amount): Amount { + check(currency == other.currency) { "Can only subtract from same currency" } + var resultValue = value + var resultFraction = fraction + if (resultFraction < other.fraction) { + if (resultValue < 1L) + throw AmountOverflowException() + resultValue-- + resultFraction += FRACTIONAL_BASE + } + check(resultFraction >= other.fraction) + resultFraction -= other.fraction + if (resultValue < other.value) + throw AmountOverflowException() + resultValue -= other.value + return Amount(currency, resultValue, resultFraction) + } + + fun isZero(): Boolean { + return value == 0L && fraction == 0 + } + + fun toJSONString(): String { + return "$currency:$amountStr" + } + + override fun toString(): String { + return "$amountStr $currency" + } + + override fun compareTo(other: Amount): Int { + check(currency == other.currency) { "Can only compare amounts with the same currency" } + when { + value == other.value -> { + if (fraction < other.fraction) return -1 + if (fraction > other.fraction) return 1 + return 0 + } + value < other.value -> return -1 + else -> return 1 + } + } + +} + +@Serializer(forClass = Amount::class) +object KotlinXAmountSerializer: KSerializer { + override fun serialize(encoder: Encoder, value: Amount) { + encoder.encodeString(value.toJSONString()) + } + + override fun deserialize(decoder: Decoder): Amount { + return Amount.fromJSONString(decoder.decodeString()) + } +} diff --git a/taler-kotlin-common/src/commonMain/kotlin/net/taler/common/Time.kt b/taler-kotlin-common/src/commonMain/kotlin/net/taler/common/Time.kt new file mode 100644 index 0000000..962e004 --- /dev/null +++ b/taler-kotlin-common/src/commonMain/kotlin/net/taler/common/Time.kt @@ -0,0 +1,81 @@ +/* + * 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 kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.taler.common.Duration.Companion.FOREVER +import kotlin.math.max + +expect fun nowMillis(): Long + +@Serializable +data class Timestamp( + @SerialName("t_ms") + val ms: Long +) : Comparable { + + companion object { + const val NEVER: Long = -1 // TODO or UINT64_MAX? + fun now(): Timestamp = Timestamp(nowMillis()) + } + + /** + * Returns a copy of this [Timestamp] rounded to seconds. + */ + fun truncateSeconds(): Timestamp { + if (ms == NEVER) return Timestamp(ms) + return Timestamp((ms / 1000L) * 1000L) + } + + operator fun minus(other: Timestamp): Duration = when { + ms == NEVER -> Duration(FOREVER) + other.ms == NEVER -> throw Error("Invalid argument for timestamp comparision") + ms < other.ms -> Duration(0) + else -> Duration(ms - other.ms) + } + + operator fun minus(other: Duration): Timestamp = when { + ms == NEVER -> this + other.ms == FOREVER -> Timestamp(0) + else -> Timestamp(max(0, ms - other.ms)) + } + + override fun compareTo(other: Timestamp): Int { + return if (ms == NEVER) { + if (other.ms == NEVER) 0 + else 1 + } else { + if (other.ms == NEVER) -1 + else ms.compareTo(other.ms) + } + } + +} + +@Serializable +data class Duration( + /** + * Duration in milliseconds. + */ + @SerialName("d_ms") + val ms: Long +) { + companion object { + const val FOREVER: Long = -1 // TODO or UINT64_MAX? + } +} diff --git a/taler-kotlin-common/src/commonMain/kotlin/net/taler/common/Version.kt b/taler-kotlin-common/src/commonMain/kotlin/net/taler/common/Version.kt new file mode 100644 index 0000000..8774115 --- /dev/null +++ b/taler-kotlin-common/src/commonMain/kotlin/net/taler/common/Version.kt @@ -0,0 +1,70 @@ +/* + * 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 kotlin.math.sign + +/** + * Semantic versioning, but libtool-style. + * See https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html + */ +data class Version( + val current: Int, + val revision: Int, + val age: Int +) { + companion object { + fun parse(v: String): Version? { + val elements = v.split(":") + if (elements.size != 3) return null + val (currentStr, revisionStr, ageStr) = elements + val current = currentStr.toIntOrNull() + val revision = revisionStr.toIntOrNull() + val age = ageStr.toIntOrNull() + if (current == null || revision == null || age == null) return null + return Version(current, revision, age) + } + } + + /** + * Compare two libtool-style versions. + * + * Returns a [VersionMatchResult] or null if the given version was null. + */ + fun compare(other: Version?): VersionMatchResult? { + if (other == null) return null + val compatible = current - age <= other.current && + current >= other.current - other.age + val currentCmp = sign((current - other.current).toDouble()).toInt() + return VersionMatchResult(compatible, currentCmp) + } + + /** + * Result of comparing two libtool versions. + */ + data class VersionMatchResult( + /** + * Is the first version compatible with the second? + */ + val compatible: Boolean, + /** + * Is the first version older (-1), newer (+1) or identical (0)? + */ + val currentCmp: Int + ) + +} diff --git a/taler-kotlin-common/src/commonTest/kotlin/net/taler/common/AmountTest.kt b/taler-kotlin-common/src/commonTest/kotlin/net/taler/common/AmountTest.kt new file mode 100644 index 0000000..e184307 --- /dev/null +++ b/taler-kotlin-common/src/commonTest/kotlin/net/taler/common/AmountTest.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 kotlin.random.Random +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlin.test.fail + +class AmountTest { + + companion object { + fun getRandomAmount() = getRandomAmount(getRandomString(1, Random.nextInt(1, 12))) + fun getRandomAmount(currency: String): Amount { + val value = Random.nextLong(0, Amount.MAX_VALUE) + val fraction = Random.nextInt(0, Amount.MAX_FRACTION) + return Amount(currency, value, fraction) + } + } + + @Test + fun testFromJSONString() { + var str = "TESTKUDOS:23.42" + var amount = Amount.fromJSONString(str) + assertEquals(str, amount.toJSONString()) + assertEquals("TESTKUDOS", amount.currency) + assertEquals(23, amount.value) + assertEquals((0.42 * 1e8).toInt(), amount.fraction) + assertEquals("23.42 TESTKUDOS", amount.toString()) + + str = "EUR:500000000.00000001" + amount = Amount.fromJSONString(str) + assertEquals(str, amount.toJSONString()) + assertEquals("EUR", amount.currency) + assertEquals(500000000, amount.value) + assertEquals(1, amount.fraction) + assertEquals("500000000.00000001 EUR", amount.toString()) + + str = "EUR:1500000000.00000003" + amount = Amount.fromJSONString(str) + assertEquals(str, amount.toJSONString()) + assertEquals("EUR", amount.currency) + assertEquals(1500000000, amount.value) + assertEquals(3, amount.fraction) + assertEquals("1500000000.00000003 EUR", amount.toString()) + } + + @Test + fun testFromJSONStringAcceptsMaxValuesRejectsAbove() { + val maxValue = 4503599627370496 + val str = "TESTKUDOS123:$maxValue.99999999" + val amount = Amount.fromJSONString(str) + assertEquals(str, amount.toJSONString()) + assertEquals("TESTKUDOS123", amount.currency) + assertEquals(maxValue, amount.value) + assertEquals("$maxValue.99999999 TESTKUDOS123", amount.toString()) + + // longer currency not accepted + assertThrows("longer currency was accepted") { + Amount.fromJSONString("TESTKUDOS1234:$maxValue.99999999") + } + + // max value + 1 not accepted + assertThrows("max value + 1 was accepted") { + Amount.fromJSONString("TESTKUDOS123:${maxValue + 1}.99999999") + } + + // max fraction + 1 not accepted + assertThrows("max fraction + 1 was accepted") { + Amount.fromJSONString("TESTKUDOS123:$maxValue.999999990") + } + } + + @Test + fun testFromJSONStringRejections() { + assertThrows { + Amount.fromJSONString("TESTKUDOS:0,5") + } + assertThrows { + Amount.fromJSONString("+TESTKUDOS:0.5") + } + assertThrows { + Amount.fromJSONString("0.5") + } + assertThrows { + Amount.fromJSONString(":0.5") + } + assertThrows { + Amount.fromJSONString("EUR::0.5") + } + assertThrows { + Amount.fromJSONString("EUR:.5") + } + } + + @Test + fun testAddition() { + assertEquals( + Amount.fromJSONString("EUR:2"), + Amount.fromJSONString("EUR:1") + Amount.fromJSONString("EUR:1") + ) + assertEquals( + Amount.fromJSONString("EUR:3"), + Amount.fromJSONString("EUR:1.5") + Amount.fromJSONString("EUR:1.5") + ) + assertEquals( + Amount.fromJSONString("EUR:500000000.00000002"), + Amount.fromJSONString("EUR:500000000.00000001") + Amount.fromJSONString("EUR:0.00000001") + ) + assertThrows("addition didn't overflow") { + Amount.fromJSONString("EUR:4503599627370496.99999999") + Amount.fromJSONString("EUR:0.00000001") + } + assertThrows("addition didn't overflow") { + Amount.fromJSONString("EUR:4000000000000000") + Amount.fromJSONString("EUR:4000000000000000") + } + } + + @Test + fun testTimes() { + assertEquals( + Amount.fromJSONString("EUR:2"), + Amount.fromJSONString("EUR:2") * 1 + ) + assertEquals( + Amount.fromJSONString("EUR:2"), + Amount.fromJSONString("EUR:1") * 2 + ) + assertEquals( + Amount.fromJSONString("EUR:4.5"), + Amount.fromJSONString("EUR:1.5") * 3 + ) + assertEquals(Amount.fromJSONString("EUR:0"), Amount.fromJSONString("EUR:1.11") * 0) + assertEquals(Amount.fromJSONString("EUR:1.11"), Amount.fromJSONString("EUR:1.11") * 1) + assertEquals(Amount.fromJSONString("EUR:2.22"), Amount.fromJSONString("EUR:1.11") * 2) + assertEquals(Amount.fromJSONString("EUR:3.33"), Amount.fromJSONString("EUR:1.11") * 3) + assertEquals(Amount.fromJSONString("EUR:4.44"), Amount.fromJSONString("EUR:1.11") * 4) + assertEquals(Amount.fromJSONString("EUR:5.55"), Amount.fromJSONString("EUR:1.11") * 5) + assertEquals( + Amount.fromJSONString("EUR:1500000000.00000003"), + Amount.fromJSONString("EUR:500000000.00000001") * 3 + ) + assertThrows("times didn't overflow") { + Amount.fromJSONString("EUR:4000000000000000") * 2 + } + } + + @Test + fun testSubtraction() { + assertEquals( + Amount.fromJSONString("EUR:0"), + Amount.fromJSONString("EUR:1") - Amount.fromJSONString("EUR:1") + ) + assertEquals( + Amount.fromJSONString("EUR:1.5"), + Amount.fromJSONString("EUR:3") - Amount.fromJSONString("EUR:1.5") + ) + assertEquals( + Amount.fromJSONString("EUR:500000000.00000001"), + Amount.fromJSONString("EUR:500000000.00000002") - Amount.fromJSONString("EUR:0.00000001") + ) + assertThrows("subtraction didn't underflow") { + Amount.fromJSONString("EUR:23.42") - Amount.fromJSONString("EUR:42.23") + } + assertThrows("subtraction didn't underflow") { + Amount.fromJSONString("EUR:0.5") - Amount.fromJSONString("EUR:0.50000001") + } + } + + @Test + fun testIsZero() { + assertTrue(Amount.zero("EUR").isZero()) + assertTrue(Amount.fromJSONString("EUR:0").isZero()) + assertTrue(Amount.fromJSONString("EUR:0.0").isZero()) + assertTrue(Amount.fromJSONString("EUR:0.00000").isZero()) + assertTrue((Amount.fromJSONString("EUR:1.001") - Amount.fromJSONString("EUR:1.001")).isZero()) + + assertFalse(Amount.fromJSONString("EUR:0.00000001").isZero()) + assertFalse(Amount.fromJSONString("EUR:1.0").isZero()) + assertFalse(Amount.fromJSONString("EUR:0001.0").isZero()) + } + + @Test + fun testComparision() { + assertTrue(Amount.fromJSONString("EUR:0") <= Amount.fromJSONString("EUR:0")) + assertTrue(Amount.fromJSONString("EUR:0") <= Amount.fromJSONString("EUR:0.00000001")) + assertTrue(Amount.fromJSONString("EUR:0") < Amount.fromJSONString("EUR:0.00000001")) + assertTrue(Amount.fromJSONString("EUR:0") < Amount.fromJSONString("EUR:1")) + assertEquals(Amount.fromJSONString("EUR:0"), Amount.fromJSONString("EUR:0")) + assertEquals(Amount.fromJSONString("EUR:42"), Amount.fromJSONString("EUR:42")) + assertEquals( + Amount.fromJSONString("EUR:42.00000001"), + Amount.fromJSONString("EUR:42.00000001") + ) + assertTrue(Amount.fromJSONString("EUR:42.00000001") >= Amount.fromJSONString("EUR:42.00000001")) + assertTrue(Amount.fromJSONString("EUR:42.00000002") >= Amount.fromJSONString("EUR:42.00000001")) + assertTrue(Amount.fromJSONString("EUR:42.00000002") > Amount.fromJSONString("EUR:42.00000001")) + assertTrue(Amount.fromJSONString("EUR:0.00000002") > Amount.fromJSONString("EUR:0.00000001")) + assertTrue(Amount.fromJSONString("EUR:0.00000001") > Amount.fromJSONString("EUR:0")) + assertTrue(Amount.fromJSONString("EUR:2") > Amount.fromJSONString("EUR:1")) + + assertThrows("could compare amounts with different currencies") { + Amount.fromJSONString("EUR:0.5") < Amount.fromJSONString("USD:0.50000001") + } + } + + private inline fun assertThrows( + msg: String? = null, + function: () -> Any + ) { + try { + function.invoke() + fail(msg) + } catch (e: Exception) { + assertTrue(e is T) + } + } + +} diff --git a/taler-kotlin-common/src/commonTest/kotlin/net/taler/common/TestUtils.kt b/taler-kotlin-common/src/commonTest/kotlin/net/taler/common/TestUtils.kt new file mode 100644 index 0000000..e3a6c17 --- /dev/null +++ b/taler-kotlin-common/src/commonTest/kotlin/net/taler/common/TestUtils.kt @@ -0,0 +1,26 @@ +/* + * 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 kotlin.random.Random + +private val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') +fun getRandomString(minLength: Int = 1, maxLength: Int = Random.nextInt(0, 1337)) = + (minLength..maxLength) + .map { Random.nextInt(0, charPool.size) } + .map(charPool::get) + .joinToString("") diff --git a/taler-kotlin-common/src/commonTest/kotlin/net/taler/common/VersionTest.kt b/taler-kotlin-common/src/commonTest/kotlin/net/taler/common/VersionTest.kt new file mode 100644 index 0000000..f4f17ea --- /dev/null +++ b/taler-kotlin-common/src/commonTest/kotlin/net/taler/common/VersionTest.kt @@ -0,0 +1,65 @@ +/* + * 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 kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class VersionTest { + + @Test + fun testParse() { + assertNull(Version.parse("")) + assertNull(Version.parse("foo")) + assertNull(Version.parse("foo:bar:foo")) + assertNull(Version.parse("0:0:0:")) + assertNull(Version.parse("0:0:")) + assertEquals(Version(0, 0, 0), Version.parse("0:0:0")) + assertEquals(Version(1, 2, 3), Version.parse("1:2:3")) + assertEquals(Version(1337, 42, 23), Version.parse("1337:42:23")) + } + + @Test + fun testComparision() { + assertEquals( + Version.VersionMatchResult(true, 0), + Version.parse("0:0:0")!!.compare(Version.parse("0:0:0")) + ) + assertEquals( + Version.VersionMatchResult(true, -1), + Version.parse("0:0:0")!!.compare(Version.parse("1:0:1")) + ) + assertEquals( + Version.VersionMatchResult(true, -1), + Version.parse("0:0:0")!!.compare(Version.parse("1:5:1")) + ) + assertEquals( + Version.VersionMatchResult(false, -1), + Version.parse("0:0:0")!!.compare(Version.parse("1:5:0")) + ) + assertEquals( + Version.VersionMatchResult(false, 1), + Version.parse("1:0:0")!!.compare(Version.parse("0:5:0")) + ) + assertEquals( + Version.VersionMatchResult(true, 0), + Version.parse("1:0:1")!!.compare(Version.parse("1:5:1")) + ) + } + +} diff --git a/taler-kotlin-common/src/jsMain/kotlin/net/taler/common/Time.kt b/taler-kotlin-common/src/jsMain/kotlin/net/taler/common/Time.kt new file mode 100644 index 0000000..b114022 --- /dev/null +++ b/taler-kotlin-common/src/jsMain/kotlin/net/taler/common/Time.kt @@ -0,0 +1,23 @@ +/* + * 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 kotlin.js.Date + +actual fun nowMillis(): Long { + return Date().getMilliseconds().toLong() +} diff --git a/taler-kotlin-common/src/jvmMain/kotlin/net/taler/common/Time.kt b/taler-kotlin-common/src/jvmMain/kotlin/net/taler/common/Time.kt new file mode 100644 index 0000000..6cd9040 --- /dev/null +++ b/taler-kotlin-common/src/jvmMain/kotlin/net/taler/common/Time.kt @@ -0,0 +1,21 @@ +/* + * 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 + +actual fun nowMillis(): Long { + return System.currentTimeMillis() +} diff --git a/taler-kotlin-common/src/main/AndroidManifest.xml b/taler-kotlin-common/src/main/AndroidManifest.xml deleted file mode 100644 index 902ddc1..0000000 --- a/taler-kotlin-common/src/main/AndroidManifest.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - 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 deleted file mode 100644 index 992f93b..0000000 --- a/taler-kotlin-common/src/main/java/net/taler/common/Amount.kt +++ /dev/null @@ -1,246 +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.common - -import android.annotation.SuppressLint -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 -import kotlinx.serialization.Decoder -import kotlinx.serialization.Encoder -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.Serializer -import org.json.JSONObject -import java.lang.Math.floorDiv -import kotlin.math.pow -import kotlin.math.roundToInt - -class AmountParserException(msg: String? = null, cause: Throwable? = null) : Exception(msg, cause) -class AmountOverflowException(msg: String? = null, cause: Throwable? = null) : Exception(msg, cause) - -@Serializable(with = KotlinXAmountSerializer::class) -@JsonSerialize(using = AmountSerializer::class) -@JsonDeserialize(using = AmountDeserializer::class) -data class Amount( - /** - * name of the currency using either a three-character ISO 4217 currency code, - * or a regional currency identifier starting with a "*" followed by at most 10 characters. - * ISO 4217 exponents in the name are not supported, - * although the "fraction" is corresponds to an ISO 4217 exponent of 6. - */ - val currency: String, - - /** - * The integer part may be at most 2^52. - * Note that "1" here would correspond to 1 EUR or 1 USD, depending on currency, not 1 cent. - */ - val value: Long, - - /** - * Unsigned 32 bit fractional value to be added to value representing - * an additional currency fraction, in units of one hundred millionth (1e-8) - * of the base currency value. For example, a fraction - * of 50_000_000 would correspond to 50 cents. - */ - val fraction: Int -) : Comparable { - - companion object { - - private const val FRACTIONAL_BASE: Int = 100000000 // 1e8 - - @Suppress("unused") - private val REGEX = Regex("""^[-_*A-Za-z0-9]{1,12}:([0-9]+)\.?([0-9]+)?$""") - private val REGEX_CURRENCY = Regex("""^[-_*A-Za-z0-9]{1,12}$""") - private val MAX_VALUE = 2.0.pow(52) - private const val MAX_FRACTION_LENGTH = 8 - private const val MAX_FRACTION = 99_999_999 - - @Throws(AmountParserException::class) - @SuppressLint("CheckedExceptions") - fun zero(currency: String): Amount { - return Amount(checkCurrency(currency), 0, 0) - } - - @Throws(AmountParserException::class) - @SuppressLint("CheckedExceptions") - fun fromJSONString(str: String): Amount { - val split = str.split(":") - if (split.size != 2) throw AmountParserException("Invalid Amount Format") - return fromString(split[0], split[1]) - } - - @Throws(AmountParserException::class) - @SuppressLint("CheckedExceptions") - fun fromString(currency: String, str: String): Amount { - // value - val valueSplit = str.split(".") - val value = checkValue(valueSplit[0].toLongOrNull()) - // fraction - val fraction: Int = if (valueSplit.size > 1) { - val fractionStr = valueSplit[1] - if (fractionStr.length > MAX_FRACTION_LENGTH) - throw AmountParserException("Fraction $fractionStr too long") - val fraction = "0.$fractionStr".toDoubleOrNull() - ?.times(FRACTIONAL_BASE) - ?.roundToInt() - checkFraction(fraction) - } else 0 - return Amount(checkCurrency(currency), value, fraction) - } - - @Throws(AmountParserException::class) - @SuppressLint("CheckedExceptions") - fun fromJsonObject(json: JSONObject): Amount { - val currency = checkCurrency(json.optString("currency")) - val value = checkValue(json.optString("value").toLongOrNull()) - val fraction = checkFraction(json.optString("fraction").toIntOrNull()) - return Amount(currency, value, fraction) - } - - @Throws(AmountParserException::class) - private fun checkCurrency(currency: String): String { - if (!REGEX_CURRENCY.matches(currency)) - throw AmountParserException("Invalid currency: $currency") - return currency - } - - @Throws(AmountParserException::class) - private fun checkValue(value: Long?): Long { - if (value == null || value > MAX_VALUE) - throw AmountParserException("Value $value greater than $MAX_VALUE") - return value - } - - @Throws(AmountParserException::class) - private fun checkFraction(fraction: Int?): Int { - if (fraction == null || fraction > MAX_FRACTION) - throw AmountParserException("Fraction $fraction greater than $MAX_FRACTION") - return fraction - } - - } - - val amountStr: String - get() = if (fraction == 0) "$value" else { - var f = fraction - var fractionStr = "" - while (f > 0) { - fractionStr += f / (FRACTIONAL_BASE / 10) - f = (f * 10) % FRACTIONAL_BASE - } - "$value.$fractionStr" - } - - @Throws(AmountOverflowException::class) - operator fun plus(other: Amount): Amount { - check(currency == other.currency) { "Can only subtract from same currency" } - val resultValue = value + other.value + floorDiv(fraction + other.fraction, FRACTIONAL_BASE) - if (resultValue > MAX_VALUE) - throw AmountOverflowException() - val resultFraction = (fraction + other.fraction) % FRACTIONAL_BASE - return Amount(currency, resultValue, resultFraction) - } - - @Throws(AmountOverflowException::class) - operator fun times(factor: Int): Amount { - if (factor == 0) return zero(currency) - var result = this - for (i in 1 until factor) result += this - return result - } - - @Throws(AmountOverflowException::class) - operator fun minus(other: Amount): Amount { - check(currency == other.currency) { "Can only subtract from same currency" } - var resultValue = value - var resultFraction = fraction - if (resultFraction < other.fraction) { - if (resultValue < 1L) - throw AmountOverflowException() - resultValue-- - resultFraction += FRACTIONAL_BASE - } - check(resultFraction >= other.fraction) - resultFraction -= other.fraction - if (resultValue < other.value) - throw AmountOverflowException() - resultValue -= other.value - return Amount(currency, resultValue, resultFraction) - } - - fun isZero(): Boolean { - return value == 0L && fraction == 0 - } - - fun toJSONString(): String { - return "$currency:$amountStr" - } - - override fun toString(): String { - return "$amountStr $currency" - } - - override fun compareTo(other: Amount): Int { - check(currency == other.currency) { "Can only compare amounts with the same currency" } - when { - value == other.value -> { - if (fraction < other.fraction) return -1 - if (fraction > other.fraction) return 1 - return 0 - } - value < other.value -> return -1 - else -> return 1 - } - } - -} - -@Serializer(forClass = Amount::class) -object KotlinXAmountSerializer: KSerializer { - override fun serialize(encoder: Encoder, value: Amount) { - encoder.encodeString(value.toJSONString()) - } - - override fun deserialize(decoder: Decoder): Amount { - return Amount.fromJSONString(decoder.decodeString()) - } -} - -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-common/src/main/java/net/taler/common/AndroidUtils.kt b/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt deleted file mode 100644 index b46f306..0000000 --- a/taler-kotlin-common/src/main/java/net/taler/common/AndroidUtils.kt +++ /dev/null @@ -1,123 +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.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-common/src/main/java/net/taler/common/ByteArrayUtils.kt b/taler-kotlin-common/src/main/java/net/taler/common/ByteArrayUtils.kt deleted file mode 100644 index fba0d07..0000000 --- a/taler-kotlin-common/src/main/java/net/taler/common/ByteArrayUtils.kt +++ /dev/null @@ -1,53 +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.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/CombinedLiveData.kt b/taler-kotlin-common/src/main/java/net/taler/common/CombinedLiveData.kt deleted file mode 100644 index 4e7016b..0000000 --- a/taler-kotlin-common/src/main/java/net/taler/common/CombinedLiveData.kt +++ /dev/null @@ -1,51 +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.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-common/src/main/java/net/taler/common/ContractTerms.kt b/taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.kt deleted file mode 100644 index b891ef7..0000000 --- a/taler-kotlin-common/src/main/java/net/taler/common/ContractTerms.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.common - -import androidx.annotation.RequiresApi -import com.fasterxml.jackson.annotation.JsonIgnore -import com.fasterxml.jackson.annotation.JsonIgnoreProperties -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 -@JsonIgnoreProperties(ignoreUnknown = true) -data class ContractTerms( - val summary: String, - @SerialName("summary_i18n") - val summaryI18n: Map? = null, - val amount: Amount, - @SerialName("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-common/src/main/java/net/taler/common/Event.kt b/taler-kotlin-common/src/main/java/net/taler/common/Event.kt deleted file mode 100644 index 779247f..0000000 --- a/taler-kotlin-common/src/main/java/net/taler/common/Event.kt +++ /dev/null @@ -1,51 +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.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-common/src/main/java/net/taler/common/NfcManager.kt b/taler-kotlin-common/src/main/java/net/taler/common/NfcManager.kt deleted file mode 100644 index 11e1e1e..0000000 --- a/taler-kotlin-common/src/main/java/net/taler/common/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.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 deleted file mode 100644 index e2a9a55..0000000 --- a/taler-kotlin-common/src/main/java/net/taler/common/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.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-common/src/main/java/net/taler/common/SignedAmount.kt b/taler-kotlin-common/src/main/java/net/taler/common/SignedAmount.kt deleted file mode 100644 index 03a0d6e..0000000 --- a/taler-kotlin-common/src/main/java/net/taler/common/SignedAmount.kt +++ /dev/null @@ -1,40 +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.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-common/src/main/java/net/taler/common/TalerUtils.kt b/taler-kotlin-common/src/main/java/net/taler/common/TalerUtils.kt deleted file mode 100644 index 444caa4..0000000 --- a/taler-kotlin-common/src/main/java/net/taler/common/TalerUtils.kt +++ /dev/null @@ -1,58 +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.common - -import androidx.annotation.RequiresApi -import androidx.core.os.LocaleListCompat -import java.util.* -import kotlin.collections.ArrayList - -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-common/src/main/java/net/taler/common/Version.kt b/taler-kotlin-common/src/main/java/net/taler/common/Version.kt deleted file mode 100644 index 8774115..0000000 --- a/taler-kotlin-common/src/main/java/net/taler/common/Version.kt +++ /dev/null @@ -1,70 +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.common - -import kotlin.math.sign - -/** - * Semantic versioning, but libtool-style. - * See https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html - */ -data class Version( - val current: Int, - val revision: Int, - val age: Int -) { - companion object { - fun parse(v: String): Version? { - val elements = v.split(":") - if (elements.size != 3) return null - val (currentStr, revisionStr, ageStr) = elements - val current = currentStr.toIntOrNull() - val revision = revisionStr.toIntOrNull() - val age = ageStr.toIntOrNull() - if (current == null || revision == null || age == null) return null - return Version(current, revision, age) - } - } - - /** - * Compare two libtool-style versions. - * - * Returns a [VersionMatchResult] or null if the given version was null. - */ - fun compare(other: Version?): VersionMatchResult? { - if (other == null) return null - val compatible = current - age <= other.current && - current >= other.current - other.age - val currentCmp = sign((current - other.current).toDouble()).toInt() - return VersionMatchResult(compatible, currentCmp) - } - - /** - * Result of comparing two libtool versions. - */ - data class VersionMatchResult( - /** - * Is the first version compatible with the second? - */ - val compatible: Boolean, - /** - * Is the first version older (-1), newer (+1) or identical (0)? - */ - val currentCmp: Int - ) - -} diff --git a/taler-kotlin-common/src/main/res/drawable/selectable_background.xml b/taler-kotlin-common/src/main/res/drawable/selectable_background.xml deleted file mode 100644 index 3c383a8..0000000 --- a/taler-kotlin-common/src/main/res/drawable/selectable_background.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - diff --git a/taler-kotlin-common/src/main/res/values-night/colors.xml b/taler-kotlin-common/src/main/res/values-night/colors.xml deleted file mode 100644 index 10bdbb9..0000000 --- a/taler-kotlin-common/src/main/res/values-night/colors.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - #2E2E2E - #363636 - diff --git a/taler-kotlin-common/src/main/res/values/colors.xml b/taler-kotlin-common/src/main/res/values/colors.xml deleted file mode 100644 index c916442..0000000 --- a/taler-kotlin-common/src/main/res/values/colors.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - #E4E4E4 - #DADADA - - #388E3C - #C62828 - diff --git a/taler-kotlin-common/src/main/res/values/strings.xml b/taler-kotlin-common/src/main/res/values/strings.xml deleted file mode 100644 index a5b1df1..0000000 --- a/taler-kotlin-common/src/main/res/values/strings.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - Invalid version. Please try again later! - App too old. Please upgrade! - Service outdated. Please wait until it gets upgraded. - diff --git a/taler-kotlin-common/src/nativeMain/kotlin/net/taler/common/Time.kt b/taler-kotlin-common/src/nativeMain/kotlin/net/taler/common/Time.kt new file mode 100644 index 0000000..8a4091a --- /dev/null +++ b/taler-kotlin-common/src/nativeMain/kotlin/net/taler/common/Time.kt @@ -0,0 +1,23 @@ +/* + * 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 kotlin.system.getTimeMillis + +actual fun nowMillis(): Long { + return getTimeMillis() +} diff --git a/taler-kotlin-common/src/test/java/net/taler/common/AmountTest.kt b/taler-kotlin-common/src/test/java/net/taler/common/AmountTest.kt deleted file mode 100644 index 97d9667..0000000 --- a/taler-kotlin-common/src/test/java/net/taler/common/AmountTest.kt +++ /dev/null @@ -1,311 +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.common - -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.KotlinModule -import com.fasterxml.jackson.module.kotlin.readValue -import org.json.JSONObject -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Assert.fail -import org.junit.Test - -class AmountTest { - - @Test - fun `test fromJSONString() works`() { - var str = "TESTKUDOS:23.42" - var amount = Amount.fromJSONString(str) - assertEquals(str, amount.toJSONString()) - assertEquals("TESTKUDOS", amount.currency) - assertEquals(23, amount.value) - assertEquals((0.42 * 1e8).toInt(), amount.fraction) - assertEquals("23.42 TESTKUDOS", amount.toString()) - - str = "EUR:500000000.00000001" - amount = Amount.fromJSONString(str) - assertEquals(str, amount.toJSONString()) - assertEquals("EUR", amount.currency) - assertEquals(500000000, amount.value) - assertEquals(1, amount.fraction) - assertEquals("500000000.00000001 EUR", amount.toString()) - - str = "EUR:1500000000.00000003" - amount = Amount.fromJSONString(str) - assertEquals(str, amount.toJSONString()) - assertEquals("EUR", amount.currency) - assertEquals(1500000000, amount.value) - assertEquals(3, amount.fraction) - assertEquals("1500000000.00000003 EUR", amount.toString()) - } - - @Test - fun `test fromJSONString() accepts max values, rejects above`() { - val maxValue = 4503599627370496 - val str = "TESTKUDOS123:$maxValue.99999999" - val amount = Amount.fromJSONString(str) - assertEquals(str, amount.toJSONString()) - assertEquals("TESTKUDOS123", amount.currency) - assertEquals(maxValue, amount.value) - assertEquals("$maxValue.99999999 TESTKUDOS123", amount.toString()) - - // longer currency not accepted - assertThrows("longer currency was accepted") { - Amount.fromJSONString("TESTKUDOS1234:$maxValue.99999999") - } - - // max value + 1 not accepted - assertThrows("max value + 1 was accepted") { - Amount.fromJSONString("TESTKUDOS123:${maxValue + 1}.99999999") - } - - // max fraction + 1 not accepted - assertThrows("max fraction + 1 was accepted") { - Amount.fromJSONString("TESTKUDOS123:$maxValue.999999990") - } - } - - @Test - fun `test JSON deserialization()`() { - val mapper = ObjectMapper().registerModule(KotlinModule()) - var str = "TESTKUDOS:23.42" - var amount: Amount = mapper.readValue("\"$str\"") - assertEquals(str, amount.toJSONString()) - assertEquals("TESTKUDOS", amount.currency) - assertEquals(23, amount.value) - assertEquals((0.42 * 1e8).toInt(), amount.fraction) - assertEquals("23.42 TESTKUDOS", amount.toString()) - - str = "EUR:500000000.00000001" - amount = mapper.readValue("\"$str\"") - assertEquals(str, amount.toJSONString()) - assertEquals("EUR", amount.currency) - assertEquals(500000000, amount.value) - assertEquals(1, amount.fraction) - assertEquals("500000000.00000001 EUR", amount.toString()) - - str = "EUR:1500000000.00000003" - amount = mapper.readValue("\"$str\"") - assertEquals(str, amount.toJSONString()) - assertEquals("EUR", amount.currency) - assertEquals(1500000000, amount.value) - assertEquals(3, amount.fraction) - assertEquals("1500000000.00000003 EUR", amount.toString()) - } - - @Test - fun `test fromJSONString() rejections`() { - assertThrows { - Amount.fromJSONString("TESTKUDOS:0,5") - } - assertThrows { - Amount.fromJSONString("+TESTKUDOS:0.5") - } - assertThrows { - Amount.fromJSONString("0.5") - } - assertThrows { - Amount.fromJSONString(":0.5") - } - assertThrows { - Amount.fromJSONString("EUR::0.5") - } - assertThrows { - Amount.fromJSONString("EUR:.5") - } - } - - @Test - fun `test fromJsonObject() works`() { - val map = mapOf( - "currency" to "TESTKUDOS", - "value" to "23", - "fraction" to "42000000" - ) - - val amount = Amount.fromJsonObject(JSONObject(map)) - assertEquals("TESTKUDOS:23.42", amount.toJSONString()) - assertEquals("TESTKUDOS", amount.currency) - assertEquals(23, amount.value) - assertEquals(42000000, amount.fraction) - assertEquals("23.42 TESTKUDOS", amount.toString()) - } - - @Test - fun `test fromJsonObject() accepts max values, rejects above`() { - val maxValue = 4503599627370496 - val maxFraction = 99999999 - var map = mapOf( - "currency" to "TESTKUDOS123", - "value" to "$maxValue", - "fraction" to "$maxFraction" - ) - - val amount = Amount.fromJsonObject(JSONObject(map)) - assertEquals("TESTKUDOS123:$maxValue.$maxFraction", amount.toJSONString()) - assertEquals("TESTKUDOS123", amount.currency) - assertEquals(maxValue, amount.value) - assertEquals(maxFraction, amount.fraction) - assertEquals("$maxValue.$maxFraction TESTKUDOS123", amount.toString()) - - // longer currency not accepted - assertThrows("longer currency was accepted") { - map = mapOf( - "currency" to "TESTKUDOS1234", - "value" to "$maxValue", - "fraction" to "$maxFraction" - ) - Amount.fromJsonObject(JSONObject(map)) - } - - // max value + 1 not accepted - assertThrows("max value + 1 was accepted") { - map = mapOf( - "currency" to "TESTKUDOS123", - "value" to "${maxValue + 1}", - "fraction" to "$maxFraction" - ) - Amount.fromJsonObject(JSONObject(map)) - } - - // max fraction + 1 not accepted - assertThrows("max fraction + 1 was accepted") { - map = mapOf( - "currency" to "TESTKUDOS123", - "value" to "$maxValue", - "fraction" to "${maxFraction + 1}" - ) - Amount.fromJsonObject(JSONObject(map)) - } - } - - @Test - fun `test addition`() { - assertEquals( - Amount.fromJSONString("EUR:2"), - Amount.fromJSONString("EUR:1") + Amount.fromJSONString("EUR:1") - ) - assertEquals( - Amount.fromJSONString("EUR:3"), - Amount.fromJSONString("EUR:1.5") + Amount.fromJSONString("EUR:1.5") - ) - assertEquals( - Amount.fromJSONString("EUR:500000000.00000002"), - Amount.fromJSONString("EUR:500000000.00000001") + Amount.fromJSONString("EUR:0.00000001") - ) - assertThrows("addition didn't overflow") { - Amount.fromJSONString("EUR:4503599627370496.99999999") + Amount.fromJSONString("EUR:0.00000001") - } - assertThrows("addition didn't overflow") { - Amount.fromJSONString("EUR:4000000000000000") + Amount.fromJSONString("EUR:4000000000000000") - } - } - - @Test - fun `test times`() { - assertEquals( - Amount.fromJSONString("EUR:2"), - Amount.fromJSONString("EUR:2") * 1 - ) - assertEquals( - Amount.fromJSONString("EUR:2"), - Amount.fromJSONString("EUR:1") * 2 - ) - assertEquals( - Amount.fromJSONString("EUR:4.5"), - Amount.fromJSONString("EUR:1.5") * 3 - ) - assertEquals( - Amount.fromJSONString("EUR:1500000000.00000003"), - Amount.fromJSONString("EUR:500000000.00000001") * 3 - ) - assertThrows("times didn't overflow") { - Amount.fromJSONString("EUR:4000000000000000") * 2 - } - } - - @Test - fun `test subtraction`() { - assertEquals( - Amount.fromJSONString("EUR:0"), - Amount.fromJSONString("EUR:1") - Amount.fromJSONString("EUR:1") - ) - assertEquals( - Amount.fromJSONString("EUR:1.5"), - Amount.fromJSONString("EUR:3") - Amount.fromJSONString("EUR:1.5") - ) - assertEquals( - Amount.fromJSONString("EUR:500000000.00000001"), - Amount.fromJSONString("EUR:500000000.00000002") - Amount.fromJSONString("EUR:0.00000001") - ) - assertThrows("subtraction didn't underflow") { - Amount.fromJSONString("EUR:23.42") - Amount.fromJSONString("EUR:42.23") - } - assertThrows("subtraction didn't underflow") { - Amount.fromJSONString("EUR:0.5") - Amount.fromJSONString("EUR:0.50000001") - } - } - - @Test - fun `test isZero()`() { - assertTrue(Amount.zero("EUR").isZero()) - assertTrue(Amount.fromJSONString("EUR:0").isZero()) - assertTrue(Amount.fromJSONString("EUR:0.0").isZero()) - assertTrue(Amount.fromJSONString("EUR:0.00000").isZero()) - assertTrue((Amount.fromJSONString("EUR:1.001") - Amount.fromJSONString("EUR:1.001")).isZero()) - - assertFalse(Amount.fromJSONString("EUR:0.00000001").isZero()) - assertFalse(Amount.fromJSONString("EUR:1.0").isZero()) - assertFalse(Amount.fromJSONString("EUR:0001.0").isZero()) - } - - @Test - fun `test comparision`() { - assertTrue(Amount.fromJSONString("EUR:0") <= Amount.fromJSONString("EUR:0")) - assertTrue(Amount.fromJSONString("EUR:0") <= Amount.fromJSONString("EUR:0.00000001")) - assertTrue(Amount.fromJSONString("EUR:0") < Amount.fromJSONString("EUR:0.00000001")) - assertTrue(Amount.fromJSONString("EUR:0") < Amount.fromJSONString("EUR:1")) - assertTrue(Amount.fromJSONString("EUR:0") == Amount.fromJSONString("EUR:0")) - assertTrue(Amount.fromJSONString("EUR:42") == Amount.fromJSONString("EUR:42")) - assertTrue(Amount.fromJSONString("EUR:42.00000001") == Amount.fromJSONString("EUR:42.00000001")) - assertTrue(Amount.fromJSONString("EUR:42.00000001") >= Amount.fromJSONString("EUR:42.00000001")) - assertTrue(Amount.fromJSONString("EUR:42.00000002") >= Amount.fromJSONString("EUR:42.00000001")) - assertTrue(Amount.fromJSONString("EUR:42.00000002") > Amount.fromJSONString("EUR:42.00000001")) - assertTrue(Amount.fromJSONString("EUR:0.00000002") > Amount.fromJSONString("EUR:0.00000001")) - assertTrue(Amount.fromJSONString("EUR:0.00000001") > Amount.fromJSONString("EUR:0")) - assertTrue(Amount.fromJSONString("EUR:2") > Amount.fromJSONString("EUR:1")) - - assertThrows("could compare amounts with different currencies") { - Amount.fromJSONString("EUR:0.5") < Amount.fromJSONString("USD:0.50000001") - } - } - - private inline fun assertThrows( - msg: String? = null, - function: () -> Any - ) { - try { - function.invoke() - fail(msg) - } catch (e: Exception) { - assertTrue(e is T) - } - } - -} diff --git a/taler-kotlin-common/src/test/java/net/taler/common/VersionTest.kt b/taler-kotlin-common/src/test/java/net/taler/common/VersionTest.kt deleted file mode 100644 index 70f30eb..0000000 --- a/taler-kotlin-common/src/test/java/net/taler/common/VersionTest.kt +++ /dev/null @@ -1,65 +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.common - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Test - -class VersionTest { - - @Test - fun testParse() { - assertNull(Version.parse("")) - assertNull(Version.parse("foo")) - assertNull(Version.parse("foo:bar:foo")) - assertNull(Version.parse("0:0:0:")) - assertNull(Version.parse("0:0:")) - assertEquals(Version(0, 0, 0), Version.parse("0:0:0")) - assertEquals(Version(1, 2, 3), Version.parse("1:2:3")) - assertEquals(Version(1337, 42, 23), Version.parse("1337:42:23")) - } - - @Test - fun testComparision() { - assertEquals( - Version.VersionMatchResult(true, 0), - Version.parse("0:0:0")!!.compare(Version.parse("0:0:0")) - ) - assertEquals( - Version.VersionMatchResult(true, -1), - Version.parse("0:0:0")!!.compare(Version.parse("1:0:1")) - ) - assertEquals( - Version.VersionMatchResult(true, -1), - Version.parse("0:0:0")!!.compare(Version.parse("1:5:1")) - ) - assertEquals( - Version.VersionMatchResult(false, -1), - Version.parse("0:0:0")!!.compare(Version.parse("1:5:0")) - ) - assertEquals( - Version.VersionMatchResult(false, 1), - Version.parse("1:0:0")!!.compare(Version.parse("0:5:0")) - ) - assertEquals( - Version.VersionMatchResult(true, 0), - Version.parse("1:0:1")!!.compare(Version.parse("1:5:1")) - ) - } - -} -- cgit v1.2.3