/* * 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 */ package net.taler.wallet.transactions import android.os.Bundle import android.util.Log import android.view.ActionMode 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.ViewGroup import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView.OnQueryTextListener import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.recyclerview.selection.SelectionPredicates import androidx.recyclerview.selection.SelectionTracker import androidx.recyclerview.selection.StorageStrategy import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL import com.google.android.material.dialog.MaterialAlertDialogBuilder import net.taler.common.fadeIn import net.taler.common.fadeOut import net.taler.common.showError import net.taler.wallet.MainViewModel import net.taler.wallet.R import net.taler.wallet.TAG import net.taler.wallet.balances.BalanceState.Success import net.taler.wallet.databinding.FragmentTransactionsBinding import net.taler.wallet.showError interface OnTransactionClickListener { fun onTransactionClicked(transaction: Transaction) } class TransactionsFragment : Fragment(), OnTransactionClickListener, ActionMode.Callback { private val model: MainViewModel by activityViewModels() private val transactionManager by lazy { model.transactionManager } private val balanceManager by lazy { model.balanceManager } private lateinit var ui: FragmentTransactionsBinding private val transactionAdapter by lazy { TransactionAdapter(this) } private val scopeInfo by lazy { transactionManager.selectedScope!! } private var tracker: SelectionTracker? = null private var actionMode: ActionMode? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View { ui = FragmentTransactionsBinding.inflate(inflater, container, false) return ui.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { ui.list.apply { adapter = transactionAdapter addItemDecoration(DividerItemDecoration(context, VERTICAL)) } val tracker = SelectionTracker.Builder( "transaction-selection-id", ui.list, transactionAdapter.keyProvider, TransactionLookup(ui.list, transactionAdapter), StorageStrategy.createStringStorage() ).withSelectionPredicate( SelectionPredicates.createSelectAnything() ).build() savedInstanceState?.let { tracker.onRestoreInstanceState(it) } transactionAdapter.tracker = tracker this.tracker = tracker tracker.addObserver(object : SelectionTracker.SelectionObserver() { override fun onItemStateChanged(key: String, selected: Boolean) { if (selected && actionMode == null) { actionMode = requireActivity().startActionMode(this@TransactionsFragment) updateActionModeTitle() } else if (actionMode != null) { if (selected || tracker.hasSelection()) { updateActionModeTitle() } else { actionMode!!.finish() } } } }) balanceManager.state.observe(viewLifecycleOwner) { state -> if (state !is Success) return@observe val balances = state.balances // hide extra fab when in single currency mode (uses MainFragment's FAB) if (balances.size == 1) ui.mainFab.visibility = INVISIBLE balances.find { it.scopeInfo == scopeInfo }?.let { balance -> ui.amount.text = balance.available.toString(showSymbol = false) transactionAdapter.setCurrencySpec(balance.available.spec) } } transactionManager.progress.observe(viewLifecycleOwner) { show -> if (show) ui.progressBar.fadeIn() else ui.progressBar.fadeOut() } transactionManager.transactions.observe(viewLifecycleOwner) { result -> onTransactionsResult(result) } ui.sendButton.setOnClickListener { findNavController().navigate(R.id.sendFunds) } ui.receiveButton.setOnClickListener { findNavController().navigate(R.id.action_global_receiveFunds) } ui.mainFab.setOnClickListener { model.scanCode() } ui.mainFab.setOnLongClickListener { findNavController().navigate(R.id.action_nav_transactions_to_nav_uri_input) true } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) tracker?.onSaveInstanceState(outState) } @Deprecated("Deprecated in Java") override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { inflater.inflate(R.menu.transactions, menu) setupSearch(menu.findItem(R.id.action_search)) } override fun onStart() { super.onStart() requireActivity().title = getString(R.string.transactions_detail_title_currency, scopeInfo.currency) } private fun setupSearch(item: MenuItem) { item.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { override fun onMenuItemActionExpand(item: MenuItem) = true override fun onMenuItemActionCollapse(item: MenuItem): Boolean { onSearchClosed() return true } }) val searchView = item.actionView as SearchView searchView.setOnQueryTextListener(object : OnQueryTextListener { override fun onQueryTextChange(newText: String) = false override fun onQueryTextSubmit(query: String): Boolean { // workaround to avoid issues with some emulators and keyboard devices // firing twice if a keyboard enter is used // see https://code.google.com/p/android/issues/detail?id=24599 searchView.clearFocus() onSearch(query) return true } }) } override fun onTransactionClicked(transaction: Transaction) { if (actionMode != null) return // don't react on clicks while in action mode if (transaction.detailPageNav != 0) { transactionManager.selectTransaction(transaction) findNavController().navigate(transaction.detailPageNav) } } private fun onTransactionsResult(result: TransactionsResult) = when (result) { is TransactionsResult.Error -> { ui.list.fadeOut() ui.emptyState.text = getString(R.string.transactions_error, result.error.userFacingMsg) ui.emptyState.fadeIn() } is TransactionsResult.Success -> { if (result.transactions.isEmpty()) { val isSearch = transactionManager.searchQuery.value != null ui.emptyState.setText(if (isSearch) R.string.transactions_empty_search else R.string.transactions_empty) ui.emptyState.fadeIn() ui.list.fadeOut() } else { ui.emptyState.fadeOut() transactionAdapter.update(result.transactions) ui.list.fadeIn() } } } private fun onSearch(query: String) { ui.list.fadeOut() ui.progressBar.fadeIn() transactionManager.searchQuery.value = query } private fun onSearchClosed() { transactionManager.searchQuery.value = null } override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { val inflater = mode.menuInflater inflater.inflate(R.menu.transactions_action_mode, menu) return true } override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { return false // no update needed } override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { when (item.itemId) { R.id.transaction_delete -> { tracker?.selection?.toList()?.let { transactionIds -> MaterialAlertDialogBuilder( requireContext(), R.style.MaterialAlertDialog_Material3, ) .setTitle(R.string.transactions_delete) .setMessage(R.string.transactions_delete_selected_dialog_message) .setNeutralButton(R.string.cancel) { dialog, _ -> dialog.cancel() } .setNegativeButton(R.string.transactions_delete) { dialog, _ -> transactionManager.deleteTransactions(transactionIds) { Log.e(TAG, "Error deleteTransaction $it") if (model.devMode.value == true) { showError(it) } else { showError(it.userFacingMsg) } } dialog.dismiss() } .show() } mode.finish() } R.id.transaction_select_all -> transactionAdapter.selectAll() } return true } override fun onDestroyActionMode(mode: ActionMode) { tracker?.clearSelection() actionMode = null } private fun updateActionModeTitle() { tracker?.selection?.size()?.toString()?.let { num -> actionMode?.title = num } } }