diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt index 2e4edb843c..b9042a5902 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.DialogInterface import android.content.Intent import android.os.Build +import android.os.Bundle import android.provider.Settings import android.text.SpannableStringBuilder import android.text.Spanned @@ -13,7 +14,11 @@ import android.view.View import android.view.WindowManager import android.widget.TextView import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher import androidx.annotation.StringRes +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricPrompt +import androidx.biometric.BiometricPrompt.PromptInfo import androidx.core.content.ContextCompat import androidx.lifecycle.ViewModelProvider import androidx.navigation.Navigation @@ -25,6 +30,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import mobi.upod.timedurationpicker.TimeDurationPicker import mobi.upod.timedurationpicker.TimeDurationPickerDialog import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.BiometricDeviceAuthentication +import org.thoughtcrime.securesms.BiometricDeviceLockContract import org.thoughtcrime.securesms.PassphraseChangeActivity import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.ClickPreference @@ -59,6 +66,8 @@ private val TAG = Log.tag(PrivacySettingsFragment::class.java) class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privacy) { private lateinit var viewModel: PrivacySettingsViewModel + private lateinit var biometricAuth: BiometricDeviceAuthentication + private lateinit var biometricDeviceLockLauncher: ActivityResultLauncher private val incognitoSummary: CharSequence by lazy { SpannableStringBuilder(getString(R.string.preferences__this_setting_is_not_a_guarantee)) @@ -70,11 +79,35 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac ) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + biometricDeviceLockLauncher = registerForActivityResult(BiometricDeviceLockContract()) { result: Int -> + if (result == BiometricDeviceAuthentication.AUTHENTICATED) { + viewModel.togglePaymentLock(false) + } + } + val promptInfo = PromptInfo.Builder() + .setAllowedAuthenticators(BiometricDeviceAuthentication.ALLOWED_AUTHENTICATORS) + .setTitle(requireContext().getString(R.string.BiometricDeviceAuthentication__signal)) + .setConfirmationRequired(false) + .build() + biometricAuth = BiometricDeviceAuthentication( + BiometricManager.from(requireActivity()), + BiometricPrompt(requireActivity(), BiometricAuthenticationListener()), + promptInfo + ) + } + override fun onResume() { super.onResume() viewModel.refreshBlockedCount() } + override fun onPause() { + super.onPause() + biometricAuth.cancelAuthentication() + } + override fun bindAdapter(adapter: MappingAdapter) { adapter.registerFactory(ValueClickPreference::class.java, LayoutFactory(::ValueClickPreferenceViewHolder, R.layout.value_click_preference_item)) @@ -323,8 +356,10 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac onClick = { if (!ServiceUtil.getKeyguardManager(requireContext()).isKeyguardSecure) { showGoToPhoneSettings() + } else if (state.paymentLock) { + biometricAuth.authenticate(requireContext(), true) { biometricDeviceLockLauncher?.launch(getString(R.string.BiometricDeviceAuthentication__signal)) } } else { - viewModel.togglePaymentLock() + viewModel.togglePaymentLock(true) } } ) @@ -512,4 +547,20 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac valueText.text = model.value.resolve(context) } } + + inner class BiometricAuthenticationListener : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errorString: CharSequence) { + Log.w(TAG, "Authentication error: $errorCode") + onAuthenticationFailed() + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + Log.i(TAG, "onAuthenticationSucceeded") + viewModel.togglePaymentLock(false) + } + + override fun onAuthenticationFailed() { + Log.w(TAG, "Unable to authenticate payment lock") + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsViewModel.kt index 6d2a2e7c9a..1a897c3bed 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsViewModel.kt @@ -74,8 +74,8 @@ class PrivacySettingsViewModel( refresh() } - fun togglePaymentLock() { - SignalStore.paymentsValues().paymentLock = state.value?.let { !it.paymentLock } ?: false + fun togglePaymentLock(enable: Boolean) { + SignalStore.paymentsValues().paymentLock = enable refresh() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.kt index 6fa68d7db5..fd5a328da4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.kt @@ -55,6 +55,7 @@ internal class PaymentsValues internal constructor(store: KeyValueStore) : Signa const val MOB_PAYMENTS_ENABLED = "mob_payments_enabled" } + @get:JvmName("isPaymentLockEnabled") var paymentLock: Boolean by booleanValue(PAYMENT_LOCK_ENABLED, false) var paymentLockTimestamp: Long by longValue(PAYMENT_LOCK_TIMESTAMP, 0) var paymentLockSkipCount: Int by integerValue(PAYMENT_LOCK_SKIP_COUNT, 0) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaIntentFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaIntentFactory.kt index 326fa61702..1ecff88560 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaIntentFactory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaIntentFactory.kt @@ -34,7 +34,7 @@ object MediaIntentFactory { val initialCaption: String? = null, val leftIsRecent: Boolean = false, val hideAllMedia: Boolean = false, - val showThread: Boolean= false, + val showThread: Boolean = false, val sorting: Int, val isVideoGif: Boolean ) : Parcelable diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/backup/PaymentsRecoveryStartFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/backup/PaymentsRecoveryStartFragment.java index 52783129b4..974766248f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/backup/PaymentsRecoveryStartFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/backup/PaymentsRecoveryStartFragment.java @@ -65,7 +65,7 @@ public class PaymentsRecoveryStartFragment extends Fragment { message.setText(getDescription(state)); message.setLink(getString(R.string.PaymentsRecoveryStartFragment__learn_more__view)); startButton.setOnClickListener(v -> { - if (state == RecoveryPhraseStates.FROM_PAYMENTS_MENU_WITH_MNEMONIC_CONFIRMED && ServiceUtil.getKeyguardManager(requireContext()).isKeyguardSecure() && SignalStore.paymentsValues().getPaymentLock()) { + if (state == RecoveryPhraseStates.FROM_PAYMENTS_MENU_WITH_MNEMONIC_CONFIRMED && ServiceUtil.getKeyguardManager(requireContext()).isKeyguardSecure() && SignalStore.paymentsValues().isPaymentLockEnabled()) { BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo .Builder() .setAllowedAuthenticators(BiometricDeviceAuthentication.ALLOWED_AUTHENTICATORS) diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/confirm/ConfirmPaymentFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/confirm/ConfirmPaymentFragment.java index bac3bcf42b..2b9acdc058 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/confirm/ConfirmPaymentFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/confirm/ConfirmPaymentFragment.java @@ -211,7 +211,7 @@ public class ConfirmPaymentFragment extends BottomSheetDialogFragment { private boolean isPaymentLockEnabled(Context context) { - return SignalStore.paymentsValues().getPaymentLock() && ServiceUtil.getKeyguardManager(context).isKeyguardSecure(); + return SignalStore.paymentsValues().isPaymentLockEnabled() && ServiceUtil.getKeyguardManager(context).isKeyguardSecure(); } private class Callbacks implements ConfirmPaymentAdapter.Callbacks { diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java index c7788ce644..22e162d937 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java @@ -222,6 +222,9 @@ public class PaymentsHomeFragment extends LoggingFragment { }); break; case ACTIVATED: + if (!SignalStore.paymentsValues().isPaymentLockEnabled()) { + SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), R.id.action_paymentsHome_to_securitySetup); + } return; default: throw new IllegalStateException("Unsupported event type: " + paymentStateEvent.name()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeViewModel.java index 7cbde5424f..66bc21d477 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeViewModel.java @@ -203,6 +203,7 @@ public class PaymentsHomeViewModel extends ViewModel { @Override public void onComplete(@Nullable Void result) { store.update(state -> state.updatePaymentsEnabled(PaymentsHomeState.PaymentsState.ACTIVATED)); + paymentStateEvents.postValue(PaymentStateEvent.ACTIVATED); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/securitysetup/PaymentsSecuritySetupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/payments/securitysetup/PaymentsSecuritySetupFragment.kt new file mode 100644 index 0000000000..54f0696449 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/securitysetup/PaymentsSecuritySetupFragment.kt @@ -0,0 +1,46 @@ +package org.thoughtcrime.securesms.payments.securitysetup + +import android.os.Bundle +import android.view.View +import androidx.activity.OnBackPressedCallback +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.databinding.PaymentsSecuritySetupFragmentBinding +import org.thoughtcrime.securesms.payments.preferences.PaymentsHomeFragmentDirections +import org.thoughtcrime.securesms.util.navigation.safeNavigate + +/** + * Fragment to let user know to enable payment lock to protect their funds + */ +class PaymentsSecuritySetupFragment : Fragment(R.layout.payments_security_setup_fragment) { + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val binding = PaymentsSecuritySetupFragmentBinding.bind(view) + + requireActivity().onBackPressedDispatcher.addCallback( + viewLifecycleOwner, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + showSkipDialog() + } + } + ) + binding.paymentsSecuritySetupEnableLock.setOnClickListener { + findNavController().safeNavigate(PaymentsHomeFragmentDirections.actionPaymentsHomeToPrivacySettings(true)) + } + binding.paymentsSecuritySetupFragmentNotNow.setOnClickListener { showSkipDialog() } + binding.toolbar.setNavigationOnClickListener { showSkipDialog() } + } + + private fun showSkipDialog() { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.PaymentsSecuritySetupFragment__skip_this_step)) + .setMessage(getString(R.string.PaymentsSecuritySetupFragment__skipping_this_step)) + .setPositiveButton(R.string.PaymentsSecuritySetupFragment__skip) { _, _ -> findNavController().popBackStack() } + .setNegativeButton(R.string.PaymentsSecuritySetupFragment__cancel) { _, _ -> } + .setCancelable(false) + .show() + } +} diff --git a/app/src/main/res/drawable/ic_payment_security_setup_lock.xml b/app/src/main/res/drawable/ic_payment_security_setup_lock.xml new file mode 100644 index 0000000000..2696890429 --- /dev/null +++ b/app/src/main/res/drawable/ic_payment_security_setup_lock.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/layout/payments_security_setup_fragment.xml b/app/src/main/res/layout/payments_security_setup_fragment.xml new file mode 100644 index 0000000000..f28c96d797 --- /dev/null +++ b/app/src/main/res/layout/payments_security_setup_fragment.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/navigation/payments_preferences.xml b/app/src/main/res/navigation/payments_preferences.xml index f052ec26e7..8d0c8798c2 100644 --- a/app/src/main/res/navigation/payments_preferences.xml +++ b/app/src/main/res/navigation/payments_preferences.xml @@ -112,6 +112,14 @@ + + + + + + + + + + + + Not Now + + + Security setup + + Protect your funds + + Help prevent a person with your phone from accessing your funds by adding another layer of security. You can disable this option in Settings. + + Enable payment lock + + Not now + + Skip this step? + + Skipping this step could allow anyone who has physical access to your phone to transfer funds or view your recovery phrase. + + Cancel + + Skip + Add funds Your Wallet Address