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 }