diff options
author | Torsten Grote <t@grobox.de> | 2020-03-18 14:24:41 -0300 |
---|---|---|
committer | Torsten Grote <t@grobox.de> | 2020-03-18 14:24:41 -0300 |
commit | a4796ec47d89a851b260b6fc195494547208a025 (patch) | |
tree | d2941b68ff2ce22c523e7aa634965033b1100560 /wallet/src/main/java/net/taler/wallet/history | |
download | taler-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')
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) + } + +} |