taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

SettingsFragment.kt (13828B)


      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.settings
     18 
     19 import android.app.Activity.RESULT_OK
     20 import android.app.KeyguardManager
     21 import android.content.Context.KEYGUARD_SERVICE
     22 import android.content.Intent
     23 import android.os.Build
     24 import android.os.Bundle
     25 import android.provider.Settings.ACTION_BIOMETRIC_ENROLL
     26 import android.provider.Settings.ACTION_FINGERPRINT_ENROLL
     27 import android.provider.Settings.ACTION_SECURITY_SETTINGS
     28 import android.provider.Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED
     29 import android.view.View
     30 import android.widget.Toast
     31 import androidx.activity.result.contract.ActivityResultContracts
     32 import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
     33 import androidx.activity.result.contract.ActivityResultContracts.OpenDocument
     34 import androidx.biometric.BiometricManager
     35 import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
     36 import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
     37 import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
     38 import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS
     39 import androidx.fragment.app.activityViewModels
     40 import androidx.lifecycle.Lifecycle
     41 import androidx.lifecycle.lifecycleScope
     42 import androidx.lifecycle.repeatOnLifecycle
     43 import androidx.navigation.fragment.findNavController
     44 import androidx.preference.Preference
     45 import androidx.preference.PreferenceFragmentCompat
     46 import androidx.preference.SwitchPreference
     47 import com.google.android.material.dialog.MaterialAlertDialogBuilder
     48 import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG
     49 import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_SHORT
     50 import com.google.android.material.snackbar.Snackbar
     51 import kotlinx.coroutines.launch
     52 import net.taler.common.showError
     53 import net.taler.wallet.BuildConfig.FLAVOR
     54 import net.taler.wallet.BuildConfig.VERSION_CODE
     55 import net.taler.wallet.BuildConfig.VERSION_NAME
     56 import net.taler.wallet.main.MainViewModel
     57 import net.taler.wallet.R
     58 import net.taler.wallet.showError
     59 import net.taler.wallet.withdraw.TestWithdrawStatus
     60 import java.lang.System.currentTimeMillis
     61 
     62 
     63 class SettingsFragment : PreferenceFragmentCompat() {
     64 
     65     private val model: MainViewModel by activityViewModels()
     66     private val settingsManager get() = model.settingsManager
     67     private val withdrawManager by lazy { model.withdrawManager }
     68     private lateinit var biometricManager: BiometricManager
     69 
     70     private lateinit var prefDevMode: SwitchPreference
     71     private lateinit var prefBiometricLock: SwitchPreference
     72     private lateinit var prefWithdrawTest: Preference
     73     private lateinit var prefLogcat: Preference
     74     private lateinit var prefStats: Preference
     75     private lateinit var prefExportDb: Preference
     76     private lateinit var prefImportDb: Preference
     77     private lateinit var prefVersionApp: Preference
     78     private lateinit var prefVersionCore: Preference
     79     private lateinit var prefVersionExchange: Preference
     80     private lateinit var prefVersionMerchant: Preference
     81     private lateinit var prefTest: Preference
     82     private lateinit var prefReset: Preference
     83     private val devPrefs by lazy {
     84         listOf(
     85             prefVersionCore,
     86             prefWithdrawTest,
     87             prefLogcat,
     88             prefStats,
     89             prefExportDb,
     90             prefImportDb,
     91             prefVersionExchange,
     92             prefVersionMerchant,
     93             prefTest,
     94             prefReset,
     95         )
     96     }
     97 
     98     private val biometricEnrollLauncher = registerForActivityResult(
     99         ActivityResultContracts.StartActivityForResult(),
    100     ) { result ->
    101         if (result.resultCode == RESULT_OK) {
    102             enableBiometrics(false)
    103         }
    104     }
    105 
    106     private val logLauncher = registerForActivityResult(CreateDocument("text/plain")) { uri ->
    107         settingsManager.exportLogcat(uri)
    108     }
    109     private val dbExportLauncher =
    110         registerForActivityResult(CreateDocument("application/json")) { uri ->
    111             Snackbar.make(requireView(), getString(R.string.settings_db_export_message), LENGTH_LONG).show()
    112             settingsManager.exportDb(uri)
    113         }
    114     private val dbImportLauncher =
    115         registerForActivityResult(OpenDocument()) { uri ->
    116             Snackbar.make(requireView(), getString(R.string.settings_db_import_message), LENGTH_LONG).show()
    117             findNavController().navigate(R.id.action_global_main)
    118             settingsManager.importDb(uri)
    119         }
    120 
    121     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
    122         setPreferencesFromResource(R.xml.settings_main, rootKey)
    123         prefDevMode = findPreference("pref_dev_mode")!!
    124         prefBiometricLock = findPreference("pref_biometric_lock")!!
    125         prefWithdrawTest = findPreference("pref_testkudos")!!
    126         prefLogcat = findPreference("pref_logcat")!!
    127         prefStats = findPreference("pref_stats")!!
    128         prefExportDb = findPreference("pref_export_db")!!
    129         prefImportDb = findPreference("pref_import_db")!!
    130         prefVersionApp = findPreference("pref_version_app")!!
    131         prefVersionCore = findPreference("pref_version_core")!!
    132         prefVersionExchange = findPreference("pref_version_protocol_exchange")!!
    133         prefVersionMerchant = findPreference("pref_version_protocol_merchant")!!
    134         prefTest = findPreference("pref_test")!!
    135         prefReset = findPreference("pref_reset")!!
    136     }
    137 
    138     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    139         super.onViewCreated(view, savedInstanceState)
    140         biometricManager = BiometricManager.from(requireContext())
    141 
    142         prefVersionApp.summary = "$VERSION_NAME ($FLAVOR $VERSION_CODE)"
    143         prefVersionCore.summary = "${model.walletVersion} (${model.walletVersionHash?.take(7)})"
    144         model.exchangeVersion?.let { prefVersionExchange.summary = it }
    145         model.merchantVersion?.let { prefVersionMerchant.summary = it }
    146 
    147         viewLifecycleOwner.lifecycleScope.launch {
    148             repeatOnLifecycle(Lifecycle.State.STARTED) {
    149                 settingsManager.getBiometricLockEnabled(requireContext()).collect { enabled ->
    150                     prefBiometricLock.isChecked = enabled
    151                 }
    152             }
    153         }
    154 
    155         prefBiometricLock.setOnPreferenceChangeListener { _, newValue ->
    156             val enabled = newValue as Boolean
    157             if (enabled) {
    158                 return@setOnPreferenceChangeListener enableBiometrics(true)
    159             } else {
    160                 disableBiometrics()
    161                 true
    162             }
    163         }
    164 
    165         viewLifecycleOwner.lifecycleScope.launch {
    166             repeatOnLifecycle(Lifecycle.State.STARTED) {
    167                 settingsManager.getDevModeEnabled(requireContext()).collect { enabled ->
    168                     prefDevMode.isChecked = enabled
    169                     devPrefs.forEach { it.isVisible = enabled }
    170                 }
    171             }
    172         }
    173 
    174         prefDevMode.setOnPreferenceChangeListener { _, newValue ->
    175             settingsManager.setDevModeEnabled(requireContext(), newValue as Boolean)
    176             true
    177         }
    178 
    179         viewLifecycleOwner.lifecycleScope.launch {
    180             repeatOnLifecycle(Lifecycle.State.STARTED) {
    181                 withdrawManager.withdrawTestStatus.collect { status ->
    182                     if (status is TestWithdrawStatus.None) return@collect
    183                     val loading = status is TestWithdrawStatus.Withdrawing
    184                     prefWithdrawTest.isEnabled = !loading
    185                     model.showProgressBar.value = loading
    186                     if (status is TestWithdrawStatus.Error) {
    187                         requireActivity().showError(R.string.withdraw_error_test, status.message)
    188                     }
    189                     withdrawManager.resetTestWithdrawal()
    190                 }
    191             }
    192         }
    193 
    194         prefWithdrawTest.setOnPreferenceClickListener {
    195             withdrawManager.withdrawTestBalance()
    196             Snackbar.make(requireView(), getString(R.string.settings_test_withdrawal), LENGTH_LONG).show()
    197             findNavController().navigate(R.id.action_global_main)
    198             true
    199         }
    200 
    201         prefLogcat.setOnPreferenceClickListener {
    202             logLauncher.launch("taler-wallet-log-${currentTimeMillis()}.txt")
    203             true
    204         }
    205         prefStats.setOnPreferenceClickListener {
    206             findNavController().navigate(R.id.action_main_to_performanceStats)
    207             true
    208         }
    209         prefExportDb.setOnPreferenceClickListener {
    210             dbExportLauncher.launch("taler-wallet-db-${currentTimeMillis()}.json")
    211             true
    212         }
    213         prefImportDb.setOnPreferenceClickListener {
    214             showImportDialog()
    215             true
    216         }
    217         prefTest.setOnPreferenceClickListener {
    218             settingsManager.runIntegrationTest { error ->
    219                 requireActivity().showError(error)
    220             }
    221             Snackbar.make(requireView(), getString(R.string.settings_test_running), LENGTH_LONG).show()
    222             findNavController().navigate(R.id.action_global_main)
    223             true
    224         }
    225         prefReset.setOnPreferenceClickListener {
    226             showResetDialog()
    227             true
    228         }
    229     }
    230 
    231     override fun onStart() {
    232         super.onStart()
    233         requireActivity().title = getString(R.string.menu_settings)
    234     }
    235 
    236     private fun enableBiometrics(prompt: Boolean): Boolean {
    237         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    238             when (biometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)) {
    239                 BIOMETRIC_SUCCESS -> {
    240                     settingsManager.setBiometricLockEnabled(requireContext(), true)
    241                     return true
    242                 }
    243 
    244                 BIOMETRIC_ERROR_NONE_ENROLLED -> {
    245                     Toast.makeText(
    246                         requireContext(),
    247                         getString(R.string.biometric_auth_unavailable),
    248                         Toast.LENGTH_SHORT,
    249                     ).show()
    250 
    251                     if (prompt) {
    252                         promptAuthEnrollment()
    253                     }
    254                 }
    255 
    256                 else -> Toast.makeText(
    257                     requireContext(),
    258                     getString(R.string.biometric_auth_unavailable),
    259                     Toast.LENGTH_SHORT,
    260                 ).show()
    261             }
    262         } else {
    263             val keyguardManager = requireContext()
    264                 .getSystemService(KEYGUARD_SERVICE) as KeyguardManager
    265             if (keyguardManager.isDeviceSecure) {
    266                 settingsManager.setBiometricLockEnabled(requireContext(), true)
    267                 return true
    268             } else {
    269                 Toast.makeText(
    270                     requireContext(),
    271                     getString(R.string.biometric_auth_unavailable),
    272                     Toast.LENGTH_SHORT,
    273                 ).show()
    274 
    275                 if (prompt) {
    276                 promptAuthEnrollment()
    277                 }
    278             }
    279         }
    280 
    281         return false
    282     }
    283 
    284     /**
    285      * Prompt the user to enroll valid credentials
    286      */
    287     private fun promptAuthEnrollment() {
    288         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    289             val intent = Intent(ACTION_BIOMETRIC_ENROLL).apply {
    290                 putExtra(
    291                     EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
    292                     BIOMETRIC_STRONG or DEVICE_CREDENTIAL
    293                 )
    294             }
    295             biometricEnrollLauncher.launch(intent)
    296         } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    297             val intent = Intent(ACTION_FINGERPRINT_ENROLL)
    298             biometricEnrollLauncher.launch(intent)
    299         } else {
    300             val intent = Intent(ACTION_SECURITY_SETTINGS)
    301             biometricEnrollLauncher.launch(intent)
    302         }
    303     }
    304 
    305     private fun disableBiometrics() {
    306         settingsManager.setBiometricLockEnabled(requireContext(), false)
    307     }
    308 
    309     private fun showImportDialog() {
    310         MaterialAlertDialogBuilder(requireContext(), R.style.MaterialAlertDialog_Material3)
    311             .setMessage(R.string.settings_dialog_import_message)
    312             .setNegativeButton(R.string.import_db) { _, _ ->
    313                 dbImportLauncher.launch(arrayOf("application/json"))
    314             }
    315             .setPositiveButton(R.string.cancel) { _, _ ->
    316                 Snackbar.make(requireView(), getString(R.string.settings_alert_import_canceled), LENGTH_SHORT).show()
    317             }
    318             .show()
    319     }
    320 
    321     private fun showResetDialog() {
    322         MaterialAlertDialogBuilder(requireContext(), R.style.MaterialAlertDialog_Material3)
    323             .setMessage(R.string.settings_dialog_reset_message)
    324             .setNegativeButton(R.string.reset) { _, _ ->
    325                 settingsManager.clearDb {
    326                     model.dangerouslyReset()
    327                 }
    328                 Snackbar.make(requireView(), getString(R.string.settings_alert_reset_done), LENGTH_SHORT).show()
    329             }
    330             .setPositiveButton(R.string.cancel) { _, _ ->
    331                 Snackbar.make(requireView(), getString(R.string.settings_alert_reset_canceled), LENGTH_SHORT).show()
    332             }
    333             .show()
    334     }
    335 }