From 7d1a28081d74db9d528a9138c42c24a04bd741a9 Mon Sep 17 00:00:00 2001 From: Andrew Gunnerson Date: Fri, 25 Oct 2024 18:43:54 -0400 Subject: [PATCH] PreferenceBaseActivity: Add support for legacy device credential auth Although androidx.biometric can handle biometric auth on legacy Android versions, we're intentionally not using it. The legacy device credential API does both biometric and device credential auth and it is not possible to disable just the biometric part, so just let the legacy API handle everything. Issue: #93 Signed-off-by: Andrew Gunnerson --- .../chiller3/rsaf/PreferenceBaseActivity.kt | 111 +++++++++++++----- 1 file changed, 83 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/chiller3/rsaf/PreferenceBaseActivity.kt b/app/src/main/java/com/chiller3/rsaf/PreferenceBaseActivity.kt index ddb18b6..efc0f1b 100644 --- a/app/src/main/java/com/chiller3/rsaf/PreferenceBaseActivity.kt +++ b/app/src/main/java/com/chiller3/rsaf/PreferenceBaseActivity.kt @@ -6,6 +6,7 @@ package com.chiller3.rsaf import android.app.ActivityManager +import android.app.KeyguardManager import android.content.Intent import android.os.Build import android.os.Bundle @@ -16,6 +17,7 @@ import android.view.ViewGroup import android.view.WindowManager import android.widget.Toast import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.biometric.BiometricManager.Authenticators import androidx.biometric.BiometricPrompt @@ -32,8 +34,17 @@ abstract class PreferenceBaseActivity : AppCompatActivity() { // within the app. private var bioAuthenticated = false private var lastPause = 0L + + private fun supportsModernDeviceCredential() = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R } + protected abstract val actionBarTitle: CharSequence? + + protected abstract val showUpButton: Boolean + + protected abstract fun createFragment(): PreferenceBaseFragment + private val tag = javaClass.simpleName private lateinit var prefs: Preferences @@ -41,11 +52,15 @@ abstract class PreferenceBaseActivity : AppCompatActivity() { private lateinit var activityManager: ActivityManager private var isCoveredBySafeActivity = false - protected abstract val actionBarTitle: CharSequence? - - protected abstract val showUpButton: Boolean - - protected abstract fun createFragment(): PreferenceBaseFragment + private val requestLegacyDeviceCredential = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + onAuthenticationSucceeded() + } else { + // We can't know the reason. + onAuthenticationFailed() + } + } override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() @@ -107,28 +122,14 @@ abstract class PreferenceBaseActivity : AppCompatActivity() { this, mainExecutor, object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { - Toast.makeText( - this@PreferenceBaseActivity, - getString(R.string.biometric_error, errString), - Toast.LENGTH_LONG, - ).show() - finish() - } - - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - bioAuthenticated = true - refreshGlobalVisibility() - } - - override fun onAuthenticationFailed() { - Toast.makeText( - this@PreferenceBaseActivity, - R.string.biometric_failure, - Toast.LENGTH_LONG, - ).show() - finish() - } + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) = + this@PreferenceBaseActivity.onAuthenticationError(errorCode, errString) + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) = + this@PreferenceBaseActivity.onAuthenticationSucceeded() + + override fun onAuthenticationFailed() = + this@PreferenceBaseActivity.onAuthenticationFailed() }, ) @@ -158,7 +159,7 @@ abstract class PreferenceBaseActivity : AppCompatActivity() { if (!prefs.requireAuth) { bioAuthenticated = true } else { - startBiometricAuth() + startAuth() } } @@ -214,6 +215,14 @@ abstract class PreferenceBaseActivity : AppCompatActivity() { super.onWindowAttributesChanged(params) } + private fun startAuth() { + if (supportsModernDeviceCredential()) { + startBiometricAuth() + } else { + startLegacyDeviceCredentialAuth() + } + } + private fun startBiometricAuth() { Log.d(tag, "Starting biometric authentication") @@ -225,6 +234,52 @@ abstract class PreferenceBaseActivity : AppCompatActivity() { bioPrompt.authenticate(promptInfo) } + + private fun startLegacyDeviceCredentialAuth() { + Log.d(tag, "Starting legacy device credential authentication") + + val keyGuardManager = getSystemService(KeyguardManager::class.java) + @Suppress("DEPRECATION") + val intent = keyGuardManager?.createConfirmDeviceCredentialIntent( + getString(R.string.biometric_title), + "", + ) + + if (intent != null) { + requestLegacyDeviceCredential.launch(intent) + } else { + onAuthenticationFailed() + } + } + + fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + if (!supportsModernDeviceCredential() && errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON) { + startLegacyDeviceCredentialAuth() + return + } + + Toast.makeText( + this@PreferenceBaseActivity, + getString(R.string.biometric_error, errString), + Toast.LENGTH_LONG, + ).show() + finish() + } + + private fun onAuthenticationSucceeded() { + bioAuthenticated = true + refreshGlobalVisibility() + } + + private fun onAuthenticationFailed() { + Toast.makeText( + this@PreferenceBaseActivity, + R.string.biometric_failure, + Toast.LENGTH_LONG, + ).show() + finish() + } + private fun refreshTaskState() { // This is an awful hack, but we need it to be able to only apply the view hiding in the // topmost activity to ensure that predictive back gestures still work.