summaryrefslogtreecommitdiff
path: root/taler-kotlin-android/src/main/java/net/taler/common
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2022-05-16 19:58:31 +0200
committerFlorian Dold <florian@dold.me>2022-05-19 11:05:18 +0200
commit023ea96d26a65d29d408ef78c9405411bb842afd (patch)
tree658717478320443c4dd3cb6c5c906cfa443d2cad /taler-kotlin-android/src/main/java/net/taler/common
parent29e19d02b7befa0c8e18b9c73ac912bb256aa7ee (diff)
downloadtaler-android-023ea96d26a65d29d408ef78c9405411bb842afd.tar.gz
taler-android-023ea96d26a65d29d408ef78c9405411bb842afd.tar.bz2
taler-android-023ea96d26a65d29d408ef78c9405411bb842afd.zip
-remove multiplatform dependency, library upgrade WIP
Diffstat (limited to 'taler-kotlin-android/src/main/java/net/taler/common')
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/Amount.kt199
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt1
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt2
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/TalerUri.kt62
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/Time.kt129
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/Version.kt70
6 files changed, 460 insertions, 3 deletions
diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt b/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt
new file mode 100644
index 0000000..a36c0ab
--- /dev/null
+++ b/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt
@@ -0,0 +1,199 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.common
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Serializer
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlin.math.floor
+import kotlin.math.pow
+import kotlin.math.roundToInt
+
+public class AmountParserException(msg: String? = null, cause: Throwable? = null) : Exception(msg, cause)
+public class AmountOverflowException(msg: String? = null, cause: Throwable? = null) : Exception(msg, cause)
+
+@Serializable(with = KotlinXAmountSerializer::class)
+public 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<Amount> {
+
+ public companion object {
+
+ private const val FRACTIONAL_BASE: Int = 100000000 // 1e8
+
+ private val REGEX_CURRENCY = Regex("""^[-_*A-Za-z0-9]{1,12}$""")
+ public val MAX_VALUE: Long = 2.0.pow(52).toLong()
+ private const val MAX_FRACTION_LENGTH = 8
+ public const val MAX_FRACTION: Int = 99_999_999
+
+ public fun zero(currency: String): Amount {
+ return Amount(checkCurrency(currency), 0, 0)
+ }
+
+ public fun fromJSONString(str: String): Amount {
+ val split = str.split(":")
+ if (split.size != 2) throw AmountParserException("Invalid Amount Format")
+ return fromString(split[0], split[1])
+ }
+
+ public 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)
+ }
+
+ public fun min(currency: String): Amount = Amount(currency, 0, 1)
+ public 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
+ }
+
+ }
+
+ public 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"
+ }
+
+ public 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)
+ }
+
+ public 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
+ }
+
+ public 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)
+ }
+
+ public fun isZero(): Boolean {
+ return value == 0L && fraction == 0
+ }
+
+ public 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
+ }
+ }
+
+}
+
+@Suppress("EXPERIMENTAL_API_USAGE")
+@Serializer(forClass = Amount::class)
+internal object KotlinXAmountSerializer : KSerializer<Amount> {
+ 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-android/src/main/java/net/taler/common/AndroidUtils.kt b/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt
index 4ac2e73..d86e744 100644
--- a/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt
+++ b/taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt
@@ -49,7 +49,6 @@ import androidx.navigation.fragment.findNavController
import com.github.pedrovgs.lynx.LynxActivity
import com.github.pedrovgs.lynx.LynxConfig
import net.taler.lib.android.ErrorBottomSheet
-import net.taler.lib.common.Version
fun View.fadeIn(endAction: () -> Unit = {}) {
if (visibility == VISIBLE && alpha == 1f) return
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
index fb30692..910cc36 100644
--- a/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt
+++ b/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt
@@ -20,8 +20,6 @@ import android.os.Build
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.taler.common.TalerUtils.getLocalizedString
-import net.taler.lib.common.Amount
-import net.taler.lib.common.Timestamp
@Serializable
data class ContractTerms(
diff --git a/taler-kotlin-android/src/main/java/net/taler/common/TalerUri.kt b/taler-kotlin-android/src/main/java/net/taler/common/TalerUri.kt
new file mode 100644
index 0000000..a1b7225
--- /dev/null
+++ b/taler-kotlin-android/src/main/java/net/taler/common/TalerUri.kt
@@ -0,0 +1,62 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.common
+
+import java.util.Locale
+
+public object TalerUri {
+
+ private const val SCHEME = "taler://"
+ private const val SCHEME_INSECURE = "taler+http://"
+ private const val AUTHORITY_PAY = "pay"
+ private const val AUTHORITY_WITHDRAW = "withdraw"
+ private const val AUTHORITY_REFUND = "refund"
+ private const val AUTHORITY_TIP = "tip"
+
+ public data class WithdrawUriResult(
+ val bankIntegrationApiBaseUrl: String,
+ val withdrawalOperationId: String
+ )
+
+ /**
+ * Parses a withdraw URI and returns a bank status URL or null if the URI was invalid.
+ */
+ public fun parseWithdrawUri(uri: String): WithdrawUriResult? {
+ val (resultScheme, prefix) = when {
+ uri.startsWith(SCHEME, ignoreCase = true) -> {
+ Pair("https://", "${SCHEME}${AUTHORITY_WITHDRAW}/")
+ }
+ uri.startsWith(SCHEME_INSECURE, ignoreCase = true) -> {
+ Pair("http://", "${SCHEME_INSECURE}${AUTHORITY_WITHDRAW}/")
+ }
+ else -> return null
+ }
+ if (!uri.startsWith(prefix)) return null
+ val parts = uri.let {
+ (if (it.endsWith("/")) it.dropLast(1) else it).substring(prefix.length).split('/')
+ }
+ if (parts.size < 2) return null
+ val host = parts[0].lowercase(Locale.ROOT)
+ val pathSegments = parts.slice(1 until parts.size - 1).joinToString("/")
+ val withdrawId = parts.last()
+ if (withdrawId.isBlank()) return null
+ val url = "${resultScheme}${host}/${pathSegments}"
+
+ return WithdrawUriResult(url, withdrawId)
+ }
+
+}
diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Time.kt b/taler-kotlin-android/src/main/java/net/taler/common/Time.kt
new file mode 100644
index 0000000..61bbce8
--- /dev/null
+++ b/taler-kotlin-android/src/main/java/net/taler/common/Time.kt
@@ -0,0 +1,129 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.common
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.builtins.serializer
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.JsonTransformingSerializer
+import kotlinx.serialization.json.contentOrNull
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.longOrNull
+import kotlin.math.max
+
+@Serializable
+data class Timestamp(
+ @SerialName("t_ms")
+ @Serializable(NeverSerializer::class)
+ val old_ms: Long? = null,
+ @SerialName("t_s")
+ @Serializable(NeverSerializer::class)
+ private val s: Long? = null,
+) : Comparable<Timestamp> {
+
+ constructor(ms: Long) : this(ms, null)
+
+ companion object {
+ private const val NEVER: Long = -1
+ fun now(): Timestamp = Timestamp(System.currentTimeMillis())
+ fun never(): Timestamp = Timestamp(NEVER)
+ }
+
+ val ms: Long = if (s != null) {
+ s * 1000L
+ } else if (old_ms !== null) {
+ old_ms
+ } else {
+ throw Exception("timestamp didn't have t_s or t_ms")
+ }
+
+
+ /**
+ * 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(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 == Duration.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(
+ @SerialName("d_ms")
+ @Serializable(ForeverSerializer::class) val old_ms: Long? = null,
+ @SerialName("d_s")
+ @Serializable(ForeverSerializer::class)
+ private val s: Long? = null,
+) {
+ val ms: Long = if (s != null) {
+ s * 1000L
+ } else if (old_ms !== null) {
+ old_ms
+ } else {
+ throw Exception("duration didn't have d_s or d_ms")
+ }
+
+ constructor(ms: Long) : this(ms, null)
+
+ companion object {
+ internal const val FOREVER: Long = -1
+ fun forever(): Duration = Duration(FOREVER)
+ }
+}
+
+internal abstract class MinusOneSerializer(private val keyword: String) :
+ JsonTransformingSerializer<Long>(Long.serializer()) {
+
+ override fun transformDeserialize(element: JsonElement): JsonElement {
+ return if (element.jsonPrimitive.contentOrNull == keyword) return JsonPrimitive(-1)
+ else super.transformDeserialize(element)
+ }
+
+ override fun transformSerialize(element: JsonElement): JsonElement {
+ return if (element.jsonPrimitive.longOrNull == -1L) return JsonPrimitive(keyword)
+ else element
+ }
+}
+
+internal object NeverSerializer : MinusOneSerializer("never")
+internal object ForeverSerializer : MinusOneSerializer("forever")
diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Version.kt b/taler-kotlin-android/src/main/java/net/taler/common/Version.kt
new file mode 100644
index 0000000..f46913e
--- /dev/null
+++ b/taler-kotlin-android/src/main/java/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 <http://www.gnu.org/licenses/>
+ */
+
+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
+ */
+public data class Version(
+ val current: Int,
+ val revision: Int,
+ val age: Int
+) {
+ public companion object {
+ public 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.
+ */
+ public 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.
+ */
+ public 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
+ )
+
+}