ExchangeListFragment.kt (10612B)
1 /* 2 * This file is part of GNU Taler 3 * (C) 2020 Taler Systems S.A. 4 * 5 * GNU Taler is free software; you can redistribute it and/or modify it under the 6 * terms of the GNU General Public License as published by the Free Software 7 * Foundation; either version 3, or (at your option) any later version. 8 * 9 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY 10 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along with 14 * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 package net.taler.wallet.exchanges 18 19 import android.os.Bundle 20 import android.view.LayoutInflater 21 import android.view.Menu 22 import android.view.MenuInflater 23 import android.view.MenuItem 24 import android.view.View 25 import android.view.ViewGroup 26 import android.view.ViewGroup.MarginLayoutParams 27 import android.widget.Toast 28 import android.widget.Toast.LENGTH_LONG 29 import androidx.core.os.bundleOf 30 import androidx.core.view.MenuProvider 31 import androidx.core.view.ViewCompat 32 import androidx.core.view.WindowInsetsCompat 33 import androidx.core.view.marginBottom 34 import androidx.core.view.marginLeft 35 import androidx.core.view.marginRight 36 import androidx.core.view.updateLayoutParams 37 import androidx.core.view.updatePadding 38 import androidx.fragment.app.Fragment 39 import androidx.fragment.app.activityViewModels 40 import androidx.lifecycle.Lifecycle.State.RESUMED 41 import androidx.lifecycle.lifecycleScope 42 import androidx.navigation.fragment.findNavController 43 import androidx.recyclerview.widget.DividerItemDecoration 44 import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL 45 import com.google.android.material.dialog.MaterialAlertDialogBuilder 46 import com.google.android.material.snackbar.Snackbar 47 import kotlinx.coroutines.launch 48 import net.taler.common.Amount 49 import net.taler.common.EventObserver 50 import net.taler.common.fadeIn 51 import net.taler.common.fadeOut 52 import net.taler.common.showError 53 import net.taler.wallet.main.MainViewModel 54 import net.taler.wallet.R 55 import net.taler.wallet.databinding.FragmentExchangeListBinding 56 import net.taler.wallet.showError 57 58 open class ExchangeListFragment : Fragment(), ExchangeClickListener { 59 60 protected val model: MainViewModel by activityViewModels() 61 private val exchangeManager by lazy { model.exchangeManager } 62 private val transactionManager by lazy { model.transactionManager } 63 private val balanceManager by lazy { model.balanceManager } 64 65 protected lateinit var ui: FragmentExchangeListBinding 66 protected open val isSelectOnly = false 67 private val exchangeAdapter by lazy { ExchangeAdapter(isSelectOnly, this, model.devMode.value == true) } 68 69 override fun onCreateView( 70 inflater: LayoutInflater, 71 container: ViewGroup?, 72 savedInstanceState: Bundle?, 73 ): View? { 74 ui = FragmentExchangeListBinding.inflate(inflater, container, false) 75 return ui.root 76 } 77 78 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 79 setupInsets() 80 81 requireActivity().addMenuProvider(object : MenuProvider { 82 override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { 83 if (model.devMode.value == true) { 84 menuInflater.inflate(R.menu.exchange_list, menu) 85 } 86 } 87 88 override fun onMenuItemSelected(menuItem: MenuItem): Boolean { 89 if (menuItem.itemId == R.id.action_add_dev_exchanges) { 90 exchangeManager.addDevExchanges() 91 } 92 return true 93 } 94 }, viewLifecycleOwner, RESUMED) 95 96 ui.list.apply { 97 adapter = exchangeAdapter 98 addItemDecoration(DividerItemDecoration(context, VERTICAL)) 99 } 100 ui.addExchangeFab.setOnClickListener { 101 AddExchangeDialogFragment().show(parentFragmentManager, "ADD_EXCHANGE") 102 } 103 104 // TODO: refactor and unify progress bar handling 105 // exchangeManager.progress.observe(viewLifecycleOwner) { show -> 106 // if (show) ui.progressBar.fadeIn() else ui.progressBar.fadeOut() 107 // } 108 109 exchangeManager.exchanges.observe(viewLifecycleOwner) { exchanges -> 110 onExchangeUpdate(exchanges) 111 } 112 113 exchangeManager.addError.observe(viewLifecycleOwner, EventObserver { error -> 114 onAddExchangeFailed() 115 if (model.devMode.value == true) { 116 showError(error) 117 } 118 }) 119 120 exchangeManager.listError.observe(viewLifecycleOwner, EventObserver { error -> 121 onListExchangeFailed() 122 if (model.devMode.value == true) { 123 showError(error) 124 } 125 }) 126 127 exchangeManager.deleteError.observe(viewLifecycleOwner, EventObserver { error -> 128 if (model.devMode.value == true) { 129 showError(error) 130 } else { 131 showError(error.userFacingMsg) 132 } 133 }) 134 135 exchangeManager.reloadError.observe(viewLifecycleOwner, EventObserver { error -> 136 if (model.devMode.value == true) { 137 showError(error) 138 } else { 139 showError(error.userFacingMsg) 140 } 141 }) 142 } 143 144 protected open fun onExchangeUpdate(exchanges: List<ExchangeItem>) { 145 exchangeAdapter.update(exchanges) 146 if (exchanges.isEmpty()) { 147 ui.emptyState.fadeIn() 148 ui.list.fadeOut() 149 } else { 150 ui.emptyState.fadeOut() 151 ui.list.fadeIn() 152 } 153 } 154 155 private fun onAddExchangeFailed() { 156 Toast.makeText(requireContext(), R.string.exchange_add_error, LENGTH_LONG).show() 157 } 158 159 private fun onListExchangeFailed() { 160 Toast.makeText(requireContext(), R.string.exchange_list_error, LENGTH_LONG).show() 161 } 162 163 override fun onExchangeSelected(item: ExchangeItem) { 164 throw AssertionError("must not get triggered here") 165 } 166 167 override fun onManualWithdraw(item: ExchangeItem) { 168 model.withdrawManager.resetWithdrawal() 169 val args = bundleOf( 170 "editableCurrency" to false, 171 "exchangeBaseUrl" to item.exchangeBaseUrl, 172 "amount" to item.currency?.let { Amount.zero(it).toJSONString() }, 173 ) 174 findNavController().navigate(R.id.action_global_promptWithdraw, args) 175 } 176 177 override fun onPeerReceive(item: ExchangeItem) { 178 model.selectScope(item.scopeInfo) 179 findNavController().navigate(R.id.action_global_outgoingPull) 180 } 181 182 override fun onExchangeReload(item: ExchangeItem) { 183 exchangeManager.reload(item.exchangeBaseUrl) 184 } 185 186 override fun onExchangeDelete(item: ExchangeItem) { 187 val optionsArray = arrayOf(getString(R.string.exchange_delete_force)) 188 val checkedArray = BooleanArray(1) { false } 189 190 MaterialAlertDialogBuilder(requireContext(), R.style.MaterialAlertDialog_Material3) 191 .setTitle(R.string.exchange_delete) 192 .setMultiChoiceItems(optionsArray, checkedArray) { _, which, isChecked -> 193 checkedArray[which] = isChecked 194 } 195 .setNegativeButton(R.string.transactions_delete) { _, _ -> 196 exchangeManager.delete(item.exchangeBaseUrl, checkedArray[0]) 197 } 198 .setPositiveButton(R.string.cancel) { _, _ -> } 199 .show() 200 } 201 202 override fun onExchangeTosView(item: ExchangeItem) { 203 val bundle = bundleOf( 204 "exchangeBaseUrl" to item.exchangeBaseUrl, 205 "readOnly" to true, 206 ) 207 findNavController().navigate(R.id.action_global_reviewExchangeTOS, bundle) 208 } 209 210 override fun onExchangeTosAccept(item: ExchangeItem) { 211 val bundle = bundleOf("exchangeBaseUrl" to item.exchangeBaseUrl) 212 findNavController().navigate(R.id.action_global_reviewExchangeTOS, bundle) 213 } 214 215 override fun onExchangeTosForget(item: ExchangeItem) { 216 viewLifecycleOwner.lifecycleScope.launch { 217 exchangeManager.getExchangeTos(item.exchangeBaseUrl)?.let { tos -> 218 exchangeManager.forgetCurrentTos(item.exchangeBaseUrl, tos.currentEtag) 219 } 220 } 221 } 222 223 override fun onExchangeGlobalCurrencyAdd(item: ExchangeItem) { 224 item.currency?.let { 225 balanceManager.addGlobalCurrencyExchange( 226 currency = item.currency, 227 exchange = item, 228 onSuccess = { 229 Snackbar.make( 230 requireView(), 231 getString(R.string.exchange_global_add_success), 232 Snackbar.LENGTH_LONG 233 ).show() 234 }, 235 onError = { error -> 236 showError(error) 237 }, 238 ) 239 } 240 } 241 242 override fun onExchangeGlobalCurrencyDelete(item: ExchangeItem) { 243 item.currency?.let { 244 balanceManager.removeGlobalCurrencyExchange( 245 currency = item.currency, 246 exchange = item, 247 onSuccess = { 248 Snackbar.make( 249 requireView(), 250 getString(R.string.exchange_global_delete_success), 251 Snackbar.LENGTH_LONG 252 ).show() 253 }, 254 onError = { error -> 255 showError(error) 256 }, 257 ) 258 } 259 } 260 261 private fun setupInsets() { 262 ViewCompat.setOnApplyWindowInsetsListener(ui.list) { v, windowInsets -> 263 val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) 264 v.updatePadding( 265 bottom = insets.bottom, 266 left = insets.left, 267 right = insets.right, 268 ) 269 windowInsets 270 } 271 272 val fabMarginBottom = ui.addExchangeFab.marginBottom 273 val fabMarginLeft = ui.addExchangeFab.marginLeft 274 val fabMarginRight = ui.addExchangeFab.marginRight 275 ViewCompat.setOnApplyWindowInsetsListener(ui.addExchangeFab) { v, windowInsets -> 276 val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) 277 v.updateLayoutParams<MarginLayoutParams> { 278 bottomMargin = fabMarginBottom + insets.bottom 279 leftMargin = fabMarginLeft + insets.left 280 rightMargin = fabMarginRight + insets.right 281 } 282 windowInsets 283 } 284 } 285 }