summaryrefslogtreecommitdiff
path: root/app/src/main/java/net/taler/merchantpos
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/net/taler/merchantpos')
-rw-r--r--app/src/main/java/net/taler/merchantpos/MainViewModel.kt2
-rw-r--r--app/src/main/java/net/taler/merchantpos/MerchantHistory.kt190
-rw-r--r--app/src/main/java/net/taler/merchantpos/Utils.kt24
-rw-r--r--app/src/main/java/net/taler/merchantpos/history/HistoryManager.kt104
-rw-r--r--app/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt146
5 files changed, 276 insertions, 190 deletions
diff --git a/app/src/main/java/net/taler/merchantpos/MainViewModel.kt b/app/src/main/java/net/taler/merchantpos/MainViewModel.kt
index 1bb57b9..560ca59 100644
--- a/app/src/main/java/net/taler/merchantpos/MainViewModel.kt
+++ b/app/src/main/java/net/taler/merchantpos/MainViewModel.kt
@@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PRO
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import net.taler.merchantpos.config.ConfigManager
+import net.taler.merchantpos.history.HistoryManager
import net.taler.merchantpos.order.OrderManager
import net.taler.merchantpos.payment.PaymentManager
@@ -39,6 +40,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
addConfigurationReceiver(orderManager)
}
val paymentManager = PaymentManager(configManager, queue, mapper)
+ val historyManager = HistoryManager(configManager, queue, mapper)
override fun onCleared() {
queue.cancelAll { !it.isCanceled }
diff --git a/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt b/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt
deleted file mode 100644
index 997a1e6..0000000
--- a/app/src/main/java/net/taler/merchantpos/MerchantHistory.kt
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * 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.merchantpos
-
-import android.annotation.SuppressLint
-import android.os.Bundle
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.TextView
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.lifecycle.MutableLiveData
-import androidx.navigation.fragment.findNavController
-import androidx.recyclerview.widget.DividerItemDecoration
-import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView.Adapter
-import androidx.recyclerview.widget.RecyclerView.ViewHolder
-import com.android.volley.Request.Method.GET
-import com.android.volley.RequestQueue
-import com.android.volley.Response.ErrorListener
-import com.android.volley.Response.Listener
-import com.android.volley.toolbox.Volley
-import com.google.android.material.snackbar.Snackbar
-import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT
-import kotlinx.android.synthetic.main.fragment_merchant_history.*
-import net.taler.merchantpos.HistoryItemAdapter.HistoryItemViewHolder
-import net.taler.merchantpos.MerchantHistoryDirections.Companion.actionGlobalMerchantSettings
-import net.taler.merchantpos.config.MerchantRequest
-import org.json.JSONObject
-import java.time.Instant
-import java.time.ZoneId
-import java.time.format.DateTimeFormatter
-import java.time.format.FormatStyle.SHORT
-import java.util.*
-
-/**
- * Fragment to display the merchant's payment history,
- * received from the backend.
- */
-class MerchantHistory : Fragment() {
-
- companion object {
- const val TAG = "taler-merchant"
- }
-
- private lateinit var queue: RequestQueue
- private val model: MainViewModel by activityViewModels()
- private val historyListAdapter = HistoryItemAdapter(listOf())
-
- private val isLoading = MutableLiveData<Boolean>().apply { value = false }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- queue = Volley.newRequestQueue(context)
- }
-
- override fun onCreateView(
- inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- return inflater.inflate(R.layout.fragment_merchant_history, container, false)
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- list_history.apply {
- layoutManager = LinearLayoutManager(requireContext())
- addItemDecoration(DividerItemDecoration(context, VERTICAL))
- adapter = historyListAdapter
- }
-
- swipeRefresh.isRefreshing = false
- swipeRefresh.setOnRefreshListener {
- Log.v(TAG, "refreshing!")
- fetchHistory()
- }
-
- this.isLoading.observe(viewLifecycleOwner, androidx.lifecycle.Observer { loading ->
- Log.v(TAG, "setting refreshing to $loading")
- swipeRefresh.isRefreshing = loading
- })
- }
-
- override fun onStart() {
- super.onStart()
- if (model.configManager.merchantConfig?.instance == null) {
- actionGlobalMerchantSettings().navigate(findNavController())
- } else {
- fetchHistory()
- }
- }
-
- private fun fetchHistory() {
- isLoading.value = true
- val merchantConfig = model.configManager.merchantConfig!!
- val params = mapOf("instance" to merchantConfig.instance)
- val req = MerchantRequest(GET, merchantConfig, "history", params, null,
- Listener { onHistoryResponse(it) },
- ErrorListener { onNetworkError() })
- queue.add(req)
- }
-
- private fun onHistoryResponse(body: JSONObject) {
- this.isLoading.value = false
- Log.v(TAG, "got history response $body")
- // TODO use jackson instead of manual parsing
- val data = arrayListOf<HistoryItem>()
- val historyJson = body.getJSONArray("history")
- for (i in 0 until historyJson.length()) {
- val item = historyJson.getJSONObject(i)
- val orderId = item.getString("order_id")
- val summary = item.getString("summary")
- val timestampObj = item.getJSONObject("timestamp")
- val timestamp = Instant.ofEpochSecond(timestampObj.getLong("t_ms"))
- val amount = Amount.fromString(item.getString("amount"))
- data.add(HistoryItem(orderId, amount, summary, timestamp))
- }
- historyListAdapter.setData(data)
- }
-
- private fun onNetworkError() {
- this.isLoading.value = false
- Snackbar.make(view!!, R.string.error_network, LENGTH_SHORT).show()
- }
-
-}
-
-data class HistoryItem(
- val orderId: String,
- val amount: Amount,
- val summary: String,
- val timestamp: Instant
-)
-
-class HistoryItemAdapter(private var items: List<HistoryItem>) : Adapter<HistoryItemViewHolder>() {
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryItemViewHolder {
- val v = LayoutInflater.from(parent.context).inflate(R.layout.history_row, parent, false)
- return HistoryItemViewHolder(v)
- }
-
- override fun getItemCount() = items.size
-
- override fun onBindViewHolder(holder: HistoryItemViewHolder, position: Int) {
- holder.bind(items[position])
- }
-
- fun setData(items: List<HistoryItem>) {
- this.items = items
- this.notifyDataSetChanged()
- }
-
- class HistoryItemViewHolder(v: View) : ViewHolder(v) {
-
- private val summaryTextView: TextView = v.findViewById(R.id.text_history_summary)
- private val amountTextView: TextView = v.findViewById(R.id.text_history_amount)
- private val timestampTextView: TextView = v.findViewById(R.id.text_history_time)
- private val orderIdTextView: TextView = v.findViewById(R.id.text_history_order_id)
- private val formatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(SHORT)
- .withLocale(Locale.getDefault())
- .withZone(ZoneId.systemDefault())
-
- fun bind(item: HistoryItem) {
- summaryTextView.text = item.summary
- val amount = item.amount
- @SuppressLint("SetTextI18n")
- amountTextView.text = "${amount.amount} ${amount.currency}"
- timestampTextView.text = formatter.format(item.timestamp)
- orderIdTextView.text = item.orderId
- }
- }
-
-}
diff --git a/app/src/main/java/net/taler/merchantpos/Utils.kt b/app/src/main/java/net/taler/merchantpos/Utils.kt
index 2f6d4f8..507d142 100644
--- a/app/src/main/java/net/taler/merchantpos/Utils.kt
+++ b/app/src/main/java/net/taler/merchantpos/Utils.kt
@@ -16,6 +16,16 @@
package net.taler.merchantpos
+import android.content.Context
+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.View
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
@@ -97,6 +107,14 @@ fun topSnackbar(view: View, @StringRes resId: Int, @Duration duration: Int) {
fun NavDirections.navigate(nav: NavController) = nav.navigate(this)
+fun Long.toRelativeTime(context: Context): CharSequence {
+ val now = System.currentTimeMillis()
+ return if (now - this > DAY_IN_MILLIS * 2) {
+ val flags = FORMAT_SHOW_TIME or FORMAT_SHOW_DATE or FORMAT_ABBREV_MONTH or FORMAT_NO_YEAR
+ formatDateTime(context, this, flags)
+ } else getRelativeTimeSpanString(this, now, MINUTE_IN_MILLIS, FORMAT_ABBREV_RELATIVE)
+}
+
class CombinedLiveData<T, K, S>(
source1: LiveData<T>,
source2: LiveData<K>,
@@ -125,3 +143,9 @@ class CombinedLiveData<T, K, S>(
throw UnsupportedOperationException()
}
}
+
+/**
+ * Use this with 'when' expressions when you need it to handle all possibilities/branches.
+ */
+val <T> T.exhaustive: T
+ get() = this
diff --git a/app/src/main/java/net/taler/merchantpos/history/HistoryManager.kt b/app/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
new file mode 100644
index 0000000..1459876
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/history/HistoryManager.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.merchantpos.history
+
+import androidx.annotation.UiThread
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.android.volley.Request.Method.GET
+import com.android.volley.RequestQueue
+import com.android.volley.Response.ErrorListener
+import com.android.volley.Response.Listener
+import com.fasterxml.jackson.annotation.JsonIgnore
+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.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import net.taler.merchantpos.Amount
+import net.taler.merchantpos.config.ConfigManager
+import net.taler.merchantpos.config.MerchantRequest
+import org.json.JSONObject
+
+@JsonInclude(NON_EMPTY)
+class Timestamp(
+ @JsonProperty("t_ms")
+ val ms: Long
+)
+
+data class HistoryItem(
+ @JsonProperty("order_id")
+ val orderId: String,
+ @JsonProperty("amount")
+ val amountStr: String,
+ val summary: String,
+ val timestamp: Timestamp
+) {
+ @get:JsonIgnore
+ val amount: Amount by lazy { Amount.fromString(amountStr) }
+
+ @get:JsonIgnore
+ val time = timestamp.ms
+}
+
+sealed class HistoryResult {
+ object Error : HistoryResult()
+ class Success(val items: List<HistoryItem>) : HistoryResult()
+}
+
+class HistoryManager(
+ private val configManager: ConfigManager,
+ private val queue: RequestQueue,
+ private val mapper: ObjectMapper
+) {
+
+ private val mIsLoading = MutableLiveData(false)
+ val isLoading: LiveData<Boolean> = mIsLoading
+
+ private val mItems = MutableLiveData<HistoryResult>()
+ val items: LiveData<HistoryResult> = mItems
+
+ @UiThread
+ internal fun fetchHistory() {
+ mIsLoading.value = true
+ val merchantConfig = configManager.merchantConfig!!
+ val params = mapOf("instance" to merchantConfig.instance)
+ val req = MerchantRequest(GET, merchantConfig, "history", params, null,
+ Listener { onHistoryResponse(it) },
+ ErrorListener { onNetworkError() })
+ queue.add(req)
+ }
+
+ @UiThread
+ private fun onHistoryResponse(body: JSONObject) {
+ mIsLoading.value = false
+ val items = arrayListOf<HistoryItem>()
+ val historyJson = body.getJSONArray("history")
+ for (i in 0 until historyJson.length()) {
+ val historyItem: HistoryItem = mapper.readValue(historyJson.getString(i))
+ items.add(historyItem)
+ }
+ mItems.value = HistoryResult.Success(items)
+ }
+
+ @UiThread
+ private fun onNetworkError() {
+ mIsLoading.value = false
+ mItems.value = HistoryResult.Error
+ }
+
+}
diff --git a/app/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt b/app/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt
new file mode 100644
index 0000000..5299f28
--- /dev/null
+++ b/app/src/main/java/net/taler/merchantpos/history/MerchantHistoryFragment.kt
@@ -0,0 +1,146 @@
+/*
+ * 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.merchantpos.history
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView.Adapter
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT
+import kotlinx.android.synthetic.main.fragment_merchant_history.*
+import net.taler.merchantpos.MainViewModel
+import net.taler.merchantpos.R
+import net.taler.merchantpos.exhaustive
+import net.taler.merchantpos.history.HistoryItemAdapter.HistoryItemViewHolder
+import net.taler.merchantpos.history.MerchantHistoryFragmentDirections.Companion.actionGlobalMerchantSettings
+import net.taler.merchantpos.navigate
+import net.taler.merchantpos.toRelativeTime
+import java.util.*
+
+/**
+ * Fragment to display the merchant's payment history, received from the backend.
+ */
+class MerchantHistoryFragment : Fragment() {
+
+ companion object {
+ const val TAG = "taler-merchant"
+ }
+
+ private val model: MainViewModel by activityViewModels()
+ private val historyManager by lazy { model.historyManager }
+
+ private val historyListAdapter = HistoryItemAdapter()
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_merchant_history, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ list_history.apply {
+ layoutManager = LinearLayoutManager(requireContext())
+ addItemDecoration(DividerItemDecoration(context, VERTICAL))
+ adapter = historyListAdapter
+ }
+
+ swipeRefresh.setOnRefreshListener {
+ Log.v(TAG, "refreshing!")
+ historyManager.fetchHistory()
+ }
+ historyManager.isLoading.observe(viewLifecycleOwner, Observer { loading ->
+ Log.v(TAG, "setting refreshing to $loading")
+ swipeRefresh.isRefreshing = loading
+ })
+ historyManager.items.observe(viewLifecycleOwner, Observer { result ->
+ when (result) {
+ is HistoryResult.Error -> onError()
+ is HistoryResult.Success -> historyListAdapter.setData(result.items)
+ }.exhaustive
+ })
+ }
+
+ override fun onStart() {
+ super.onStart()
+ if (model.configManager.merchantConfig?.instance == null) {
+ actionGlobalMerchantSettings().navigate(findNavController())
+ } else {
+ historyManager.fetchHistory()
+ }
+ }
+
+ private fun onError() {
+ Snackbar.make(view!!, R.string.error_network, LENGTH_SHORT).show()
+ }
+
+}
+
+internal class HistoryItemAdapter : Adapter<HistoryItemViewHolder>() {
+
+ private val items = ArrayList<HistoryItem>()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryItemViewHolder {
+ val v =
+ LayoutInflater.from(parent.context).inflate(R.layout.list_item_history, parent, false)
+ return HistoryItemViewHolder(v)
+ }
+
+ override fun getItemCount() = items.size
+
+ override fun onBindViewHolder(holder: HistoryItemViewHolder, position: Int) {
+ holder.bind(items[position])
+ }
+
+ fun setData(items: List<HistoryItem>) {
+ this.items.clear()
+ this.items.addAll(items)
+ this.notifyDataSetChanged()
+ }
+
+ internal class HistoryItemViewHolder(private val v: View) : ViewHolder(v) {
+
+ private val orderSummaryView: TextView = v.findViewById(R.id.orderSummaryView)
+ private val orderAmountView: TextView = v.findViewById(R.id.orderAmountView)
+ private val orderTimeView: TextView = v.findViewById(R.id.orderTimeView)
+ private val orderIdView: TextView = v.findViewById(R.id.orderIdView)
+
+ fun bind(item: HistoryItem) {
+ orderSummaryView.text = item.summary
+ val amount = item.amount
+ @SuppressLint("SetTextI18n")
+ orderAmountView.text = "${amount.amount} ${amount.currency}"
+ orderIdView.text = v.context.getString(R.string.history_ref_no, item.orderId)
+ orderTimeView.text = item.time.toRelativeTime(v.context)
+ }
+
+ }
+
+}