summaryrefslogtreecommitdiff
path: root/taler-kotlin-android/src/main/java/net/taler/common
diff options
context:
space:
mode:
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.kt105
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/AndroidUtils.kt32
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt133
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt10
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/CurrencySpecification.kt36
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/Event.kt4
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/TalerUri.kt2
-rw-r--r--taler-kotlin-android/src/main/java/net/taler/common/Time.kt68
8 files changed, 262 insertions, 128 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
index 18fb6cb..3e3bd0a 100644
--- a/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt
+++ b/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt
@@ -16,17 +16,24 @@
package net.taler.common
+import android.os.Build
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.Serializer
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
+import java.text.DecimalFormat
+import java.text.DecimalFormatSymbols
+import java.text.NumberFormat
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)
+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(
@@ -50,25 +57,23 @@ public data class Amount(
* of the base currency value. For example, a fraction
* of 50_000_000 would correspond to 50 cents.
*/
- val fraction: Int
+ val fraction: Int,
+
+ /**
+ * Currency specification for amount
+ */
+ val spec: CurrencySpecification? = null,
) : Comparable<Amount> {
public companion object {
private const val FRACTIONAL_BASE: Int = 100000000 // 1e8
- public val SEGWIT_MIN = Amount("BTC", 0, 294)
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 fromDouble(currency: String, value: Double): Amount {
- val intPart = Math.floor(value).toLong()
- val fraPart = Math.floor((value - intPart) * FRACTIONAL_BASE).toInt()
- return Amount(currency, intPart, fraPart)
- }
-
public fun zero(currency: String): Amount {
return Amount(checkCurrency(currency), 0, 0)
}
@@ -88,14 +93,35 @@ public data class Amount(
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)
+ checkFraction(fractionStr.getFraction())
} else 0
return Amount(checkCurrency(currency), value, fraction)
}
+ public fun isValidAmountStr(str: String): Boolean {
+ if (str.count { it == '.' } > 1) return false
+ val split = str.split(".")
+ try {
+ checkValue(split[0].toLongOrNull())
+ } catch (e: AmountParserException) {
+ return false
+ }
+ // also check fraction, if it exists
+ if (split.size > 1) {
+ val fractionStr = split[1]
+ if (fractionStr.length > MAX_FRACTION_LENGTH) return false
+ val fraction = fractionStr.getFraction() ?: return false
+ return fraction <= MAX_FRACTION
+ }
+ return true
+ }
+
+ private fun String.getFraction(): Int? {
+ return "0.$this".toDoubleOrNull()
+ ?.times(FRACTIONAL_BASE)
+ ?.roundToInt()
+ }
+
public fun min(currency: String): Amount = Amount(currency, 0, 1)
public fun max(currency: String): Amount = Amount(currency, MAX_VALUE, MAX_FRACTION)
@@ -133,7 +159,8 @@ public data class Amount(
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()
+ 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
@@ -152,6 +179,8 @@ public data class Amount(
return Amount(checkCurrency(currency), this.value, this.fraction)
}
+ fun withSpec(spec: CurrencySpecification?) = copy(spec = spec)
+
public operator fun minus(other: Amount): Amount {
check(currency == other.currency) { "Can only subtract from same currency" }
var resultValue = value
@@ -178,8 +207,50 @@ public data class Amount(
return "$currency:$amountStr"
}
- override fun toString(): String {
- return "$amountStr $currency"
+ override fun toString() = toString(
+ showSymbol = true,
+ negative = false,
+ )
+
+ fun toString(
+ showSymbol: Boolean = true,
+ negative: Boolean = false,
+ symbols: DecimalFormatSymbols = DecimalFormat().decimalFormatSymbols,
+ ): String {
+ // We clone the object to safely/cleanly modify it
+ val s = symbols.clone() as DecimalFormatSymbols
+ val amount = (if (negative) "-$amountStr" else amountStr).toBigDecimal()
+
+ // No currency spec, so we render normally
+ if (spec == null) {
+ val format = NumberFormat.getInstance()
+ format.maximumFractionDigits = MAX_FRACTION_LENGTH
+ format.minimumFractionDigits = 0
+ if (Build.VERSION.SDK_INT >= 34) {
+ s.groupingSeparator = s.monetaryGroupingSeparator
+ }
+ s.decimalSeparator = s.monetaryDecimalSeparator
+ (format as DecimalFormat).decimalFormatSymbols = s
+
+ val fmt = format.format(amount)
+ return if (showSymbol) "$fmt $currency" else fmt
+ }
+
+ // There is currency spec, so we can do things right
+ val format = NumberFormat.getCurrencyInstance()
+ format.maximumFractionDigits = spec.numFractionalNormalDigits
+ format.minimumFractionDigits = spec.numFractionalTrailingZeroDigits
+ s.currencySymbol = spec.symbol ?: ""
+ (format as DecimalFormat).decimalFormatSymbols = s
+
+ val fmt = format.format(amount)
+ return if (showSymbol) {
+ // If no symbol, then we use the currency string
+ if (spec.symbol != null) fmt else "$fmt $currency"
+ } else {
+ // We should do better than manually removing the symbol here
+ fmt.replace(s.currencySymbol, "").trim()
+ }
}
override fun compareTo(other: Amount): Int {
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 7dde872..8f3e5d5 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
@@ -23,6 +23,7 @@ import android.content.Context.CONNECTIVITY_SERVICE
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.Uri
import android.os.Build.VERSION.SDK_INT
import android.os.Looper
import android.text.format.DateUtils.DAY_IN_MILLIS
@@ -97,14 +98,22 @@ fun Context.isOnline(): Boolean {
}
fun FragmentActivity.showError(mainText: String, detailText: String = "") = ErrorBottomSheet
- .newInstance(mainText, detailText)
- .show(supportFragmentManager, "ERROR_BOTTOM_SHEET")
+ .newInstance(mainText, detailText)
+ .show(supportFragmentManager, "ERROR_BOTTOM_SHEET")
fun FragmentActivity.showError(@StringRes mainId: Int, detailText: String = "") {
showError(getString(mainId), detailText)
}
-fun Fragment.startActivitySafe(intent: Intent) {
+fun Fragment.showError(mainText: String, detailText: String = "") = ErrorBottomSheet
+ .newInstance(mainText, detailText)
+ .show(parentFragmentManager, "ERROR_BOTTOM_SHEET")
+
+fun Fragment.showError(@StringRes mainId: Int, detailText: String = "") {
+ showError(getString(mainId), detailText)
+}
+
+fun Context.startActivitySafe(intent: Intent) {
try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
@@ -112,6 +121,23 @@ fun Fragment.startActivitySafe(intent: Intent) {
}
}
+fun Context.openUri(uri: String, title: String) {
+ val intent = Intent(Intent.ACTION_VIEW).apply {
+ data = Uri.parse(uri)
+ }
+
+ startActivitySafe(Intent.createChooser(intent, title))
+}
+
+fun Context.shareText(text: String) {
+ val intent = Intent(Intent.ACTION_SEND).apply {
+ putExtra(Intent.EXTRA_TEXT, text)
+ type = "text/plain"
+ }
+
+ startActivitySafe(Intent.createChooser(intent, null))
+}
+
fun Fragment.navigate(directions: NavDirections) = findNavController().navigate(directions)
fun Long.toRelativeTime(context: Context): CharSequence {
diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt b/taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt
index 32885df..4b77f85 100644
--- a/taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt
+++ b/taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt
@@ -13,6 +13,7 @@
* 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/>
*/
+
// Copyright (c) 2020 Figure Technologies Inc.
// The contents of this file were derived from an implementation
// by the btcsuite developers https://github.com/btcsuite/btcutil.
@@ -23,27 +24,17 @@
// modified version of https://gist.github.com/iramiller/4ebfcdfbc332a9722c4a4abeb4e16454
-import net.taler.common.CyptoUtils
+package net.taler.common
+
+import java.util.Locale.ROOT
import kotlin.experimental.and
import kotlin.experimental.or
-
infix fun Int.min(b: Int): Int = b.takeIf { this > b } ?: this
infix fun UByte.shl(bitCount: Int) = ((this.toInt() shl bitCount) and 0xff).toUByte()
infix fun UByte.shr(bitCount: Int) = (this.toInt() shr bitCount).toUByte()
/**
- * Given an array of bytes, associate an HRP and return a Bech32Data instance.
- */
-fun ByteArray.toBech32Data(hrp: String) =
- Bech32Data(hrp, Bech32.convertBits(this, 8, 5, true))
-
-/**
- * Using a string in bech32 encoded address format, parses out and returns a Bech32Data instance
- */
-fun String.toBech32Data() = Bech32.decode(this)
-
-/**
* Bech32 Data encoding instance containing data for encoding as well as a human readable prefix
*/
data class Bech32Data(val hrp: String, val fiveBitData: ByteArray) {
@@ -71,6 +62,30 @@ data class Bech32Data(val hrp: String, val fiveBitData: ByteArray) {
override fun toString(): String {
return "bech32 : ${this.address}\nhuman: ${this.hrp} \nbytes"
}
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as Bech32Data
+
+ if (hrp != other.hrp) return false
+ if (!fiveBitData.contentEquals(other.fiveBitData)) return false
+ if (!data.contentEquals(other.data)) return false
+ if (address != other.address) return false
+ if (!checksum.contentEquals(other.checksum)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = hrp.hashCode()
+ result = 31 * result + fiveBitData.contentHashCode()
+ result = 31 * result + data.contentHashCode()
+ result = 31 * result + address.hashCode()
+ result = 31 * result + checksum.contentHashCode()
+ return result
+ }
}
/**
@@ -80,32 +95,32 @@ class Bech32 {
companion object {
const val CHECKSUM_SIZE = 6
- const val MIN_VALID_LENGTH = 8
- const val MAX_VALID_LENGTH = 90
+ private const val MIN_VALID_LENGTH = 8
+ private const val MAX_VALID_LENGTH = 90
const val MIN_VALID_CODEPOINT = 33
- const val MAX_VALID_CODEPOINT = 126
+ private const val MAX_VALID_CODEPOINT = 126
const val charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
- val gen = intArrayOf(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3)
+ private val gen = intArrayOf(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3)
fun generateFakeSegwitAddress(reservePub: String?, addr: String): List<String> {
- if (reservePub == null || reservePub.isEmpty()) return listOf<String>()
+ if (reservePub == null || reservePub.isEmpty()) return listOf()
val pub = CyptoUtils.decodeCrock(reservePub)
if (pub.size != 32) return listOf()
- val first_rnd = pub.copyOfRange(0,4)
- val second_rnd = pub.copyOfRange(0,4)
+ val firstRnd = pub.copyOfRange(0, 4)
+ val secondRnd = pub.copyOfRange(0, 4)
- first_rnd[0] = first_rnd[0].and(0b0111_1111);
- second_rnd[0] = second_rnd[0].or(0b1000_0000.toByte());
+ firstRnd[0] = firstRnd[0].and(0b0111_1111)
+ secondRnd[0] = secondRnd[0].or(0b1000_0000.toByte())
- val first_part = ByteArray(20);
- first_rnd.copyInto( first_part, 0, 0, 4)
- pub.copyInto( first_part, 4, 0, 16)
+ val firstPart = ByteArray(20)
+ firstRnd.copyInto(firstPart, 0, 0, 4)
+ pub.copyInto(firstPart, 4, 0, 16)
- val second_part = ByteArray(20);
- second_rnd.copyInto( second_part, 0, 0, 4)
- pub.copyInto( second_part, 4, 16, 32)
+ val secondPart = ByteArray(20)
+ secondRnd.copyInto(secondPart, 0, 0, 4)
+ pub.copyInto(secondPart, 4, 16, 32)
val zero = ByteArray(1)
zero[0] = 0
@@ -117,8 +132,8 @@ class Bech32 {
}
return listOf(
- Bech32Data(hrp, zero + convertBits(first_part, 8, 5, true)).address,
- Bech32Data(hrp, zero + convertBits(second_part, 8, 5, true)).address,
+ Bech32Data(hrp, zero + convertBits(firstPart, 8, 5, true)).address,
+ Bech32Data(hrp, zero + convertBits(secondPart, 8, 5, true)).address,
)
}
@@ -126,22 +141,31 @@ class Bech32 {
* Decodes a Bech32 String
*/
fun decode(bech32: String): Bech32Data {
- require(bech32.length >= MIN_VALID_LENGTH && bech32.length <= MAX_VALID_LENGTH) { "invalid bech32 string length" }
- require(bech32.toCharArray().none { c -> c.toInt() < MIN_VALID_CODEPOINT || c.toInt() > MAX_VALID_CODEPOINT })
- { "invalid character in bech32: ${bech32.toCharArray().map { c -> c.toInt() }
- .filter { c -> c.toInt() < MIN_VALID_CODEPOINT || c.toInt() > MAX_VALID_CODEPOINT }}" }
+ require(bech32.length in MIN_VALID_LENGTH..MAX_VALID_LENGTH) { "invalid bech32 string length" }
+ require(bech32.toCharArray()
+ .none { c -> c.code < MIN_VALID_CODEPOINT || c.code > MAX_VALID_CODEPOINT })
+ {
+ "invalid character in bech32: ${
+ bech32.toCharArray().map { c -> c.code }
+ .filter { c -> c < MIN_VALID_CODEPOINT || c > MAX_VALID_CODEPOINT }
+ }"
+ }
- require(bech32.equals(bech32.toLowerCase()) || bech32.equals(bech32.toUpperCase()))
+ require(bech32 == bech32.lowercase(ROOT) || bech32 == bech32.uppercase(ROOT))
{ "bech32 must be either all upper or lower case" }
- require(bech32.substring(1).dropLast(CHECKSUM_SIZE).contains('1')) { "invalid index of '1'" }
+ require(bech32.substring(1).dropLast(CHECKSUM_SIZE)
+ .contains('1')) { "invalid index of '1'" }
- val hrp = bech32.substringBeforeLast('1').toLowerCase()
- val dataString = bech32.substringAfterLast('1').toLowerCase()
+ val hrp = bech32.substringBeforeLast('1').lowercase(ROOT)
+ val dataString = bech32.substringAfterLast('1').lowercase(ROOT)
- require(dataString.toCharArray().all { c -> charset.contains(c) }) { "invalid data encoding character in bech32"}
+ require(dataString.toCharArray()
+ .all { c -> charset.contains(c) }) { "invalid data encoding character in bech32" }
val dataBytes = dataString.map { c -> charset.indexOf(c).toByte() }.toByteArray()
- val checkBytes = dataString.takeLast(CHECKSUM_SIZE).map { c -> charset.indexOf(c).toByte() }.toByteArray()
+ val checkBytes =
+ dataString.takeLast(CHECKSUM_SIZE).map { c -> charset.indexOf(c).toByte() }
+ .toByteArray()
val actualSum = checksum(hrp, dataBytes.dropLast(CHECKSUM_SIZE).toTypedArray())
require(1 == polymod(expandHrp(hrp).plus(dataBytes.map { d -> d.toInt() }))) { "checksum failed: $checkBytes != $actualSum" }
@@ -154,10 +178,10 @@ class Bech32 {
* This process is used to convert from base64 (from 8) to base32 (to 5) or the inverse.
*/
fun convertBits(data: ByteArray, fromBits: Int, toBits: Int, pad: Boolean): ByteArray {
- require (fromBits in 1..8 && toBits in 1..8) { "only bit groups between 1 and 8 are supported"}
+ require(fromBits in 1..8 && toBits in 1..8) { "only bit groups between 1 and 8 are supported" }
// resulting bytes with each containing the toBits bits from the input set.
- var regrouped = arrayListOf<Byte>()
+ val regrouped = arrayListOf<Byte>()
var nextByte = 0.toUByte()
var filledBits = 0
@@ -174,8 +198,9 @@ class Bech32 {
val remainToBits = toBits - filledBits
// we extract the remaining bits unless that is more than we need.
- val toExtract = remainFromBits.takeUnless { remainToBits < remainFromBits } ?: remainToBits
- check(toExtract >= 0) { "extract should be positive"}
+ val toExtract =
+ remainFromBits.takeUnless { remainToBits < remainFromBits } ?: remainToBits
+ check(toExtract >= 0) { "extract should be positive" }
// move existing bits to the left to make room for bits toExtract, copy in bits to extract
nextByte = (nextByte shl toExtract) or (b shr (8 - toExtract))
@@ -218,24 +243,24 @@ class Bech32 {
* Calculates a bech32 checksum based on BIP 173 specification
*/
fun checksum(hrp: String, data: Array<Byte>): ByteArray {
- var values = expandHrp(hrp)
+ val values = expandHrp(hrp)
.plus(data.map { d -> d.toInt() })
- .plus(Array<Int>(6){ _ -> 0}.toIntArray())
+ .plus(Array(6) { 0 }.toIntArray())
- var poly = polymod(values) xor 1
+ val poly = polymod(values) xor 1
return (0..5).map {
- ((poly shr (5 * (5-it))) and 31).toByte()
+ ((poly shr (5 * (5 - it))) and 31).toByte()
}.toByteArray()
}
/**
* Expands the human readable prefix per BIP173 for Checksum encoding
*/
- fun expandHrp(hrp: String) =
- hrp.map { c -> c.toInt() shr 5 }
+ private fun expandHrp(hrp: String) =
+ hrp.map { c -> c.code shr 5 }
.plus(0)
- .plus(hrp.map { c -> c.toInt() and 31 })
+ .plus(hrp.map { c -> c.code and 31 })
.toIntArray()
/**
@@ -243,9 +268,9 @@ class Bech32 {
*/
fun polymod(values: IntArray): Int {
var chk = 1
- return values.map {
- var b = chk shr 25
- chk = ((chk and 0x1ffffff) shl 5) xor it
+ return values.map { num ->
+ val b = chk shr 25
+ chk = ((chk and 0x1ffffff) shl 5) xor num
(0..4).map {
if (((b shr it) and 1) == 1) {
chk = chk xor gen[it]
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 910cc36..5b614fe 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
@@ -42,7 +42,7 @@ abstract class Product {
abstract val productId: String?
abstract val description: String
abstract val descriptionI18n: Map<String, String>?
- abstract val price: Amount
+ abstract val price: Amount?
abstract val location: String?
abstract val image: String?
val localizedDescription: String
@@ -60,14 +60,14 @@ data class ContractProduct(
override val description: String,
@SerialName("description_i18n")
override val descriptionI18n: Map<String, String>? = null,
- override val price: Amount,
+ override val price: Amount? = null,
@SerialName("delivery_location")
override val location: String? = null,
override val image: String? = null,
- val quantity: Int
+ val quantity: Int = 1,
) : Product() {
- val totalPrice: Amount by lazy {
- price * quantity
+ val totalPrice: Amount? by lazy {
+ price?.let { price * quantity }
}
}
diff --git a/taler-kotlin-android/src/main/java/net/taler/common/CurrencySpecification.kt b/taler-kotlin-android/src/main/java/net/taler/common/CurrencySpecification.kt
new file mode 100644
index 0000000..02113f4
--- /dev/null
+++ b/taler-kotlin-android/src/main/java/net/taler/common/CurrencySpecification.kt
@@ -0,0 +1,36 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 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
+
+@Serializable
+data class CurrencySpecification(
+ val name: String,
+ @SerialName("num_fractional_input_digits")
+ val numFractionalInputDigits: Int,
+ @SerialName("num_fractional_normal_digits")
+ val numFractionalNormalDigits: Int,
+ @SerialName("num_fractional_trailing_zero_digits")
+ val numFractionalTrailingZeroDigits: Int,
+ @SerialName("alt_unit_names")
+ val altUnitNames: Map<Int, String>,
+) {
+ // TODO: add support for alt units
+ val symbol: String? get() = altUnitNames[0]
+} \ No newline at end of file
diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Event.kt b/taler-kotlin-android/src/main/java/net/taler/common/Event.kt
index 752e20e..868063c 100644
--- a/taler-kotlin-android/src/main/java/net/taler/common/Event.kt
+++ b/taler-kotlin-android/src/main/java/net/taler/common/Event.kt
@@ -49,7 +49,7 @@ fun <T> T.toEvent() = Event(this)
* [onEvent] is *only* called if the [Event]'s contents has not been consumed.
*/
class EventObserver<T>(private val onEvent: (T) -> Unit) : Observer<Event<T>> {
- override fun onChanged(event: Event<T>?) {
- event?.getIfNotConsumed()?.let { onEvent(it) }
+ override fun onChanged(value: Event<T>) {
+ value.getIfNotConsumed()?.let { onEvent(it) }
}
}
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
index a1b7225..999408c 100644
--- a/taler-kotlin-android/src/main/java/net/taler/common/TalerUri.kt
+++ b/taler-kotlin-android/src/main/java/net/taler/common/TalerUri.kt
@@ -45,7 +45,7 @@ public object TalerUri {
}
else -> return null
}
- if (!uri.startsWith(prefix)) return null
+ if (!uri.startsWith(prefix, ignoreCase = true)) return null
val parts = uri.let {
(if (it.endsWith("/")) it.dropLast(1) else it).substring(prefix.length).split('/')
}
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
index 61bbce8..f280d5f 100644
--- a/taler-kotlin-android/src/main/java/net/taler/common/Time.kt
+++ b/taler-kotlin-android/src/main/java/net/taler/common/Time.kt
@@ -29,50 +29,31 @@ 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,
+ private val s: Long,
) : Comparable<Timestamp> {
- constructor(ms: Long) : this(ms, null)
-
companion object {
private const val NEVER: Long = -1
- fun now(): Timestamp = Timestamp(System.currentTimeMillis())
+ fun now(): Timestamp = fromMillis(System.currentTimeMillis())
fun never(): Timestamp = Timestamp(NEVER)
+ fun fromMillis(ms: Long) = Timestamp(ms / 1000L)
}
- 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)
- }
+ val ms: Long = s * 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: Timestamp): RelativeTime = when {
+ ms == NEVER -> RelativeTime.fromMillis(RelativeTime.FOREVER)
+ other.ms == NEVER -> throw Error("Invalid argument for timestamp comparison")
+ ms < other.ms -> RelativeTime.fromMillis(0)
+ else -> RelativeTime.fromMillis(ms - other.ms)
}
- operator fun minus(other: Duration): Timestamp = when {
+ operator fun minus(other: RelativeTime): Timestamp = when {
ms == NEVER -> this
- other.ms == Duration.FOREVER -> Timestamp(0)
- else -> Timestamp(max(0, ms - other.ms))
+ other.ms == RelativeTime.FOREVER -> fromMillis(0)
+ else -> fromMillis(max(0, ms - other.ms))
}
override fun compareTo(other: Timestamp): Int {
@@ -88,26 +69,21 @@ data class Timestamp(
}
@Serializable
-data class Duration(
- @SerialName("d_ms")
- @Serializable(ForeverSerializer::class) val old_ms: Long? = null,
- @SerialName("d_s")
+data class RelativeTime(
+ /**
+ * Duration in microseconds or "forever" to represent an infinite duration.
+ * Numeric values are capped at 2^53 - 1 inclusive.
+ */
+ @SerialName("d_us")
@Serializable(ForeverSerializer::class)
- private val s: Long? = null,
+ private val s: Long,
) {
- 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)
+ val ms: Long = s * 1000L
companion object {
internal const val FOREVER: Long = -1
- fun forever(): Duration = Duration(FOREVER)
+ fun forever(): RelativeTime = fromMillis(FOREVER)
+ fun fromMillis(ms: Long) = RelativeTime(ms / 100L)
}
}