diff options
Diffstat (limited to 'app/src/main/java/net/taler/wallet')
5 files changed, 377 insertions, 50 deletions
diff --git a/app/src/main/java/net/taler/wallet/Amount.kt b/app/src/main/java/net/taler/wallet/Amount.kt new file mode 100644 index 0000000..656228f --- /dev/null +++ b/app/src/main/java/net/taler/wallet/Amount.kt @@ -0,0 +1,120 @@ +/* + This file is part of GNU Taler + (C) 2019 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/> + */ + +@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS") + +package net.taler.wallet + +import org.json.JSONObject +import kotlin.math.round + +private const val FRACTIONAL_BASE = 1e8 + +data class Amount(val currency: String, val amount: String) { + fun isZero(): Boolean { + return amount.toDouble() == 0.0 + } + + companion object { + fun fromJson(jsonAmount: JSONObject): Amount { + val amountCurrency = jsonAmount.getString("currency") + val amountValue = jsonAmount.getString("value") + val amountFraction = jsonAmount.getString("fraction") + val amountIntValue = Integer.parseInt(amountValue) + val amountIntFraction = Integer.parseInt(amountFraction) + return Amount( + amountCurrency, + (amountIntValue + amountIntFraction / FRACTIONAL_BASE).toString() + ) + } + + fun fromString(strAmount: String): Amount { + val components = strAmount.split(":") + return Amount(components[0], components[1]) + } + } +} + +class ParsedAmount( + /** + * 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, + + /** + * unsigned 32 bit value in the currency, + * note that "1" here would correspond to 1 EUR or 1 USD, depending on currency, not 1 cent. + */ + val value: UInt, + + /** + * unsigned 32 bit fractional value to be added to value + * representing an additional currency fraction, + * in units of one millionth (1e-6) of the base currency value. + * For example, a fraction of 500,000 would correspond to 50 cents. + */ + val fraction: Double +) { + companion object { + fun parseAmount(str: String): ParsedAmount { + val split = str.split(":") + check(split.size == 2) + val currency = split[0] + val valueSplit = split[1].split(".") + val value = valueSplit[0].toUInt() + val fraction: Double = if (valueSplit.size > 1) { + round("0.${valueSplit[1]}".toDouble() * FRACTIONAL_BASE) + } else 0.0 + return ParsedAmount(currency, value, fraction) + } + } + + operator fun minus(other: ParsedAmount): ParsedAmount { + check(currency == other.currency) { "Can only subtract from same currency" } + var resultValue = value + var resultFraction = fraction + if (resultFraction < other.fraction) { + if (resultValue < 1u) { + return ParsedAmount(currency, 0u, 0.0) + } + resultValue-- + resultFraction += FRACTIONAL_BASE + } + check(resultFraction >= other.fraction) + resultFraction -= other.fraction + if (resultValue < other.value) { + return ParsedAmount(currency, 0u, 0.0) + } + resultValue -= other.value + return ParsedAmount(currency, resultValue, resultFraction) + } + + fun toJSONString(): String { + return "$currency:${getValueString()}" + } + + override fun toString(): String { + return "${getValueString()} $currency" + } + + private fun getValueString(): String { + return "$value${(fraction / FRACTIONAL_BASE).toString().substring(1)}" + } + +} diff --git a/app/src/main/java/net/taler/wallet/WalletViewModel.kt b/app/src/main/java/net/taler/wallet/WalletViewModel.kt index 94d2d8a..f556ff3 100644 --- a/app/src/main/java/net/taler/wallet/WalletViewModel.kt +++ b/app/src/main/java/net/taler/wallet/WalletViewModel.kt @@ -30,32 +30,6 @@ import org.json.JSONObject const val TAG = "taler-wallet" -data class Amount(val currency: String, val amount: String) { - fun isZero(): Boolean { - return amount.toDouble() == 0.0 - } - - companion object { - const val FRACTIONAL_BASE = 1e8 - fun fromJson(jsonAmount: JSONObject): Amount { - val amountCurrency = jsonAmount.getString("currency") - val amountValue = jsonAmount.getString("value") - val amountFraction = jsonAmount.getString("fraction") - val amountIntValue = Integer.parseInt(amountValue) - val amountIntFraction = Integer.parseInt(amountFraction) - return Amount( - amountCurrency, - (amountIntValue + amountIntFraction / FRACTIONAL_BASE).toString() - ) - } - - fun fromString(strAmount: String): Amount { - val components = strAmount.split(":") - return Amount(components[0], components[1]) - } - } -} - data class BalanceEntry(val available: Amount, val pendingIncoming: Amount) diff --git a/app/src/main/java/net/taler/wallet/history/HistoryEvent.kt b/app/src/main/java/net/taler/wallet/history/HistoryEvent.kt index 34f3164..31473f6 100644 --- a/app/src/main/java/net/taler/wallet/history/HistoryEvent.kt +++ b/app/src/main/java/net/taler/wallet/history/HistoryEvent.kt @@ -1,10 +1,30 @@ +/* + This file is part of GNU Taler + (C) 2019 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.wallet.history +import androidx.annotation.DrawableRes +import androidx.annotation.LayoutRes +import androidx.annotation.StringRes import com.fasterxml.jackson.annotation.* import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY import com.fasterxml.jackson.annotation.JsonSubTypes.Type import com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME +import net.taler.wallet.R enum class ReserveType { /** @@ -20,7 +40,22 @@ enum class ReserveType { } @JsonInclude(NON_EMPTY) -class ReserveCreationDetail(val type: ReserveType) +class ReserveCreationDetail(val type: ReserveType, val bankUrl: String?) + +enum class RefreshReason { + @JsonProperty("manual") + MANUAL, + @JsonProperty("pay") + PAY, + @JsonProperty("refund") + REFUND, + @JsonProperty("abort-pay") + ABORT_PAY, + @JsonProperty("recoup") + RECOUP, + @JsonProperty("backup-restored") + BACKUP_RESTORED +} @JsonInclude(NON_EMPTY) @@ -45,7 +80,7 @@ class ReserveShortInfo( val reserveCreationDetail: ReserveCreationDetail ) -class History: ArrayList<HistoryEvent>() +class History : ArrayList<HistoryEvent>() @JsonTypeInfo( use = NAME, @@ -56,7 +91,11 @@ class History: ArrayList<HistoryEvent>() Type(value = ExchangeAddedEvent::class, name = "exchange-added"), Type(value = ExchangeUpdatedEvent::class, name = "exchange-updated"), Type(value = ReserveBalanceUpdatedEvent::class, name = "reserve-balance-updated"), - Type(value = HistoryWithdrawnEvent::class, name = "withdrawn") + Type(value = HistoryWithdrawnEvent::class, name = "withdrawn"), + Type(value = HistoryOrderAcceptedEvent::class, name = "order-accepted"), + Type(value = HistoryOrderRefusedEvent::class, name = "order-refused"), + Type(value = HistoryPaymentSentEvent::class, name = "payment-sent"), + Type(value = HistoryRefreshedEvent::class, name = "refreshed") ) @JsonIgnoreProperties( value = [ @@ -64,7 +103,13 @@ class History: ArrayList<HistoryEvent>() ] ) abstract class HistoryEvent( - val timestamp: Timestamp + val timestamp: Timestamp, + @get:LayoutRes + open val layout: Int = R.layout.history_row, + @get:StringRes + open val title: Int = 0, + @get:DrawableRes + open val icon: Int = R.drawable.ic_account_balance ) @@ -73,13 +118,17 @@ class ExchangeAddedEvent( timestamp: Timestamp, val exchangeBaseUrl: String, val builtIn: Boolean -) : HistoryEvent(timestamp) +) : HistoryEvent(timestamp) { + override val title = R.string.history_event_exchange_added +} @JsonTypeName("exchange-updated") class ExchangeUpdatedEvent( timestamp: Timestamp, val exchangeBaseUrl: String -) : HistoryEvent(timestamp) +) : HistoryEvent(timestamp) { + override val title = R.string.history_event_exchange_updated +} @JsonTypeName("reserve-balance-updated") @@ -99,7 +148,9 @@ class ReserveBalanceUpdatedEvent( * considering ongoing withdrawals from that reserve. */ val amountExpected: String -) : HistoryEvent(timestamp) +) : HistoryEvent(timestamp) { + override val title = R.string.history_event_reserve_balance_updated +} @JsonTypeName("withdrawn") class HistoryWithdrawnEvent( @@ -123,8 +174,94 @@ class HistoryWithdrawnEvent( * Amount that actually was added to the wallet's balance. */ val amountWithdrawnEffective: String -) : HistoryEvent(timestamp) +) : HistoryEvent(timestamp) { + override val layout = R.layout.history_withdrawn + override val title = R.string.history_event_withdrawn + override val icon = R.drawable.history_withdrawn +} +@JsonTypeName("order-accepted") +class HistoryOrderAcceptedEvent( + timestamp: Timestamp, + /** + * Condensed info about the order. + */ + val orderShortInfo: OrderShortInfo +) : HistoryEvent(timestamp) { + override val icon = R.drawable.ic_add_circle + override val title = R.string.history_event_order_accepted +} + +@JsonTypeName("order-refused") +class HistoryOrderRefusedEvent( + timestamp: Timestamp, + /** + * Condensed info about the order. + */ + val orderShortInfo: OrderShortInfo +) : HistoryEvent(timestamp) { + override val icon = R.drawable.ic_cancel + override val title = R.string.history_event_order_refused +} + +@JsonTypeName("payment-sent") +class HistoryPaymentSentEvent( + timestamp: Timestamp, + /** + * Condensed info about the order that we already paid for. + */ + val orderShortInfo: OrderShortInfo, + /** + * Set to true if the payment has been previously sent + * to the merchant successfully, possibly with a different session ID. + */ + val replay: Boolean, + /** + * Number of coins that were involved in the payment. + */ + val numCoins: Int, + /** + * Amount that was paid, including deposit and wire fees. + */ + val amountPaidWithFees: String, + /** + * Session ID that the payment was (re-)submitted under. + */ + val sessionId: String? +) : HistoryEvent(timestamp) { + override val layout = R.layout.history_payment_sent + override val title = R.string.history_event_payment_sent + override val icon = R.drawable.ic_cash_usd_outline +} + +@JsonTypeName("refreshed") +class HistoryRefreshedEvent( + timestamp: Timestamp, + /** + * Amount that is now available again because it has + * been refreshed. + */ + val amountRefreshedEffective: String, + /** + * Amount that we spent for refreshing. + */ + val amountRefreshedRaw: String, + /** + * Why was the refreshing done? + */ + val refreshReason: RefreshReason, + val numInputCoins: Int, + val numRefreshedInputCoins: Int, + val numOutputCoins: Int, + /** + * Identifier for a refresh group, contains one or + * more refresh session IDs. + */ + val refreshGroupId: String +) : HistoryEvent(timestamp) { + override val icon = R.drawable.ic_history_black_24dp + override val title = R.string.history_event_refreshed +} @JsonTypeInfo( use = NAME, @@ -145,3 +282,26 @@ class WithdrawalSourceTip( class WithdrawalSourceReserve( val reservePub: String ) : WithdrawalSource() + +data class OrderShortInfo( + /** + * Wallet-internal identifier of the proposal. + */ + val proposalId: String, + /** + * Order ID, uniquely identifies the order within a merchant instance. + */ + val orderId: String, + /** + * Base URL of the merchant. + */ + val merchantBaseUrl: String, + /** + * Amount that must be paid for the contract. + */ + val amount: String, + /** + * Summary of the proposal, given by the merchant. + */ + val summary: String +) diff --git a/app/src/main/java/net/taler/wallet/history/ReserveTransaction.kt b/app/src/main/java/net/taler/wallet/history/ReserveTransaction.kt index 880639f..f4cfcb8 100644 --- a/app/src/main/java/net/taler/wallet/history/ReserveTransaction.kt +++ b/app/src/main/java/net/taler/wallet/history/ReserveTransaction.kt @@ -1,3 +1,19 @@ +/* + This file is part of GNU Taler + (C) 2019 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.wallet.history import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt b/app/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt index 37dc742..14fc1e9 100644 --- a/app/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt +++ b/app/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt @@ -1,13 +1,31 @@ +/* + This file is part of GNU Taler + (C) 2019 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.wallet.history import android.text.format.DateUtils.* import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ImageView import android.widget.TextView import androidx.annotation.CallSuper import androidx.recyclerview.widget.RecyclerView.Adapter import androidx.recyclerview.widget.RecyclerView.ViewHolder +import net.taler.wallet.ParsedAmount.Companion.parseAmount import net.taler.wallet.R @@ -18,16 +36,14 @@ internal class WalletHistoryAdapter(private var history: History = History()) : setHasStableIds(false) } - override fun getItemViewType(position: Int): Int = when (history[position]) { - is HistoryWithdrawnEvent -> R.layout.history_withdrawn - else -> R.layout.history_row - } + override fun getItemViewType(position: Int): Int = history[position].layout override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryEventViewHolder { val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) return when (viewType) { - R.layout.history_withdrawn -> HistoryWithdrawnEventViewHolder(view) - else -> HistoryEventViewHolder(view) + R.layout.history_withdrawn -> HistoryWithdrawnViewHolder(view) + R.layout.history_payment_sent -> HistoryPaymentSentViewHolder(view) + else -> GenericHistoryEventViewHolder(view) } } @@ -45,14 +61,17 @@ internal class WalletHistoryAdapter(private var history: History = History()) : } -internal open class HistoryEventViewHolder(protected val v: View) : ViewHolder(v) { +internal abstract class HistoryEventViewHolder(protected val v: View) : ViewHolder(v) { - protected val title: TextView = v.findViewById(R.id.title) + private val icon: ImageView = v.findViewById(R.id.icon) + private val title: TextView = v.findViewById(R.id.title) private val time: TextView = v.findViewById(R.id.time) @CallSuper open fun bind(event: HistoryEvent) { - title.text = event::class.java.simpleName + icon.setImageResource(event.icon) + if (event.title == 0) title.text = event::class.java.simpleName + else title.setText(event.title) time.text = getRelativeTime(event.timestamp.ms) } @@ -61,22 +80,60 @@ internal open class HistoryEventViewHolder(protected val v: View) : ViewHolder(v return getRelativeTimeSpanString(timestamp, now, MINUTE_IN_MILLIS, FORMAT_ABBREV_RELATIVE) } - protected fun getString(resId: Int) = v.context.getString(resId) +} + +internal class GenericHistoryEventViewHolder(v: View) : HistoryEventViewHolder(v) { + + private val info: TextView = v.findViewById(R.id.info) + + override fun bind(event: HistoryEvent) { + super.bind(event) + info.text = when (event) { + is ExchangeAddedEvent -> event.exchangeBaseUrl + is ExchangeUpdatedEvent -> event.exchangeBaseUrl + is ReserveBalanceUpdatedEvent -> parseAmount(event.amountReserveBalance).toString() + is HistoryPaymentSentEvent -> event.orderShortInfo.summary + is HistoryOrderAcceptedEvent -> event.orderShortInfo.summary + is HistoryOrderRefusedEvent -> event.orderShortInfo.summary + is HistoryRefreshedEvent -> { + "${parseAmount(event.amountRefreshedRaw)} - ${parseAmount(event.amountRefreshedEffective)}" + } + else -> "" + } + } } -internal class HistoryWithdrawnEventViewHolder(v: View) : HistoryEventViewHolder(v) { +internal class HistoryWithdrawnViewHolder(v: View) : HistoryEventViewHolder(v) { - private val amountWithdrawnRaw: TextView = v.findViewById(R.id.amountWithdrawnRaw) - private val amountWithdrawnEffective: TextView = v.findViewById(R.id.amountWithdrawnEffective) + private val exchangeUrl: TextView = v.findViewById(R.id.exchangeUrl) + private val amountWithdrawn: TextView = v.findViewById(R.id.amountWithdrawn) + private val fee: TextView = v.findViewById(R.id.fee) override fun bind(event: HistoryEvent) { super.bind(event) event as HistoryWithdrawnEvent - title.text = getString(R.string.history_event_withdrawn) - amountWithdrawnRaw.text = event.amountWithdrawnRaw - amountWithdrawnEffective.text = event.amountWithdrawnEffective + exchangeUrl.text = event.exchangeBaseUrl + val parsedEffective = parseAmount(event.amountWithdrawnEffective) + val parsedRaw = parseAmount(event.amountWithdrawnRaw) + amountWithdrawn.text = parsedRaw.toString() + fee.text = (parsedRaw - parsedEffective).toString() + } + +} + +internal class HistoryPaymentSentViewHolder(v: View) : HistoryEventViewHolder(v) { + + private val summary: TextView = v.findViewById(R.id.summary) + private val amountPaidWithFees: TextView = v.findViewById(R.id.amountPaidWithFees) + + override fun bind(event: HistoryEvent) { + super.bind(event) + event as HistoryPaymentSentEvent + + summary.text = event.orderShortInfo.summary + amountPaidWithFees.text = parseAmount(event.amountPaidWithFees).toString() } } |