summaryrefslogtreecommitdiff
path: root/wallet/src/main/java/net/taler/wallet/history
diff options
context:
space:
mode:
authorTorsten Grote <t@grobox.de>2020-03-18 14:24:41 -0300
committerTorsten Grote <t@grobox.de>2020-03-18 14:24:41 -0300
commita4796ec47d89a851b260b6fc195494547208a025 (patch)
treed2941b68ff2ce22c523e7aa634965033b1100560 /wallet/src/main/java/net/taler/wallet/history
downloadtaler-android-a4796ec47d89a851b260b6fc195494547208a025.tar.gz
taler-android-a4796ec47d89a851b260b6fc195494547208a025.tar.bz2
taler-android-a4796ec47d89a851b260b6fc195494547208a025.zip
Merge all three apps into one repository
Diffstat (limited to 'wallet/src/main/java/net/taler/wallet/history')
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt452
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/HistoryManager.kt71
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt50
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt58
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt243
-rw-r--r--wallet/src/main/java/net/taler/wallet/history/WalletHistoryFragment.kt115
6 files changed, 989 insertions, 0 deletions
diff --git a/wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt b/wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt
new file mode 100644
index 0000000..9e5c99d
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/history/HistoryEvent.kt
@@ -0,0 +1,452 @@
+/*
+ * 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.wallet.history
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.LayoutRes
+import androidx.annotation.StringRes
+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.JsonProperty
+import com.fasterxml.jackson.annotation.JsonSubTypes
+import com.fasterxml.jackson.annotation.JsonSubTypes.Type
+import com.fasterxml.jackson.annotation.JsonTypeInfo
+import com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY
+import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME
+import com.fasterxml.jackson.annotation.JsonTypeName
+import net.taler.wallet.ParsedAmount.Companion.parseAmount
+import net.taler.wallet.R
+import org.json.JSONObject
+
+enum class ReserveType {
+ /**
+ * Manually created.
+ */
+ @JsonProperty("manual")
+ MANUAL,
+ /**
+ * Withdrawn from a bank that has "tight" Taler integration
+ */
+ @JsonProperty("taler-bank-withdraw")
+ @Suppress("unused")
+ TALER_BANK_WITHDRAW,
+}
+
+@JsonInclude(NON_EMPTY)
+class ReserveCreationDetail(val type: ReserveType, val bankUrl: String?)
+
+enum class RefreshReason {
+ @JsonProperty("manual")
+ @Suppress("unused")
+ MANUAL,
+ @JsonProperty("pay")
+ PAY,
+ @JsonProperty("refund")
+ @Suppress("unused")
+ REFUND,
+ @JsonProperty("abort-pay")
+ @Suppress("unused")
+ ABORT_PAY,
+ @JsonProperty("recoup")
+ @Suppress("unused")
+ RECOUP,
+ @JsonProperty("backup-restored")
+ @Suppress("unused")
+ BACKUP_RESTORED
+}
+
+
+@JsonInclude(NON_EMPTY)
+class Timestamp(
+ @JsonProperty("t_ms")
+ val ms: Long
+)
+
+@JsonInclude(NON_EMPTY)
+class ReserveShortInfo(
+ /**
+ * The exchange that the reserve will be at.
+ */
+ val exchangeBaseUrl: String,
+ /**
+ * Key to query more details
+ */
+ val reservePub: String,
+ /**
+ * Detail about how the reserve has been created.
+ */
+ val reserveCreationDetail: ReserveCreationDetail
+)
+
+typealias History = ArrayList<HistoryEvent>
+
+@JsonTypeInfo(
+ use = NAME,
+ include = PROPERTY,
+ property = "type",
+ defaultImpl = HistoryUnknownEvent::class
+)
+/** missing:
+AuditorComplaintSent = "auditor-complained-sent",
+AuditorComplaintProcessed = "auditor-complaint-processed",
+AuditorTrustAdded = "auditor-trust-added",
+AuditorTrustRemoved = "auditor-trust-removed",
+ExchangeTermsAccepted = "exchange-terms-accepted",
+ExchangePolicyChanged = "exchange-policy-changed",
+ExchangeTrustAdded = "exchange-trust-added",
+ExchangeTrustRemoved = "exchange-trust-removed",
+FundsDepositedToSelf = "funds-deposited-to-self",
+FundsRecouped = "funds-recouped",
+ReserveCreated = "reserve-created",
+ */
+@JsonSubTypes(
+ 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 = HistoryOrderAcceptedEvent::class, name = "order-accepted"),
+ Type(value = HistoryOrderRefusedEvent::class, name = "order-refused"),
+ Type(value = HistoryOrderRedirectedEvent::class, name = "order-redirected"),
+ Type(value = HistoryPaymentSentEvent::class, name = "payment-sent"),
+ Type(value = HistoryPaymentAbortedEvent::class, name = "payment-aborted"),
+ Type(value = HistoryTipAcceptedEvent::class, name = "tip-accepted"),
+ Type(value = HistoryTipDeclinedEvent::class, name = "tip-declined"),
+ Type(value = HistoryRefundedEvent::class, name = "refund"),
+ Type(value = HistoryRefreshedEvent::class, name = "refreshed")
+)
+@JsonIgnoreProperties(
+ value = [
+ "eventId"
+ ]
+)
+abstract class HistoryEvent(
+ 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,
+ open val showToUser: Boolean = false
+) {
+ open lateinit var json: JSONObject
+}
+
+
+class HistoryUnknownEvent(timestamp: Timestamp) : HistoryEvent(timestamp) {
+ override val title = R.string.history_event_unknown
+}
+
+@JsonTypeName("exchange-added")
+class ExchangeAddedEvent(
+ timestamp: Timestamp,
+ val exchangeBaseUrl: String,
+ val builtIn: Boolean
+) : HistoryEvent(timestamp) {
+ override val title = R.string.history_event_exchange_added
+}
+
+@JsonTypeName("exchange-updated")
+class ExchangeUpdatedEvent(
+ timestamp: Timestamp,
+ val exchangeBaseUrl: String
+) : HistoryEvent(timestamp) {
+ override val title = R.string.history_event_exchange_updated
+}
+
+
+@JsonTypeName("reserve-balance-updated")
+class ReserveBalanceUpdatedEvent(
+ timestamp: Timestamp,
+ val newHistoryTransactions: List<ReserveTransaction>,
+ /**
+ * Condensed information about the reserve.
+ */
+ val reserveShortInfo: ReserveShortInfo,
+ /**
+ * Amount currently left in the reserve.
+ */
+ val amountReserveBalance: String,
+ /**
+ * Amount we expected to be in the reserve at that time,
+ * considering ongoing withdrawals from that reserve.
+ */
+ val amountExpected: String
+) : HistoryEvent(timestamp) {
+ override val title = R.string.history_event_reserve_balance_updated
+}
+
+@JsonTypeName("withdrawn")
+class HistoryWithdrawnEvent(
+ timestamp: Timestamp,
+ /**
+ * Exchange that was withdrawn from.
+ */
+ val exchangeBaseUrl: String,
+ /**
+ * Unique identifier for the withdrawal session, can be used to
+ * query more detailed information from the wallet.
+ */
+ val withdrawSessionId: String,
+ val withdrawalSource: WithdrawalSource,
+ /**
+ * Amount that has been subtracted from the reserve's balance
+ * for this withdrawal.
+ */
+ val amountWithdrawnRaw: String,
+ /**
+ * Amount that actually was added to the wallet's balance.
+ */
+ val amountWithdrawnEffective: String
+) : HistoryEvent(timestamp) {
+ override val layout = R.layout.history_receive
+ override val title = R.string.history_event_withdrawn
+ override val icon = R.drawable.history_withdrawn
+ override val showToUser = true
+}
+
+@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
+ override val title = R.string.history_event_payment_sent
+ override val icon = R.drawable.ic_cash_usd_outline
+ override val showToUser = true
+}
+
+@JsonTypeName("payment-aborted")
+class HistoryPaymentAbortedEvent(
+ timestamp: Timestamp,
+ /**
+ * Condensed info about the order that we already paid for.
+ */
+ val orderShortInfo: OrderShortInfo,
+ /**
+ * Amount that was lost due to refund and refreshing fees.
+ */
+ val amountLost: String
+) : HistoryEvent(timestamp) {
+ override val layout = R.layout.history_payment
+ override val title = R.string.history_event_payment_aborted
+ override val icon = R.drawable.history_payment_aborted
+ override val showToUser = true
+}
+
+@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 layout = R.layout.history_payment
+ override val icon = R.drawable.history_refresh
+ override val title = R.string.history_event_refreshed
+ override val showToUser =
+ !(parseAmount(amountRefreshedRaw) - parseAmount(amountRefreshedEffective)).isZero()
+}
+
+@JsonTypeName("order-redirected")
+class HistoryOrderRedirectedEvent(
+ timestamp: Timestamp,
+ /**
+ * Condensed info about the new order that contains a
+ * product (identified by the fulfillment URL) that we've already paid for.
+ */
+ val newOrderShortInfo: OrderShortInfo,
+ /**
+ * Condensed info about the order that we already paid for.
+ */
+ val alreadyPaidOrderShortInfo: OrderShortInfo
+) : HistoryEvent(timestamp) {
+ override val icon = R.drawable.ic_directions
+ override val title = R.string.history_event_order_redirected
+}
+
+@JsonTypeName("tip-accepted")
+class HistoryTipAcceptedEvent(
+ timestamp: Timestamp,
+ /**
+ * Unique identifier for the tip to query more information.
+ */
+ val tipId: String,
+ /**
+ * Raw amount of the tip, without extra fees that apply.
+ */
+ val tipRaw: String
+) : HistoryEvent(timestamp) {
+ override val icon = R.drawable.history_tip_accepted
+ override val title = R.string.history_event_tip_accepted
+ override val layout = R.layout.history_receive
+ override val showToUser = true
+}
+
+@JsonTypeName("tip-declined")
+class HistoryTipDeclinedEvent(
+ timestamp: Timestamp,
+ /**
+ * Unique identifier for the tip to query more information.
+ */
+ val tipId: String,
+ /**
+ * Raw amount of the tip, without extra fees that apply.
+ */
+ val tipAmount: String
+) : HistoryEvent(timestamp) {
+ override val icon = R.drawable.history_tip_declined
+ override val title = R.string.history_event_tip_declined
+ override val layout = R.layout.history_receive
+ override val showToUser = true
+}
+
+@JsonTypeName("refund")
+class HistoryRefundedEvent(
+ timestamp: Timestamp,
+ val orderShortInfo: OrderShortInfo,
+ /**
+ * Unique identifier for this refund.
+ * (Identifies multiple refund permissions that were obtained at once.)
+ */
+ val refundGroupId: String,
+ /**
+ * Part of the refund that couldn't be applied because
+ * the refund permissions were expired.
+ */
+ val amountRefundedInvalid: String,
+ /**
+ * Amount that has been refunded by the merchant.
+ */
+ val amountRefundedRaw: String,
+ /**
+ * Amount will be added to the wallet's balance after fees and refreshing.
+ */
+ val amountRefundedEffective: String
+) : HistoryEvent(timestamp) {
+ override val icon = R.drawable.history_refund
+ override val title = R.string.history_event_refund
+ override val layout = R.layout.history_receive
+ override val showToUser = true
+}
+
+@JsonTypeInfo(
+ use = NAME,
+ include = PROPERTY,
+ property = "type"
+)
+@JsonSubTypes(
+ Type(value = WithdrawalSourceReserve::class, name = "reserve")
+)
+abstract class WithdrawalSource
+
+@Suppress("unused")
+@JsonTypeName("tip")
+class WithdrawalSourceTip(
+ val tipId: String
+) : WithdrawalSource()
+
+@JsonTypeName("reserve")
+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/wallet/src/main/java/net/taler/wallet/history/HistoryManager.kt b/wallet/src/main/java/net/taler/wallet/history/HistoryManager.kt
new file mode 100644
index 0000000..c350daa
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/history/HistoryManager.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.wallet.history
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.asLiveData
+import androidx.lifecycle.switchMap
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.onStart
+import net.taler.wallet.backend.WalletBackendApi
+
+@Suppress("EXPERIMENTAL_API_USAGE")
+class HistoryManager(
+ private val walletBackendApi: WalletBackendApi,
+ private val mapper: ObjectMapper
+) {
+
+ private val mProgress = MutableLiveData<Boolean>()
+ val progress: LiveData<Boolean> = mProgress
+
+ val showAll = MutableLiveData<Boolean>()
+
+ val history: LiveData<History> = showAll.switchMap { showAll ->
+ loadHistory(showAll)
+ .onStart { mProgress.postValue(true) }
+ .onCompletion { mProgress.postValue(false) }
+ .asLiveData(Dispatchers.IO)
+ }
+
+ private fun loadHistory(showAll: Boolean) = callbackFlow {
+ walletBackendApi.sendRequest("getHistory", null) { isError, result ->
+ if (isError) {
+ // TODO show error message in [WalletHistory] fragment
+ close()
+ return@sendRequest
+ }
+ val history = History()
+ val json = result.getJSONArray("history")
+ for (i in 0 until json.length()) {
+ val event: HistoryEvent = mapper.readValue(json.getString(i))
+ event.json = json.getJSONObject(i)
+ history.add(event)
+ }
+ history.reverse() // show latest first
+ offer(if (showAll) history else history.filter { it.showToUser } as History)
+ close()
+ }
+ awaitClose()
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt b/wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt
new file mode 100644
index 0000000..f51dba9
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/history/JsonDialogFragment.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.wallet.history
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.DialogFragment
+import kotlinx.android.synthetic.main.fragment_json.*
+import net.taler.wallet.R
+
+class JsonDialogFragment : DialogFragment() {
+
+ companion object {
+ fun new(json: String): JsonDialogFragment {
+ return JsonDialogFragment().apply {
+ arguments = Bundle().apply { putString("json", json) }
+ }
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_json, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val json = arguments!!.getString("json")
+ jsonView.text = json
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt b/wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt
new file mode 100644
index 0000000..45c539c
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/history/ReserveTransaction.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.wallet.history
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.annotation.JsonSubTypes
+import com.fasterxml.jackson.annotation.JsonTypeInfo
+import com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY
+import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME
+import com.fasterxml.jackson.annotation.JsonTypeName
+
+
+@JsonTypeInfo(
+ use = NAME,
+ include = PROPERTY,
+ property = "type"
+)
+@JsonSubTypes(
+ JsonSubTypes.Type(value = ReserveDepositTransaction::class, name = "DEPOSIT")
+)
+abstract class ReserveTransaction
+
+
+@JsonTypeName("DEPOSIT")
+class ReserveDepositTransaction(
+ /**
+ * Amount withdrawn.
+ */
+ val amount: String,
+ /**
+ * Sender account payto://-URL
+ */
+ @JsonProperty("sender_account_url")
+ val senderAccountUrl: String,
+ /**
+ * Transfer details uniquely identifying the transfer.
+ */
+ @JsonProperty("wire_reference")
+ val wireReference: String,
+ /**
+ * Timestamp of the incoming wire transfer.
+ */
+ val timestamp: Timestamp
+) : ReserveTransaction()
diff --git a/wallet/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt b/wallet/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt
new file mode 100644
index 0000000..71bdebc
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/history/WalletHistoryAdapter.kt
@@ -0,0 +1,243 @@
+/*
+ * 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.wallet.history
+
+import android.annotation.SuppressLint
+import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG
+import android.text.format.DateUtils.DAY_IN_MILLIS
+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.MINUTE_IN_MILLIS
+import android.text.format.DateUtils.formatDateTime
+import android.text.format.DateUtils.getRelativeTimeSpanString
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.CallSuper
+import androidx.core.net.toUri
+import androidx.recyclerview.widget.RecyclerView.Adapter
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import net.taler.wallet.BuildConfig
+import net.taler.wallet.ParsedAmount
+import net.taler.wallet.ParsedAmount.Companion.parseAmount
+import net.taler.wallet.R
+
+
+internal class WalletHistoryAdapter(
+ private val listener: OnEventClickListener,
+ private var history: History = History()
+) : Adapter<WalletHistoryAdapter.HistoryEventViewHolder>() {
+
+ init {
+ setHasStableIds(false)
+ }
+
+ 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_receive -> HistoryReceiveViewHolder(view)
+ R.layout.history_payment -> HistoryPaymentViewHolder(view)
+ else -> GenericHistoryEventViewHolder(view)
+ }
+ }
+
+ override fun getItemCount(): Int = history.size
+
+ override fun onBindViewHolder(holder: HistoryEventViewHolder, position: Int) {
+ val event = history[position]
+ holder.bind(event)
+ }
+
+ fun update(updatedHistory: History) {
+ this.history = updatedHistory
+ this.notifyDataSetChanged()
+ }
+
+ internal abstract inner class HistoryEventViewHolder(protected val v: View) : ViewHolder(v) {
+
+ private val icon: ImageView = v.findViewById(R.id.icon)
+ protected val title: TextView = v.findViewById(R.id.title)
+ private val time: TextView = v.findViewById(R.id.time)
+
+ @CallSuper
+ open fun bind(event: HistoryEvent) {
+ if (BuildConfig.DEBUG) { // doesn't produce recycling issues, no need to cover all cases
+ v.setOnClickListener { listener.onEventClicked(event) }
+ } else {
+ v.background = null
+ }
+ 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)
+ }
+
+ private fun getRelativeTime(timestamp: Long): CharSequence {
+ val now = System.currentTimeMillis()
+ return if (now - timestamp > DAY_IN_MILLIS * 2) {
+ formatDateTime(
+ v.context,
+ timestamp,
+ FORMAT_SHOW_TIME or FORMAT_SHOW_DATE or FORMAT_ABBREV_MONTH or FORMAT_NO_YEAR
+ )
+ } else {
+ getRelativeTimeSpanString(timestamp, now, MINUTE_IN_MILLIS, FORMAT_ABBREV_RELATIVE)
+ }
+ }
+
+ }
+
+ internal inner 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 HistoryOrderRedirectedEvent -> event.newOrderShortInfo.summary
+ else -> ""
+ }
+ }
+
+ }
+
+ internal inner class HistoryReceiveViewHolder(v: View) : HistoryEventViewHolder(v) {
+
+ private val summary: TextView = v.findViewById(R.id.summary)
+ private val amountWithdrawn: TextView = v.findViewById(R.id.amountWithdrawn)
+ private val feeLabel: TextView = v.findViewById(R.id.feeLabel)
+ private val fee: TextView = v.findViewById(R.id.fee)
+
+ override fun bind(event: HistoryEvent) {
+ super.bind(event)
+ when (event) {
+ is HistoryWithdrawnEvent -> bind(event)
+ is HistoryRefundedEvent -> bind(event)
+ is HistoryTipAcceptedEvent -> bind(event)
+ is HistoryTipDeclinedEvent -> bind(event)
+ }
+ }
+
+ private fun bind(event: HistoryWithdrawnEvent) {
+ title.text = getHostname(event.exchangeBaseUrl)
+ summary.setText(event.title)
+
+ val parsedEffective = parseAmount(event.amountWithdrawnEffective)
+ val parsedRaw = parseAmount(event.amountWithdrawnRaw)
+ showAmounts(parsedEffective, parsedRaw)
+ }
+
+ private fun bind(event: HistoryRefundedEvent) {
+ title.text = event.orderShortInfo.summary
+ summary.setText(event.title)
+
+ val parsedEffective = parseAmount(event.amountRefundedEffective)
+ val parsedRaw = parseAmount(event.amountRefundedRaw)
+ showAmounts(parsedEffective, parsedRaw)
+ }
+
+ private fun bind(event: HistoryTipAcceptedEvent) {
+ title.setText(event.title)
+ summary.text = null
+ val amount = parseAmount(event.tipRaw)
+ showAmounts(amount, amount)
+ }
+
+ private fun bind(event: HistoryTipDeclinedEvent) {
+ title.setText(event.title)
+ summary.text = null
+ val amount = parseAmount(event.tipAmount)
+ showAmounts(amount, amount)
+ amountWithdrawn.paintFlags = amountWithdrawn.paintFlags or STRIKE_THRU_TEXT_FLAG
+ }
+
+ private fun showAmounts(effective: ParsedAmount, raw: ParsedAmount) {
+ @SuppressLint("SetTextI18n")
+ amountWithdrawn.text = "+$raw"
+ val calculatedFee = raw - effective
+ if (calculatedFee.isZero()) {
+ fee.visibility = GONE
+ feeLabel.visibility = GONE
+ } else {
+ @SuppressLint("SetTextI18n")
+ fee.text = "-$calculatedFee"
+ fee.visibility = VISIBLE
+ feeLabel.visibility = VISIBLE
+ }
+ amountWithdrawn.paintFlags = fee.paintFlags
+ }
+
+ private fun getHostname(url: String): String {
+ return url.toUri().host!!
+ }
+
+ }
+
+ internal inner class HistoryPaymentViewHolder(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)
+ summary.setText(event.title)
+ when (event) {
+ is HistoryPaymentSentEvent -> bind(event)
+ is HistoryPaymentAbortedEvent -> bind(event)
+ is HistoryRefreshedEvent -> bind(event)
+ }
+ }
+
+ private fun bind(event: HistoryPaymentSentEvent) {
+ title.text = event.orderShortInfo.summary
+ @SuppressLint("SetTextI18n")
+ amountPaidWithFees.text = "-${parseAmount(event.amountPaidWithFees)}"
+ }
+
+ private fun bind(event: HistoryPaymentAbortedEvent) {
+ title.text = event.orderShortInfo.summary
+ @SuppressLint("SetTextI18n")
+ amountPaidWithFees.text = "-${parseAmount(event.amountLost)}"
+ }
+
+ private fun bind(event: HistoryRefreshedEvent) {
+ title.text = ""
+ val fee =
+ parseAmount(event.amountRefreshedRaw) - parseAmount(event.amountRefreshedEffective)
+ @SuppressLint("SetTextI18n")
+ if (fee.isZero()) amountPaidWithFees.text = null
+ else amountPaidWithFees.text = "-$fee"
+ }
+
+ }
+
+}
diff --git a/wallet/src/main/java/net/taler/wallet/history/WalletHistoryFragment.kt b/wallet/src/main/java/net/taler/wallet/history/WalletHistoryFragment.kt
new file mode 100644
index 0000000..4f8ab82
--- /dev/null
+++ b/wallet/src/main/java/net/taler/wallet/history/WalletHistoryFragment.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.wallet.history
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL
+import kotlinx.android.synthetic.main.fragment_show_balance.*
+import kotlinx.android.synthetic.main.fragment_show_history.*
+import net.taler.wallet.R
+import net.taler.wallet.WalletViewModel
+
+interface OnEventClickListener {
+ fun onEventClicked(event: HistoryEvent)
+}
+
+class WalletHistoryFragment : Fragment(), OnEventClickListener {
+
+ private val model: WalletViewModel by activityViewModels()
+ private val historyManager by lazy { model.historyManager }
+ private lateinit var showAllItem: MenuItem
+ private var reloadHistoryItem: MenuItem? = null
+ private val historyAdapter = WalletHistoryAdapter(this)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setHasOptionsMenu(true)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_show_history, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ historyList.apply {
+ layoutManager = LinearLayoutManager(context)
+ adapter = historyAdapter
+ addItemDecoration(DividerItemDecoration(context, VERTICAL))
+ }
+
+ model.devMode.observe(viewLifecycleOwner, Observer { enabled ->
+ reloadHistoryItem?.isVisible = enabled
+ })
+ historyManager.progress.observe(viewLifecycleOwner, Observer { show ->
+ historyProgressBar.visibility = if (show) VISIBLE else INVISIBLE
+ })
+ historyManager.history.observe(viewLifecycleOwner, Observer { history ->
+ historyEmptyState.visibility = if (history.isEmpty()) VISIBLE else INVISIBLE
+ historyAdapter.update(history)
+ })
+
+ // kicks off initial load, needs to be adapted if showAll state is ever saved
+ if (savedInstanceState == null) historyManager.showAll.value = model.devMode.value
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ inflater.inflate(R.menu.history, menu)
+ showAllItem = menu.findItem(R.id.show_all_history)
+ showAllItem.isChecked = historyManager.showAll.value == true
+ reloadHistoryItem = menu.findItem(R.id.reload_history).apply {
+ isVisible = model.devMode.value!!
+ }
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.show_all_history -> {
+ item.isChecked = !item.isChecked
+ historyManager.showAll.value = item.isChecked
+ true
+ }
+ R.id.reload_history -> {
+ historyManager.showAll.value = showAllItem.isChecked
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ override fun onEventClicked(event: HistoryEvent) {
+ if (model.devMode.value != true) return
+ JsonDialogFragment.new(event.json.toString(4))
+ .show(parentFragmentManager, null)
+ }
+
+}