diff options
Diffstat (limited to 'app/src/main/java/net/taler/merchantpos')
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) + } + + } + +} |