ConfigFragment.kt (8762B)
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.cashier.config 18 19 import android.os.Bundle 20 import android.text.method.LinkMovementMethod 21 import android.view.LayoutInflater 22 import android.view.View 23 import android.view.View.INVISIBLE 24 import android.view.View.VISIBLE 25 import android.view.ViewGroup 26 import android.view.inputmethod.InputMethodManager 27 import androidx.core.content.ContextCompat.getSystemService 28 import androidx.core.text.HtmlCompat 29 import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY 30 import androidx.fragment.app.Fragment 31 import androidx.fragment.app.activityViewModels 32 import androidx.lifecycle.Observer 33 import androidx.lifecycle.lifecycleScope 34 import androidx.navigation.fragment.findNavController 35 import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG 36 import com.google.android.material.snackbar.Snackbar 37 import kotlinx.coroutines.Dispatchers 38 import kotlinx.coroutines.launch 39 import kotlinx.coroutines.withContext 40 import net.taler.cashier.MainViewModel 41 import net.taler.cashier.R 42 import net.taler.cashier.databinding.FragmentConfigBinding 43 import net.taler.lib.android.exhaustive 44 import net.taler.lib.android.handleChallengeResponse 45 import net.taler.lib.android.showError 46 47 private const val URL_BANK_TEST = "https://bank.demo.taler.net" 48 private const val URL_BANK_TEST_REGISTER = "https://bank.demo.taler.net/webui/#/register" 49 50 class ConfigFragment : Fragment() { 51 52 private val viewModel: MainViewModel by activityViewModels() 53 private val configManager by lazy { viewModel.configManager } 54 55 private lateinit var ui: FragmentConfigBinding 56 57 override fun onCreateView( 58 inflater: LayoutInflater, 59 container: ViewGroup?, 60 savedInstanceState: Bundle?, 61 ): View { 62 ui = FragmentConfigBinding.inflate(inflater, container, false) 63 return ui.root 64 } 65 66 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 67 if (savedInstanceState == null) { 68 if (configManager.config.bankUrl.isBlank()) { 69 ui.urlView.editText!!.setText(URL_BANK_TEST) 70 } else { 71 ui.urlView.editText!!.setText(configManager.config.bankUrl) 72 } 73 ui.usernameView.editText!!.setText(configManager.config.username) 74 ui.passwordView.editText!!.setText(configManager.config.password) 75 } else { 76 ui.urlView.editText!!.setText(savedInstanceState.getCharSequence("urlView")) 77 ui.usernameView.editText!!.setText(savedInstanceState.getCharSequence("usernameView")) 78 ui.passwordView.editText!!.setText(savedInstanceState.getCharSequence("passwordView")) 79 } 80 ui.saveButton.setOnClickListener { 81 val config = Config( 82 bankUrl = ui.urlView.editText!!.text.toString().trim(), 83 username = ui.usernameView.editText!!.text.toString().trim(), 84 password = ui.passwordView.editText!!.text.toString().trim() 85 ) 86 if (checkConfig(config)) { 87 // show progress 88 ui.saveButton.visibility = INVISIBLE 89 ui.progressBar.visibility = VISIBLE 90 // kick off check and observe result 91 configManager.checkAndSaveConfig(config) 92 configManager.configResult.observe(viewLifecycleOwner, onConfigResult) 93 // hide keyboard 94 val inputMethodManager = 95 getSystemService(requireContext(), InputMethodManager::class.java)!! 96 inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) 97 } 98 } 99 ui.demoView.text = HtmlCompat.fromHtml( 100 getString(R.string.config_demo_hint, URL_BANK_TEST_REGISTER), FROM_HTML_MODE_LEGACY 101 ) 102 ui.demoView.movementMethod = LinkMovementMethod.getInstance() 103 } 104 105 override fun onStart() { 106 super.onStart() 107 // focus on password if it is the only missing value (like after locking) 108 if (ui.urlView.editText!!.text.isNotBlank() 109 && ui.usernameView.editText!!.text.isNotBlank() 110 && ui.passwordView.editText!!.text.isBlank() 111 ) { 112 ui.passwordView.editText!!.requestFocus() 113 } 114 } 115 116 override fun onSaveInstanceState(outState: Bundle) { 117 super.onSaveInstanceState(outState) 118 // for some reason automatic restore isn't working at the moment!? 119 outState.putCharSequence("urlView", ui.urlView.editText?.text?.trim()) 120 outState.putCharSequence("usernameView", ui.usernameView.editText?.text?.trim()) 121 outState.putCharSequence("passwordView", ui.passwordView.editText?.text?.trim()) 122 } 123 124 private fun checkConfig(config: Config): Boolean { 125 if (!config.bankUrl.startsWith("https://") && 126 !config.bankUrl.startsWith("http://") 127 ) { 128 ui.urlView.error = getString(R.string.config_bank_url_error) 129 ui.urlView.requestFocus() 130 return false 131 } 132 if (config.username.isBlank()) { 133 ui.usernameView.error = getString(R.string.config_username_error) 134 ui.usernameView.requestFocus() 135 return false 136 } 137 ui.urlView.isErrorEnabled = false 138 return true 139 } 140 141 private val onConfigResult = Observer<ConfigResult?> { result -> 142 if (result == null) return@Observer 143 when (result) { 144 is ConfigResult.Success -> { 145 val action = ConfigFragmentDirections.actionConfigFragmentToBalanceFragment() 146 findNavController().navigate(action) 147 } 148 ConfigResult.Offline -> { 149 Snackbar.make(requireView(), R.string.config_error_offline, LENGTH_LONG).show() 150 } 151 is ConfigResult.Error -> { 152 if (result.authError) { 153 Snackbar.make(requireView(), R.string.config_error_auth, LENGTH_LONG).show() 154 } else { 155 requireActivity().showError(getString(R.string.config_error), result.msg) 156 } 157 } 158 is ConfigResult.TanRequired -> { 159 lifecycleScope.launch { 160 val baseUrl = ui.urlView.editText!!.text.toString().trim() 161 val username = ui.usernameView.editText!!.text.toString().trim() 162 val solvedIds = handleChallengeResponse( 163 result.challenges, 164 result.combiAnd, 165 onRequestChallenge = { challengeId -> 166 withContext(Dispatchers.IO) { 167 configManager.requestChallenge(baseUrl, username, challengeId) 168 } 169 }, 170 onConfirmChallenge = { challengeId, tan -> 171 withContext(Dispatchers.IO) { 172 configManager.confirmChallenge(baseUrl, username, challengeId, tan) 173 } 174 } 175 ) 176 if (solvedIds.isNotEmpty()) { 177 val config = Config( 178 bankUrl = baseUrl, 179 username = username, 180 password = ui.passwordView.editText!!.text.toString().trim() 181 ) 182 configManager.checkAndSaveConfig(config, solvedIds) 183 } else { 184 ui.saveButton.visibility = VISIBLE 185 ui.progressBar.visibility = INVISIBLE 186 configManager.configResult.removeObservers(viewLifecycleOwner) 187 } 188 } 189 return@Observer 190 } 191 ConfigResult.Unknown -> { 192 requireActivity().showError(getString(R.string.config_error), "Unknown error") 193 } 194 }.exhaustive 195 ui.saveButton.visibility = VISIBLE 196 ui.progressBar.visibility = INVISIBLE 197 configManager.configResult.removeObservers(viewLifecycleOwner) 198 } 199 200 }